From b1a3aada7f109bbbea918cac8fe4515e1ae5d8f4 Mon Sep 17 00:00:00 2001 From: bneradt Date: Thu, 13 Aug 2026 13:33:59 -0500 Subject: [PATCH 1/3] Add pytest replay test framework AuTest's stalled file-format support leaves replay tests tied to thin Python registration wrappers. Collect replay YAML directly so every scenario has an independent pytest result and sandbox. Add ATS-specific DNS, Proxy Verifier, Traffic Server, log, and metric lifecycle support, then convert all 96 ATSReplayTest registrations to .test.yaml files. Keep bespoke AuTests in place and run both suites from the existing autest targets. --- AGENTS.md | 16 +- CMakeLists.txt | 5 + doc/developer-guide/testing/autests.en.rst | 202 ++++--- tests/CMakeLists.txt | 26 + tests/README.md | 31 +- tests/ats_testkit/config.py | 120 ++++ tests/ats_testkit/plugin.py | 154 +++++ tests/ats_testkit/process.py | 146 +++++ tests/ats_testkit/replay.py | 540 ++++++++++++++++++ tests/ats_testkit/runtime.py | 214 +++++++ .../gold_tests/at_headers/at_headers.test.py | 21 - ...aders.replay.yaml => at_headers.test.yaml} | 0 .../cache/alternate-caching.test.py | 31 - tests/gold_tests/cache/cache-auth.test.py | 29 - tests/gold_tests/cache/cache-control.test.py | 46 -- tests/gold_tests/cache/cache-cookie.test.py | 37 -- .../gold_tests/cache/cache-exact-url.test.py | 24 - .../cache/cache-heuristic-status.test.py | 28 - .../cache/cache-range-response.test.py | 25 - .../cache/cache-read-retry-mode.test.py | 30 - .../cache/cache-read-retry-stale.test.py | 30 - .../cache/cache-request-method.test.py | 37 -- .../cache-write-lock-stale-revalidate.test.py | 32 -- .../cache/cache-write-retry-callback.test.py | 25 - .../cache/cache_volume_defaults.test.py | 24 - ...y.yaml => cache_volume_defaults.test.yaml} | 0 .../cache/cache_volume_features.test.py | 26 - ...y.yaml => cache_volume_features.test.yaml} | 0 .../cache/conditional-get-hit.test.py | 25 - .../gold_tests/cache/negative-caching.test.py | 12 - .../cache/negative-revalidating.test.py | 31 - .../cache/proxy_serve_stale.test.py | 27 - ...ml => alternate-caching-quality.test.yaml} | 3 + ...> alternate-caching-update-size.test.yaml} | 0 ...ge.replay.yaml => auth-s-maxage.test.yaml} | 0 ...lay.yaml => cache-control-basic.test.yaml} | 0 ...y.yaml => cache-control-max-age.test.yaml} | 0 ...ay.yaml => cache-control-pragma.test.yaml} | 0 ....yaml => cache-control-s-maxage.test.yaml} | 0 ....replay.yaml => cache-exact-url.test.yaml} | 0 ....yaml => cache-heuristic-status.test.yaml} | 0 ...ay.yaml => cache-range-response.test.yaml} | 0 ....yaml => cache-read-retry-stale.test.yaml} | 0 ...replay.yaml => cache-read-retry.test.yaml} | 0 ...=> cache-write-lock-stale-retry.test.yaml} | 0 ...=> cache-write-lock-stale-serve.test.yaml} | 0 ...l => cache-write-retry-callback.test.yaml} | 0 ...ml => conditional-get-cache-hit.test.yaml} | 0 ...> cookie-all-but-text-with-excp.test.yaml} | 0 ...lay.yaml => cookie-all-but-text.test.yaml} | 0 ...lay.yaml => cookie-bypass-cache.test.yaml} | 0 ...y.yaml => cookie-cache-img-only.test.yaml} | 0 ...t.replay.yaml => cookie-default.test.yaml} | 0 ...ed.replay.yaml => delete_cached.test.yaml} | 0 ...ay.yaml => head_with_get_cached.test.yaml} | 0 ...y.yaml => ignore_authentication.test.yaml} | 0 ... => negative-caching-customized.test.yaml} | 0 ...aml => negative-caching-default.test.yaml} | 0 ...ml => negative-caching-disabled.test.yaml} | 0 ...> negative-caching-malformed-cc.test.yaml} | 0 ... negative-revalidating-disabled.test.yaml} | 0 ...> negative-revalidating-enabled.test.yaml} | 0 ...l => negative-revalidating-list.test.yaml} | 0 ...post_with_post_caching_disabled.test.yaml} | 0 ... post_with_post_caching_enabled.test.yaml} | 0 ...post_with_post_caching_override.test.yaml} | 0 ...eplay.yaml => proxy_serve_stale.test.yaml} | 0 ...> proxy_serve_stale_origin_down.test.yaml} | 0 ...> request-cache-control-default.test.yaml} | 0 ...uest-cache-control-honor-client.test.yaml} | 0 ... response-cache-control-default.test.yaml} | 0 ... response-cache-control-ignored.test.yaml} | 0 ....yaml => targeted-cache-control.test.yaml} | 0 ...lay.yaml => varied_transactions.test.yaml} | 0 .../cache/targeted-cache-control.test.py | 25 - tests/gold_tests/cache/vary-handling.test.py | 25 - .../chunked_encoding/chunked_encoding.test.py | 3 - .../chunked_flow_control.test.py | 24 - .../chunked_encoding/chunked_not_last.test.py | 25 - ...yaml => chunked_h1_flow_control.test.yaml} | 0 ...yaml => chunked_h2_flow_control.test.yaml} | 0 ...replay.yaml => chunked_not_last.test.yaml} | 0 ...> chunked_passthru_flow_control.test.yaml} | 0 ...ed.replay.yaml => large_chunked.test.yaml} | 2 + .../connect_down_policy.test.py | 3 - ...y.yaml => connect_down_policy_4.test.yaml} | 0 tests/gold_tests/cripts/cripts_bundle.test.py | 49 -- ...le.replay.yaml => cripts_bundle.test.yaml} | 3 +- tests/gold_tests/dns/connect_attempts.test.py | 36 -- ...connect_attempts_rr_max_retries.test.yaml} | 0 ...=> connect_attempts_rr_no_retry.test.yaml} | 0 ... => connect_attempts_rr_retries.test.yaml} | 0 ...ect_attempts_single_max_retries.test.yaml} | 0 ...client_reset_keeps_session_reading.test.py | 23 - ...ent_reset_keeps_session_reading.test.yaml} | 4 + tests/gold_tests/h3/h3_active_timeout.test.py | 26 - tests/gold_tests/h3/h3_flow_control.test.py | 27 - tests/gold_tests/h3/h3_proxy_verifier.test.py | 27 - .../gold_tests/h3/h3_stream_lifetime.test.py | 27 - ...eplay.yaml => h3_active_timeout.test.yaml} | 2 + ....replay.yaml => h3_flow_control.test.yaml} | 2 + ...eplay.yaml => h3_proxy_verifier.test.yaml} | 2 + ...play.yaml => h3_stream_lifetime.test.yaml} | 2 + tests/gold_tests/headers/accept_webp.test.py | 26 - .../headers/cachedDuplicateHeaders.test.py | 26 - .../gold_tests/headers/cachedIMSRange.test.py | 26 - .../headers/domain-blacklist-30x.test.py | 26 - .../headers/hopbyhop_connection.test.py | 23 - tests/gold_tests/headers/hsts.test.py | 26 - .../headers/invalid_range_header.test.py | 26 - .../headers/malformed_host_port.test.py | 24 - .../normalized_ae_match_vary_cache.test.py | 27 - .../headers/oversized_field.test.py | 27 - tests/gold_tests/headers/range.test.py | 26 - .../headers/range_transform.test.py | 18 - ....replay.yaml => oversized_field.test.yaml} | 0 ...webp.replay.yaml => accept_webp.test.yaml} | 0 ...-test.replay.yaml => cache-test.test.yaml} | 0 ...replay.yaml => cached_ims_range.test.yaml} | 0 ...ay.yaml => domain-blacklist-30x.test.yaml} | 0 ...lay.yaml => hopbyhop_connection.test.yaml} | 0 .../{hsts.replay.yaml => hsts.test.yaml} | 0 ...y.yaml => invalid_range_request.test.yaml} | 0 ...lay.yaml => malformed_host_port.test.yaml} | 0 ...rmalized_ae_varied_transactions.test.yaml} | 0 .../{range.replay.yaml => range.test.yaml} | 0 ....replay.yaml => range_transform.test.yaml} | 1 + .../ip_allow/ip_allow_subjects.test.py | 30 - ...l => ip_allow_subjects_overflow.test.yaml} | 0 ...yaml => ip_allow_subjects_valid.test.yaml} | 0 .../logging/log-debug-client-ip.test.py | 31 - ...aml => log-debug-client-ip-http.test.yaml} | 0 ...ml => log-debug-client-ip-http2.test.yaml} | 0 ...ml => log-debug-client-ip-http3.test.yaml} | 3 + ...ml => log-debug-client-ip-https.test.yaml} | 0 ...play.yaml => minimum_keep_alive.test.yaml} | 4 + .../per_server_connection_min.test.py | 29 - .../parent_retry_availability.test.py | 26 - .../parent_retry_non_retryable_post.test.py | 25 - ...parent_retry_failure_stays_down.test.yaml} | 0 ...parent_retry_non_retryable_post.test.yaml} | 4 + ...> parent_retry_success_restores.test.yaml} | 0 .../filter_body/filter_body.test.py | 24 - ...body.replay.yaml => filter_body.test.yaml} | 2 + .../header_rewrite_bundle.test.py | 24 - ...y.yaml => header_rewrite_bundle.test.yaml} | 0 .../header_rewrite_post_remap.test.py | 24 - ...ml => header_rewrite_post_remap.test.yaml} | 0 .../prefetch_query_path_traversal.test.py | 24 - ...> prefetch_query_path_traversal.test.yaml} | 0 ....replay.yaml => slice_long_etag.test.yaml} | 2 + .../pluginTest/slice/slice_long_etag.test.py | 27 - .../pluginTest/test_hooks/body_buffer.test.py | 27 - ...ffer.replay.yaml => body_buffer.test.yaml} | 2 + ...lay.yaml => zero_post_copy_size.test.yaml} | 2 + .../webp_transform_invalid_input.test.py | 25 - ...=> webp_transform_invalid_input.test.yaml} | 2 + .../x_remap_long_url/x_remap_long_url.test.py | 30 - ...replay.yaml => x_remap_long_url.test.yaml} | 2 + tests/gold_tests/post/post-no-cl.test.py | 25 - ...no-cl.replay.yaml => post-no-cl.test.yaml} | 0 .../gold_tests/remap/regex_map_anchor.test.py | 35 -- ...replay.yaml => regex_map_anchor.test.yaml} | 0 .../statistics/metric_response_429.test.py | 20 - ...lay.yaml => metric_response_429.test.yaml} | 0 ... outbound_sni_server_name_plain.test.yaml} | 0 ...tls_outbound_sni_server_name_plain.test.py | 25 - tests/pyproject.toml | 6 +- tests/pytest_tests/test_config.py | 102 ++++ tests/pytest_tests/test_process.py | 64 +++ 170 files changed, 1544 insertions(+), 1738 deletions(-) create mode 100644 tests/ats_testkit/config.py create mode 100644 tests/ats_testkit/plugin.py create mode 100644 tests/ats_testkit/process.py create mode 100644 tests/ats_testkit/replay.py create mode 100644 tests/ats_testkit/runtime.py delete mode 100644 tests/gold_tests/at_headers/at_headers.test.py rename tests/gold_tests/at_headers/replay/{at_headers.replay.yaml => at_headers.test.yaml} (100%) delete mode 100644 tests/gold_tests/cache/alternate-caching.test.py delete mode 100644 tests/gold_tests/cache/cache-auth.test.py delete mode 100644 tests/gold_tests/cache/cache-control.test.py delete mode 100644 tests/gold_tests/cache/cache-cookie.test.py delete mode 100644 tests/gold_tests/cache/cache-exact-url.test.py delete mode 100644 tests/gold_tests/cache/cache-heuristic-status.test.py delete mode 100644 tests/gold_tests/cache/cache-range-response.test.py delete mode 100644 tests/gold_tests/cache/cache-read-retry-mode.test.py delete mode 100644 tests/gold_tests/cache/cache-read-retry-stale.test.py delete mode 100644 tests/gold_tests/cache/cache-request-method.test.py delete mode 100644 tests/gold_tests/cache/cache-write-lock-stale-revalidate.test.py delete mode 100644 tests/gold_tests/cache/cache-write-retry-callback.test.py delete mode 100644 tests/gold_tests/cache/cache_volume_defaults.test.py rename tests/gold_tests/cache/{cache_volume_defaults.replay.yaml => cache_volume_defaults.test.yaml} (100%) delete mode 100644 tests/gold_tests/cache/cache_volume_features.test.py rename tests/gold_tests/cache/{cache_volume_features.replay.yaml => cache_volume_features.test.yaml} (100%) delete mode 100644 tests/gold_tests/cache/conditional-get-hit.test.py delete mode 100644 tests/gold_tests/cache/negative-revalidating.test.py delete mode 100644 tests/gold_tests/cache/proxy_serve_stale.test.py rename tests/gold_tests/cache/replay/{alternate-caching-quality.yaml => alternate-caching-quality.test.yaml} (97%) rename tests/gold_tests/cache/replay/{alternate-caching-update-size.yaml => alternate-caching-update-size.test.yaml} (100%) rename tests/gold_tests/cache/replay/{auth-s-maxage.replay.yaml => auth-s-maxage.test.yaml} (100%) rename tests/gold_tests/cache/replay/{cache-control-basic.replay.yaml => cache-control-basic.test.yaml} (100%) rename tests/gold_tests/cache/replay/{cache-control-max-age.replay.yaml => cache-control-max-age.test.yaml} (100%) rename tests/gold_tests/cache/replay/{cache-control-pragma.replay.yaml => cache-control-pragma.test.yaml} (100%) rename tests/gold_tests/cache/replay/{cache-control-s-maxage.replay.yaml => cache-control-s-maxage.test.yaml} (100%) rename tests/gold_tests/cache/replay/{cache-exact-url.replay.yaml => cache-exact-url.test.yaml} (100%) rename tests/gold_tests/cache/replay/{cache-heuristic-status.replay.yaml => cache-heuristic-status.test.yaml} (100%) rename tests/gold_tests/cache/replay/{cache-range-response.replay.yaml => cache-range-response.test.yaml} (100%) rename tests/gold_tests/cache/replay/{cache-read-retry-stale.replay.yaml => cache-read-retry-stale.test.yaml} (100%) rename tests/gold_tests/cache/replay/{cache-read-retry.replay.yaml => cache-read-retry.test.yaml} (100%) rename tests/gold_tests/cache/replay/{cache-write-lock-stale-retry.replay.yaml => cache-write-lock-stale-retry.test.yaml} (100%) rename tests/gold_tests/cache/replay/{cache-write-lock-stale-serve.replay.yaml => cache-write-lock-stale-serve.test.yaml} (100%) rename tests/gold_tests/cache/replay/{cache-write-retry-callback.replay.yaml => cache-write-retry-callback.test.yaml} (100%) rename tests/gold_tests/cache/replay/{conditional-get-cache-hit.yaml => conditional-get-cache-hit.test.yaml} (100%) rename tests/gold_tests/cache/replay/{cookie-all-but-text-with-excp.replay.yaml => cookie-all-but-text-with-excp.test.yaml} (100%) rename tests/gold_tests/cache/replay/{cookie-all-but-text.replay.yaml => cookie-all-but-text.test.yaml} (100%) rename tests/gold_tests/cache/replay/{cookie-bypass-cache.replay.yaml => cookie-bypass-cache.test.yaml} (100%) rename tests/gold_tests/cache/replay/{cookie-cache-img-only.replay.yaml => cookie-cache-img-only.test.yaml} (100%) rename tests/gold_tests/cache/replay/{cookie-default.replay.yaml => cookie-default.test.yaml} (100%) rename tests/gold_tests/cache/replay/{delete_cached.replay.yaml => delete_cached.test.yaml} (100%) rename tests/gold_tests/cache/replay/{head_with_get_cached.replay.yaml => head_with_get_cached.test.yaml} (100%) rename tests/gold_tests/cache/replay/{ignore_authentication.replay.yaml => ignore_authentication.test.yaml} (100%) rename tests/gold_tests/cache/replay/{negative-caching-customized.replay.yaml => negative-caching-customized.test.yaml} (100%) rename tests/gold_tests/cache/replay/{negative-caching-default.replay.yaml => negative-caching-default.test.yaml} (100%) rename tests/gold_tests/cache/replay/{negative-caching-disabled.replay.yaml => negative-caching-disabled.test.yaml} (100%) rename tests/gold_tests/cache/replay/{negative-caching-malformed-cc.replay.yaml => negative-caching-malformed-cc.test.yaml} (100%) rename tests/gold_tests/cache/replay/{negative-revalidating-disabled.replay.yaml => negative-revalidating-disabled.test.yaml} (100%) rename tests/gold_tests/cache/replay/{negative-revalidating-enabled.replay.yaml => negative-revalidating-enabled.test.yaml} (100%) rename tests/gold_tests/cache/replay/{negative-revalidating-list.replay.yaml => negative-revalidating-list.test.yaml} (100%) rename tests/gold_tests/cache/replay/{post_with_post_caching_disabled.replay.yaml => post_with_post_caching_disabled.test.yaml} (100%) rename tests/gold_tests/cache/replay/{post_with_post_caching_enabled.replay.yaml => post_with_post_caching_enabled.test.yaml} (100%) rename tests/gold_tests/cache/replay/{post_with_post_caching_override.replay.yaml => post_with_post_caching_override.test.yaml} (100%) rename tests/gold_tests/cache/replay/{proxy_serve_stale.replay.yaml => proxy_serve_stale.test.yaml} (100%) rename tests/gold_tests/cache/replay/{proxy_serve_stale_origin_down.replay.yaml => proxy_serve_stale_origin_down.test.yaml} (100%) rename tests/gold_tests/cache/replay/{request-cache-control-default.replay.yaml => request-cache-control-default.test.yaml} (100%) rename tests/gold_tests/cache/replay/{request-cache-control-honor-client.replay.yaml => request-cache-control-honor-client.test.yaml} (100%) rename tests/gold_tests/cache/replay/{response-cache-control-default.replay.yaml => response-cache-control-default.test.yaml} (100%) rename tests/gold_tests/cache/replay/{response-cache-control-ignored.replay.yaml => response-cache-control-ignored.test.yaml} (100%) rename tests/gold_tests/cache/replay/{targeted-cache-control.replay.yaml => targeted-cache-control.test.yaml} (100%) rename tests/gold_tests/cache/replay/{varied_transactions.replay.yaml => varied_transactions.test.yaml} (100%) delete mode 100644 tests/gold_tests/cache/targeted-cache-control.test.py delete mode 100644 tests/gold_tests/cache/vary-handling.test.py delete mode 100644 tests/gold_tests/chunked_encoding/chunked_flow_control.test.py delete mode 100644 tests/gold_tests/chunked_encoding/chunked_not_last.test.py rename tests/gold_tests/chunked_encoding/replays/{chunked_h1_flow_control.replay.yaml => chunked_h1_flow_control.test.yaml} (100%) rename tests/gold_tests/chunked_encoding/replays/{chunked_h2_flow_control.replay.yaml => chunked_h2_flow_control.test.yaml} (100%) rename tests/gold_tests/chunked_encoding/replays/{chunked_not_last.replay.yaml => chunked_not_last.test.yaml} (100%) rename tests/gold_tests/chunked_encoding/replays/{chunked_passthru_flow_control.replay.yaml => chunked_passthru_flow_control.test.yaml} (100%) rename tests/gold_tests/chunked_encoding/replays/{large_chunked.replay.yaml => large_chunked.test.yaml} (99%) rename tests/gold_tests/connect_down_policy/replay/{connect_down_policy_4.replay.yaml => connect_down_policy_4.test.yaml} (100%) delete mode 100644 tests/gold_tests/cripts/cripts_bundle.test.py rename tests/gold_tests/cripts/{cripts_bundle.replay.yaml => cripts_bundle.test.yaml} (99%) delete mode 100644 tests/gold_tests/dns/connect_attempts.test.py rename tests/gold_tests/dns/replay/{connect_attempts_rr_max_retries.replay.yaml => connect_attempts_rr_max_retries.test.yaml} (100%) rename tests/gold_tests/dns/replay/{connect_attempts_rr_no_retry.replay.yaml => connect_attempts_rr_no_retry.test.yaml} (100%) rename tests/gold_tests/dns/replay/{connect_attempts_rr_retries.replay.yaml => connect_attempts_rr_retries.test.yaml} (100%) rename tests/gold_tests/dns/replay/{connect_attempts_single_max_retries.replay.yaml => connect_attempts_single_max_retries.test.yaml} (100%) delete mode 100644 tests/gold_tests/h2/http2_client_reset_keeps_session_reading.test.py rename tests/gold_tests/h2/replay/{http2_client_reset_keeps_session_reading.replay.yaml => http2_client_reset_keeps_session_reading.test.yaml} (98%) delete mode 100644 tests/gold_tests/h3/h3_active_timeout.test.py delete mode 100644 tests/gold_tests/h3/h3_flow_control.test.py delete mode 100644 tests/gold_tests/h3/h3_proxy_verifier.test.py delete mode 100644 tests/gold_tests/h3/h3_stream_lifetime.test.py rename tests/gold_tests/h3/replays/{h3_active_timeout.replay.yaml => h3_active_timeout.test.yaml} (98%) rename tests/gold_tests/h3/replays/{h3_flow_control.replay.yaml => h3_flow_control.test.yaml} (98%) rename tests/gold_tests/h3/replays/{h3_proxy_verifier.replay.yaml => h3_proxy_verifier.test.yaml} (99%) rename tests/gold_tests/h3/replays/{h3_stream_lifetime.replay.yaml => h3_stream_lifetime.test.yaml} (99%) delete mode 100644 tests/gold_tests/headers/accept_webp.test.py delete mode 100644 tests/gold_tests/headers/cachedDuplicateHeaders.test.py delete mode 100644 tests/gold_tests/headers/cachedIMSRange.test.py delete mode 100644 tests/gold_tests/headers/domain-blacklist-30x.test.py delete mode 100644 tests/gold_tests/headers/hopbyhop_connection.test.py delete mode 100644 tests/gold_tests/headers/hsts.test.py delete mode 100644 tests/gold_tests/headers/invalid_range_header.test.py delete mode 100644 tests/gold_tests/headers/malformed_host_port.test.py delete mode 100644 tests/gold_tests/headers/normalized_ae_match_vary_cache.test.py delete mode 100644 tests/gold_tests/headers/oversized_field.test.py delete mode 100644 tests/gold_tests/headers/range.test.py delete mode 100644 tests/gold_tests/headers/range_transform.test.py rename tests/gold_tests/headers/replay/{oversized_field.replay.yaml => oversized_field.test.yaml} (100%) rename tests/gold_tests/headers/replays/{accept_webp.replay.yaml => accept_webp.test.yaml} (100%) rename tests/gold_tests/headers/replays/{cache-test.replay.yaml => cache-test.test.yaml} (100%) rename tests/gold_tests/headers/replays/{cached_ims_range.replay.yaml => cached_ims_range.test.yaml} (100%) rename tests/gold_tests/headers/replays/{domain-blacklist-30x.replay.yaml => domain-blacklist-30x.test.yaml} (100%) rename tests/gold_tests/headers/replays/{hopbyhop_connection.replay.yaml => hopbyhop_connection.test.yaml} (100%) rename tests/gold_tests/headers/replays/{hsts.replay.yaml => hsts.test.yaml} (100%) rename tests/gold_tests/headers/replays/{invalid_range_request.replay.yaml => invalid_range_request.test.yaml} (100%) rename tests/gold_tests/headers/replays/{malformed_host_port.replay.yaml => malformed_host_port.test.yaml} (100%) rename tests/gold_tests/headers/replays/{normalized_ae_varied_transactions.replay.yaml => normalized_ae_varied_transactions.test.yaml} (100%) rename tests/gold_tests/headers/replays/{range.replay.yaml => range.test.yaml} (100%) rename tests/gold_tests/headers/replays/{range_transform.replay.yaml => range_transform.test.yaml} (99%) delete mode 100644 tests/gold_tests/ip_allow/ip_allow_subjects.test.py rename tests/gold_tests/ip_allow/replay/{ip_allow_subjects_overflow.replay.yaml => ip_allow_subjects_overflow.test.yaml} (100%) rename tests/gold_tests/ip_allow/replay/{ip_allow_subjects_valid.replay.yaml => ip_allow_subjects_valid.test.yaml} (100%) delete mode 100644 tests/gold_tests/logging/log-debug-client-ip.test.py rename tests/gold_tests/logging/replay/{log-debug-client-ip-http.replay.yaml => log-debug-client-ip-http.test.yaml} (100%) rename tests/gold_tests/logging/replay/{log-debug-client-ip-http2.replay.yaml => log-debug-client-ip-http2.test.yaml} (100%) rename tests/gold_tests/logging/replay/{log-debug-client-ip-http3.replay.yaml => log-debug-client-ip-http3.test.yaml} (98%) rename tests/gold_tests/logging/replay/{log-debug-client-ip-https.replay.yaml => log-debug-client-ip-https.test.yaml} (100%) rename tests/gold_tests/origin_connection/{minimum_keep_alive.replay.yaml => minimum_keep_alive.test.yaml} (96%) delete mode 100644 tests/gold_tests/origin_connection/per_server_connection_min.test.py delete mode 100644 tests/gold_tests/parent_proxy/parent_retry_availability.test.py delete mode 100644 tests/gold_tests/parent_proxy/parent_retry_non_retryable_post.test.py rename tests/gold_tests/parent_proxy/replays/{parent_retry_failure_stays_down.replay.yaml => parent_retry_failure_stays_down.test.yaml} (100%) rename tests/gold_tests/parent_proxy/replays/{parent_retry_non_retryable_post.replay.yaml => parent_retry_non_retryable_post.test.yaml} (93%) rename tests/gold_tests/parent_proxy/replays/{parent_retry_success_restores.replay.yaml => parent_retry_success_restores.test.yaml} (100%) delete mode 100644 tests/gold_tests/pluginTest/filter_body/filter_body.test.py rename tests/gold_tests/pluginTest/filter_body/{filter_body.replay.yaml => filter_body.test.yaml} (99%) delete mode 100644 tests/gold_tests/pluginTest/header_rewrite/header_rewrite_bundle.test.py rename tests/gold_tests/pluginTest/header_rewrite/{header_rewrite_bundle.replay.yaml => header_rewrite_bundle.test.yaml} (100%) delete mode 100644 tests/gold_tests/pluginTest/header_rewrite/header_rewrite_post_remap.test.py rename tests/gold_tests/pluginTest/header_rewrite/{header_rewrite_post_remap.replay.yaml => header_rewrite_post_remap.test.yaml} (100%) delete mode 100644 tests/gold_tests/pluginTest/prefetch/prefetch_query_path_traversal.test.py rename tests/gold_tests/pluginTest/prefetch/{prefetch_query_path_traversal.replay.yaml => prefetch_query_path_traversal.test.yaml} (100%) rename tests/gold_tests/pluginTest/slice/replay/{slice_long_etag.replay.yaml => slice_long_etag.test.yaml} (99%) delete mode 100644 tests/gold_tests/pluginTest/slice/slice_long_etag.test.py delete mode 100644 tests/gold_tests/pluginTest/test_hooks/body_buffer.test.py rename tests/gold_tests/pluginTest/test_hooks/replay/{body_buffer.replay.yaml => body_buffer.test.yaml} (99%) rename tests/gold_tests/pluginTest/test_hooks/replay/{zero_post_copy_size.replay.yaml => zero_post_copy_size.test.yaml} (98%) delete mode 100644 tests/gold_tests/pluginTest/webp_transform/webp_transform_invalid_input.test.py rename tests/gold_tests/pluginTest/webp_transform/{webp_transform_invalid_input.replay.yaml => webp_transform_invalid_input.test.yaml} (99%) delete mode 100644 tests/gold_tests/pluginTest/xdebug/x_remap_long_url/x_remap_long_url.test.py rename tests/gold_tests/pluginTest/xdebug/x_remap_long_url/{x_remap_long_url.replay.yaml => x_remap_long_url.test.yaml} (99%) delete mode 100644 tests/gold_tests/post/post-no-cl.test.py rename tests/gold_tests/post/replay/{post-no-cl.replay.yaml => post-no-cl.test.yaml} (100%) delete mode 100644 tests/gold_tests/remap/regex_map_anchor.test.py rename tests/gold_tests/remap/replay/{regex_map_anchor.replay.yaml => regex_map_anchor.test.yaml} (100%) delete mode 100644 tests/gold_tests/statistics/metric_response_429.test.py rename tests/gold_tests/statistics/replay/{metric_response_429.replay.yaml => metric_response_429.test.yaml} (100%) rename tests/gold_tests/tls/replay/{outbound_sni_server_name_plain.replay.yaml => outbound_sni_server_name_plain.test.yaml} (100%) delete mode 100644 tests/gold_tests/tls/tls_outbound_sni_server_name_plain.test.py create mode 100644 tests/pytest_tests/test_config.py create mode 100644 tests/pytest_tests/test_process.py diff --git a/AGENTS.md b/AGENTS.md index d866f1845ff..47580ba8e02 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -116,14 +116,14 @@ Most end-to-end test coverage is in `tests/gold_tests/`. The CI system uses the Docker image `ci.trafficserver.apache.org/ats/fedora:43` (Fedora version updated regularly). -### Writing Autests +### Writing End-to-End Tests -**New tests should use the `Test.ATSReplayTest()` approach**, which references a -`replay.yaml` file that describes the test configuration and traffic patterns -using the Proxy Verifier format. This is simpler, more maintainable, and -parseable by tools. +**New tests should normally be direct pytest replay tests.** Name the Proxy +Verifier replay `.test.yaml`; the file's `autest` section describes +DNS, server, client, and ATS setup, and pytest collects it without a companion +`.test.py` wrapper. Run these with `cmake --build build -t pytest-replay`. -If `ATSReplayTest` is not a good fit (say, the test needs a custom client), then +If a direct replay test is not a good fit (say, the test needs a custom client), then organize the test around a test class with member functions that configure any servers, the ATS process, and the client. See `tests/gold_tests/ats_probe/ats_probe.test.py` for an example of a test organized @@ -133,8 +133,8 @@ In autests, launch Python helpers with `{sys.executable}` rather than a hardcoded `python3`, so the test runs under the same interpreter the harness uses. -**For complete details on writing autests, see:** -- `doc/developer-guide/testing/autests.en.rst` - Comprehensive guide to autest +**For complete details on writing end-to-end tests, see:** +- `doc/developer-guide/testing/autests.en.rst` - Comprehensive end-to-end test guide - Proxy Verifier format: https://github.com/yahoo/proxy-verifier - AuTest framework: https://autestsuite.bitbucket.io/ diff --git a/CMakeLists.txt b/CMakeLists.txt index f91b3546861..0e8de39044e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -804,11 +804,16 @@ if(ENABLE_AUTEST) "" CACHE STRING "Additional options for autest (default \"\")" ) + set(PYTEST_OPTIONS + "" + CACHE STRING "Additional options for pytest replay tests (default \"\")" + ) # Also create AUTEST_OPTIONS_LIST from the string for use in cmake targets. # This prevents cmake from escaping spaces in the arguments, which confuses # the autest command. The original AUTEST_OPTIONS string is used in the # autest.sh script. separate_arguments(AUTEST_OPTIONS_LIST UNIX_COMMAND "${AUTEST_OPTIONS}") + separate_arguments(PYTEST_OPTIONS_LIST UNIX_COMMAND "${PYTEST_OPTIONS}") file(READ "${CMAKE_SOURCE_DIR}/tests/proxy-verifier-version.txt" PROXY_VERIFIER_VERSION) string(STRIP "${PROXY_VERIFIER_VERSION}" PROXY_VERIFIER_VERSION) file(READ "${CMAKE_SOURCE_DIR}/tests/proxy-verifier-checksum.txt" PROXY_VERIFIER_SHA1) diff --git a/doc/developer-guide/testing/autests.en.rst b/doc/developer-guide/testing/autests.en.rst index 9d11b525035..fe8fe3ee8c5 100644 --- a/doc/developer-guide/testing/autests.en.rst +++ b/doc/developer-guide/testing/autests.en.rst @@ -19,35 +19,33 @@ .. _autest-writing: -Writing Autests -*************** +Writing End-to-End Tests +************************ -|TS| has two types of developer tests: (1) Catch2 tests for unit tests and (2) -AuTest framework tests for end-to-end testing. The Catch tests reside next the -the associated source code being tested while AuTests are located in -``tests/gold_tests/`` with ``.test.py`` extensions. +|TS| uses Catch2 for unit tests and pytest plus AuTest for end-to-end tests. +Catch2 tests reside next to the associated source code. End-to-end tests are +under ``tests/gold_tests/``. New replay-driven tests are collected directly by +pytest; tests that need bespoke process orchestration continue to use AuTest. * For catch test framework documentation, see: https://github.com/catchorg/Catch2 * For AuTest framework documentation, see: https://autestsuite.bitbucket.io/index.html -This document focuses on AuTest framework tests because they are rather unique -to the |TS| project while Catch2 documentation can be found elsewhere. +This document focuses on end-to-end tests because Catch2 documentation is +available elsewhere. File Structure and Naming ========================== -Here is a summary of the file structure and naming conventions for AuTest tests: +The end-to-end runners use distinct filename conventions: -- AuTest tests are placed in appropriate subdirectories under +- Tests are placed in appropriate subdirectories under ``tests/gold_tests/`` (e.g., ``cache/``, ``pluginTest/``, ``tls/``, etc.) -- AuTest test files have a descriptive name with a ``.test.py`` extension (e.g., - ``cache-auth.test.py``, ``stats_over_http.test.py``). When the tests are run, - be aware that their names, sans the ``.test.py`` extension, are used to - identify the test. -- The ``.test.py`` typically are thin and reference the associated - ``replay.yaml`` file that describes the test via the ``Test.ATSReplayTest()`` - method. +- Replay-driven pytest files have a descriptive ``.test.yaml`` extension. The + file is both the test registration and the Proxy Verifier replay. +- Bespoke AuTest files have a descriptive ``.test.py`` extension. Use these + only when the direct replay runner cannot express the required client, + server, or lifecycle. - ``tests/gold_tests/autest-site`` is a special directory. AuTest, a general testing framework, is extended to add domain specific support, |TS| in this case, via ``.test.ext`` extension files. The files in here customize the @@ -55,10 +53,31 @@ Here is a summary of the file structure and naming conventions for AuTest tests: availabe to the ``Test`` and ``TestRun`` AuTest objects, specific ``Process`` objects available to test, ``Skip`` conditions for individual tests, etc. -Running Autests -=============== +Running End-to-End Tests +======================== -If |TS| cmake build is configured via ``-DENABLE_AUTEST=ON``, tests can be run with: +Configure the build with ``-DENABLE_AUTEST=ON``. The ``autest`` build target +runs both direct pytest replays and the remaining AuTests, preserving the +existing CI entry point: + +.. code-block:: bash + + cmake --build build --target autest + +Run only the pytest replay suite with: + +.. code-block:: bash + + cmake --build build --target pytest-replay + +Pass pytest selection or concurrency options at configure time. For example: + +.. code-block:: bash + + cmake -B build -DENABLE_AUTEST=ON -DPYTEST_OPTIONS="-n 4 -k cache-control" + cmake --build build --target pytest-replay + +For a bespoke AuTest, use ``autest.sh`` directly: .. code-block:: bash @@ -82,77 +101,49 @@ processes. Each worker gets an isolated port range to avoid conflicts: Without ``-j``, tests run sequentially. -Recommended Approach: ATSReplayTest -==================================== - -Currently, many tests are specified largely entirely using the generic AuTest -framework specific syntax via ``.test.py`` files. These use the generic AuTest -framework syntax, which is generically very capable, but not tuned to the -specific |TS| environment and not generally parseable by code editors and AI -tools. - -AuTest itself has a solution for this via its extensibility mechanism. A -concerted effort is underway to make more full use of the AuTest extension -mechanism to simplify the test writing process specifically for |TS|. At a high -level, the extension is called ``ATSReplayTest`` and it is used in a ``test.py`` -file to reference an associated ``replay.yaml`` file which fully describes the -test. The goal is that a large percentage, maybe 90%, of the tests can be -written using this approach while certain requests, perhaps requiring ad-hoc -clients and servers, will be written using the generic AuTest framework syntax. - -The traffic portion of the ``replay.yaml`` files specify Proxy Verifier HTTP -traffic behavior and follow the replay and verification syntax described -extensively in its project's README.md file here: -https://github.com/yahoo/proxy-verifier +Recommended Approach: Direct Replay Tests +========================================== -Simple Test File Structure ---------------------------- +New tests should normally be a single ``.test.yaml`` file. Pytest recognizes +that suffix through the ATS testkit plugin, constructs isolated DNS, Proxy +Verifier server, ATS, and Proxy Verifier client processes, and reports the file +as one pytest item. ATS readiness is based on the fully-initialized log message, +not merely an open port. -Here is an example of a test file using ``ATSReplayTest``: +The YAML format is parseable without executing Python and keeps the test +topology, ATS configuration, traffic, and validation together. Tests requiring +ad-hoc clients or servers can still use the generic AuTest syntax. -.. code-block:: python - - ''' - Brief description of what the test validates - ''' - # Licensed to the Apache Software Foundation (ASF) under one - # or more contributor license agreements. See the NOTICE file - # distributed with this work for additional information - # regarding copyright ownership. The ASF licenses this file - # to you under the Apache License, Version 2.0 (the - # "License"); you may not use this file except in compliance - # with the License. You may obtain a copy of the License at - # - # http://www.apache.org/licenses/LICENSE-2.0 - # - # Unless required by applicable law or agreed to in writing, software - # distributed under the License 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. +The traffic portion of the ``.test.yaml`` files specifies Proxy Verifier HTTP +traffic behavior and follows the replay and verification syntax described +extensively in its project's README.md file here: +https://github.com/yahoo/proxy-verifier - Test.Summary = ''' - Brief description of test purpose - ''' +Test File Structure +------------------- - Test.ATSReplayTest(replay_file="replay/my-test.replay.yaml") +Name the replay itself ``.test.yaml``. No companion ``.test.py`` +registration is needed. If a feature has multiple configurations, keep each +configuration in a separate file: -For tests with multiple |TS| configuration scenarios, you can call -``Test.ATSReplayTest()`` multiple times, each with a different replay file -specifying different configurations of |TS|, dns, servers, clients, etc. +.. code-block:: text -.. code-block:: python + replay/scenario1.test.yaml + replay/scenario2.test.yaml + replay/scenario3.test.yaml - Test.ATSReplayTest(replay_file="replay/scenario1.replay.yaml") - Test.ATSReplayTest(replay_file="replay/scenario2.replay.yaml") - Test.ATSReplayTest(replay_file="replay/scenario3.replay.yaml") +Each file is an independent pytest item and therefore has its own result, +sandbox, ports, and failure artifacts. Keep multiple sessions in one file when +they intentionally share ATS state, such as a cache prime followed by a cache +hit. Replay File Structure ---------------------- The replay file contains both the test configuration (in the ``autest`` YAML node) and the traffic replay and verification specification (in the ``sessions`` -YAML node). Here is an example: +YAML node). The ``autest`` key retains its name for compatibility with existing +replay files. Here is an example: .. code-block:: yaml @@ -175,38 +166,33 @@ YAML node). Here is an example: meta: version: "1.0" - # Configuration section for autest integration + # Configuration section for ATS pytest integration autest: description: 'Test description for this scenario' # Optional (but typical) DNS configuration. dns: name: 'dns' - # Other MakeDNServer parameters can be set, see microDNS.test.ext # Required: Server configuration. server: name: 'server' - # Other AddVerifierServerProcess parameters can be set, see verifier_server.test.ext # Required: Client configuration. client: name: 'client' - # Other AddVerifierClientProcess parameters can be set, see verifier_client.test.ext + # Optional one-shot client timeout in seconds. + timeout: 60 # Required: ATS configuration. ats: name: 'ts' - # Optional: Enable cache (default is determined by process_config) - # enable_cache: true - - # Other parameters can be set, see trafficserver.test.ext - # Optional: ATS process configuration process_config: enable_cache: true - # Other MakeATSProcess parameters can be set, see trafficserver.test.ext + enable_tls: false + enable_quic: false # ATS records.config settings records_config: @@ -256,6 +242,11 @@ YAML node). Here is an example: - metric: "proxy.process.http.200_responses" value: 1 + # Optional runtime capability gates. + requires: + ats_features: [TS_HAS_CRIPTS] + plugins: [example.so] + # Traffic specification using Proxy Verifier format # client-request and server-response generate request and response traffic # toward the ATS proxy. @@ -292,7 +283,7 @@ YAML node). Here is an example: - [Content-Length, "4"] - [Cache-Control, "max-age=300"] - # Verify respone headers from ATS. + # Verify response headers from ATS. proxy-response: status: 200 headers: @@ -332,28 +323,26 @@ autest Configuration Section The ``autest`` section configures the test environment: -- **description** (required): A brief description of what this test scenario - validates. This is helpful to document the intetion of the test for the human - reading the file and is also helpful when tests fail as this description is included - in failure output. +- **description** or **summary** (one required): A brief description of what + this test scenario validates. This documents the intention for human readers + and is included in test reports and failure output. - **dns** (optional): DNS server configuration with ``name`` and optional - ``process_config``. See the ``microDNS.test.ext`` file for more details. Including - the DNS allows remap entries to contain hostnames rather than localhost IP - addresses. + records. Including DNS allows remap entries to contain hostnames rather than + localhost IP addresses. - **server** (required): Proxy Verifier Server configuration with ``name`` and - optional ``process_config``. See the ``verifier_server.test.ext`` file for - more details. This acts as the HTTP origin server that receives requests from - |TS| as they are proxied from the client to the server. It also provides any - request verification and generates the configured HTTP response. + optional ``process_config``. This acts as the HTTP origin server that receives + requests from |TS|, verifies them, and generates configured responses. - **client** (required): Proxy Verifier Client configuration with ``name`` and - optional ``process_config``. See the ``verifier_client.test.ext`` file for - more details. This acts as the HTTP client that requests content from the - server via the |TS| proxy and validates the response. + optional ``process_config`` and ``timeout``. This requests content through + |TS| and validates the response. +- **requires** (optional): Runtime gates such as ATS build features, installed + plugins, or minimum OpenSSL and Proxy Verifier versions. An unmet requirement + skips the item. - **ats** (required): |TS| configuration including: - **name**: ATS process name - - **enable_tls**: Set to ``true`` for HTTPS testing - - **process_config**: Parameters passed to ``MakeATSProcess`` (e.g., ``enable_cache``) + - **process_config**: ATS lifecycle options such as ``enable_cache``, + ``enable_tls``, ``enable_quic``, and ``enable_cripts`` - **records_config**: Dictionary of records.config settings - **remap_config**: List of remap rules (string or dict format) - **cache_config**: List of cache.config rules @@ -387,8 +376,8 @@ Metric Verification The ``metric_checks`` section allows you to verify |TS| metric values after all traffic in the test has completed. Each entry specifies a metric name and its -expected value. After the traffic test run, ``ATSReplayTest`` automatically adds -follow-up test runs that use ``traffic_ctl metric get`` to verify each metric. +expected value. After traffic completes, the pytest replay runner uses +``traffic_ctl metric get`` while ATS remains running to verify each metric. .. code-block:: yaml @@ -397,6 +386,7 @@ follow-up test runs that use ``traffic_ctl metric get`` to verify each metric. value: 1 - metric: "proxy.process.http.200_responses" value: 2 + delay: 5 Sessions and Transactions ~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index aa09cd1cf83..8d5694a8b72 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -60,10 +60,35 @@ configure_file(pyproject.toml pyproject.toml COPYONLY) configure_file(autest.sh.in autest.sh) configure_file(autest-parallel.py.in autest-parallel.py) +set(PYTEST_REPLAY_COMMAND + ${CMAKE_COMMAND} -E env PYTHONPATH=${CMAKE_CURRENT_SOURCE_DIR}:$ENV{PYTHONPATH} ${RUNUV} run pytest -p + ats_testkit.plugin ${CMAKE_CURRENT_SOURCE_DIR}/pytest_tests ${CMAKE_GOLD_DIR} --ats-bin=${CMAKE_INSTALL_PREFIX}/bin + --proxy-verifier-bin ${PROXY_VERIFIER_PATH} --build-root ${CMAKE_BINARY_DIR} --sandbox ${AUTEST_SANDBOX} + ${PYTEST_OPTIONS_LIST} +) + +add_custom_target( + pytest-replay + COMMAND ${CMAKE_COMMAND} --build ${CMAKE_BINARY_DIR} --target install + COMMAND ${RUNUV} sync + COMMAND ${PYTEST_REPLAY_COMMAND} + WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} + USES_TERMINAL +) + +add_custom_target( + pytest-replay-no-install + COMMAND ${RUNUV} sync + COMMAND ${PYTEST_REPLAY_COMMAND} + WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} + USES_TERMINAL +) + add_custom_target( autest COMMAND ${CMAKE_COMMAND} --build ${CMAKE_BINARY_DIR} --target install COMMAND ${RUNUV} sync + COMMAND ${PYTEST_REPLAY_COMMAND} COMMAND ${CMAKE_COMMAND} -E env PYTHONPATH=${CMAKE_GOLD_DIR}/remap:$ENV{PYTHONPATH} ${RUNUV} run autest --directory ${CMAKE_GOLD_DIR} --ats-bin=${CMAKE_INSTALL_PREFIX}/bin --proxy-verifier-bin ${PROXY_VERIFIER_PATH} --build-root ${CMAKE_BINARY_DIR} --sandbox ${AUTEST_SANDBOX} ${CURL_UDS_FLAG} ${AUTEST_OPTIONS_LIST} @@ -74,6 +99,7 @@ add_custom_target( add_custom_target( autest_no_install COMMAND ${RUNUV} sync + COMMAND ${PYTEST_REPLAY_COMMAND} COMMAND ${CMAKE_COMMAND} -E env PYTHONPATH=${CMAKE_GOLD_DIR}/remap:$ENV{PYTHONPATH} ${RUNUV} run autest --directory ${CMAKE_GOLD_DIR} --ats-bin=${CMAKE_INSTALL_PREFIX}/bin --proxy-verifier-bin ${PROXY_VERIFIER_PATH} --build-root ${CMAKE_BINARY_DIR} --sandbox ${AUTEST_SANDBOX} ${CURL_UDS_FLAG} ${AUTEST_OPTIONS_LIST} diff --git a/tests/README.md b/tests/README.md index cbeda199083..875744b2ea8 100644 --- a/tests/README.md +++ b/tests/README.md @@ -6,7 +6,7 @@ This directory contains different tests for Apache Trafficserver. It is recommen ## Layout The current layout is: -**gold_tests/** - contains all the tests that run on the Reusable Gold Testing System (AuTest) +**gold_tests/** - contains pytest replay tests and bespoke tests that run on AuTest **tools/** - contains programs used to help with testing. @@ -15,27 +15,38 @@ The current layout is: # Basic setup -To enable autests, you need to set the ENABLE_AUTESTS cmake variable. You can add this to your build with: +To enable end-to-end tests, set the ENABLE_AUTEST CMake variable: $ cmake -B build -DENABLE_AUTEST=ON -This will turn on building the autest plugins and helper tools as well as create an `autest` target. +This builds the required plugins and helper tools and creates the end-to-end test targets. # Running tests -Running the tests can be done using the `autest` target +Run all pytest replays followed by the remaining bespoke AuTests with the +`autest` target: $ cmake --build build -t autest -This will build ATS, install it to a temporary directory, setup the virtual environment via uv, and run all of the autests. +This builds and installs ATS, sets up the virtual environment via uv, and runs +both suites. To run only replay tests, use: -To run autest again, or to run individual tests, the cmake build generates a helper script in the build directory at -`/tests/autest.sh`. This script can be used to run individual tests and further configure autest. + $ cmake --build build -t pytest-replay + +Replay tests are Proxy Verifier files named `.test.yaml`. Pytest +collects each file directly, so no companion `.test.py` registration is needed. +Configure `PYTEST_OPTIONS` to pass selection or concurrency arguments: + + $ cmake -B build -DENABLE_AUTEST=ON -DPYTEST_OPTIONS="-n 4 -k cache" + +To run an individual bespoke AuTest, the CMake build generates a helper script +at `/tests/autest.sh`. This script can run individual tests and further +configure AuTest. To run a single test, you can use the `--filter` flag to name -which test to run. The tests are in files whose names are the test name -, and are suffixed with `.test.py`. Thus, the `something_descriptive` -test will be specified in a file named `something_descriptive.test.py`. +which test to run. Bespoke AuTest files are suffixed with `.test.py`. Thus, the +`something_descriptive` test is specified in a file named +`something_descriptive.test.py`. The corresponding `autest.sh` command is: $ ./autest.sh --filter=something_descriptive diff --git a/tests/ats_testkit/config.py b/tests/ats_testkit/config.py new file mode 100644 index 00000000000..19fef0e25f3 --- /dev/null +++ b/tests/ats_testkit/config.py @@ -0,0 +1,120 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. +"""Replay test configuration loading and serialization.""" + +from __future__ import annotations + +from collections.abc import Mapping +from dataclasses import dataclass +from pathlib import Path +from typing import Any +import copy +import re + +import yaml + + +class ReplayConfigError(ValueError): + """Report invalid pytest metadata in a replay file.""" + + +@dataclass(frozen=True) +class ReplaySpec: + """A Proxy Verifier replay file plus its ATS test metadata.""" + + path: Path + document: dict[str, Any] + autest: dict[str, Any] + + @property + def description(self) -> str: + """Return the human-readable test description.""" + + key = "description" if "description" in self.autest else "summary" + return str(self.autest[key]) + + @classmethod + def load(cls, path: Path) -> "ReplaySpec": + """Load and minimally validate @a path without starting test programs.""" + + try: + document = yaml.safe_load(path.read_text()) + except (OSError, yaml.YAMLError) as error: + raise ReplayConfigError(f"Could not load {path}: {error}") from error + if not isinstance(document, dict): + raise ReplayConfigError(f"{path} must contain a YAML mapping") + autest = document.get("autest") + if not isinstance(autest, dict): + raise ReplayConfigError(f"{path} must contain an 'autest' mapping") + if "description" not in autest and "summary" not in autest: + raise ReplayConfigError(f"{path} is missing 'autest.description' or 'autest.summary'") + for required in ("server", "client", "ats"): + if required not in autest: + raise ReplayConfigError(f"{path} is missing 'autest.{required}'") + for process in ("server", "client", "ats"): + if not isinstance(autest[process], dict): + raise ReplayConfigError(f"{path}: 'autest.{process}' must be a mapping") + return cls(path=path, document=document, autest=autest) + + +def merge_flat_records(records: Mapping[str, Any], defaults: Mapping[str, Any] | None = None) -> dict[str, Any]: + """Convert legacy dotted record names to a nested records.yaml mapping.""" + + result: dict[str, Any] = copy.deepcopy(dict(defaults or {})) + for original_name, value in records.items(): + name = original_name + for prefix in ("proxy.config.", "local.config."): + if name.startswith(prefix): + name = name[len(prefix):] + break + cursor = result + parts = name.split(".") + for part in parts[:-1]: + child = cursor.setdefault(part, {}) + if not isinstance(child, dict): + raise ReplayConfigError(f"Record {original_name} conflicts with an existing scalar") + cursor = child + cursor[parts[-1]] = value + return {"records": result} + + +def write_yaml(path: Path, document: Any) -> None: + """Write one ATS YAML configuration document.""" + + path.write_text(yaml.safe_dump(document, sort_keys=False)) + + +def format_plugin_entry(entry: str | Mapping[str, Any]) -> str: + """Render one plugin.config entry.""" + + if isinstance(entry, str): + return entry + if isinstance(entry, Mapping): + return " ".join([str(entry["name"]), *(str(argument) for argument in entry.get("args", []))]) + raise ReplayConfigError(f"Unsupported plugin_config entry: {entry!r}") + + +def replace_server_ports(value: str, http_port: int, https_port: int) -> str: + """Replace server port placeholders in an ATS configuration value.""" + + return value.replace("{SERVER_HTTP_PORT}", str(http_port)).replace("{SERVER_HTTPS_PORT}", str(https_port)) + + +def version_tuple(value: str) -> tuple[int, ...]: + """Return the numeric portion of a program version for minimum checks.""" + + match = re.search(r"\d+(?:\.\d+)+", value) + return tuple(int(part) for part in match.group(0).split(".")) if match else () diff --git a/tests/ats_testkit/plugin.py b/tests/ats_testkit/plugin.py new file mode 100644 index 00000000000..ead77737682 --- /dev/null +++ b/tests/ats_testkit/plugin.py @@ -0,0 +1,154 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. +"""Collect ``*.test.yaml`` Proxy Verifier files as pytest items.""" + +from __future__ import annotations + +from collections.abc import Callable, Iterator +from pathlib import Path +from typing import Any +import os + +import pytest + +from .config import ReplayConfigError, ReplaySpec +from .process import ProcessError +from .replay import ReplaySkip, ReplayTest +from .runtime import TestRuntime + + +def pytest_addoption(parser: pytest.Parser) -> None: + """Register ATS end-to-end runtime paths.""" + + group = parser.getgroup("ATS replay tests") + group.addoption("--ats-bin", help="Directory containing installed ATS executables") + group.addoption("--proxy-verifier-bin", help="Directory containing verifier-client and verifier-server") + group.addoption("--build-root", help="ATS build directory containing test plugins") + group.addoption("--sandbox", help="Directory for isolated test process trees") + + +def pytest_configure(config: pytest.Config) -> None: + """Register the replay marker used for selection.""" + + config.addinivalue_line("markers", "ats_replay: ATS system test driven by a Proxy Verifier replay file") + + +def pytest_ignore_collect(collection_path: Path, config: pytest.Config) -> bool | None: + """Leave legacy ``*.test.py`` files exclusively to AuTest.""" + + del config + return True if collection_path.name.endswith(".test.py") else None + + +def pytest_collect_file(file_path: Path, parent: pytest.Collector) -> pytest.File | None: + """Collect replay files whose basename ends in ``.test.yaml`` or ``.test.yml``.""" + + if file_path.name.endswith((".test.yaml", ".test.yml")): + return ReplayFile.from_parent(parent, path=file_path) + return None + + +class ReplayFile(pytest.File): + """Represent one directly collected replay file.""" + + def collect(self) -> Iterator[pytest.Item]: + """Load collection metadata and yield one test item.""" + + try: + spec = ReplaySpec.load(Path(self.path)) + except ReplayConfigError as error: + raise self.CollectError(str(error)) from error + suffix = ".test.yaml" if spec.path.name.endswith(".test.yaml") else ".test.yml" + item = ReplayItem.from_parent(self, name=spec.path.name.removesuffix(suffix), spec=spec) + item.add_marker("ats_replay") + yield item + + +class ReplayItem(pytest.Item): + """Execute one replay file as a pytest test item.""" + + def __init__(self, *, spec: ReplaySpec, **kwargs: Any) -> None: + super().__init__(**kwargs) + self.spec = spec + + def runtest(self) -> None: + """Run this item's replay lifecycle.""" + + runtime = get_runtime(self.config) + try: + ReplayTest(self.spec, runtime, self.nodeid).run() + except ReplaySkip as reason: + pytest.skip(str(reason)) + + def repr_failure(self, excinfo: pytest.ExceptionInfo[BaseException], style: str | None = None) -> str: + """Present system-test failures without pytest's internal collector frames.""" + + if isinstance(excinfo.value, (AssertionError, ProcessError, ReplayConfigError)): + return str(excinfo.value) + return super().repr_failure(excinfo, style=style) + + def reportinfo(self) -> tuple[Path, int, str]: + """Identify the replay file in pytest reports.""" + + return self.spec.path, 0, self.spec.description + + +def get_runtime(config: pytest.Config) -> TestRuntime: + """Build and cache the runtime selected by pytest command-line options.""" + + cache_name = "_ats_test_runtime" + cached = getattr(config, cache_name, None) + if cached is not None: + return cached + + repository_root = Path(__file__).resolve().parents[2] + values = { + "ats_bin": config.getoption("ats_bin") or os.environ.get("ATS_BIN"), + "verifier_bin": config.getoption("proxy_verifier_bin") or os.environ.get("PROXY_VERIFIER_BIN"), + "build_root": config.getoption("build_root") or os.environ.get("ATS_BUILD_ROOT"), + "sandbox": config.getoption("sandbox") or os.environ.get("ATS_TEST_SANDBOX"), + } + missing = [name for name, value in values.items() if not value] + if missing: + raise pytest.UsageError("Replay tests require " + ", ".join(f"--{name.replace('_', '-')}" for name in missing)) + worker = os.environ.get("PYTEST_XDIST_WORKER", "main") + runtime = TestRuntime.create( + repository_root=repository_root, + build_root=Path(values["build_root"]), + ats_bin=Path(values["ats_bin"]), + verifier_bin=Path(values["verifier_bin"]), + sandbox_root=Path(values["sandbox"]) / worker, + ) + setattr(config, cache_name, runtime) + return runtime + + +@pytest.fixture(scope="session") +def ats_test_runtime(pytestconfig: pytest.Config) -> TestRuntime: + """Provide installed ATS paths and capabilities to handwritten tests.""" + + return get_runtime(pytestconfig) + + +@pytest.fixture +def ats_replay(ats_test_runtime: TestRuntime, request: pytest.FixtureRequest) -> Callable[[Path], None]: + """Return a helper that executes a replay file from a handwritten pytest test.""" + + def run(path: Path) -> None: + spec = ReplaySpec.load(path) + ReplayTest(spec, ats_test_runtime, request.node.nodeid).run() + + return run diff --git a/tests/ats_testkit/process.py b/tests/ats_testkit/process.py new file mode 100644 index 00000000000..567c4b0754d --- /dev/null +++ b/tests/ats_testkit/process.py @@ -0,0 +1,146 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. +"""Process lifecycle helpers for ATS end-to-end tests.""" + +from __future__ import annotations + +from collections.abc import Callable, Iterable, Sequence +from pathlib import Path +import os +import signal +import subprocess +import time + + +class ProcessError(RuntimeError): + """Report an unexpected process exit or readiness failure.""" + + +class ManagedProcess: + """Own a subprocess and its captured output files.""" + + def __init__( + self, + name: str, + command: Sequence[str], + run_directory: Path, + environment: dict[str, str] | None = None, + expected_return_codes: Iterable[int] = (0,), + ) -> None: + self.name = name + self.command = [str(argument) for argument in command] + self.run_directory = run_directory + self.environment = environment + self.expected_return_codes = set(expected_return_codes) + self.stdout_path = run_directory / f"{name}.stdout" + self.stderr_path = run_directory / f"{name}.stderr" + self._stdout = None + self._stderr = None + self._process: subprocess.Popen[bytes] | None = None + + @property + def return_code(self) -> int | None: + """Return the process exit status, or None while it is running.""" + + return None if self._process is None else self._process.poll() + + def start(self) -> None: + """Start the process in a new process group.""" + + self.run_directory.mkdir(parents=True, exist_ok=True) + self._stdout = self.stdout_path.open("wb") + self._stderr = self.stderr_path.open("wb") + self._process = subprocess.Popen( + self.command, + cwd=self.run_directory, + env=self.environment, + stdout=self._stdout, + stderr=self._stderr, + start_new_session=True, + ) + + def wait_until(self, ready: Callable[[], bool], timeout: float, description: str) -> None: + """Wait until @a ready succeeds while ensuring the process remains alive.""" + + deadline = time.monotonic() + timeout + while time.monotonic() < deadline: + return_code = self.return_code + if return_code is not None: + self._close_streams() + raise ProcessError( + f"{self.name} exited with status {return_code} while waiting for {description}.\n{self.output()}") + if ready(): + return + time.sleep(0.05) + + raise ProcessError(f"Timed out after {timeout:g}s waiting for {self.name}: {description}.\n{self.output()}") + + def wait(self, timeout: float) -> None: + """Wait for a one-shot process and validate its return code.""" + + if self._process is None: + raise ProcessError(f"{self.name} has not been started") + try: + return_code = self._process.wait(timeout=timeout) + except subprocess.TimeoutExpired as error: + self.stop() + raise ProcessError(f"{self.name} timed out after {timeout:g}s.\n{self.output()}") from error + finally: + self._close_streams() + + if return_code not in self.expected_return_codes: + raise ProcessError( + f"{self.name} exited with status {return_code}; expected " + f"{sorted(self.expected_return_codes)}.\n{self.output()}") + + def stop(self, timeout: float = 5.0) -> None: + """Stop the process group, escalating to SIGKILL if necessary.""" + + if self._process is None: + self._close_streams() + return + + return_code = self._process.poll() + if return_code is None: + try: + os.killpg(self._process.pid, signal.SIGTERM) + self._process.wait(timeout=timeout) + except ProcessLookupError: + pass + except subprocess.TimeoutExpired: + os.killpg(self._process.pid, signal.SIGKILL) + self._process.wait(timeout=timeout) + self._close_streams() + + def output(self) -> str: + """Return the captured stdout and stderr for diagnostics.""" + + self._flush_streams() + sections = [] + for label, path in (("stdout", self.stdout_path), ("stderr", self.stderr_path)): + if path.exists(): + sections.append(f"--- {self.name} {label} ---\n{path.read_text(errors='replace')}") + return "\n".join(sections) + + def _flush_streams(self) -> None: + for stream in (self._stdout, self._stderr): + if stream is not None and not stream.closed: + stream.flush() + + def _close_streams(self) -> None: + for stream in (self._stdout, self._stderr): + if stream is not None and not stream.closed: + stream.close() diff --git a/tests/ats_testkit/replay.py b/tests/ats_testkit/replay.py new file mode 100644 index 00000000000..b32a7ab5cb3 --- /dev/null +++ b/tests/ats_testkit/replay.py @@ -0,0 +1,540 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. +"""Execute Proxy Verifier replay files as isolated ATS system tests.""" + +from __future__ import annotations + +from collections.abc import Iterable, Mapping +from pathlib import Path +from typing import Any +import difflib +import grp +import json +import os +import pwd +import re +import shlex +import shutil +import socket +import subprocess +import tempfile +import time + +from dnslib import DNSRecord + +from .config import ReplayConfigError, ReplaySpec, format_plugin_entry, merge_flat_records, replace_server_ports, write_yaml +from .process import ManagedProcess +from .runtime import TestRuntime + + +class ReplaySkip(RuntimeError): + """Signal that runtime capabilities do not satisfy replay requirements.""" + + +class ReplayTest: + """Run one replay file as one independently reportable test item.""" + + def __init__(self, spec: ReplaySpec, runtime: TestRuntime, node_name: str | None = None) -> None: + self.spec = spec + self.runtime = runtime + self.node_name = node_name or spec.path.stem + self.sandbox = runtime.item_sandbox(spec.path, self.node_name) + self.processes: list[ManagedProcess] = [] + self._temporary_directories: list[Path] = [] + self.http_port = runtime.allocate_port() + self.https_port = runtime.allocate_port() + self.server_http_port = runtime.allocate_port() + self.server_https_port = runtime.allocate_port() + self.manager_port = runtime.allocate_port() + self.admin_port = runtime.allocate_port() + self.dns_port: int | None = None + self.ats_environment: dict[str, str] | None = None + self.ats_paths: dict[str, Path] = {} + + @property + def test_directory(self) -> Path: + """Return the legacy test directory associated with this replay.""" + + parent = self.spec.path.parent + return parent.parent if parent.name in ("replay", "replays") else parent + + def run(self) -> None: + """Execute the replay and validate all declared outputs.""" + + reason = self.runtime.requirement_failure(self.spec.autest.get("requires", {})) + if reason: + raise ReplaySkip(reason) + self.runtime.prepare_sandbox(self.sandbox) + + server = None + ats = None + client = None + try: + self._start_dns() + server = self._start_verifier_server() + ats = self._start_ats() + client = self._run_verifier_client() + self._validate_background_processes(server, ats) + self._check_metrics() + finally: + for process in reversed(self.processes): + process.stop() + for directory in self._temporary_directories: + shutil.rmtree(directory, ignore_errors=True) + + if client is not None: + self._validate_process_output(client, self.spec.autest["client"], "Violation|Invalid status") + if server is not None: + self._validate_process_output(server, self.spec.autest["server"], "Violation") + if ats is not None: + self._validate_ats_logs() + + def _start_dns(self) -> ManagedProcess | None: + config = self.spec.autest.get("dns") + if config is None: + return None + self.dns_port = self.runtime.allocate_port(socket.SOCK_DGRAM) + name = str(config.get("name", "dns")) + directory = self.sandbox / name + directory.mkdir() + default = config.get("process_config", {}).get("default", "127.0.0.1") + otherwise = [default] if isinstance(default, str) else default + mappings = [] + for hostname, addresses in config.get("records", {}).items(): + mappings.append({hostname if hostname.endswith(".") else hostname + ".": addresses}) + zone_file = directory / "dns_file.json" + zone_file.write_text(json.dumps({"mappings": mappings, "otherwise": otherwise})) + process = ManagedProcess(name, ["microdns", "INADDR_LOOPBACK", str(self.dns_port), str(zone_file)], directory) + process.start() + self.processes.append(process) + + def responds() -> bool: + try: + query = DNSRecord.question("pytest-readiness.invalid") + query.send("127.0.0.1", self.dns_port or 0, timeout=0.1) + return True + except (OSError, socket.timeout): + return False + + process.wait_until(responds, 10, f"DNS replies on 127.0.0.1:{self.dns_port}") + return process + + def _start_verifier_server(self) -> ManagedProcess: + config = self.spec.autest["server"] + process_config = dict(config.get("process_config", {})) + name = str(config.get("name", "server")) + directory = self.sandbox / name + directory.mkdir() + ssl_dir = self.runtime.test_tools / "proxy-verifier" / "ssl" + command = [ + self.runtime.verifier_bin / "verifier-server", + "run", + "--listen-http", + f"127.0.0.1:{self.server_http_port}", + "--listen-https", + f"127.0.0.1:{self.server_https_port}", + "--server-cert", + ssl_dir / "server.pem", + "--ca-certs", + ssl_dir / "ca.pem", + "--tls-secrets-log-file", + directory / "tls_secrets.txt", + self.spec.path, + ] + if process_config.get("verbose", True): + command.extend(["--verbose", "diag"]) + command.extend(shlex.split(str(process_config.get("other_args", "")))) + process = ManagedProcess( + name, + command, + directory, + expected_return_codes=self._return_codes(config), + ) + process.start() + self.processes.append(process) + process.wait_until( + lambda: self._tcp_open(self.server_http_port), + 10, + f"HTTP listener on 127.0.0.1:{self.server_http_port}", + ) + return process + + def _start_ats(self) -> ManagedProcess: + config = self.spec.autest["ats"] + process_config = dict(config.get("process_config", {})) + name = str(config.get("name", "ts")) + ts_root = self.sandbox / name + paths = self._prepare_ats_tree(ts_root) + self.ats_paths = paths + enable_tls = bool(process_config.get("enable_tls", False)) + enable_quic = bool(process_config.get("enable_quic", False)) + enable_cache = bool(process_config.get("enable_cache", config.get("enable_cache", True))) + enable_cripts = bool(process_config.get("enable_cripts", False)) + + records: dict[str, Any] = { + "config_update_interval_ms": 20, + "http": { + "server_ports": self._server_ports(enable_tls, enable_quic), + "wait_for_cache": 1, + }, + } + if not enable_cache: + records["http"]["cache"] = {"http": 0} + records["http"].pop("wait_for_cache") + if enable_quic: + records.setdefault("udp", {})["threads"] = 1 + if self.dns_port is not None: + records["dns"] = {"nameservers": f"127.0.0.1:{self.dns_port}", "resolv_conf": "NULL"} + + if enable_tls: + self._configure_tls(config, records, paths) + if enable_cripts: + compiler = paths["bin"] / "cripts_compiler.sh" + shutil.copy2(self.runtime.repository_root / "tools" / "cripts" / "compiler.sh", compiler) + compiler.chmod(0o755) + records.setdefault("plugin", {})["compiler_path"] = str(compiler) + + configured_records = config.get("records_config", {}) + write_yaml(paths["config"] / "records.yaml", merge_flat_records(configured_records, records)) + self._write_ats_configs(config, paths) + + environment = os.environ.copy() + environment.update( + { + "TS_ROOT": str(ts_root), + "TS_RUNROOT": str(paths["config"] / "runroot.yaml"), + "PROXY_CONFIG_BIN_PATH": str(paths["bin"]), + "PROXY_CONFIG_CONFIG_DIR": str(paths["config"]), + "PROXY_CONFIG_BODY_FACTORY_TEMPLATE_SETS_DIR": str(paths["body_factory"]), + "PROXY_CONFIG_CACHE_DIR": str(paths["cache"]), + "PROXY_CONFIG_PLUGIN_PLUGIN_DIR": str(paths["plugin"]), + "PROXY_CONFIG_LOG_LOGFILE_DIR": str(paths["log"]), + "PROXY_CONFIG_LOCAL_STATE_DIR": str(paths["rpc_runtime"]), + "PROXY_CONFIG_SSL_DIR": str(paths["ssl"]), + "PROXY_CONFIG_STORAGE_DIR": str(paths["storage"]), + "PROXY_CONFIG_PROCESS_MANAGER_MGMT_PORT": str(self.manager_port), + "PROXY_CONFIG_ADMIN_SYNTHETIC_PORT": str(self.admin_port), + "PROXY_CONFIG_ADMIN_AUTOCONF_PORT": str(self.admin_port), + "PATH": str(paths["bin"]) + os.pathsep + os.environ.get("PATH", ""), + }) + if enable_cripts: + environment["ATS_ROOT"] = self.runtime.layout["PREFIX"] + self.ats_environment = environment + traffic_out = paths["log"] / "traffic.out" + process = ManagedProcess( + name, + [paths["bin"] / "traffic_server", "--bind_stdout", traffic_out, "--bind_stderr", traffic_out], + ts_root, + environment=environment, + ) + self._chown_for_ats(ts_root) + process.start() + self.processes.append(process) + startup_timeout = float(config.get("startup_timeout", 60 if enable_cripts else 10)) + diags_log = paths["log"] / "diags.log" + process.wait_until( + lambda: diags_log.exists() and "NOTE: Traffic Server is fully initialized" in diags_log.read_text(errors="replace"), + startup_timeout, + "the fully initialized log message", + ) + return process + + def _run_verifier_client(self) -> ManagedProcess: + config = self.spec.autest["client"] + process_config = dict(config.get("process_config", {})) + name = str(config.get("name", "client")) + directory = self.sandbox / name + directory.mkdir() + ats_process_config = self.spec.autest["ats"].get("process_config", {}) + enable_tls = bool(ats_process_config.get("enable_tls", False)) + enable_quic = bool(ats_process_config.get("enable_quic", False)) + ssl_dir = self.runtime.test_tools / "proxy-verifier" / "ssl" + http_ports = process_config.pop("http_ports", [self.http_port]) + https_ports = process_config.pop("https_ports", [self.https_port] if enable_tls else []) + http3_ports = process_config.pop("http3_ports", [self.https_port] if enable_quic else []) + command: list[str | Path] = [self.runtime.verifier_bin / "verifier-client", "run", self.spec.path] + if http_ports: + command.extend(["--connect-http", self._address_argument(http_ports)]) + if https_ports: + command.extend(["--connect-https", self._address_argument(https_ports)]) + if http3_ports: + qlog = directory / "qlog_directory" + qlog.mkdir() + command.extend(["--connect-http3", self._address_argument(http3_ports), "--qlog-dir", qlog]) + if https_ports or http3_ports: + command.extend( + [ + "--client-cert", + ssl_dir / "client.pem", + "--ca-certs", + ssl_dir / "ca.pem", + "--tls-secrets-log-file", + directory / "tls_secrets.txt", + ]) + if process_config.get("verbose", True): + command.extend(["--verbose", "diag"]) + other_args = str(process_config.get("other_args", "")) + command.extend(shlex.split(other_args)) + poll_timeout = process_config.get("poll_timeout") + if poll_timeout is not None: + command.extend(["--poll-timeout", str(poll_timeout)]) + if "thread-limit" not in other_args and not process_config.get("run_parallel", False): + command.extend(["--thread-limit", "1"]) + process = ManagedProcess( + name, + command, + directory, + expected_return_codes=self._return_codes(config), + ) + process.start() + self.processes.append(process) + process.wait(timeout=float(config.get("timeout", 60))) + return process + + def _prepare_ats_tree(self, root: Path) -> dict[str, Path]: + names = ("bin", "config", "body_factory", "plugin", "log", "runtime", "ssl", "storage", "cache") + paths = {name: root / name for name in names} + for path in paths.values(): + path.mkdir(parents=True, exist_ok=True) + rpc_runtime = Path(tempfile.mkdtemp(prefix="ats-pytest-", dir="/tmp")) + self._temporary_directories.append(rpc_runtime) + paths["rpc_runtime"] = rpc_runtime + self._link_directory(Path(self.runtime.layout["BINDIR"]), paths["bin"]) + self._link_directory(Path(self.runtime.layout["PLUGINDIR"]), paths["plugin"]) + body_factory = Path(self.runtime.layout["SYSCONFDIR"]) / "body_factory" + if body_factory.is_dir(): + shutil.copytree(body_factory, paths["body_factory"], dirs_exist_ok=True) + min_config = self.runtime.repository_root / "tests" / "gold_tests" / "autest-site" / "min_cfg" + for source in min_config.iterdir(): + if source.is_file() and source.name != "readme.txt": + shutil.copy2(source, paths["config"] / source.name) + write_yaml( + paths["config"] / "runroot.yaml", + { + # Keep the JSON-RPC Unix socket below sockaddr_un.sun_path's + # small platform limit, even under a deeply nested CI sandbox. + "runtimedir": str(paths["rpc_runtime"]), + "cachedir": str(paths["cache"]), + "localstatedir": str(paths["runtime"]), + "bindir": str(paths["bin"]), + "prefix": str(root), + "logdir": str(paths["log"]), + "sysconfdir": str(paths["config"]), + }, + ) + self._chown_for_ats(paths["rpc_runtime"]) + return paths + + def _configure_tls(self, config: Mapping[str, Any], records: dict[str, Any], paths: dict[str, Path]) -> None: + ssl_multicert = config.get("ssl_multicert_yaml") + if ssl_multicert is None: + for filename in ("server.pem", "server.key"): + shutil.copy2(self.runtime.test_tools / "ssl" / filename, paths["ssl"] / filename) + ssl_multicert = [ + "ssl_multicert:", + " - ssl_cert_name: server.pem", + " ssl_key_name: server.key", + ' dest_ip: "*"', + ] + records.setdefault("ssl", {}).setdefault("server", {})["cert"] = {"path": str(paths["ssl"])} + records["ssl"]["server"]["private_key"] = {"path": str(paths["ssl"])} + (paths["config"] / "ssl_multicert.yaml").write_text("\n".join(ssl_multicert) + "\n") + if "sni_yaml" in config: + write_yaml(paths["config"] / "sni.yaml", config["sni_yaml"]) + + def _write_ats_configs(self, config: Mapping[str, Any], paths: dict[str, Path]) -> None: + config_dir = paths["config"] + plugin_lines = [format_plugin_entry(entry) for entry in config.get("plugin_config", [])] + (config_dir / "plugin.config").write_text("\n".join(plugin_lines) + ("\n" if plugin_lines else "")) + for plugin_value in config.get("copy_custom_plugin", []): + source = self.runtime.resolve_artifact(self.test_directory, str(plugin_value)) + if not source.is_file(): + raise ReplayConfigError(f"Custom plugin does not exist: {source}") + destination = paths["plugin"] / source.name + if destination.exists() or destination.is_symlink(): + destination.unlink() + shutil.copy2(source, destination) + + replacements = (self.server_http_port, self.server_https_port) + for key, filename in (("parent_config", "parent.config"), ("cache_config", "cache.config")): + lines = [replace_server_ports(str(line), *replacements) for line in config.get(key, [])] + (config_dir / filename).write_text("\n".join(lines) + ("\n" if lines else "")) + if "logging_yaml" in config: + write_yaml(config_dir / "logging.yaml", config["logging_yaml"]) + if "storage_yaml" in config: + write_yaml(config_dir / "storage.yaml", config["storage_yaml"]) + + remap_lines = [] + for entry in config.get("remap_config", []): + if isinstance(entry, str): + remap_lines.append(replace_server_ports(entry, *replacements)) + continue + line = f"map {entry['from']} {replace_server_ports(str(entry['to']), *replacements)}" + for plugin in entry.get("plugins", []): + line += f" @plugin={plugin['name']}" + for argument in plugin.get("args", []): + line += f" @pparam={argument}" + for option in entry.get("options", []): + line += f" {option}" + remap_lines.append(line) + (config_dir / "remap.config").write_text("\n".join(remap_lines) + ("\n" if remap_lines else "")) + + for value in config.get("copy_to_config_dir", []): + source = (self.test_directory / value).resolve() + destination = config_dir / value + if source.is_dir(): + shutil.copytree(source, destination, dirs_exist_ok=True) + else: + destination.parent.mkdir(parents=True, exist_ok=True) + shutil.copy2(source, destination) + + def _check_metrics(self) -> None: + checks = self.spec.autest["ats"].get("metric_checks", []) + for check in checks: + time.sleep(float(check.get("delay", 2))) + metric = str(check["metric"]) + command = [self.ats_paths["bin"] / "traffic_ctl", "metric", "get", metric] + result = subprocess.run(command, env=self.ats_environment, capture_output=True, text=True, timeout=10) + if result.returncode != 0: + raise AssertionError(f"Could not read metric {metric}:\n{result.stdout}{result.stderr}") + fields = result.stdout.split() + if len(fields) < 2: + raise AssertionError(f"Metric {metric} was absent: {result.stdout!r}") + actual_text = fields[-1] + if "value" in check and re.fullmatch(str(check["value"]), actual_text) is None: + raise AssertionError(f"Metric {metric} was {actual_text}, expected {check['value']}") + if "min" in check and float(actual_text) < float(check["min"]): + raise AssertionError(f"Metric {metric} was {actual_text}, expected at least {check['min']}") + if "value" not in check and "min" not in check: + raise ReplayConfigError(f"metric_checks entry for {metric} must specify value or min") + + def _validate_background_processes(self, server: ManagedProcess, ats: ManagedProcess) -> None: + """Detect a server or unit-under-test exit before intentional teardown.""" + + if ats.return_code is not None: + raise AssertionError(f"ATS exited unexpectedly with status {ats.return_code}.\n{ats.output()}") + if server.return_code is not None and server.return_code not in server.expected_return_codes: + raise AssertionError( + f"Proxy Verifier server exited with status {server.return_code}; expected " + f"{sorted(server.expected_return_codes)}.\n{server.output()}") + + def _validate_process_output(self, process: ManagedProcess, config: Mapping[str, Any], default_excludes: str) -> None: + output = "" + for path in (process.stdout_path, process.stderr_path): + if path.exists(): + output += path.read_text(errors="replace") + if re.search(default_excludes, output): + raise AssertionError(f"Unexpected Proxy Verifier diagnostic in {process.name}:\n{output}") + self._validate_text(output, config.get("log_validation", {}), process.name) + + def _validate_ats_logs(self) -> None: + config = self.spec.autest["ats"] + if not config.get("process_config", {}).get("disable_log_checks", False): + diags = (self.ats_paths["log"] / "diags.log").read_text(errors="replace") + for expression in ("ERROR:", "FATAL:", "Unrecognized configuration value"): + if expression in diags: + raise AssertionError(f"Unexpected {expression!r} in diags.log:\n{diags}") + + validation = config.get("log_validation", {}) + path_by_key = { + "traffic_out": self.ats_paths["log"] / "traffic.out", + "diags_log": self.ats_paths["log"] / "diags.log", + "error_log": self.ats_paths["log"] / "error.log", + } + for key, path in path_by_key.items(): + rules = validation.get(key, {}) + if rules: + self._validate_text(path.read_text(errors="replace") if path.exists() else "", rules, key) + access = validation.get("access_log", {}) + if access: + self._validate_gold(self.ats_paths["log"] / access["filename"], self.test_directory / access["gold_file"]) + + def _validate_text(self, content: str, rules: Mapping[str, Any], label: str) -> None: + flags = re.MULTILINE + for entry in rules.get("contains", []): + entry_flags = flags | (re.DOTALL if entry.get("multiline", False) else 0) + if re.search(str(entry["expression"]), content, entry_flags) is None: + description = entry.get("description", f"{label} should contain {entry['expression']!r}") + raise AssertionError(f"{description}\n--- {label} ---\n{content}") + for entry in rules.get("excludes", []): + if re.search(str(entry["expression"]), content, flags) is not None: + description = entry.get("description", f"{label} should exclude {entry['expression']!r}") + raise AssertionError(f"{description}\n--- {label} ---\n{content}") + if "gold_file" in rules: + actual_path = self.sandbox / f"{label}.actual" + actual_path.write_text(content) + self._validate_gold(actual_path, self.test_directory / rules["gold_file"]) + + @staticmethod + def _validate_gold(actual_path: Path, expected_path: Path) -> None: + actual = (actual_path.read_text(errors="replace") if actual_path.exists() else "").replace("\r\n", "\n") + expected = expected_path.read_text(errors="replace").replace("\r\n", "\n") + pattern = "\\A" + ".*?".join(re.escape(part) for part in re.split(r"(?:\{\}|``)", expected)) + "\\Z" + if re.match(pattern, actual, re.DOTALL) is None: + difference = "".join( + difflib.unified_diff(expected.splitlines(True), actual.splitlines(True), str(expected_path), str(actual_path))) + raise AssertionError(f"Output did not match gold file:\n{difference}") + + def _server_ports(self, enable_tls: bool, enable_quic: bool) -> str: + value = str(self.http_port) + if enable_tls: + value += f" {self.https_port}:ssl" + if enable_quic: + value += f" {self.https_port}:quic" + return value + + @staticmethod + def _return_codes(config: Mapping[str, Any]) -> Iterable[int]: + codes = config.get("return_code", 0) + return codes if isinstance(codes, list) else [codes] + + @staticmethod + def _address_argument(ports: Iterable[int]) -> str: + return ",".join(f"127.0.0.1:{port}" for port in ports) + + @staticmethod + def _tcp_open(port: int) -> bool: + try: + with socket.create_connection(("127.0.0.1", port), timeout=0.1): + return True + except OSError: + return False + + @staticmethod + def _link_directory(source: Path, destination: Path) -> None: + for entry in source.iterdir(): + target = destination / entry.name + if not target.exists(): + target.symlink_to(entry, target_is_directory=entry.is_dir()) + + @staticmethod + def _chown_for_ats(root: Path) -> None: + if os.geteuid() != 0: + return + try: + uid = pwd.getpwnam("nobody").pw_uid + try: + gid = grp.getgrnam("nogroup").gr_gid + except KeyError: + gid = grp.getgrnam("nobody").gr_gid + except KeyError: + return + for directory, directories, files in os.walk(root): + os.chown(directory, uid, gid) + for name in [*directories, *files]: + path = Path(directory) / name + if not path.is_symlink(): + os.chown(path, uid, gid) diff --git a/tests/ats_testkit/runtime.py b/tests/ats_testkit/runtime.py new file mode 100644 index 00000000000..a11378464a1 --- /dev/null +++ b/tests/ats_testkit/runtime.py @@ -0,0 +1,214 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. +"""Installed ATS and test-program discovery.""" + +from __future__ import annotations + +from dataclasses import dataclass +from pathlib import Path +from typing import Any +import hashlib +import json +import os +import re +import shutil +import socket +import subprocess + +import fcntl + +from .config import version_tuple + + +class RuntimeError(ValueError): + """Report an invalid end-to-end test runtime.""" + + +@dataclass(frozen=True) +class TestRuntime: + """Paths and build features shared by replay test items.""" + + repository_root: Path + build_root: Path + ats_bin: Path + verifier_bin: Path + sandbox_root: Path + layout: dict[str, str] + features: dict[str, Any] + + @classmethod + def create( + cls, + repository_root: Path, + build_root: Path, + ats_bin: Path, + verifier_bin: Path, + sandbox_root: Path, + ) -> "TestRuntime": + """Validate paths and query the installed ATS layout.""" + + traffic_layout = ats_bin / "traffic_layout" + required = [ + traffic_layout, + ats_bin / "traffic_server", + ats_bin / "traffic_ctl", + verifier_bin / "verifier-client", + verifier_bin / "verifier-server", + ] + missing = [str(path) for path in required if not path.is_file()] + if missing: + raise RuntimeError("Missing required test programs: " + ", ".join(missing)) + + try: + layout = json.loads(subprocess.check_output([traffic_layout, "--json"], text=True)) + features = json.loads(subprocess.check_output([traffic_layout, "--features", "--json"], text=True)) + except (subprocess.CalledProcessError, json.JSONDecodeError) as error: + raise RuntimeError(f"Could not query {traffic_layout}: {error}") from error + + return cls( + repository_root=repository_root.resolve(), + build_root=build_root.resolve(), + ats_bin=ats_bin.resolve(), + verifier_bin=verifier_bin.resolve(), + sandbox_root=sandbox_root.resolve(), + layout={ + key: value.rstrip("/") for key, value in layout.items() + }, + features=features, + ) + + @property + def test_tools(self) -> Path: + """Return the source test-tools directory.""" + + return self.repository_root / "tests" / "tools" + + @property + def build_gold_tests(self) -> Path: + """Return the build-tree mirror of gold_tests.""" + + return self.build_root / "tests" / "gold_tests" + + @property + def test_plugins(self) -> Path: + """Return the directory containing test-only plugins.""" + + return self.build_root / "tests" / "tools" / "plugins" / ".libs" + + def allocate_port(self, socket_type: int = socket.SOCK_STREAM) -> int: + """Allocate a unique available port across all xdist workers.""" + + common_sandbox = self.sandbox_root.parent + common_sandbox.mkdir(parents=True, exist_ok=True) + state_path = common_sandbox / ".port-counter" + with state_path.open("a+") as state: + fcntl.flock(state, fcntl.LOCK_EX) + state.seek(0) + content = state.read().strip() + candidate = int(content) if content else 10000 + for _ in range(50000): + candidate = 10000 if candidate >= 60000 else candidate + 1 + with socket.socket(socket.AF_INET, socket_type) as probe: + try: + probe.bind(("127.0.0.1", candidate)) + except OSError: + continue + state.seek(0) + state.truncate() + state.write(str(candidate)) + state.flush() + fcntl.flock(state, fcntl.LOCK_UN) + return candidate + fcntl.flock(state, fcntl.LOCK_UN) + raise RuntimeError("Could not allocate a test port") + + def item_sandbox(self, replay_path: Path, node_name: str) -> Path: + """Return a deterministic, path-length-conscious sandbox for one item.""" + + try: + relative = replay_path.relative_to(self.repository_root / "tests" / "gold_tests") + except ValueError: + relative = replay_path.resolve() + identity = f"{relative}:{node_name}" + digest = hashlib.sha256(identity.encode()).hexdigest()[:10] + stem = re.sub(r"[^A-Za-z0-9_.-]+", "_", replay_path.stem)[:48] + return self.sandbox_root / f"{stem}-{digest}" + + def prepare_sandbox(self, path: Path) -> None: + """Create an empty item sandbox without allowing a broad deletion target.""" + + resolved = path.resolve() + if resolved.parent != self.sandbox_root or resolved == self.sandbox_root: + raise RuntimeError(f"Refusing to clean unsafe sandbox path: {resolved}") + if resolved.exists(): + shutil.rmtree(resolved) + resolved.mkdir(parents=True) + + def requirement_failure(self, requirements: dict[str, Any]) -> str | None: + """Return why a replay should be skipped, or None when it can run.""" + + for feature in requirements.get("ats_features", []): + if not self.features.get(feature): + return f"ATS was built without {feature}" + + plugin_dir = Path(self.layout["PLUGINDIR"]) + for plugin in requirements.get("plugins", []): + if not (plugin_dir / plugin).is_file(): + return f"ATS plugin is not installed: {plugin}" + + minimum_openssl = requirements.get("openssl") + if minimum_openssl: + output = subprocess.check_output(["openssl", "version"], text=True) + if version_tuple(output) < version_tuple(str(minimum_openssl)): + return f"OpenSSL {minimum_openssl} or newer is required" + + minimum_verifier = requirements.get("proxy_verifier") + if minimum_verifier: + output = subprocess.check_output([self.verifier_bin / "verifier-client", "--version"], text=True) + if version_tuple(output) < version_tuple(str(minimum_verifier)): + return f"Proxy Verifier {minimum_verifier} or newer is required" + + curl_requirements = requirements.get("curl_features", []) + if curl_requirements: + curl_output = subprocess.check_output(["curl", "--version"], text=True).lower() + for feature in curl_requirements: + if str(feature).lower() not in curl_output: + return f"curl does not provide {feature}" + return None + + def resolve_artifact(self, test_directory: Path, value: str) -> Path: + """Resolve a source or build artifact referenced by replay metadata.""" + + variables = { + "AtsTestPluginsDir": self.test_plugins, + "ATS_TEST_PLUGINS_DIR": self.test_plugins, + "AtsBuildGoldTestsDir": self.build_gold_tests, + "RepoDir": self.repository_root, + } + expanded = re.sub( + r"\{([A-Za-z_][A-Za-z0-9_]*)\}", + lambda match: str(variables.get(match.group(1), match.group(0))), + value, + ) + candidate = Path(os.path.expandvars(expanded)) + if candidate.is_absolute(): + return candidate + candidates = [test_directory / candidate] + gold_source = self.repository_root / "tests" / "gold_tests" + if test_directory.is_relative_to(gold_source): + candidates.append(self.build_gold_tests / test_directory.relative_to(gold_source) / candidate) + candidates.append(self.build_gold_tests / candidate) + return next((path for path in candidates if path.exists()), candidates[-1]) diff --git a/tests/gold_tests/at_headers/at_headers.test.py b/tests/gold_tests/at_headers/at_headers.test.py deleted file mode 100644 index e92c6038042..00000000000 --- a/tests/gold_tests/at_headers/at_headers.test.py +++ /dev/null @@ -1,21 +0,0 @@ -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -Test.Summary = ''' -Verify that @ headers are stripped before global plugin and remap hooks run. -''' - -Test.ATSReplayTest(replay_file="replay/at_headers.replay.yaml") diff --git a/tests/gold_tests/at_headers/replay/at_headers.replay.yaml b/tests/gold_tests/at_headers/replay/at_headers.test.yaml similarity index 100% rename from tests/gold_tests/at_headers/replay/at_headers.replay.yaml rename to tests/gold_tests/at_headers/replay/at_headers.test.yaml diff --git a/tests/gold_tests/cache/alternate-caching.test.py b/tests/gold_tests/cache/alternate-caching.test.py deleted file mode 100644 index 7f3b698c672..00000000000 --- a/tests/gold_tests/cache/alternate-caching.test.py +++ /dev/null @@ -1,31 +0,0 @@ -''' -Test the alternate caching features -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import os - -Test.Summary = ''' -Test the alternate caching feature. -''' - -# Verify cache alternate replacement behavior. -Test.ATSReplayTest(replay_file="replay/alternate-caching-update-size.yaml") - -# Verify TSHttpAltInfoQualitySet affects alternate selection. -tr = Test.ATSReplayTest(replay_file="replay/alternate-caching-quality.yaml") -Test.PrepareTestPlugin(os.path.join(Test.Variables.AtsTestPluginsDir, 'http_alt_info_quality.so'), tr.Processes.ts_alt_quality) diff --git a/tests/gold_tests/cache/cache-auth.test.py b/tests/gold_tests/cache/cache-auth.test.py deleted file mode 100644 index e60064c3a47..00000000000 --- a/tests/gold_tests/cache/cache-auth.test.py +++ /dev/null @@ -1,29 +0,0 @@ -''' -Test authorization-related caching behaviors -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -Test.Summary = ''' -Test authorization-related caching behaviors -''' - -# Verify proxy.config.http.cache.ignore_authentication behavior. -Test.ATSReplayTest(replay_file="replay/ignore_authentication.replay.yaml") - -# Verify that s-maxage allows serving cached responses to requests with -# Authorization headers per RFC 7234 section 3.2 -Test.ATSReplayTest(replay_file="replay/auth-s-maxage.replay.yaml") diff --git a/tests/gold_tests/cache/cache-control.test.py b/tests/gold_tests/cache/cache-control.test.py deleted file mode 100644 index 809798b8fb1..00000000000 --- a/tests/gold_tests/cache/cache-control.test.py +++ /dev/null @@ -1,46 +0,0 @@ -''' -Test cached responses and requests with bodies -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -Test.Summary = ''' -Test cached responses and cache-control directives -''' - -# Basic cache operations: miss, hit, no-cache-control, stale, and only-if-cached -Test.ATSReplayTest(replay_file="replay/cache-control-basic.replay.yaml") - -# Max-age directives in both clients and responses -Test.ATSReplayTest(replay_file="replay/cache-control-max-age.replay.yaml") - -# S-maxage directives in responses -Test.ATSReplayTest(replay_file="replay/cache-control-s-maxage.replay.yaml") - -# Interaction between cache-control no-cache and pragma header -Test.ATSReplayTest(replay_file="replay/cache-control-pragma.replay.yaml") - -# Request cache-control directives in default configuration -Test.ATSReplayTest(replay_file="replay/request-cache-control-default.replay.yaml") - -# Request cache-control directives when ATS honors client requests to bypass cache -Test.ATSReplayTest(replay_file="replay/request-cache-control-honor-client.replay.yaml") - -# Response cache-control directives in default configuration -Test.ATSReplayTest(replay_file="replay/response-cache-control-default.replay.yaml") - -# Response cache-control directives when ATS ignores server requests to bypass cache -Test.ATSReplayTest(replay_file="replay/response-cache-control-ignored.replay.yaml") diff --git a/tests/gold_tests/cache/cache-cookie.test.py b/tests/gold_tests/cache/cache-cookie.test.py deleted file mode 100644 index 64805406755..00000000000 --- a/tests/gold_tests/cache/cache-cookie.test.py +++ /dev/null @@ -1,37 +0,0 @@ -''' -Test cookie-related caching behaviors -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -Test.Summary = ''' -Test cookie-related caching behaviors -''' - -# Verify correct caching behavior in default configuration -Test.ATSReplayTest(replay_file="replay/cookie-default.replay.yaml") - -# Verify correct caching behavior when not caching responses to cookies -Test.ATSReplayTest(replay_file="replay/cookie-bypass-cache.replay.yaml") - -# Verify correct caching behavior when caching only image responses to cookies -Test.ATSReplayTest(replay_file="replay/cookie-cache-img-only.replay.yaml") - -# Verify correct caching behavior when caching all but text responses to cookies -Test.ATSReplayTest(replay_file="replay/cookie-all-but-text.replay.yaml") - -# Verify correct caching behavior for all but text with exceptions -Test.ATSReplayTest(replay_file="replay/cookie-all-but-text-with-excp.replay.yaml") diff --git a/tests/gold_tests/cache/cache-exact-url.test.py b/tests/gold_tests/cache/cache-exact-url.test.py deleted file mode 100644 index 42a7434016f..00000000000 --- a/tests/gold_tests/cache/cache-exact-url.test.py +++ /dev/null @@ -1,24 +0,0 @@ -''' -Test exact URL rules in cache.config. -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -Test.Summary = ''' -Verify that exact URL cache.config rules match only the configured URL. -''' - -Test.ATSReplayTest(replay_file="replay/cache-exact-url.replay.yaml") diff --git a/tests/gold_tests/cache/cache-heuristic-status.test.py b/tests/gold_tests/cache/cache-heuristic-status.test.py deleted file mode 100644 index e191be26345..00000000000 --- a/tests/gold_tests/cache/cache-heuristic-status.test.py +++ /dev/null @@ -1,28 +0,0 @@ -''' -Test heuristic caching of status codes per RFC 9110 Section 15.1. -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -Test.Summary = ''' -Test heuristic caching of status codes per RFC 9110 Section 15.1. - -Verifies that responses with heuristically cacheable status codes (200, 203, -204, 300, 301, 308, 410) are cached when only Last-Modified is present, and -that non-cacheable codes (302, 307, 400, 403) are not. -''' - -Test.ATSReplayTest(replay_file="replay/cache-heuristic-status.replay.yaml") diff --git a/tests/gold_tests/cache/cache-range-response.test.py b/tests/gold_tests/cache/cache-range-response.test.py deleted file mode 100644 index f82ee7940d1..00000000000 --- a/tests/gold_tests/cache/cache-range-response.test.py +++ /dev/null @@ -1,25 +0,0 @@ -''' -Verify that caching a range request when origin returns full response works. -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -Test.Summary = ''' -Verify correct caching behavior for range requests. -''' - -# Verify range request is transformed from a 200 response. -Test.ATSReplayTest(replay_file="replay/cache-range-response.replay.yaml") diff --git a/tests/gold_tests/cache/cache-read-retry-mode.test.py b/tests/gold_tests/cache/cache-read-retry-mode.test.py deleted file mode 100644 index 117a7b7bae8..00000000000 --- a/tests/gold_tests/cache/cache-read-retry-mode.test.py +++ /dev/null @@ -1,30 +0,0 @@ -''' -Test cache_open_write_fail_action = 5 (READ_RETRY mode) -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -Test.Summary = ''' -Test cache_open_write_fail_action = 5 (READ_RETRY mode) to verify: -1. Basic read-while-writer behavior with fail_action=5 -2. READ_RETRY mode configuration is accepted and functional -3. System does not crash under write lock contention -4. Requests are served correctly when read retries are exhausted -''' - -Test.ContinueOnFail = True - -Test.ATSReplayTest(replay_file="replay/cache-read-retry.replay.yaml") diff --git a/tests/gold_tests/cache/cache-read-retry-stale.test.py b/tests/gold_tests/cache/cache-read-retry-stale.test.py deleted file mode 100644 index 8ec31e2aa0c..00000000000 --- a/tests/gold_tests/cache/cache-read-retry-stale.test.py +++ /dev/null @@ -1,30 +0,0 @@ -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -Test.Summary = ''' -Smoke test for cache_open_write_fail_action = 6 (READ_RETRY_STALE_ON_REVALIDATE) to verify: -1. Action 6 is accepted by the configuration system -2. Basic caching works correctly with action 6 enabled -3. The system does not crash - -Note: The stale fallback behavior (serving stale on read retry exhaustion) is -difficult to reliably test due to timing sensitivity. This test focuses on -verifying the feature is functional without causing instability. -''' - -Test.ContinueOnFail = True - -Test.ATSReplayTest(replay_file="replay/cache-read-retry-stale.replay.yaml") diff --git a/tests/gold_tests/cache/cache-request-method.test.py b/tests/gold_tests/cache/cache-request-method.test.py deleted file mode 100644 index e08c0f471fd..00000000000 --- a/tests/gold_tests/cache/cache-request-method.test.py +++ /dev/null @@ -1,37 +0,0 @@ -''' -Verify correct caching behavior with respect to request method. -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -Test.Summary = ''' -Verify correct caching behavior with respect to request method. -''' - -# Verify correct POST response handling when caching POST responses is disabled -Test.ATSReplayTest(replay_file="replay/post_with_post_caching_disabled.replay.yaml") - -# Verify correct POST response handling when caching POST responses is enabled -Test.ATSReplayTest(replay_file="replay/post_with_post_caching_enabled.replay.yaml") - -# Verify correct POST response handling when caching POST responses is enabled via overridable config -Test.ATSReplayTest(replay_file="replay/post_with_post_caching_override.replay.yaml") - -# Verify correct HEAD response handling with cached GET response -Test.ATSReplayTest(replay_file="replay/head_with_get_cached.replay.yaml") - -# Verify DELETE request handling - RFC 9111 4.4. Invalidating Stored Responses -Test.ATSReplayTest(replay_file="replay/delete_cached.replay.yaml") diff --git a/tests/gold_tests/cache/cache-write-lock-stale-revalidate.test.py b/tests/gold_tests/cache/cache-write-lock-stale-revalidate.test.py deleted file mode 100644 index 1ff37247c84..00000000000 --- a/tests/gold_tests/cache/cache-write-lock-stale-revalidate.test.py +++ /dev/null @@ -1,32 +0,0 @@ -''' -Verify losing the cache write lock while revalidating a stale object is handled. -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -Test.Summary = ''' -Verify that a transaction which loses the cache write lock while revalidating a -stale object serves that object instead of tripping over the cache read -connection it still holds. -''' - -Test.ContinueOnFail = True - -# STALE_ON_REVALIDATE (action 2) serves the stale object directly. -Test.ATSReplayTest(replay_file="replay/cache-write-lock-stale-serve.replay.yaml") - -# READ_RETRY_STALE_ON_REVALIDATE (action 6) retries the cache read first. -Test.ATSReplayTest(replay_file="replay/cache-write-lock-stale-retry.replay.yaml") diff --git a/tests/gold_tests/cache/cache-write-retry-callback.test.py b/tests/gold_tests/cache/cache-write-retry-callback.test.py deleted file mode 100644 index 28e3e2ec64b..00000000000 --- a/tests/gold_tests/cache/cache-write-retry-callback.test.py +++ /dev/null @@ -1,25 +0,0 @@ -''' -Verify a cache write retry does not cancel the cache read that follows it. -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -Test.Summary = ''' -Verify a transaction that loses the cache write lock and retries can still -receive the cache read callback that its retry triggers. -''' - -Test.ATSReplayTest(replay_file="replay/cache-write-retry-callback.replay.yaml") diff --git a/tests/gold_tests/cache/cache_volume_defaults.test.py b/tests/gold_tests/cache/cache_volume_defaults.test.py deleted file mode 100644 index 8e6cde2f85a..00000000000 --- a/tests/gold_tests/cache/cache_volume_defaults.test.py +++ /dev/null @@ -1,24 +0,0 @@ -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -Test.Summary = ''' -Test proxy.config.cache.default_volumes configuration: -- Verify that default_volumes is used as fallback when no other volume selection applies -- Verify that @volume= directive takes priority over default_volumes -- Verify that hosting.config takes priority over default_volumes -''' - -Test.ATSReplayTest(replay_file="cache_volume_defaults.replay.yaml") diff --git a/tests/gold_tests/cache/cache_volume_defaults.replay.yaml b/tests/gold_tests/cache/cache_volume_defaults.test.yaml similarity index 100% rename from tests/gold_tests/cache/cache_volume_defaults.replay.yaml rename to tests/gold_tests/cache/cache_volume_defaults.test.yaml diff --git a/tests/gold_tests/cache/cache_volume_features.test.py b/tests/gold_tests/cache/cache_volume_features.test.py deleted file mode 100644 index 8b6c198114e..00000000000 --- a/tests/gold_tests/cache/cache_volume_features.test.py +++ /dev/null @@ -1,26 +0,0 @@ -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -Test.Summary = ''' -Comprehensive test suite for cache volume features: -- Per-volume RAM cache configuration (ram_cache, ram_cache_size, ram_cache_cutoff) -- Per-volume settings take effect on the first start after a cache clear -- @volume= directive in remap.config for volume selection -- Integration between both features -''' -# TODO: hosting.config + @volume= priority interaction test requires ats_replay.test.ext to support storage_yaml/hosting_config. - -Test.ATSReplayTest(replay_file="cache_volume_features.replay.yaml") diff --git a/tests/gold_tests/cache/cache_volume_features.replay.yaml b/tests/gold_tests/cache/cache_volume_features.test.yaml similarity index 100% rename from tests/gold_tests/cache/cache_volume_features.replay.yaml rename to tests/gold_tests/cache/cache_volume_features.test.yaml diff --git a/tests/gold_tests/cache/conditional-get-hit.test.py b/tests/gold_tests/cache/conditional-get-hit.test.py deleted file mode 100644 index 22b15e52954..00000000000 --- a/tests/gold_tests/cache/conditional-get-hit.test.py +++ /dev/null @@ -1,25 +0,0 @@ -''' -Test that conditional GETs with body don't interfere with subsequent request -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -Test.Summary = ''' -Test conditional get with body drains the body from client" -''' - -# Verify conditional get with cache hit drains client body. -Test.ATSReplayTest(replay_file="replay/conditional-get-cache-hit.yaml") diff --git a/tests/gold_tests/cache/negative-caching.test.py b/tests/gold_tests/cache/negative-caching.test.py index 853624e6348..a949751f0be 100644 --- a/tests/gold_tests/cache/negative-caching.test.py +++ b/tests/gold_tests/cache/negative-caching.test.py @@ -21,15 +21,6 @@ Test negative caching. ''' -# Negative caching disabled -Test.ATSReplayTest(replay_file="replay/negative-caching-disabled.replay.yaml") - -# Negative caching enabled with default configuration -Test.ATSReplayTest(replay_file="replay/negative-caching-default.replay.yaml") - -# Customized response caching for negative caching configuration -Test.ATSReplayTest(replay_file="replay/negative-caching-customized.replay.yaml") - # # Verify correct proxy.config.http.negative_caching_lifetime behavior. # These tests require multiple test runs with shared ATS processes, so use class-based approach. @@ -132,6 +123,3 @@ p.StartBefore(dns) p.StartBefore(server) p.StartBefore(ts) - -# Test malformed Cache-Control header with semicolons instead of commas (issue #12029) -Test.ATSReplayTest(replay_file="replay/negative-caching-malformed-cc.replay.yaml") diff --git a/tests/gold_tests/cache/negative-revalidating.test.py b/tests/gold_tests/cache/negative-revalidating.test.py deleted file mode 100644 index 30071d147e4..00000000000 --- a/tests/gold_tests/cache/negative-revalidating.test.py +++ /dev/null @@ -1,31 +0,0 @@ -''' -Test the negative revalidating feature. -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -Test.Summary = ''' -Test the negative revalidating feature. -''' - -# Verify negative revalidating disabled -Test.ATSReplayTest(replay_file="replay/negative-revalidating-disabled.replay.yaml") - -# Verify negative revalidating enabled -Test.ATSReplayTest(replay_file="replay/negative-revalidating-enabled.replay.yaml") - -# Verify negative_revalidating_list behavior -Test.ATSReplayTest(replay_file="replay/negative-revalidating-list.replay.yaml") diff --git a/tests/gold_tests/cache/proxy_serve_stale.test.py b/tests/gold_tests/cache/proxy_serve_stale.test.py deleted file mode 100644 index 95e620a2046..00000000000 --- a/tests/gold_tests/cache/proxy_serve_stale.test.py +++ /dev/null @@ -1,27 +0,0 @@ -""" -Test child proxy serving stale content when parents are exhausted -""" -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -Test.testName = "proxy_serve_stale" -Test.ContinueOnFail = True - -# Verify that stale content is served when the parent is down. -Test.ATSReplayTest(replay_file="replay/proxy_serve_stale.replay.yaml") - -# Verify that stale content is served when the origin server is down. -Test.ATSReplayTest(replay_file="replay/proxy_serve_stale_origin_down.replay.yaml") diff --git a/tests/gold_tests/cache/replay/alternate-caching-quality.yaml b/tests/gold_tests/cache/replay/alternate-caching-quality.test.yaml similarity index 97% rename from tests/gold_tests/cache/replay/alternate-caching-quality.yaml rename to tests/gold_tests/cache/replay/alternate-caching-quality.test.yaml index 06915997fd7..f37e482da9a 100644 --- a/tests/gold_tests/cache/replay/alternate-caching-quality.yaml +++ b/tests/gold_tests/cache/replay/alternate-caching-quality.test.yaml @@ -32,6 +32,8 @@ autest: ats: name: 'ts_alt_quality' + copy_custom_plugin: + - "{ATS_TEST_PLUGINS_DIR}/http_alt_info_quality.so" process_config: enable_cache: true @@ -42,6 +44,7 @@ autest: plugin_config: - "xdebug.so --enable=x-cache" + - "http_alt_info_quality.so" remap_config: - from: "http://example.com/" diff --git a/tests/gold_tests/cache/replay/alternate-caching-update-size.yaml b/tests/gold_tests/cache/replay/alternate-caching-update-size.test.yaml similarity index 100% rename from tests/gold_tests/cache/replay/alternate-caching-update-size.yaml rename to tests/gold_tests/cache/replay/alternate-caching-update-size.test.yaml diff --git a/tests/gold_tests/cache/replay/auth-s-maxage.replay.yaml b/tests/gold_tests/cache/replay/auth-s-maxage.test.yaml similarity index 100% rename from tests/gold_tests/cache/replay/auth-s-maxage.replay.yaml rename to tests/gold_tests/cache/replay/auth-s-maxage.test.yaml diff --git a/tests/gold_tests/cache/replay/cache-control-basic.replay.yaml b/tests/gold_tests/cache/replay/cache-control-basic.test.yaml similarity index 100% rename from tests/gold_tests/cache/replay/cache-control-basic.replay.yaml rename to tests/gold_tests/cache/replay/cache-control-basic.test.yaml diff --git a/tests/gold_tests/cache/replay/cache-control-max-age.replay.yaml b/tests/gold_tests/cache/replay/cache-control-max-age.test.yaml similarity index 100% rename from tests/gold_tests/cache/replay/cache-control-max-age.replay.yaml rename to tests/gold_tests/cache/replay/cache-control-max-age.test.yaml diff --git a/tests/gold_tests/cache/replay/cache-control-pragma.replay.yaml b/tests/gold_tests/cache/replay/cache-control-pragma.test.yaml similarity index 100% rename from tests/gold_tests/cache/replay/cache-control-pragma.replay.yaml rename to tests/gold_tests/cache/replay/cache-control-pragma.test.yaml diff --git a/tests/gold_tests/cache/replay/cache-control-s-maxage.replay.yaml b/tests/gold_tests/cache/replay/cache-control-s-maxage.test.yaml similarity index 100% rename from tests/gold_tests/cache/replay/cache-control-s-maxage.replay.yaml rename to tests/gold_tests/cache/replay/cache-control-s-maxage.test.yaml diff --git a/tests/gold_tests/cache/replay/cache-exact-url.replay.yaml b/tests/gold_tests/cache/replay/cache-exact-url.test.yaml similarity index 100% rename from tests/gold_tests/cache/replay/cache-exact-url.replay.yaml rename to tests/gold_tests/cache/replay/cache-exact-url.test.yaml diff --git a/tests/gold_tests/cache/replay/cache-heuristic-status.replay.yaml b/tests/gold_tests/cache/replay/cache-heuristic-status.test.yaml similarity index 100% rename from tests/gold_tests/cache/replay/cache-heuristic-status.replay.yaml rename to tests/gold_tests/cache/replay/cache-heuristic-status.test.yaml diff --git a/tests/gold_tests/cache/replay/cache-range-response.replay.yaml b/tests/gold_tests/cache/replay/cache-range-response.test.yaml similarity index 100% rename from tests/gold_tests/cache/replay/cache-range-response.replay.yaml rename to tests/gold_tests/cache/replay/cache-range-response.test.yaml diff --git a/tests/gold_tests/cache/replay/cache-read-retry-stale.replay.yaml b/tests/gold_tests/cache/replay/cache-read-retry-stale.test.yaml similarity index 100% rename from tests/gold_tests/cache/replay/cache-read-retry-stale.replay.yaml rename to tests/gold_tests/cache/replay/cache-read-retry-stale.test.yaml diff --git a/tests/gold_tests/cache/replay/cache-read-retry.replay.yaml b/tests/gold_tests/cache/replay/cache-read-retry.test.yaml similarity index 100% rename from tests/gold_tests/cache/replay/cache-read-retry.replay.yaml rename to tests/gold_tests/cache/replay/cache-read-retry.test.yaml diff --git a/tests/gold_tests/cache/replay/cache-write-lock-stale-retry.replay.yaml b/tests/gold_tests/cache/replay/cache-write-lock-stale-retry.test.yaml similarity index 100% rename from tests/gold_tests/cache/replay/cache-write-lock-stale-retry.replay.yaml rename to tests/gold_tests/cache/replay/cache-write-lock-stale-retry.test.yaml diff --git a/tests/gold_tests/cache/replay/cache-write-lock-stale-serve.replay.yaml b/tests/gold_tests/cache/replay/cache-write-lock-stale-serve.test.yaml similarity index 100% rename from tests/gold_tests/cache/replay/cache-write-lock-stale-serve.replay.yaml rename to tests/gold_tests/cache/replay/cache-write-lock-stale-serve.test.yaml diff --git a/tests/gold_tests/cache/replay/cache-write-retry-callback.replay.yaml b/tests/gold_tests/cache/replay/cache-write-retry-callback.test.yaml similarity index 100% rename from tests/gold_tests/cache/replay/cache-write-retry-callback.replay.yaml rename to tests/gold_tests/cache/replay/cache-write-retry-callback.test.yaml diff --git a/tests/gold_tests/cache/replay/conditional-get-cache-hit.yaml b/tests/gold_tests/cache/replay/conditional-get-cache-hit.test.yaml similarity index 100% rename from tests/gold_tests/cache/replay/conditional-get-cache-hit.yaml rename to tests/gold_tests/cache/replay/conditional-get-cache-hit.test.yaml diff --git a/tests/gold_tests/cache/replay/cookie-all-but-text-with-excp.replay.yaml b/tests/gold_tests/cache/replay/cookie-all-but-text-with-excp.test.yaml similarity index 100% rename from tests/gold_tests/cache/replay/cookie-all-but-text-with-excp.replay.yaml rename to tests/gold_tests/cache/replay/cookie-all-but-text-with-excp.test.yaml diff --git a/tests/gold_tests/cache/replay/cookie-all-but-text.replay.yaml b/tests/gold_tests/cache/replay/cookie-all-but-text.test.yaml similarity index 100% rename from tests/gold_tests/cache/replay/cookie-all-but-text.replay.yaml rename to tests/gold_tests/cache/replay/cookie-all-but-text.test.yaml diff --git a/tests/gold_tests/cache/replay/cookie-bypass-cache.replay.yaml b/tests/gold_tests/cache/replay/cookie-bypass-cache.test.yaml similarity index 100% rename from tests/gold_tests/cache/replay/cookie-bypass-cache.replay.yaml rename to tests/gold_tests/cache/replay/cookie-bypass-cache.test.yaml diff --git a/tests/gold_tests/cache/replay/cookie-cache-img-only.replay.yaml b/tests/gold_tests/cache/replay/cookie-cache-img-only.test.yaml similarity index 100% rename from tests/gold_tests/cache/replay/cookie-cache-img-only.replay.yaml rename to tests/gold_tests/cache/replay/cookie-cache-img-only.test.yaml diff --git a/tests/gold_tests/cache/replay/cookie-default.replay.yaml b/tests/gold_tests/cache/replay/cookie-default.test.yaml similarity index 100% rename from tests/gold_tests/cache/replay/cookie-default.replay.yaml rename to tests/gold_tests/cache/replay/cookie-default.test.yaml diff --git a/tests/gold_tests/cache/replay/delete_cached.replay.yaml b/tests/gold_tests/cache/replay/delete_cached.test.yaml similarity index 100% rename from tests/gold_tests/cache/replay/delete_cached.replay.yaml rename to tests/gold_tests/cache/replay/delete_cached.test.yaml diff --git a/tests/gold_tests/cache/replay/head_with_get_cached.replay.yaml b/tests/gold_tests/cache/replay/head_with_get_cached.test.yaml similarity index 100% rename from tests/gold_tests/cache/replay/head_with_get_cached.replay.yaml rename to tests/gold_tests/cache/replay/head_with_get_cached.test.yaml diff --git a/tests/gold_tests/cache/replay/ignore_authentication.replay.yaml b/tests/gold_tests/cache/replay/ignore_authentication.test.yaml similarity index 100% rename from tests/gold_tests/cache/replay/ignore_authentication.replay.yaml rename to tests/gold_tests/cache/replay/ignore_authentication.test.yaml diff --git a/tests/gold_tests/cache/replay/negative-caching-customized.replay.yaml b/tests/gold_tests/cache/replay/negative-caching-customized.test.yaml similarity index 100% rename from tests/gold_tests/cache/replay/negative-caching-customized.replay.yaml rename to tests/gold_tests/cache/replay/negative-caching-customized.test.yaml diff --git a/tests/gold_tests/cache/replay/negative-caching-default.replay.yaml b/tests/gold_tests/cache/replay/negative-caching-default.test.yaml similarity index 100% rename from tests/gold_tests/cache/replay/negative-caching-default.replay.yaml rename to tests/gold_tests/cache/replay/negative-caching-default.test.yaml diff --git a/tests/gold_tests/cache/replay/negative-caching-disabled.replay.yaml b/tests/gold_tests/cache/replay/negative-caching-disabled.test.yaml similarity index 100% rename from tests/gold_tests/cache/replay/negative-caching-disabled.replay.yaml rename to tests/gold_tests/cache/replay/negative-caching-disabled.test.yaml diff --git a/tests/gold_tests/cache/replay/negative-caching-malformed-cc.replay.yaml b/tests/gold_tests/cache/replay/negative-caching-malformed-cc.test.yaml similarity index 100% rename from tests/gold_tests/cache/replay/negative-caching-malformed-cc.replay.yaml rename to tests/gold_tests/cache/replay/negative-caching-malformed-cc.test.yaml diff --git a/tests/gold_tests/cache/replay/negative-revalidating-disabled.replay.yaml b/tests/gold_tests/cache/replay/negative-revalidating-disabled.test.yaml similarity index 100% rename from tests/gold_tests/cache/replay/negative-revalidating-disabled.replay.yaml rename to tests/gold_tests/cache/replay/negative-revalidating-disabled.test.yaml diff --git a/tests/gold_tests/cache/replay/negative-revalidating-enabled.replay.yaml b/tests/gold_tests/cache/replay/negative-revalidating-enabled.test.yaml similarity index 100% rename from tests/gold_tests/cache/replay/negative-revalidating-enabled.replay.yaml rename to tests/gold_tests/cache/replay/negative-revalidating-enabled.test.yaml diff --git a/tests/gold_tests/cache/replay/negative-revalidating-list.replay.yaml b/tests/gold_tests/cache/replay/negative-revalidating-list.test.yaml similarity index 100% rename from tests/gold_tests/cache/replay/negative-revalidating-list.replay.yaml rename to tests/gold_tests/cache/replay/negative-revalidating-list.test.yaml diff --git a/tests/gold_tests/cache/replay/post_with_post_caching_disabled.replay.yaml b/tests/gold_tests/cache/replay/post_with_post_caching_disabled.test.yaml similarity index 100% rename from tests/gold_tests/cache/replay/post_with_post_caching_disabled.replay.yaml rename to tests/gold_tests/cache/replay/post_with_post_caching_disabled.test.yaml diff --git a/tests/gold_tests/cache/replay/post_with_post_caching_enabled.replay.yaml b/tests/gold_tests/cache/replay/post_with_post_caching_enabled.test.yaml similarity index 100% rename from tests/gold_tests/cache/replay/post_with_post_caching_enabled.replay.yaml rename to tests/gold_tests/cache/replay/post_with_post_caching_enabled.test.yaml diff --git a/tests/gold_tests/cache/replay/post_with_post_caching_override.replay.yaml b/tests/gold_tests/cache/replay/post_with_post_caching_override.test.yaml similarity index 100% rename from tests/gold_tests/cache/replay/post_with_post_caching_override.replay.yaml rename to tests/gold_tests/cache/replay/post_with_post_caching_override.test.yaml diff --git a/tests/gold_tests/cache/replay/proxy_serve_stale.replay.yaml b/tests/gold_tests/cache/replay/proxy_serve_stale.test.yaml similarity index 100% rename from tests/gold_tests/cache/replay/proxy_serve_stale.replay.yaml rename to tests/gold_tests/cache/replay/proxy_serve_stale.test.yaml diff --git a/tests/gold_tests/cache/replay/proxy_serve_stale_origin_down.replay.yaml b/tests/gold_tests/cache/replay/proxy_serve_stale_origin_down.test.yaml similarity index 100% rename from tests/gold_tests/cache/replay/proxy_serve_stale_origin_down.replay.yaml rename to tests/gold_tests/cache/replay/proxy_serve_stale_origin_down.test.yaml diff --git a/tests/gold_tests/cache/replay/request-cache-control-default.replay.yaml b/tests/gold_tests/cache/replay/request-cache-control-default.test.yaml similarity index 100% rename from tests/gold_tests/cache/replay/request-cache-control-default.replay.yaml rename to tests/gold_tests/cache/replay/request-cache-control-default.test.yaml diff --git a/tests/gold_tests/cache/replay/request-cache-control-honor-client.replay.yaml b/tests/gold_tests/cache/replay/request-cache-control-honor-client.test.yaml similarity index 100% rename from tests/gold_tests/cache/replay/request-cache-control-honor-client.replay.yaml rename to tests/gold_tests/cache/replay/request-cache-control-honor-client.test.yaml diff --git a/tests/gold_tests/cache/replay/response-cache-control-default.replay.yaml b/tests/gold_tests/cache/replay/response-cache-control-default.test.yaml similarity index 100% rename from tests/gold_tests/cache/replay/response-cache-control-default.replay.yaml rename to tests/gold_tests/cache/replay/response-cache-control-default.test.yaml diff --git a/tests/gold_tests/cache/replay/response-cache-control-ignored.replay.yaml b/tests/gold_tests/cache/replay/response-cache-control-ignored.test.yaml similarity index 100% rename from tests/gold_tests/cache/replay/response-cache-control-ignored.replay.yaml rename to tests/gold_tests/cache/replay/response-cache-control-ignored.test.yaml diff --git a/tests/gold_tests/cache/replay/targeted-cache-control.replay.yaml b/tests/gold_tests/cache/replay/targeted-cache-control.test.yaml similarity index 100% rename from tests/gold_tests/cache/replay/targeted-cache-control.replay.yaml rename to tests/gold_tests/cache/replay/targeted-cache-control.test.yaml diff --git a/tests/gold_tests/cache/replay/varied_transactions.replay.yaml b/tests/gold_tests/cache/replay/varied_transactions.test.yaml similarity index 100% rename from tests/gold_tests/cache/replay/varied_transactions.replay.yaml rename to tests/gold_tests/cache/replay/varied_transactions.test.yaml diff --git a/tests/gold_tests/cache/targeted-cache-control.test.py b/tests/gold_tests/cache/targeted-cache-control.test.py deleted file mode 100644 index 4c6efc174f5..00000000000 --- a/tests/gold_tests/cache/targeted-cache-control.test.py +++ /dev/null @@ -1,25 +0,0 @@ -'''Test targeted cache control headers per RFC 9213.''' - -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -Test.Summary = ''' -Test targeted cache control headers per RFC 9213. -Verifies that CDN-Cache-Control is applied by default, and that remap-level -targeted header overrides still work. -''' - -Test.ATSReplayTest(replay_file="replay/targeted-cache-control.replay.yaml") diff --git a/tests/gold_tests/cache/vary-handling.test.py b/tests/gold_tests/cache/vary-handling.test.py deleted file mode 100644 index 3e27012f99f..00000000000 --- a/tests/gold_tests/cache/vary-handling.test.py +++ /dev/null @@ -1,25 +0,0 @@ -''' -Test correct handling of alternates via the Vary header. -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -Test.Summary = ''' -Test correct handling of alternates via the Vary header. -''' - -# Run traffic with max_alts behavior when set to 4. -Test.ATSReplayTest(replay_file="replay/varied_transactions.replay.yaml") diff --git a/tests/gold_tests/chunked_encoding/chunked_encoding.test.py b/tests/gold_tests/chunked_encoding/chunked_encoding.test.py index 14513a097a2..1c24420f5cc 100644 --- a/tests/gold_tests/chunked_encoding/chunked_encoding.test.py +++ b/tests/gold_tests/chunked_encoding/chunked_encoding.test.py @@ -261,6 +261,3 @@ def _configure_client(self, tr: 'TestRun') -> 'Process': TestChunkedTrailers(configure_drop_trailers=True) TestChunkedTrailers(configure_drop_trailers=False) - -# Large chunked response from origin server -Test.ATSReplayTest(replay_file="replays/large_chunked.replay.yaml") diff --git a/tests/gold_tests/chunked_encoding/chunked_flow_control.test.py b/tests/gold_tests/chunked_encoding/chunked_flow_control.test.py deleted file mode 100644 index c1ee966174e..00000000000 --- a/tests/gold_tests/chunked_encoding/chunked_flow_control.test.py +++ /dev/null @@ -1,24 +0,0 @@ -''' -Regression test: chunked-origin -> HTTP/2 client dechunk flow-control throttle. -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -Test.Summary = __doc__ - -Test.ATSReplayTest(replay_file="replays/chunked_h1_flow_control.replay.yaml") -Test.ATSReplayTest(replay_file="replays/chunked_h2_flow_control.replay.yaml") -Test.ATSReplayTest(replay_file="replays/chunked_passthru_flow_control.replay.yaml") diff --git a/tests/gold_tests/chunked_encoding/chunked_not_last.test.py b/tests/gold_tests/chunked_encoding/chunked_not_last.test.py deleted file mode 100644 index c45088c0181..00000000000 --- a/tests/gold_tests/chunked_encoding/chunked_not_last.test.py +++ /dev/null @@ -1,25 +0,0 @@ -''' -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -Test.Summary = ''' -RFC 9112 requires chunked to be the final transfer coding. Verify that ATS -rejects requests where chunked is not the last Transfer-Encoding value, and -handles responses correctly when chunked is not terminal. -''' - -Test.ATSReplayTest(replay_file='replays/chunked_not_last.replay.yaml') diff --git a/tests/gold_tests/chunked_encoding/replays/chunked_h1_flow_control.replay.yaml b/tests/gold_tests/chunked_encoding/replays/chunked_h1_flow_control.test.yaml similarity index 100% rename from tests/gold_tests/chunked_encoding/replays/chunked_h1_flow_control.replay.yaml rename to tests/gold_tests/chunked_encoding/replays/chunked_h1_flow_control.test.yaml diff --git a/tests/gold_tests/chunked_encoding/replays/chunked_h2_flow_control.replay.yaml b/tests/gold_tests/chunked_encoding/replays/chunked_h2_flow_control.test.yaml similarity index 100% rename from tests/gold_tests/chunked_encoding/replays/chunked_h2_flow_control.replay.yaml rename to tests/gold_tests/chunked_encoding/replays/chunked_h2_flow_control.test.yaml diff --git a/tests/gold_tests/chunked_encoding/replays/chunked_not_last.replay.yaml b/tests/gold_tests/chunked_encoding/replays/chunked_not_last.test.yaml similarity index 100% rename from tests/gold_tests/chunked_encoding/replays/chunked_not_last.replay.yaml rename to tests/gold_tests/chunked_encoding/replays/chunked_not_last.test.yaml diff --git a/tests/gold_tests/chunked_encoding/replays/chunked_passthru_flow_control.replay.yaml b/tests/gold_tests/chunked_encoding/replays/chunked_passthru_flow_control.test.yaml similarity index 100% rename from tests/gold_tests/chunked_encoding/replays/chunked_passthru_flow_control.replay.yaml rename to tests/gold_tests/chunked_encoding/replays/chunked_passthru_flow_control.test.yaml diff --git a/tests/gold_tests/chunked_encoding/replays/large_chunked.replay.yaml b/tests/gold_tests/chunked_encoding/replays/large_chunked.test.yaml similarity index 99% rename from tests/gold_tests/chunked_encoding/replays/large_chunked.replay.yaml rename to tests/gold_tests/chunked_encoding/replays/large_chunked.test.yaml index 9859e5a638c..d02f4acb02f 100644 --- a/tests/gold_tests/chunked_encoding/replays/large_chunked.replay.yaml +++ b/tests/gold_tests/chunked_encoding/replays/large_chunked.test.yaml @@ -20,6 +20,8 @@ meta: # Configuration section for autest integration autest: description: 'Large chunked response from origin server' + requires: + curl_features: [http2] dns: name: 'dns-chunked-origin' diff --git a/tests/gold_tests/connect_down_policy/connect_down_policy.test.py b/tests/gold_tests/connect_down_policy/connect_down_policy.test.py index 0407dd25f9a..f40af24cc45 100644 --- a/tests/gold_tests/connect_down_policy/connect_down_policy.test.py +++ b/tests/gold_tests/connect_down_policy/connect_down_policy.test.py @@ -111,6 +111,3 @@ def run(self): # Policy 2: inactive timeout should NOT mark the origin down. ConnectDownPolicy3Test(policy=2, expect_mark_down=False).run() - -# Policy 4: origin closes connection without sending any response. -Test.ATSReplayTest(replay_file="replay/connect_down_policy_4.replay.yaml") diff --git a/tests/gold_tests/connect_down_policy/replay/connect_down_policy_4.replay.yaml b/tests/gold_tests/connect_down_policy/replay/connect_down_policy_4.test.yaml similarity index 100% rename from tests/gold_tests/connect_down_policy/replay/connect_down_policy_4.replay.yaml rename to tests/gold_tests/connect_down_policy/replay/connect_down_policy_4.test.yaml diff --git a/tests/gold_tests/cripts/cripts_bundle.test.py b/tests/gold_tests/cripts/cripts_bundle.test.py deleted file mode 100644 index 8b5630b162d..00000000000 --- a/tests/gold_tests/cripts/cripts_bundle.test.py +++ /dev/null @@ -1,49 +0,0 @@ -''' -Bundle of cripts feature tests, driven by one replay-file harness. Each -.cript under files/ targets a distinct cripts feature; the replay file -configures one remap rule per cript and a transaction set per feature. - -Currently bundled: - - bundle_headers.cript : exercises cripts::Bundle::Headers across all - four hooks with every dynamic substitution - source (ID/IP/CIDR/CLIENT-URL components), - via the HRWBridge / %{...} syntax. - - direct_headers.cript : same data sources but via the direct cripts - API (UUID/IP/Url accessors), bypassing - Bundle::Headers entirely. - - server_bundle_headers.cript : Bundle::Headers on the two server hooks - — rm_headers (incl. Authorization/Cookie/ - Set-Cookie) plus set_headers with literal - values (add + overwrite). Server-only so it - also pins the regression where rm_headers() - misrouted both server targets into the - client-response list and silently dropped the - strips. - -Concurrent client sessions widen the exposure surface; under a TSAN -build this is the regression target for any bug that puts shared mutable -state back into a Cripts plugin instance. -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -Test.Summary = ''' -cripts: bundle of feature tests (bundle_headers.cript + direct_headers.cript) -''' - -Test.SkipUnless(Condition.HasATSFeature('TS_HAS_CRIPTS')) - -Test.ATSReplayTest(replay_file="cripts_bundle.replay.yaml") diff --git a/tests/gold_tests/cripts/cripts_bundle.replay.yaml b/tests/gold_tests/cripts/cripts_bundle.test.yaml similarity index 99% rename from tests/gold_tests/cripts/cripts_bundle.replay.yaml rename to tests/gold_tests/cripts/cripts_bundle.test.yaml index dc49550b2b4..72c3dfef3af 100644 --- a/tests/gold_tests/cripts/cripts_bundle.replay.yaml +++ b/tests/gold_tests/cripts/cripts_bundle.test.yaml @@ -27,6 +27,8 @@ meta: # instance. autest: description: 'cripts: bundle of feature tests (bundle_headers.cript: every Bundle::Headers dynamic substitution across all four hooks; direct_headers.cript: the equivalent direct Cripts API path)' + requires: + ats_features: [TS_HAS_CRIPTS] dns: name: 'dns' @@ -322,4 +324,3 @@ sessions: - [ X-Cript-Set-SResp, { value: "set-sresp", as: equal } ] - [ X-Cript-Over-SResp, { value: "over-sresp", as: equal } ] - [ X-Cript-Keep-SResp, { value: "should-survive", as: equal } ] - diff --git a/tests/gold_tests/dns/connect_attempts.test.py b/tests/gold_tests/dns/connect_attempts.test.py deleted file mode 100644 index e5b572d366b..00000000000 --- a/tests/gold_tests/dns/connect_attempts.test.py +++ /dev/null @@ -1,36 +0,0 @@ -''' -Verify Origin Server Connect Attempts Behavior -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -Test.Summary = ''' -Verify Origin Server Connect Attempts Behavior -''' - -# Single DNS Record -# max_retries & down_server -Test.ATSReplayTest(replay_file="replay/connect_attempts_single_max_retries.replay.yaml") - -# Multiple DNS Records -# No retry -Test.ATSReplayTest(replay_file="replay/connect_attempts_rr_no_retry.replay.yaml") - -# max_retries -Test.ATSReplayTest(replay_file="replay/connect_attempts_rr_max_retries.replay.yaml") - -# rr_retries -Test.ATSReplayTest(replay_file="replay/connect_attempts_rr_retries.replay.yaml") diff --git a/tests/gold_tests/dns/replay/connect_attempts_rr_max_retries.replay.yaml b/tests/gold_tests/dns/replay/connect_attempts_rr_max_retries.test.yaml similarity index 100% rename from tests/gold_tests/dns/replay/connect_attempts_rr_max_retries.replay.yaml rename to tests/gold_tests/dns/replay/connect_attempts_rr_max_retries.test.yaml diff --git a/tests/gold_tests/dns/replay/connect_attempts_rr_no_retry.replay.yaml b/tests/gold_tests/dns/replay/connect_attempts_rr_no_retry.test.yaml similarity index 100% rename from tests/gold_tests/dns/replay/connect_attempts_rr_no_retry.replay.yaml rename to tests/gold_tests/dns/replay/connect_attempts_rr_no_retry.test.yaml diff --git a/tests/gold_tests/dns/replay/connect_attempts_rr_retries.replay.yaml b/tests/gold_tests/dns/replay/connect_attempts_rr_retries.test.yaml similarity index 100% rename from tests/gold_tests/dns/replay/connect_attempts_rr_retries.replay.yaml rename to tests/gold_tests/dns/replay/connect_attempts_rr_retries.test.yaml diff --git a/tests/gold_tests/dns/replay/connect_attempts_single_max_retries.replay.yaml b/tests/gold_tests/dns/replay/connect_attempts_single_max_retries.test.yaml similarity index 100% rename from tests/gold_tests/dns/replay/connect_attempts_single_max_retries.replay.yaml rename to tests/gold_tests/dns/replay/connect_attempts_single_max_retries.test.yaml diff --git a/tests/gold_tests/h2/http2_client_reset_keeps_session_reading.test.py b/tests/gold_tests/h2/http2_client_reset_keeps_session_reading.test.py deleted file mode 100644 index 1e20b5aae6f..00000000000 --- a/tests/gold_tests/h2/http2_client_reset_keeps_session_reading.test.py +++ /dev/null @@ -1,23 +0,0 @@ -'''Verify an HTTP/2 stream reset does not stop the session from reading.''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -Test.Summary = __doc__ - -Test.SkipUnless( - Condition.HasOpenSSLVersion('1.1.1'), Condition.HasProxyVerifierVersion('2.8.0'), Condition.PluginExists('null_transform.so')) - -Test.ATSReplayTest(replay_file="replay/http2_client_reset_keeps_session_reading.replay.yaml") diff --git a/tests/gold_tests/h2/replay/http2_client_reset_keeps_session_reading.replay.yaml b/tests/gold_tests/h2/replay/http2_client_reset_keeps_session_reading.test.yaml similarity index 98% rename from tests/gold_tests/h2/replay/http2_client_reset_keeps_session_reading.replay.yaml rename to tests/gold_tests/h2/replay/http2_client_reset_keeps_session_reading.test.yaml index 52974a8c0b9..52e5196d51b 100644 --- a/tests/gold_tests/h2/replay/http2_client_reset_keeps_session_reading.replay.yaml +++ b/tests/gold_tests/h2/replay/http2_client_reset_keeps_session_reading.test.yaml @@ -35,6 +35,10 @@ meta: autest: description: 'Verify an HTTP/2 stream reset does not stop the session from reading' + requires: + openssl: '1.1.1' + proxy_verifier: '2.8.0' + plugins: [null_transform.so] server: name: 'server' diff --git a/tests/gold_tests/h3/h3_active_timeout.test.py b/tests/gold_tests/h3/h3_active_timeout.test.py deleted file mode 100644 index 9a159fb49c6..00000000000 --- a/tests/gold_tests/h3/h3_active_timeout.test.py +++ /dev/null @@ -1,26 +0,0 @@ -''' -Verify HTTP/3 transaction cleanup on active timeout. -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information regarding -# copyright ownership. The ASF licenses this file to you under -# the Apache License, Version 2.0 (the "License"); you may not -# use this file except in compliance with the License. You may -# obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -Test.Summary = ''' -Verify HTTP/3 transactions are removed cleanly after transaction active timeout. -''' - -Test.SkipUnless(Condition.HasATSFeature('TS_USE_QUIC')) - -Test.ATSReplayTest(replay_file="replays/h3_active_timeout.replay.yaml") diff --git a/tests/gold_tests/h3/h3_flow_control.test.py b/tests/gold_tests/h3/h3_flow_control.test.py deleted file mode 100644 index 85c4c5b5573..00000000000 --- a/tests/gold_tests/h3/h3_flow_control.test.py +++ /dev/null @@ -1,27 +0,0 @@ -''' -Verify HTTP/3 traffic progresses with small QUIC flow-control windows. -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information regarding -# copyright ownership. The ASF licenses this file to you under -# the Apache License, Version 2.0 (the "License"); you may not -# use this file except in compliance with the License. You may -# obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -Test.Summary = ''' -Verify that large HTTP/3 request and response bodies complete when ATS -advertises small initial QUIC flow-control windows. -''' - -Test.SkipUnless(Condition.HasATSFeature('TS_USE_QUIC')) - -Test.ATSReplayTest(replay_file="replays/h3_flow_control.replay.yaml") diff --git a/tests/gold_tests/h3/h3_proxy_verifier.test.py b/tests/gold_tests/h3/h3_proxy_verifier.test.py deleted file mode 100644 index a32b21dfd59..00000000000 --- a/tests/gold_tests/h3/h3_proxy_verifier.test.py +++ /dev/null @@ -1,27 +0,0 @@ -''' -Verify HTTP/3 client interop with Proxy Verifier. -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information regarding -# copyright ownership. The ASF licenses this file to you under -# the Apache License, Version 2.0 (the "License"); you may not -# use this file except in compliance with the License. You may -# obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -Test.Summary = ''' -Verify a real HTTP/3 Proxy Verifier client can complete multiple transactions -across multiple QUIC connections through ATS. -''' - -Test.SkipUnless(Condition.HasATSFeature('TS_USE_QUIC')) - -Test.ATSReplayTest(replay_file="replays/h3_proxy_verifier.replay.yaml") diff --git a/tests/gold_tests/h3/h3_stream_lifetime.test.py b/tests/gold_tests/h3/h3_stream_lifetime.test.py deleted file mode 100644 index 9715392ea78..00000000000 --- a/tests/gold_tests/h3/h3_stream_lifetime.test.py +++ /dev/null @@ -1,27 +0,0 @@ -''' -Verify HTTP/3 stream lifetime handling with concurrent streams. -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information regarding -# copyright ownership. The ASF licenses this file to you under -# the Apache License, Version 2.0 (the "License"); you may not -# use this file except in compliance with the License. You may -# obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -Test.Summary = ''' -Verify HTTP/3 transactions survive concurrent stream close and write-ready -events on the same QUIC connection. -''' - -Test.SkipUnless(Condition.HasATSFeature('TS_USE_QUIC')) - -Test.ATSReplayTest(replay_file="replays/h3_stream_lifetime.replay.yaml") diff --git a/tests/gold_tests/h3/replays/h3_active_timeout.replay.yaml b/tests/gold_tests/h3/replays/h3_active_timeout.test.yaml similarity index 98% rename from tests/gold_tests/h3/replays/h3_active_timeout.replay.yaml rename to tests/gold_tests/h3/replays/h3_active_timeout.test.yaml index f7a39324fd1..91df09ddf7e 100644 --- a/tests/gold_tests/h3/replays/h3_active_timeout.replay.yaml +++ b/tests/gold_tests/h3/replays/h3_active_timeout.test.yaml @@ -19,6 +19,8 @@ meta: autest: description: "Verify HTTP/3 transaction cleanup on active timeout" + requires: + ats_features: [TS_USE_QUIC] server: name: "server-h3-active-timeout" diff --git a/tests/gold_tests/h3/replays/h3_flow_control.replay.yaml b/tests/gold_tests/h3/replays/h3_flow_control.test.yaml similarity index 98% rename from tests/gold_tests/h3/replays/h3_flow_control.replay.yaml rename to tests/gold_tests/h3/replays/h3_flow_control.test.yaml index 0444a72eaef..4ca067aa5a2 100644 --- a/tests/gold_tests/h3/replays/h3_flow_control.replay.yaml +++ b/tests/gold_tests/h3/replays/h3_flow_control.test.yaml @@ -19,6 +19,8 @@ meta: autest: description: "Verify HTTP/3 flow control with small QUIC windows" + requires: + ats_features: [TS_USE_QUIC] server: name: "server-h3-flow-control" diff --git a/tests/gold_tests/h3/replays/h3_proxy_verifier.replay.yaml b/tests/gold_tests/h3/replays/h3_proxy_verifier.test.yaml similarity index 99% rename from tests/gold_tests/h3/replays/h3_proxy_verifier.replay.yaml rename to tests/gold_tests/h3/replays/h3_proxy_verifier.test.yaml index 817485859a5..a437f0c58f8 100644 --- a/tests/gold_tests/h3/replays/h3_proxy_verifier.replay.yaml +++ b/tests/gold_tests/h3/replays/h3_proxy_verifier.test.yaml @@ -19,6 +19,8 @@ meta: autest: description: "Verify HTTP/3 client interop with Proxy Verifier across multiple connections" + requires: + ats_features: [TS_USE_QUIC] server: name: "server-h3-proxy-verifier" diff --git a/tests/gold_tests/h3/replays/h3_stream_lifetime.replay.yaml b/tests/gold_tests/h3/replays/h3_stream_lifetime.test.yaml similarity index 99% rename from tests/gold_tests/h3/replays/h3_stream_lifetime.replay.yaml rename to tests/gold_tests/h3/replays/h3_stream_lifetime.test.yaml index b263bea5277..36cc9cae464 100644 --- a/tests/gold_tests/h3/replays/h3_stream_lifetime.replay.yaml +++ b/tests/gold_tests/h3/replays/h3_stream_lifetime.test.yaml @@ -19,6 +19,8 @@ meta: autest: description: "Verify HTTP/3 stream lifetime handling with concurrent streams" + requires: + ats_features: [TS_USE_QUIC] server: name: "server-h3-stream-lifetime" diff --git a/tests/gold_tests/headers/accept_webp.test.py b/tests/gold_tests/headers/accept_webp.test.py deleted file mode 100644 index 1d0a4232c86..00000000000 --- a/tests/gold_tests/headers/accept_webp.test.py +++ /dev/null @@ -1,26 +0,0 @@ -''' -Test how ATS handles image/webp alternates. -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -Test.Summary = ''' -Check that ATS does not reuse a cached image/webp alternate for clients that do not advertise image/webp -''' - -Test.ContinueOnFail = True - -Test.ATSReplayTest(replay_file="replays/accept_webp.replay.yaml") diff --git a/tests/gold_tests/headers/cachedDuplicateHeaders.test.py b/tests/gold_tests/headers/cachedDuplicateHeaders.test.py deleted file mode 100644 index eafe6cf81c1..00000000000 --- a/tests/gold_tests/headers/cachedDuplicateHeaders.test.py +++ /dev/null @@ -1,26 +0,0 @@ -''' -Test cached duplicate headers during revalidation. -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -Test.Summary = ''' -Test cached duplicate headers during revalidation -''' - -Test.ContinueOnFail = True - -Test.ATSReplayTest(replay_file="replays/cache-test.replay.yaml") diff --git a/tests/gold_tests/headers/cachedIMSRange.test.py b/tests/gold_tests/headers/cachedIMSRange.test.py deleted file mode 100644 index 6682053931a..00000000000 --- a/tests/gold_tests/headers/cachedIMSRange.test.py +++ /dev/null @@ -1,26 +0,0 @@ -''' -Test revalidating cached objects. -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -Test.Summary = ''' -Test revalidating cached objects -''' - -Test.ContinueOnFail = True - -Test.ATSReplayTest(replay_file="replays/cached_ims_range.replay.yaml") diff --git a/tests/gold_tests/headers/domain-blacklist-30x.test.py b/tests/gold_tests/headers/domain-blacklist-30x.test.py deleted file mode 100644 index cc3ab045a44..00000000000 --- a/tests/gold_tests/headers/domain-blacklist-30x.test.py +++ /dev/null @@ -1,26 +0,0 @@ -''' -Test that redirect responses are returned for matching domains. -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -Test.Summary = ''' -Test that redirect responses are returned for matching domains -''' - -Test.ContinueOnFail = True - -Test.ATSReplayTest(replay_file="replays/domain-blacklist-30x.replay.yaml") diff --git a/tests/gold_tests/headers/hopbyhop_connection.test.py b/tests/gold_tests/headers/hopbyhop_connection.test.py deleted file mode 100644 index 1492b7c7f58..00000000000 --- a/tests/gold_tests/headers/hopbyhop_connection.test.py +++ /dev/null @@ -1,23 +0,0 @@ -# -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. -# - -Test.Summary = "autest for dynamic Connection-token hop-by-hop stripping (forward-path)" - -# Minimal ATSReplay invocation pointing at the replay YAML - -Test.ATSReplayTest(replay_file="replays/hopbyhop_connection.replay.yaml") diff --git a/tests/gold_tests/headers/hsts.test.py b/tests/gold_tests/headers/hsts.test.py deleted file mode 100644 index 147fb7cd115..00000000000 --- a/tests/gold_tests/headers/hsts.test.py +++ /dev/null @@ -1,26 +0,0 @@ -''' -Test the HSTS response header. -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -Test.Summary = ''' -Check the HSTS header is set correctly -''' - -Test.ContinueOnFail = True - -Test.ATSReplayTest(replay_file="replays/hsts.replay.yaml") diff --git a/tests/gold_tests/headers/invalid_range_header.test.py b/tests/gold_tests/headers/invalid_range_header.test.py deleted file mode 100644 index f9613e7f7ad..00000000000 --- a/tests/gold_tests/headers/invalid_range_header.test.py +++ /dev/null @@ -1,26 +0,0 @@ -''' -Test invalid values in the Range header. -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -Test.Summary = ''' -Test invalid values in the Range header -''' - -Test.ContinueOnFail = True - -Test.ATSReplayTest(replay_file="replays/invalid_range_request.replay.yaml") diff --git a/tests/gold_tests/headers/malformed_host_port.test.py b/tests/gold_tests/headers/malformed_host_port.test.py deleted file mode 100644 index b444d4e9354..00000000000 --- a/tests/gold_tests/headers/malformed_host_port.test.py +++ /dev/null @@ -1,24 +0,0 @@ -''' -Verify malformed Host headers with extra port separators are rejected. -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -Test.Summary = ''' -Verify malformed Host headers with extra port separators are rejected. -''' - -Test.ATSReplayTest(replay_file="replays/malformed_host_port.replay.yaml") diff --git a/tests/gold_tests/headers/normalized_ae_match_vary_cache.test.py b/tests/gold_tests/headers/normalized_ae_match_vary_cache.test.py deleted file mode 100644 index 57f96dcfb0a..00000000000 --- a/tests/gold_tests/headers/normalized_ae_match_vary_cache.test.py +++ /dev/null @@ -1,27 +0,0 @@ -''' -Test cache matching with the normalized Accept-Encoding header field -and the Vary header field in response. -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -Test.Summary = ''' -Test cache matching with the normalized Accept-Encoding and the Vary header field in response -''' - -Test.ContinueOnFail = True - -Test.ATSReplayTest(replay_file="replays/normalized_ae_varied_transactions.replay.yaml") diff --git a/tests/gold_tests/headers/oversized_field.test.py b/tests/gold_tests/headers/oversized_field.test.py deleted file mode 100644 index 3dcc7205ea6..00000000000 --- a/tests/gold_tests/headers/oversized_field.test.py +++ /dev/null @@ -1,27 +0,0 @@ -''' -Verify that a header field exceeding the uint16_t field-length limit is rejected -with a 400 and never forwarded to the origin. -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -Test.Summary = ''' -Verify that a single header field whose length exceeds the uint16_t field-length -limit is rejected by the parser with a 400, rather than stored with a truncated -length. -''' - -Test.ATSReplayTest(replay_file='replay/oversized_field.replay.yaml') diff --git a/tests/gold_tests/headers/range.test.py b/tests/gold_tests/headers/range.test.py deleted file mode 100644 index 85661b6e289..00000000000 --- a/tests/gold_tests/headers/range.test.py +++ /dev/null @@ -1,26 +0,0 @@ -''' -Test range requests. -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -Test.Summary = ''' -Test range requests -''' - -Test.ContinueOnFail = True - -Test.ATSReplayTest(replay_file="replays/range.replay.yaml") diff --git a/tests/gold_tests/headers/range_transform.test.py b/tests/gold_tests/headers/range_transform.test.py deleted file mode 100644 index 63ef0ea8038..00000000000 --- a/tests/gold_tests/headers/range_transform.test.py +++ /dev/null @@ -1,18 +0,0 @@ -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -tr = Test.ATSReplayTest(replay_file="replays/range_transform.replay.yaml") -tr.Processes.Default.TimeOut = 10 diff --git a/tests/gold_tests/headers/replay/oversized_field.replay.yaml b/tests/gold_tests/headers/replay/oversized_field.test.yaml similarity index 100% rename from tests/gold_tests/headers/replay/oversized_field.replay.yaml rename to tests/gold_tests/headers/replay/oversized_field.test.yaml diff --git a/tests/gold_tests/headers/replays/accept_webp.replay.yaml b/tests/gold_tests/headers/replays/accept_webp.test.yaml similarity index 100% rename from tests/gold_tests/headers/replays/accept_webp.replay.yaml rename to tests/gold_tests/headers/replays/accept_webp.test.yaml diff --git a/tests/gold_tests/headers/replays/cache-test.replay.yaml b/tests/gold_tests/headers/replays/cache-test.test.yaml similarity index 100% rename from tests/gold_tests/headers/replays/cache-test.replay.yaml rename to tests/gold_tests/headers/replays/cache-test.test.yaml diff --git a/tests/gold_tests/headers/replays/cached_ims_range.replay.yaml b/tests/gold_tests/headers/replays/cached_ims_range.test.yaml similarity index 100% rename from tests/gold_tests/headers/replays/cached_ims_range.replay.yaml rename to tests/gold_tests/headers/replays/cached_ims_range.test.yaml diff --git a/tests/gold_tests/headers/replays/domain-blacklist-30x.replay.yaml b/tests/gold_tests/headers/replays/domain-blacklist-30x.test.yaml similarity index 100% rename from tests/gold_tests/headers/replays/domain-blacklist-30x.replay.yaml rename to tests/gold_tests/headers/replays/domain-blacklist-30x.test.yaml diff --git a/tests/gold_tests/headers/replays/hopbyhop_connection.replay.yaml b/tests/gold_tests/headers/replays/hopbyhop_connection.test.yaml similarity index 100% rename from tests/gold_tests/headers/replays/hopbyhop_connection.replay.yaml rename to tests/gold_tests/headers/replays/hopbyhop_connection.test.yaml diff --git a/tests/gold_tests/headers/replays/hsts.replay.yaml b/tests/gold_tests/headers/replays/hsts.test.yaml similarity index 100% rename from tests/gold_tests/headers/replays/hsts.replay.yaml rename to tests/gold_tests/headers/replays/hsts.test.yaml diff --git a/tests/gold_tests/headers/replays/invalid_range_request.replay.yaml b/tests/gold_tests/headers/replays/invalid_range_request.test.yaml similarity index 100% rename from tests/gold_tests/headers/replays/invalid_range_request.replay.yaml rename to tests/gold_tests/headers/replays/invalid_range_request.test.yaml diff --git a/tests/gold_tests/headers/replays/malformed_host_port.replay.yaml b/tests/gold_tests/headers/replays/malformed_host_port.test.yaml similarity index 100% rename from tests/gold_tests/headers/replays/malformed_host_port.replay.yaml rename to tests/gold_tests/headers/replays/malformed_host_port.test.yaml diff --git a/tests/gold_tests/headers/replays/normalized_ae_varied_transactions.replay.yaml b/tests/gold_tests/headers/replays/normalized_ae_varied_transactions.test.yaml similarity index 100% rename from tests/gold_tests/headers/replays/normalized_ae_varied_transactions.replay.yaml rename to tests/gold_tests/headers/replays/normalized_ae_varied_transactions.test.yaml diff --git a/tests/gold_tests/headers/replays/range.replay.yaml b/tests/gold_tests/headers/replays/range.test.yaml similarity index 100% rename from tests/gold_tests/headers/replays/range.replay.yaml rename to tests/gold_tests/headers/replays/range.test.yaml diff --git a/tests/gold_tests/headers/replays/range_transform.replay.yaml b/tests/gold_tests/headers/replays/range_transform.test.yaml similarity index 99% rename from tests/gold_tests/headers/replays/range_transform.replay.yaml rename to tests/gold_tests/headers/replays/range_transform.test.yaml index d4376dfabcd..794ea02000e 100644 --- a/tests/gold_tests/headers/replays/range_transform.replay.yaml +++ b/tests/gold_tests/headers/replays/range_transform.test.yaml @@ -36,6 +36,7 @@ autest: client: name: "client-range-transform" + timeout: 10 ats: name: "ts-range-transform" diff --git a/tests/gold_tests/ip_allow/ip_allow_subjects.test.py b/tests/gold_tests/ip_allow/ip_allow_subjects.test.py deleted file mode 100644 index 81790e5092e..00000000000 --- a/tests/gold_tests/ip_allow/ip_allow_subjects.test.py +++ /dev/null @@ -1,30 +0,0 @@ -''' -Verify that exceeding MAX_SUBJECTS for proxy.config.acl.subjects logs an error and does not crash. -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -Test.Summary = ''' -Verify that exceeding MAX_SUBJECTS for proxy.config.acl.subjects logs an error and does not crash. -''' - -# Scenario 1: Configuring 4 ACL subjects (more than MAX_SUBJECTS=3) should log -# an error but not crash. Request should still succeed with 200 OK. -Test.ATSReplayTest(replay_file="replay/ip_allow_subjects_overflow.replay.yaml") - -# Scenario 2: Configuring exactly 3 ACL subjects (equal to MAX_SUBJECTS) should -# work without error. Request should succeed with 200 OK. -Test.ATSReplayTest(replay_file="replay/ip_allow_subjects_valid.replay.yaml") diff --git a/tests/gold_tests/ip_allow/replay/ip_allow_subjects_overflow.replay.yaml b/tests/gold_tests/ip_allow/replay/ip_allow_subjects_overflow.test.yaml similarity index 100% rename from tests/gold_tests/ip_allow/replay/ip_allow_subjects_overflow.replay.yaml rename to tests/gold_tests/ip_allow/replay/ip_allow_subjects_overflow.test.yaml diff --git a/tests/gold_tests/ip_allow/replay/ip_allow_subjects_valid.replay.yaml b/tests/gold_tests/ip_allow/replay/ip_allow_subjects_valid.test.yaml similarity index 100% rename from tests/gold_tests/ip_allow/replay/ip_allow_subjects_valid.replay.yaml rename to tests/gold_tests/ip_allow/replay/ip_allow_subjects_valid.test.yaml diff --git a/tests/gold_tests/logging/log-debug-client-ip.test.py b/tests/gold_tests/logging/log-debug-client-ip.test.py deleted file mode 100644 index a6894d8a65d..00000000000 --- a/tests/gold_tests/logging/log-debug-client-ip.test.py +++ /dev/null @@ -1,31 +0,0 @@ -''' -Verify debug logging filtered by client IP. -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -Test.Summary = ''' -Verify per-client-IP debug logging emits request and response header dumps. -''' - -Test.ContinueOnFail = True - -Test.ATSReplayTest(replay_file='replay/log-debug-client-ip-http.replay.yaml') -Test.ATSReplayTest(replay_file='replay/log-debug-client-ip-https.replay.yaml') -Test.ATSReplayTest(replay_file='replay/log-debug-client-ip-http2.replay.yaml') - -if Condition.HasATSFeature('TS_HAS_QUICHE') and Condition.HasCurlFeature('http3'): - Test.ATSReplayTest(replay_file='replay/log-debug-client-ip-http3.replay.yaml') diff --git a/tests/gold_tests/logging/replay/log-debug-client-ip-http.replay.yaml b/tests/gold_tests/logging/replay/log-debug-client-ip-http.test.yaml similarity index 100% rename from tests/gold_tests/logging/replay/log-debug-client-ip-http.replay.yaml rename to tests/gold_tests/logging/replay/log-debug-client-ip-http.test.yaml diff --git a/tests/gold_tests/logging/replay/log-debug-client-ip-http2.replay.yaml b/tests/gold_tests/logging/replay/log-debug-client-ip-http2.test.yaml similarity index 100% rename from tests/gold_tests/logging/replay/log-debug-client-ip-http2.replay.yaml rename to tests/gold_tests/logging/replay/log-debug-client-ip-http2.test.yaml diff --git a/tests/gold_tests/logging/replay/log-debug-client-ip-http3.replay.yaml b/tests/gold_tests/logging/replay/log-debug-client-ip-http3.test.yaml similarity index 98% rename from tests/gold_tests/logging/replay/log-debug-client-ip-http3.replay.yaml rename to tests/gold_tests/logging/replay/log-debug-client-ip-http3.test.yaml index d0599f91a07..1a58e7f9ec0 100644 --- a/tests/gold_tests/logging/replay/log-debug-client-ip-http3.replay.yaml +++ b/tests/gold_tests/logging/replay/log-debug-client-ip-http3.test.yaml @@ -19,6 +19,9 @@ meta: autest: description: 'Verify client-IP-filtered debug logging for HTTP/3' + requires: + ats_features: [TS_HAS_QUICHE] + curl_features: [http3] server: name: 'server-log-debug-client-ip-http3' diff --git a/tests/gold_tests/logging/replay/log-debug-client-ip-https.replay.yaml b/tests/gold_tests/logging/replay/log-debug-client-ip-https.test.yaml similarity index 100% rename from tests/gold_tests/logging/replay/log-debug-client-ip-https.replay.yaml rename to tests/gold_tests/logging/replay/log-debug-client-ip-https.test.yaml diff --git a/tests/gold_tests/origin_connection/minimum_keep_alive.replay.yaml b/tests/gold_tests/origin_connection/minimum_keep_alive.test.yaml similarity index 96% rename from tests/gold_tests/origin_connection/minimum_keep_alive.replay.yaml rename to tests/gold_tests/origin_connection/minimum_keep_alive.test.yaml index 51eb27a7845..5ce491c5e04 100644 --- a/tests/gold_tests/origin_connection/minimum_keep_alive.replay.yaml +++ b/tests/gold_tests/origin_connection/minimum_keep_alive.test.yaml @@ -28,6 +28,10 @@ autest: ats: name: 'ts' + metric_checks: + - metric: proxy.process.http.current_server_connections + value: 1 + delay: 5 process_config: enable_tls: false enable_cache: false diff --git a/tests/gold_tests/origin_connection/per_server_connection_min.test.py b/tests/gold_tests/origin_connection/per_server_connection_min.test.py deleted file mode 100644 index 40bf5da9c0b..00000000000 --- a/tests/gold_tests/origin_connection/per_server_connection_min.test.py +++ /dev/null @@ -1,29 +0,0 @@ -''' -Verify the behavior of proxy.config.http.per_server.connection.min. -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -Test.Summary = __doc__ - -replay_run = Test.ATSReplayTest(replay_file='minimum_keep_alive.replay.yaml') -checker = replay_run.Processes.Process('connection-count') -checker.Command = 'sleep 5; traffic_ctl metric get proxy.process.http.current_server_connections' -checker.ReturnCode = 0 -checker.Env = replay_run.Processes.ts.Env -checker.Streams.stdout = Testers.ContainsExpression( - r'^proxy\.process\.http\.current_server_connections\s+1$', 'The origin connection pool should retain exactly one connection.') -checker.StartBefore(replay_run.Processes.ts) diff --git a/tests/gold_tests/parent_proxy/parent_retry_availability.test.py b/tests/gold_tests/parent_proxy/parent_retry_availability.test.py deleted file mode 100644 index 51a7b1448d1..00000000000 --- a/tests/gold_tests/parent_proxy/parent_retry_availability.test.py +++ /dev/null @@ -1,26 +0,0 @@ -""" -Verify the retry path restores a parent only when the retry actually succeeds. -""" -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -Test.Summary = ''' -Both outcomes of a parent retry: a parent that is still up but not serving stays -marked down, and a parent that has recovered is restored to the pool. -''' - -Test.ATSReplayTest(replay_file='replays/parent_retry_failure_stays_down.replay.yaml') -Test.ATSReplayTest(replay_file='replays/parent_retry_success_restores.replay.yaml') diff --git a/tests/gold_tests/parent_proxy/parent_retry_non_retryable_post.test.py b/tests/gold_tests/parent_proxy/parent_retry_non_retryable_post.test.py deleted file mode 100644 index 7c9a0e68dca..00000000000 --- a/tests/gold_tests/parent_proxy/parent_retry_non_retryable_post.test.py +++ /dev/null @@ -1,25 +0,0 @@ -""" -Verify parent_retry does not mark the parent down for non-retryable requests. -""" -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -Test.Summary = ''' -A POST that elicits a configured unavailable_server_retry response code (502) -from the parent must not cause the parent to be marked down. -''' - -Test.ATSReplayTest(replay_file='replays/parent_retry_non_retryable_post.replay.yaml') diff --git a/tests/gold_tests/parent_proxy/replays/parent_retry_failure_stays_down.replay.yaml b/tests/gold_tests/parent_proxy/replays/parent_retry_failure_stays_down.test.yaml similarity index 100% rename from tests/gold_tests/parent_proxy/replays/parent_retry_failure_stays_down.replay.yaml rename to tests/gold_tests/parent_proxy/replays/parent_retry_failure_stays_down.test.yaml diff --git a/tests/gold_tests/parent_proxy/replays/parent_retry_non_retryable_post.replay.yaml b/tests/gold_tests/parent_proxy/replays/parent_retry_non_retryable_post.test.yaml similarity index 93% rename from tests/gold_tests/parent_proxy/replays/parent_retry_non_retryable_post.replay.yaml rename to tests/gold_tests/parent_proxy/replays/parent_retry_non_retryable_post.test.yaml index 68c0f55d699..ab1a8de0b46 100644 --- a/tests/gold_tests/parent_proxy/replays/parent_retry_non_retryable_post.replay.yaml +++ b/tests/gold_tests/parent_proxy/replays/parent_retry_non_retryable_post.test.yaml @@ -34,6 +34,10 @@ autest: # still attempt a body-less retry that proxy-verifier reports as a body # underrun). The security-relevant assertion is on the ATS diags log. return_code: [0, 1] + process_config: + # Finish before Proxy Verifier's origin-side body read times out. That + # later timeout exercises a separate parent connection-failure path. + poll_timeout: 1000 ats: name: 'ts' diff --git a/tests/gold_tests/parent_proxy/replays/parent_retry_success_restores.replay.yaml b/tests/gold_tests/parent_proxy/replays/parent_retry_success_restores.test.yaml similarity index 100% rename from tests/gold_tests/parent_proxy/replays/parent_retry_success_restores.replay.yaml rename to tests/gold_tests/parent_proxy/replays/parent_retry_success_restores.test.yaml diff --git a/tests/gold_tests/pluginTest/filter_body/filter_body.test.py b/tests/gold_tests/pluginTest/filter_body/filter_body.test.py deleted file mode 100644 index 12931b23c4d..00000000000 --- a/tests/gold_tests/pluginTest/filter_body/filter_body.test.py +++ /dev/null @@ -1,24 +0,0 @@ -''' -Verify filter_body plugin for request/response body content filtering. -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -Test.Summary = 'Verify filter_body plugin for request/response body content filtering.' - -Test.SkipUnless(Condition.PluginExists('filter_body.so')) - -Test.ATSReplayTest(replay_file="filter_body.replay.yaml") diff --git a/tests/gold_tests/pluginTest/filter_body/filter_body.replay.yaml b/tests/gold_tests/pluginTest/filter_body/filter_body.test.yaml similarity index 99% rename from tests/gold_tests/pluginTest/filter_body/filter_body.replay.yaml rename to tests/gold_tests/pluginTest/filter_body/filter_body.test.yaml index 5cb4f8bb24d..cc652dda4e5 100644 --- a/tests/gold_tests/pluginTest/filter_body/filter_body.replay.yaml +++ b/tests/gold_tests/pluginTest/filter_body/filter_body.test.yaml @@ -23,6 +23,8 @@ meta: autest: description: 'Verify filter_body plugin for request/response body content filtering' + requires: + plugins: [filter_body.so] server: name: 'server' diff --git a/tests/gold_tests/pluginTest/header_rewrite/header_rewrite_bundle.test.py b/tests/gold_tests/pluginTest/header_rewrite/header_rewrite_bundle.test.py deleted file mode 100644 index 658f90a5f86..00000000000 --- a/tests/gold_tests/pluginTest/header_rewrite/header_rewrite_bundle.test.py +++ /dev/null @@ -1,24 +0,0 @@ -''' -Test header_rewrite with URL conditions and operators. -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -Test.Summary = ''' -A set of remap rules and tests for header_rewrite. -''' - -Test.ATSReplayTest(replay_file="header_rewrite_bundle.replay.yaml",) diff --git a/tests/gold_tests/pluginTest/header_rewrite/header_rewrite_bundle.replay.yaml b/tests/gold_tests/pluginTest/header_rewrite/header_rewrite_bundle.test.yaml similarity index 100% rename from tests/gold_tests/pluginTest/header_rewrite/header_rewrite_bundle.replay.yaml rename to tests/gold_tests/pluginTest/header_rewrite/header_rewrite_bundle.test.yaml diff --git a/tests/gold_tests/pluginTest/header_rewrite/header_rewrite_post_remap.test.py b/tests/gold_tests/pluginTest/header_rewrite/header_rewrite_post_remap.test.py deleted file mode 100644 index 3ea824b4b1e..00000000000 --- a/tests/gold_tests/pluginTest/header_rewrite/header_rewrite_post_remap.test.py +++ /dev/null @@ -1,24 +0,0 @@ -''' -Test header_rewrite POST_REMAP_HOOK support. -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -Test.Summary = ''' -Test header_rewrite attaching a ruleset to the POST_REMAP_HOOK. -''' - -Test.ATSReplayTest(replay_file="header_rewrite_post_remap.replay.yaml",) diff --git a/tests/gold_tests/pluginTest/header_rewrite/header_rewrite_post_remap.replay.yaml b/tests/gold_tests/pluginTest/header_rewrite/header_rewrite_post_remap.test.yaml similarity index 100% rename from tests/gold_tests/pluginTest/header_rewrite/header_rewrite_post_remap.replay.yaml rename to tests/gold_tests/pluginTest/header_rewrite/header_rewrite_post_remap.test.yaml diff --git a/tests/gold_tests/pluginTest/prefetch/prefetch_query_path_traversal.test.py b/tests/gold_tests/pluginTest/prefetch/prefetch_query_path_traversal.test.py deleted file mode 100644 index 908757bae4e..00000000000 --- a/tests/gold_tests/pluginTest/prefetch/prefetch_query_path_traversal.test.py +++ /dev/null @@ -1,24 +0,0 @@ -''' -Test prefetch.so query prefetch path traversal handling. -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -Test.Summary = ''' -Test prefetch.so query prefetch path traversal handling. -''' - -Test.ATSReplayTest(replay_file="prefetch_query_path_traversal.replay.yaml") diff --git a/tests/gold_tests/pluginTest/prefetch/prefetch_query_path_traversal.replay.yaml b/tests/gold_tests/pluginTest/prefetch/prefetch_query_path_traversal.test.yaml similarity index 100% rename from tests/gold_tests/pluginTest/prefetch/prefetch_query_path_traversal.replay.yaml rename to tests/gold_tests/pluginTest/prefetch/prefetch_query_path_traversal.test.yaml diff --git a/tests/gold_tests/pluginTest/slice/replay/slice_long_etag.replay.yaml b/tests/gold_tests/pluginTest/slice/replay/slice_long_etag.test.yaml similarity index 99% rename from tests/gold_tests/pluginTest/slice/replay/slice_long_etag.replay.yaml rename to tests/gold_tests/pluginTest/slice/replay/slice_long_etag.test.yaml index 5ed5de8b28d..4db9da4a131 100644 --- a/tests/gold_tests/pluginTest/slice/replay/slice_long_etag.replay.yaml +++ b/tests/gold_tests/pluginTest/slice/replay/slice_long_etag.test.yaml @@ -19,6 +19,8 @@ meta: autest: description: 'Verify slice plugin handles long ETag values without crashing' + requires: + plugins: [slice.so] server: name: 'server-long-etag' diff --git a/tests/gold_tests/pluginTest/slice/slice_long_etag.test.py b/tests/gold_tests/pluginTest/slice/slice_long_etag.test.py deleted file mode 100644 index 447822220b0..00000000000 --- a/tests/gold_tests/pluginTest/slice/slice_long_etag.test.py +++ /dev/null @@ -1,27 +0,0 @@ -''' -Verify slice plugin handles long ETag values without crashing. -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -Test.Summary = ''' -Verify the slice plugin handles long ETag values (4000 chars) without crashing -when copying between internal buffers during multi-block range requests. -''' - -Test.SkipUnless(Condition.PluginExists('slice.so'),) - -Test.ATSReplayTest(replay_file="replay/slice_long_etag.replay.yaml") diff --git a/tests/gold_tests/pluginTest/test_hooks/body_buffer.test.py b/tests/gold_tests/pluginTest/test_hooks/body_buffer.test.py deleted file mode 100644 index 16c966dd3d7..00000000000 --- a/tests/gold_tests/pluginTest/test_hooks/body_buffer.test.py +++ /dev/null @@ -1,27 +0,0 @@ -''' -Verify HTTP body buffering with request_buffer.so plugin. -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -Test.Summary = 'Verify HTTP body buffering with request_buffer.so plugin.' - -Test.SkipUnless(Condition.PluginExists('request_buffer.so')) - -Test.ATSReplayTest(replay_file="replay/body_buffer.replay.yaml") - -# Test for issue #6900: post_copy_size=0 with request_buffer_enabled should not cause 403. -Test.ATSReplayTest(replay_file="replay/zero_post_copy_size.replay.yaml") diff --git a/tests/gold_tests/pluginTest/test_hooks/replay/body_buffer.replay.yaml b/tests/gold_tests/pluginTest/test_hooks/replay/body_buffer.test.yaml similarity index 99% rename from tests/gold_tests/pluginTest/test_hooks/replay/body_buffer.replay.yaml rename to tests/gold_tests/pluginTest/test_hooks/replay/body_buffer.test.yaml index a6ae264113f..8c7f40c8e45 100644 --- a/tests/gold_tests/pluginTest/test_hooks/replay/body_buffer.replay.yaml +++ b/tests/gold_tests/pluginTest/test_hooks/replay/body_buffer.test.yaml @@ -22,6 +22,8 @@ meta: autest: description: 'Verify request body buffering with request_buffer.so plugin' + requires: + plugins: [request_buffer.so] server: name: 'body-buffer-server' diff --git a/tests/gold_tests/pluginTest/test_hooks/replay/zero_post_copy_size.replay.yaml b/tests/gold_tests/pluginTest/test_hooks/replay/zero_post_copy_size.test.yaml similarity index 98% rename from tests/gold_tests/pluginTest/test_hooks/replay/zero_post_copy_size.replay.yaml rename to tests/gold_tests/pluginTest/test_hooks/replay/zero_post_copy_size.test.yaml index c34af71d870..0b9532a3343 100644 --- a/tests/gold_tests/pluginTest/test_hooks/replay/zero_post_copy_size.replay.yaml +++ b/tests/gold_tests/pluginTest/test_hooks/replay/zero_post_copy_size.test.yaml @@ -24,6 +24,8 @@ meta: autest: description: 'Verify post_copy_size=0 with request_buffer_enabled does not cause 403 (issue #6900)' + requires: + plugins: [request_buffer.so] server: name: 'zero-copy-server' diff --git a/tests/gold_tests/pluginTest/webp_transform/webp_transform_invalid_input.test.py b/tests/gold_tests/pluginTest/webp_transform/webp_transform_invalid_input.test.py deleted file mode 100644 index e5c4961b909..00000000000 --- a/tests/gold_tests/pluginTest/webp_transform/webp_transform_invalid_input.test.py +++ /dev/null @@ -1,25 +0,0 @@ -''' -Verify webp_transform rejects invalid image input before invoking ImageMagick. -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, -# software distributed under the License 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. - -Test.Summary = 'Verify webp_transform safely passes through invalid image bodies' - -Test.SkipUnless(Condition.PluginExists('webp_transform.so')) - -Test.ATSReplayTest(replay_file='webp_transform_invalid_input.replay.yaml') diff --git a/tests/gold_tests/pluginTest/webp_transform/webp_transform_invalid_input.replay.yaml b/tests/gold_tests/pluginTest/webp_transform/webp_transform_invalid_input.test.yaml similarity index 99% rename from tests/gold_tests/pluginTest/webp_transform/webp_transform_invalid_input.replay.yaml rename to tests/gold_tests/pluginTest/webp_transform/webp_transform_invalid_input.test.yaml index ccc8cc5de24..ec7f1d59cd2 100644 --- a/tests/gold_tests/pluginTest/webp_transform/webp_transform_invalid_input.replay.yaml +++ b/tests/gold_tests/pluginTest/webp_transform/webp_transform_invalid_input.test.yaml @@ -20,6 +20,8 @@ meta: autest: description: 'Verify webp_transform safely passes through invalid image bodies' + requires: + plugins: [webp_transform.so] server: name: 'webp-transform-server' diff --git a/tests/gold_tests/pluginTest/xdebug/x_remap_long_url/x_remap_long_url.test.py b/tests/gold_tests/pluginTest/xdebug/x_remap_long_url/x_remap_long_url.test.py deleted file mode 100644 index d68f7b148b7..00000000000 --- a/tests/gold_tests/pluginTest/xdebug/x_remap_long_url/x_remap_long_url.test.py +++ /dev/null @@ -1,30 +0,0 @@ -""" -Verify xdebug X-Remap header injection does not OOB read with long URLs. -""" -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -Test.Summary = ''' -Send a request with a URL long enough that the combined from/to URL -output exceeds the 2KB stack buffer xdebug uses for the X-Remap header. -Previously, snprintf's would-have-written return value was passed -straight to TSMimeHdrFieldValueStringInsert, causing an OOB read of -adjacent stack memory into the response header. -''' - -Test.SkipUnless(Condition.PluginExists('xdebug.so')) - -Test.ATSReplayTest(replay_file='x_remap_long_url.replay.yaml') diff --git a/tests/gold_tests/pluginTest/xdebug/x_remap_long_url/x_remap_long_url.replay.yaml b/tests/gold_tests/pluginTest/xdebug/x_remap_long_url/x_remap_long_url.test.yaml similarity index 99% rename from tests/gold_tests/pluginTest/xdebug/x_remap_long_url/x_remap_long_url.replay.yaml rename to tests/gold_tests/pluginTest/xdebug/x_remap_long_url/x_remap_long_url.test.yaml index 01b01b28199..af01f26f6b3 100644 --- a/tests/gold_tests/pluginTest/xdebug/x_remap_long_url/x_remap_long_url.replay.yaml +++ b/tests/gold_tests/pluginTest/xdebug/x_remap_long_url/x_remap_long_url.test.yaml @@ -19,6 +19,8 @@ meta: autest: description: 'xdebug X-Remap injection must not OOB read on long URLs' + requires: + plugins: [xdebug.so] server: name: 'server' diff --git a/tests/gold_tests/post/post-no-cl.test.py b/tests/gold_tests/post/post-no-cl.test.py deleted file mode 100644 index 267f7c83e3f..00000000000 --- a/tests/gold_tests/post/post-no-cl.test.py +++ /dev/null @@ -1,25 +0,0 @@ -''' -Verify that POST requests without a body are accepted. -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -Test.Summary = ''' -Verify that POST request without a Content-Length or Transfer-Encoding header is -rejected with a 411. -''' - -Test.ATSReplayTest(replay_file='replay/post-no-cl.replay.yaml') diff --git a/tests/gold_tests/post/replay/post-no-cl.replay.yaml b/tests/gold_tests/post/replay/post-no-cl.test.yaml similarity index 100% rename from tests/gold_tests/post/replay/post-no-cl.replay.yaml rename to tests/gold_tests/post/replay/post-no-cl.test.yaml diff --git a/tests/gold_tests/remap/regex_map_anchor.test.py b/tests/gold_tests/remap/regex_map_anchor.test.py deleted file mode 100644 index 7b27a0036cc..00000000000 --- a/tests/gold_tests/remap/regex_map_anchor.test.py +++ /dev/null @@ -1,35 +0,0 @@ -''' -Verify regex_map performs full-hostname matching. - -A regex_map rule must match the entire request hostname, never a host in -which the configured value appears only as a substring. For a rule covering -"cdn.example.com": - - "prefix.cdn.example.com" must NOT match: rejected by the start anchor. - - "cdn.example.com.evil.com" must NOT match: rejected by full-hostname matching - (start-anchoring alone would still match it). - - "cdn.example.computer" must NOT match: shared prefix, no label boundary. - - "cdn.example.com." must NOT match: trailing-dot FQDN. - - "cdn.example.com" must match (exact), and reach the origin. - - "CDN.EXAMPLE.COM" must match (case-insensitive), and reach the origin. -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -Test.Summary = ''' -Verify regex_map performs full-hostname matching. -''' - -Test.ATSReplayTest(replay_file="replay/regex_map_anchor.replay.yaml") diff --git a/tests/gold_tests/remap/replay/regex_map_anchor.replay.yaml b/tests/gold_tests/remap/replay/regex_map_anchor.test.yaml similarity index 100% rename from tests/gold_tests/remap/replay/regex_map_anchor.replay.yaml rename to tests/gold_tests/remap/replay/regex_map_anchor.test.yaml diff --git a/tests/gold_tests/statistics/metric_response_429.test.py b/tests/gold_tests/statistics/metric_response_429.test.py deleted file mode 100644 index ae4e5e58386..00000000000 --- a/tests/gold_tests/statistics/metric_response_429.test.py +++ /dev/null @@ -1,20 +0,0 @@ -'''Verify the proxy.process.http.429_responses metric.''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -Test.Summary = __doc__ - -Test.ATSReplayTest(replay_file="replay/metric_response_429.replay.yaml") diff --git a/tests/gold_tests/statistics/replay/metric_response_429.replay.yaml b/tests/gold_tests/statistics/replay/metric_response_429.test.yaml similarity index 100% rename from tests/gold_tests/statistics/replay/metric_response_429.replay.yaml rename to tests/gold_tests/statistics/replay/metric_response_429.test.yaml diff --git a/tests/gold_tests/tls/replay/outbound_sni_server_name_plain.replay.yaml b/tests/gold_tests/tls/replay/outbound_sni_server_name_plain.test.yaml similarity index 100% rename from tests/gold_tests/tls/replay/outbound_sni_server_name_plain.replay.yaml rename to tests/gold_tests/tls/replay/outbound_sni_server_name_plain.test.yaml diff --git a/tests/gold_tests/tls/tls_outbound_sni_server_name_plain.test.py b/tests/gold_tests/tls/tls_outbound_sni_server_name_plain.test.py deleted file mode 100644 index 989255ba773..00000000000 --- a/tests/gold_tests/tls/tls_outbound_sni_server_name_plain.test.py +++ /dev/null @@ -1,25 +0,0 @@ -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information regarding -# copyright ownership. The ASF licenses this file to you under -# the Apache License, Version 2.0 (the "License"); you may not use -# this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. -''' -Verify outbound SNI server_name policy on plain inbound traffic. -''' - -Test.Summary = ''' -Verify that plain inbound transactions do not crash when outbound SNI is -configured to use the inbound TLS server name. -''' - -Test.ATSReplayTest(replay_file='replay/outbound_sni_server_name_plain.replay.yaml') diff --git a/tests/pyproject.toml b/tests/pyproject.toml index f976251df3b..3d7237dea03 100644 --- a/tests/pyproject.toml +++ b/tests/pyproject.toml @@ -46,6 +46,11 @@ dependencies = [ "python-jose", "pyyaml", + # Pytest drives replay-based end-to-end tests. xdist gives each worker an + # independent sandbox and lets CI run those tests concurrently. + "pytest>=8.0", + "pytest-xdist>=3.6", + # For the grpc tests. "grpcio", "grpcio-tools", @@ -62,4 +67,3 @@ dependencies = [ dev = [ "pyflakes", ] - diff --git a/tests/pytest_tests/test_config.py b/tests/pytest_tests/test_config.py new file mode 100644 index 00000000000..42d6a1fc974 --- /dev/null +++ b/tests/pytest_tests/test_config.py @@ -0,0 +1,102 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. +"""Unit tests for replay metadata parsing.""" + +from pathlib import Path + +import pytest +import yaml + +from ats_testkit.config import ReplayConfigError, ReplaySpec, merge_flat_records, replace_server_ports + + +def test_all_migrated_replays_are_valid() -> None: + """Verify every directly collected replay has valid test metadata.""" + + gold_tests = Path(__file__).parents[1] / "gold_tests" + replay_files = list(gold_tests.rglob("*.test.yaml")) + assert len(replay_files) == 96 + for path in replay_files: + ReplaySpec.load(path) + + +def test_no_gold_test_registers_an_ats_replay() -> None: + """Keep replay ownership out of legacy Python wrappers.""" + + gold_tests = Path(__file__).parents[1] / "gold_tests" + registrations = [path for path in gold_tests.rglob("*.test.py") if "Test.ATSReplayTest(" in path.read_text()] + assert registrations == [] + + +def test_replay_requires_autest_metadata(tmp_path: Path) -> None: + """Reject a Proxy Verifier file that has not opted into direct collection.""" + + path = tmp_path / "missing.test.yaml" + path.write_text(yaml.safe_dump({"meta": {"version": "1.0"}, "sessions": []})) + with pytest.raises(ReplayConfigError, match="'autest' mapping"): + ReplaySpec.load(path) + + +def test_replay_accepts_summary_as_description(tmp_path: Path) -> None: + """Keep compatibility with the replay-only AuTest YAML format.""" + + path = tmp_path / "summary.test.yaml" + path.write_text( + yaml.safe_dump({ + "autest": { + "summary": "Summary-only replay", + "server": {}, + "client": {}, + "ats": {}, + }, + "sessions": [], + })) + assert ReplaySpec.load(path).description == "Summary-only replay" + + +def test_flat_records_merge_with_nested_defaults() -> None: + """Build the nested records.yaml shape ATS expects.""" + + result = merge_flat_records( + { + "proxy.config.diags.debug.enabled": 1, + "proxy.config.http.cache.http": 0, + }, + {"http": { + "server_ports": "8080" + }}, + ) + assert result == { + "records": { + "http": { + "server_ports": "8080", + "cache": { + "http": 0 + } + }, + "diags": { + "debug": { + "enabled": 1 + } + }, + } + } + + +def test_server_port_substitution() -> None: + """Substitute both Proxy Verifier origin listener placeholders.""" + + assert replace_server_ports("http://host:{SERVER_HTTP_PORT}/{SERVER_HTTPS_PORT}", 1234, 5678) == "http://host:1234/5678" diff --git a/tests/pytest_tests/test_process.py b/tests/pytest_tests/test_process.py new file mode 100644 index 00000000000..48afaebdbc6 --- /dev/null +++ b/tests/pytest_tests/test_process.py @@ -0,0 +1,64 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. +"""Unit tests for managed process and gold-file behavior.""" + +from pathlib import Path +import sys + +import pytest + +from ats_testkit.process import ManagedProcess, ProcessError +from ats_testkit.replay import ReplayTest + + +def test_managed_process_captures_output(tmp_path: Path) -> None: + """Capture stdout from a successful one-shot process.""" + + process = ManagedProcess("client", [sys.executable, "-c", "print('ready')"], tmp_path) + process.start() + process.wait(5) + assert "ready" in process.output() + + +def test_managed_process_reports_unexpected_status(tmp_path: Path) -> None: + """Include captured diagnostics when a process fails.""" + + process = ManagedProcess("client", [sys.executable, "-c", "print('bad'); raise SystemExit(3)"], tmp_path) + process.start() + with pytest.raises(ProcessError, match="status 3"): + process.wait(5) + assert "bad" in process.output() + + +def test_gold_file_wildcards_match_variable_text(tmp_path: Path) -> None: + """Preserve AuTest's `` and {} wildcard tokens in migrated gold files.""" + + expected = tmp_path / "expected.gold" + actual = tmp_path / "actual.log" + expected.write_text("port=`` id={} done\n") + actual.write_text("port=43127 id=abc-123 done\n") + ReplayTest._validate_gold(actual, expected) + + +def test_gold_file_difference_fails(tmp_path: Path) -> None: + """Report a mismatch when fixed gold-file text changes.""" + + expected = tmp_path / "expected.gold" + actual = tmp_path / "actual.log" + expected.write_text("expected\n") + actual.write_text("actual\n") + with pytest.raises(AssertionError, match="did not match"): + ReplayTest._validate_gold(actual, expected) From 678cb46d9ea6d4ec0a36ca8227d80b4e70e825ab Mon Sep 17 00:00:00 2001 From: bneradt Date: Thu, 13 Aug 2026 15:05:20 -0500 Subject: [PATCH 2/3] Replace AuTest with pytest Uranium tests ATS end-to-end coverage depends on the stagnant AuTest framework and splits replay and bespoke tests across different runner abstractions. This makes the suite harder to maintain, schedule, and debug. This patch replaces AuTest with pytest-owned Uranium tests. Replay YAML is collected directly, while a native scenario layer preserves existing bespoke test behavior with isolated processes, readiness checks, cleanup, filtering, sharding, and Fedora container execution. It also adds procedural ATS and curl fixtures, converts the first basic tests to ordinary pytest, and updates the build, CI, documentation, and test layout around the urtest interface. --- .github/copilot-instructions.md | 4 +- .gitignore | 8 +- AGENTS.md | 44 +- CMakeLists.txt | 42 +- CMakePresets.json | 56 +- ci/coverage | 36 +- ci/jenkins/bin/autest.sh | 2 +- ci/rat-exclude.txt | 4 +- ci/regression | 2 +- contrib/docker/ubuntu/noble/Dockerfile | 2 +- doc/admin-guide/files/records.yaml.en.rst | 49 +- doc/admin-guide/plugins/lua.en.rst | 1 + .../plugins/txn_box/examples.en.rst | 22 +- .../txn_box/user/DirectiveReference.en.rst | 2 +- .../txn_box/user/ModifierReference.en.rst | 10 +- .../command-line/traffic_ctl.en.rst | 4 +- .../api/functions/TSHttpHookAdd.en.rst | 2 +- .../functions/TSHttpOverridableConfig.en.rst | 1 + .../api/types/TSOverridableConfigKey.en.rst | 1 + .../shm-fast-restart.en.rst | 6 +- .../config-reload-framework.en.rst | 10 +- .../jsonrpc-handler-development.en.rst | 4 +- .../testing/config-reload-ext.en.rst | 2 +- doc/developer-guide/testing/index.en.rst | 2 +- .../{autests.en.rst => uranium-tests.en.rst} | 123 +- .../c-api/txn_data_sink/txn_data_sink.cc | 4 +- include/cripts/Configs.hpp | 1 + include/proxy/http/HttpConfig.h | 1 + include/proxy/http/OverridableConfigDefs.h | 3 +- include/ts/apidefs.h.in | 1 + plugins/experimental/jax_fingerprint/README | 2 +- src/iocore/cache/CacheRead.cc | 2 +- .../net/unit_tests/test_OCSPStapling.cc | 2 +- src/proxy/http/HttpConfig.cc | 4 +- src/proxy/http/HttpTransact.cc | 14 +- src/records/RecordsConfig.cc | 6 +- .../unit_tests/test_ConfigReloadTask.cc | 2 +- tests/CMakeLists.txt | 78 +- tests/README.md | 132 +- tests/ats_testkit/plugin.py | 154 -- tests/autest-parallel.py.in | 1151 ----------- tests/autest.sh | 40 - tests/autest.sh.in | 75 - tests/gold_tests/ats_probe/ats_probe.test.py | 112 -- .../ats_probe/cache_dir_probe.test.py | 122 -- tests/gold_tests/autest-site/.editorconfig | 26 - .../autest-site/ats_replay.test.ext | 435 ----- .../gold_tests/autest-site/cli_tools.test.ext | 48 - .../autest-site/conditions.test.ext | 383 ---- .../autest-site/config_reload.test.ext | 225 --- tests/gold_tests/autest-site/curl.test.ext | 78 - .../autest-site/curl_header.test.ext | 206 -- tests/gold_tests/autest-site/httpbin.test.ext | 47 - tests/gold_tests/autest-site/init.cli.ext | 47 - tests/gold_tests/autest-site/ip.test.ext | 39 - .../autest-site/jsonrpc_client.test.ext | 293 --- .../gold_tests/autest-site/microDNS.test.ext | 123 -- .../autest-site/microserver.test.ext | 266 --- .../autest-site/ordered_set_queue.py | 110 -- tests/gold_tests/autest-site/ports.py | 313 --- tests/gold_tests/autest-site/readme.md | 31 - tests/gold_tests/autest-site/setup.cli.ext | 126 -- .../autest-site/traffic_replay.test.ext | 99 - .../autest-site/trafficserver.test.ext | 728 ------- .../trafficserver_plugins.test.ext | 109 -- tests/gold_tests/autest-site/txn_box.test.ext | 250 --- .../autest-site/verifier_client.test.ext | 230 --- .../autest-site/verifier_server.test.ext | 255 --- tests/gold_tests/autest-site/when.test.ext | 101 - .../bad_http_fmt/bad_http_fmt.test.py | 186 -- tests/gold_tests/basic/config.test.py | 30 - tests/gold_tests/basic/copy_config.test.py | 48 - tests/gold_tests/basic/copy_config2.test.py | 43 - tests/gold_tests/basic/deny0.test.py | 113 -- tests/gold_tests/bigobj/bigobj.test.py | 149 -- .../body_factory/http204_response.test.py | 86 - .../http204_response_plugin.test.py | 51 - .../body_factory/http304_response.test.py | 50 - .../body_factory/http_head_no_origin.test.py | 42 - .../body_factory/http_with_origin.test.py | 103 - .../gold_tests/cache/background_fill.test.py | 189 -- .../cache/cache-generation-clear.test.py | 101 - .../cache/cache-generation-disjoint.test.py | 101 - .../cache/cache-write-lock-contention.test.py | 175 -- .../cache/cache_config_reload.test.py | 64 - .../cache/cache_corrupt_recursive_uaf.test.py | 127 -- .../cache/cache_shm_bad_disk_dropped.test.py | 232 --- .../cache/cache_shm_concurrent_attach.test.py | 185 -- .../cache_shm_control_size_mismatch.test.py | 202 -- .../cache/cache_shm_dir_invalid.test.py | 237 --- .../cache/cache_shm_fast_restart.test.py | 247 --- .../cache/cache_shm_purge_on_disable.test.py | 232 --- .../cache/cache_shm_schema_mismatch.test.py | 207 -- .../cache/cache_shm_storage_mismatch.test.py | 210 -- .../cache/cache_shm_unclean_shutdown.test.py | 185 -- .../cache/disjoint-wait-for-cache.test.py | 104 - .../cache/host_down_range_recursion.test.py | 81 - .../gold_tests/cache/negative-caching.test.py | 125 -- .../cache/proxy_serve_stale_dns_fail.test.py | 81 - .../gold_tests/cache/storage-metrics.test.py | 129 -- .../bad_chunked_encoding.test.py | 223 --- .../chunk_extension_quoted_string.test.py | 84 - .../chunk_trailer_bare_lf.test.py | 86 - .../chunked_encoding/chunked_encoding.test.py | 263 --- .../chunked_encoding_disabled.test.py | 75 - .../chunked_encoding_h2.test.py | 120 -- .../per_client_connection_max.test.py | 247 --- .../verify_global_plugin.test.py | 143 -- .../verify_remap_plugin.test.py | 121 -- .../config_destroy_thread.test.py | 77 - tests/gold_tests/connect/connect.test.py | 257 --- .../connect/connect_handshake.test.py | 65 - .../connect/connect_parent_error_body.test.py | 72 - .../h2_malformed_request_logging.test.py | 202 -- .../connect_down_policy.test.py | 113 -- .../schedule_every_on_entire_pool.test.py | 64 - .../schedule_every_on_pool.test.py | 62 - .../schedule_every_on_thread.test.py | 62 - .../schedule_on_entire_pool.test.py | 64 - .../cont_schedule/schedule_on_pool.test.py | 49 - .../cont_schedule/schedule_on_thread.test.py | 49 - .../cont_schedule/thread_affinity.test.py | 49 - tests/gold_tests/continuations/double.test.py | 134 -- .../continuations/double_h2.test.py | 155 -- .../continuations/openclose.test.py | 142 -- .../continuations/openclose_h2.test.py | 158 -- .../continuations/session_id.test.py | 116 -- tests/gold_tests/cripts/cripts.test.py | 142 -- .../dns/dns_down_nameserver.test.py | 187 -- tests/gold_tests/dns/dns_host_down.test.py | 95 - .../gold_tests/dns/dns_reverse_lookup.test.py | 86 - tests/gold_tests/dns/dns_ttl.test.py | 143 -- tests/gold_tests/dns/splitdns.test.py | 77 - tests/gold_tests/dns/splitdns_reload.test.py | 80 - .../early_hints/early_hints.test.py | 175 -- .../forward_proxy/forward_proxy.test.py | 115 -- tests/gold_tests/h2/grpc/grpc.test.py | 164 -- .../h2/h2_origin_cert_reverify.test.py | 78 - .../h2/h2_origin_trailers_h1.test.py | 94 - tests/gold_tests/h2/h2disable.test.py | 96 - .../h2/h2disable_no_accept_threads.test.py | 96 - tests/gold_tests/h2/h2enable.test.py | 97 - .../h2/h2enable_no_accept_threads.test.py | 96 - tests/gold_tests/h2/h2get_with_body.test.py | 71 - tests/gold_tests/h2/h2origin.test.py | 123 -- .../h2/h2origin_single_thread.test.py | 92 - tests/gold_tests/h2/h2spec.test.py | 74 - tests/gold_tests/h2/http2.test.py | 286 --- .../h2/http2_close_connection.test.py | 70 - .../h2/http2_concurrent_streams.test.py | 64 - .../h2/http2_crlf_header_validation.test.py | 97 - .../h2/http2_empty_data_frame.test.py | 77 - .../gold_tests/h2/http2_flow_control.test.py | 429 ----- .../h2/http2_max_active_streams.test.py | 86 - tests/gold_tests/h2/http2_priority.test.py | 86 - tests/gold_tests/h2/http2_rst_stream.test.py | 170 -- .../h2/http2_txn_start_read_gate.test.py | 63 - .../h2/http2_write_threshold.test.py | 143 -- tests/gold_tests/h2/httpbin.test.py | 136 -- tests/gold_tests/h2/nghttp.test.py | 97 - tests/gold_tests/h3/h3_curl.test.py | 143 -- tests/gold_tests/h3/h3_go_client.test.py | 130 -- tests/gold_tests/h3/h3_python_client.test.py | 132 -- tests/gold_tests/h3/h3_range_cache.test.py | 140 -- tests/gold_tests/h3/h3_session_ticket.test.py | 110 -- tests/gold_tests/h3/h3_sni_check.test.py | 131 -- .../headers/cache_and_req_body.test.py | 159 -- .../headers/field_name_space.test.py | 53 - tests/gold_tests/headers/forwarded.test.py | 267 --- .../general-connection-failure-502.test.py | 45 - .../headers/good_request_after_bad.test.py | 192 -- tests/gold_tests/headers/http408.test.py | 56 - tests/gold_tests/headers/normalize_ae.test.py | 200 -- .../headers/oversized_field_h2.test.py | 142 -- tests/gold_tests/headers/syntax.test.py | 99 - tests/gold_tests/headers/via.test.py | 155 -- .../ip_allow/connect_destination_acl.test.py | 174 -- tests/gold_tests/ip_allow/ip_allow.test.py | 366 ---- .../ip_allow_reload_triggered.test.py | 218 --- tests/gold_tests/ip_allow/ip_category.test.py | 323 ---- .../jsonrpc/basic_plugin_handler.test.py | 186 -- .../jsonrpc/config_reload_dedup.test.py | 160 -- .../jsonrpc/config_reload_failures.test.py | 429 ----- .../jsonrpc/config_reload_full_smoke.test.py | 165 -- .../config_reload_reserve_subtask.test.py | 134 -- .../jsonrpc/config_reload_rpc.test.py | 491 ----- .../jsonrpc/config_reload_tracking.test.py | 304 --- .../jsonrpc/jsonrpc_api_schema.test.py | 220 --- tests/gold_tests/logging/all_headers.test.py | 115 -- .../gold_tests/logging/binary_log_v3.test.py | 170 -- tests/gold_tests/logging/custom-log.test.py | 92 - .../gold_tests/logging/log-field-json.test.py | 120 -- tests/gold_tests/logging/log-field.test.py | 121 -- .../gold_tests/logging/log-filenames.test.py | 262 --- tests/gold_tests/logging/log-filter.test.py | 100 - .../logging/log-milestone-fields.test.py | 176 -- tests/gold_tests/logging/log-mstsms.test.py | 119 -- tests/gold_tests/logging/log_pipe.test.py | 186 -- .../logging/log_plugin_init.test.py | 38 - .../gold_tests/logging/log_retention.test.py | 488 ----- tests/gold_tests/logging/new_log_flds.test.py | 119 -- tests/gold_tests/logging/pqsi-pqsp.test.py | 89 - tests/gold_tests/logging/qlog_quiche.test.py | 128 -- tests/gold_tests/logging/sigusr2.test.py | 164 -- .../parent_config_dest_ip.test.py | 125 -- .../strategies_ch/strategies_ch.test.py | 139 -- .../strategies_ch2/strategies_ch2.test.py | 155 -- .../strategies_ch5/strategies_ch5.test.py | 153 -- .../strategies_stale/strategies_stale.test.py | 141 -- .../zzz_strategies_peer.test.py | 177 -- .../zzz_strategies_peer2.test.py | 169 -- .../null_transform/null_transform.test.py | 64 - .../per_server_connection_max.test.py | 184 -- .../per_server_metric_enabled.test.py | 113 -- .../parent_config_reload.test.py | 76 - .../consistent_hash_ring_walk.test.py | 143 -- .../parent_proxy/parent-retry.test.py | 47 - .../delete_maxforwards_body_desync.test.py | 89 - .../pipeline/delete_maxfwd_noop_drain.test.py | 89 - .../pipeline/noop_keepalive.test.py | 92 - tests/gold_tests/pipeline/pipeline.test.py | 251 --- .../pluginTest/TSVConnFd/TSVConnFd.test.py | 72 - .../access_control/access_control.test.py | 83 - .../block_errors/block_errors.test.py | 176 -- .../pluginTest/cache_fill/cache_fill.test.py | 232 --- .../cache_promote/cache_promote.test.py | 77 - .../cache_range_requests.test.py | 368 ---- ..._requests_cache_complete_responses.test.py | 399 ---- .../cache_range_requests_cachekey.test.py | 162 -- ...che_range_requests_cachekey_global.test.py | 145 -- .../cache_range_requests_ident.test.py | 347 ---- .../cert_update/cert_update.test.py | 148 -- .../pluginTest/certifier/certifier.test.py | 294 --- .../client_context_dump.test.py | 81 - .../combo_handler/combo_handler.test.py | 182 -- .../compress-cache-untransformed.test.py | 118 -- .../compress-content-type-params.test.py | 67 - .../compress/compress-range.test.py | 73 - .../pluginTest/compress/compress.test.py | 325 ---- .../pluginTest/conf_remap/conf_remap.test.py | 72 - .../cookie_remap/bucketcookie.test.py | 109 -- .../cookie_remap/collapseslashes.test.py | 78 - .../pluginTest/cookie_remap/connector.test.py | 109 -- .../disable_pristine_host_hdr.test.py | 131 -- .../cookie_remap/existscookie.test.py | 109 -- .../cookie_remap/matchcookie.test.py | 109 -- .../pluginTest/cookie_remap/matchuri.test.py | 108 -- .../cookie_remap/notexistscookie.test.py | 108 -- .../cookie_remap/pcollapseslashes.test.py | 78 - .../cookie_remap/psubstitute.test.py | 142 -- .../cookie_remap/regexcookie.test.py | 109 -- .../pluginTest/cookie_remap/setstatus.test.py | 77 - .../pluginTest/cookie_remap/subcookie.test.py | 111 -- .../cookie_remap/substitute.test.py | 132 -- .../pluginTest/crash_test/crash_test.test.py | 101 - .../pluginTest/escalate/escalate.test.py | 294 --- tests/gold_tests/pluginTest/esi/esi.test.py | 380 ---- .../gold_tests/pluginTest/esi/esi_304.test.py | 180 -- .../esi/esi_nested_html_comment.test.py | 156 -- .../pluginTest/esi/esi_nested_include.test.py | 105 - .../esi/esi_request_size_cap.test.py | 91 - .../pluginTest/esi/esi_ssrf_validate.test.py | 111 -- .../header_rewrite_bad_run_plugin.test.py | 156 -- .../header_rewrite_set_body_from.test.py | 204 -- .../header_rewrite_url_glob.test.py | 64 - .../healthchecks/healthchecks.test.py | 212 --- .../ja3_fingerprint/ja3_fingerprint.test.py | 208 -- .../ja4_fingerprint/ja4_fingerprint.test.py | 207 -- .../jax_fingerprint/jax_fingerprint.test.py | 508 ----- .../pluginTest/lua/lua_client_hook.test.py | 57 - .../pluginTest/lua/lua_debug_tags.test.py | 51 - .../lua/lua_fetch_ipv6_cliaddr.test.py | 59 - .../lua/lua_global_shutdown.test.py | 64 - .../pluginTest/lua/lua_header_table.test.py | 41 - .../pluginTest/lua/lua_proxy_protocol.test.py | 82 - .../lua/lua_remap_after_hook.test.py | 55 - .../lua/lua_set_error_response.test.py | 98 - .../pluginTest/lua/lua_states_stats.test.py | 96 - .../pluginTest/lua/lua_verified_addr.test.py | 79 - .../pluginTest/lua/lua_watermark.test.py | 56 - .../money_trace/money_trace.test.py | 206 -- .../money_trace/money_trace_global.test.py | 106 -- .../multiplexer/multiplexer.test.py | 324 ---- .../origin_server_auth.test.py | 135 -- .../s3_url_encoding.test.py | 198 -- .../packet_mark/packet_mark.test.py | 181 -- .../per_plugin_metrics.test.py | 111 -- .../plugin_yaml/plugin_yaml.test.py | 90 - .../polite_hook_wait/polite_hook_wait.test.py | 71 - .../prefetch_bad_count_refused.test.py | 53 - .../prefetch_bad_pattern_refused.test.py | 54 - .../prefetch/prefetch_bignum.test.py | 79 - .../pluginTest/prefetch/prefetch_cmcd.test.py | 290 --- .../prefetch_empty_replacement.test.py | 90 - .../prefetch/prefetch_optional_group.test.py | 95 - .../prefetch/prefetch_overflow.test.py | 75 - .../prefetch/prefetch_simple.test.py | 75 - .../pluginTest/rate_limit/rate_limit.test.py | 132 -- .../rate_limit/rate_limit_iprep.test.py | 120 -- .../rate_limit/rate_limit_sni.test.py | 121 -- .../rate_limit/rate_limit_sni_expiry.test.py | 87 - .../rate_limit/rate_limit_sni_queue.test.py | 89 - .../rate_limit/rate_limit_sni_reject.test.py | 83 - .../redirect_limit/redirect_limit.test.py | 134 -- .../regex_remap/regex_remap.test.py | 249 --- .../regex_revalidate/regex_revalidate.test.py | 273 --- .../regex_revalidate_miss.test.py | 249 --- .../regex_revalidate_state.test.py | 139 -- .../remap_stats/remap_stats.test.py | 66 - .../remap_stats/remap_stats_post.test.py | 66 - .../server_push_preload.test.py | 104 - .../gold_tests/pluginTest/slice/slice.test.py | 185 -- .../slice/slice_conditional.test.py | 156 -- .../slice/slice_content_shrink.test.py | 94 - .../pluginTest/slice/slice_crr_ident.test.py | 220 --- .../pluginTest/slice/slice_error.test.py | 319 ---- .../pluginTest/slice/slice_ident.test.py | 187 -- .../pluginTest/slice/slice_prefetch.test.py | 214 --- .../pluginTest/slice/slice_purge.test.py | 94 - .../pluginTest/slice/slice_purge_gaps.test.py | 346 ---- .../pluginTest/slice/slice_regex.test.py | 147 -- .../pluginTest/slice/slice_rm_range.test.py | 69 - .../slice/slice_selfhealing.test.py | 423 ----- .../slice/slice_stale_generation.test.py | 327 ---- .../pluginTest/sslheaders/sslheaders.test.py | 78 - .../stale_response/stale_response.test.py | 189 -- .../pluginTest/statichit/statichit.test.py | 111 -- .../stats_over_http/stats_over_http.test.py | 400 ---- .../pluginTest/stek_share/stek_share.test.py | 331 ---- .../strategies/strategies_plugins.test.py | 411 ---- .../pluginTest/test_hooks/hook_add.test.py | 58 - .../test_hooks/ssn_start_delay_hook.test.py | 60 - .../pluginTest/test_hooks/test_hooks.test.py | 104 - .../traffic_dump/traffic_dump.test.py | 429 ----- .../traffic_dump/traffic_dump_http3.test.py | 142 -- .../traffic_dump_ip_filter.test.py | 116 -- .../traffic_dump_response_body.test.py | 144 -- .../traffic_dump_sni_filter.test.py | 128 -- .../transform/transaction_data_sink.test.py | 108 -- .../tsapi/test_TSHttpSsnInfo.test.py | 113 -- .../tsapi/test_TSHttpTxnServerAddrSet.test.py | 91 - .../test_TSHttpTxnServerAddrSet_retry.test.py | 91 - .../tsapi/test_TSHttpTxnVerifiedAddr.test.py | 65 - .../tsapi/test_TSVConnPPInfo.test.py | 112 -- .../gold_tests/pluginTest/tsapi/tsapi.test.py | 112 -- .../txn_box/basic/txn_box_basic.test.py | 44 - .../txn_box/basic/txn_box_mod.test.py | 42 - .../txn_box/basic/txn_box_multi-cfg.test.py | 43 - .../txn_box/basic/txn_box_redirect.test.py | 52 - .../txn_box/basic/txn_box_reply.test.py | 47 - .../txn_box/basic/txn_box_rxp.test.py | 37 - .../txn_box/basic/txn_box_stat.test.py | 60 - .../txn_box/basic/txn_box_tls-cert.test.py | 67 - .../txn_box/basic/txn_box_tls.test.py | 62 - .../txn_box/basic/txn_box_tuple.test.py | 29 - .../txn_box/basic/txn_box_txn-debug.test.py | 66 - .../txn_box/basic/txn_box_txn-error.test.py | 57 - .../txn_box/basic/txn_box_txn_open_1.test.py | 41 - .../txn_box/basic/txn_box_txn_open_2.test.py | 42 - .../txn_box/basic/txn_box_txn_open_3.test.py | 41 - .../txn_box/basic/txn_box_with.test.py | 37 - .../ct_header/txn_box_ct_header.test.py | 65 - .../example/txn_box_accept-encoding.test.py | 39 - .../txn_box/prod/txn_box_cors-origin.test.py | 45 - .../prod/txn_box_cors-referrer.test.py | 45 - .../txn_box/prod/txn_box_ip-acl.test.py | 50 - .../txn_box/prod/txn_box_mTLS.test.py | 76 - .../prod/txn_box_query-delete-filter.test.py | 46 - .../prod/txn_box_query-delete-regex.test.py | 46 - .../txn_box/prod/txn_box_query.test.py | 51 - .../txn_box/prod/txn_box_stanley.test.py | 51 - .../txn_box/prod/txn_box_vznith-1.test.py | 47 - .../txn_box/prod/txn_box_yts-3489.test.py | 52 - .../txn_box/ramp/txn_box_multi-ramp-1.test.py | 46 - .../txn_box/ramp/txn_box_multi-ramp-2.test.py | 45 - .../txn_box/ramp/txn_box_multi-ramp-3.test.py | 45 - .../txn_box/ramp/txn_box_ramp.test.py | 57 - .../txn_box/remap/txn_box_remap-base.test.py | 69 - .../txn_box/smoke/txn_box_smoke-2.test.py | 39 - .../txn_box/smoke/txn_box_smoke.test.py | 38 - .../static_file/txn_box_static_file.test.py | 52 - .../uri_signing/uri_signing.test.py | 252 --- .../pluginTest/url_sig/url_sig.test.py | 406 ---- .../webp_transform_cache_refused.test.py | 90 - .../webp_transform_chunked_cap.test.py | 74 - .../webp_transform_decode_limit.test.py | 117 -- .../webp_transform_max_buffer_size.test.py | 108 -- .../webp_transform_size_cap.test.py | 121 -- .../xdebug/x_cache_info/x_cache_info.test.py | 72 - .../x_effective_url/x_effective_url.test.py | 82 - .../pluginTest/xdebug/x_probe/x_probe.test.py | 81 - .../x_probe_full_json.test.py | 162 -- .../x_probe_full_json_slow_origin.test.py | 80 - .../pluginTest/xdebug/x_remap/x_remap.test.py | 82 - tests/gold_tests/post/expect_tests.test.py | 88 - tests/gold_tests/post/post-continue.test.py | 280 --- .../gold_tests/post/post-early-return.test.py | 158 -- .../simple-post-valid-buffer-check.test.py | 51 - .../post_slow_server/post_slow_server.test.py | 71 - .../post_slow_server_max_requests_in.test.py | 74 - .../proxy_protocol/proxy_protocol.test.py | 336 ---- tests/gold_tests/qmux/qmux_go_client.test.py | 128 -- .../records/records_config_to_yaml.test.py | 67 - .../records_runroot_precedence.test.py | 163 -- tests/gold_tests/records/records_yaml.test.py | 201 -- .../records/traffic_ctl_cold_config.test.py | 94 - .../records/ts_max_records_param.test.py | 56 - .../redirect/number_of_redirects.test.py | 100 - tests/gold_tests/redirect/redirect.test.py | 179 -- .../redirect/redirect_actions.test.py | 269 --- .../gold_tests/redirect/redirect_post.test.py | 91 - .../redirect/redirect_stale.test.py | 97 - .../redirect_to_same_origin_on_cache.test.py | 141 -- .../remap/basic_conf_remap_yaml.test.py | 193 -- .../gold_tests/remap/conf_remap_float.test.py | 42 - .../remap/map_with_recv_port.test.py | 82 - tests/gold_tests/remap/regex_map.test.py | 59 - tests/gold_tests/remap/remap_acl.test.py | 628 ------ tests/gold_tests/remap/remap_http.test.py | 158 -- tests/gold_tests/remap/remap_https.test.py | 125 -- .../gold_tests/remap/remap_ip_resolve.test.py | 73 - .../remap/remap_load_empty_failure.test.py | 42 - .../remap/remap_load_empty_success.test.py | 33 - .../remap/remap_load_missing_failure.test.py | 29 - .../remap/remap_load_missing_success.test.py | 33 - tests/gold_tests/remap/remap_reload.test.py | 109 -- tests/gold_tests/remap/remap_ws.test.py | 126 -- .../basic_conf_remap_yaml_yaml.test.py | 203 -- .../remap_yaml/conf_remap_float_yaml.test.py | 52 - .../map_with_recv_port_yaml.test.py | 87 - .../remap_yaml/regex_map_yaml.test.py | 65 - .../remap_yaml/remap_acl_yaml.test.py | 646 ------- .../remap_yaml/remap_http_yaml.test.py | 193 -- .../remap_yaml/remap_https_yaml.test.py | 142 -- .../remap_yaml/remap_ip_resolve_yaml.test.py | 89 - .../remap_load_empty_failure_yaml.test.py | 42 - .../remap_load_empty_success_yaml.test.py | 33 - .../remap_load_missing_failure_yaml.test.py | 29 - .../remap_load_missing_success_yaml.test.py | 33 - .../remap_yaml/remap_reload_yaml.test.py | 155 -- .../remap_yaml/remap_ws_yaml.test.py | 135 -- .../gold_tests/runroot/runroot_error.test.py | 58 - tests/gold_tests/runroot/runroot_init.test.py | 78 - .../gold_tests/runroot/runroot_remove.test.py | 70 - tests/gold_tests/runroot/runroot_use.test.py | 65 - .../gold_tests/runroot/runroot_verify.test.py | 65 - .../session_sharing/session_match.test.py | 123 -- .../crashlog_no_false_positive.test.py | 73 - tests/gold_tests/shutdown/emergency.test.py | 62 - tests/gold_tests/shutdown/fatal.test.py | 62 - .../gold_tests/slow_post/quick_server.test.py | 134 -- .../gold_tests/slow_post/server_abort.test.py | 57 - tests/gold_tests/slow_post/slow_post.test.py | 79 - .../statistics/metric_response_000.test.py | 107 -- .../thread_config/thread_config.test.py | 252 --- .../gold_tests/timeout/accept_timeout.test.py | 74 - .../gold_tests/timeout/active_timeout.test.py | 73 - tests/gold_tests/timeout/conn_timeout.test.py | 99 - .../default_inactivity_timeout.test.py | 103 - .../http2_incomplete_header_timeout.test.py | 207 -- .../timeout/http2_no_activity_timeout.test.py | 157 -- .../timeout/inactive_client_timeout.test.py | 78 - .../timeout/inactive_timeout.test.py | 67 - .../timeout/quic_no_activity_timeout.test.py | 135 -- .../timeout/quic_poll_timeout.test.py | 77 - .../timeout/tls_conn_timeout.test.py | 117 -- .../timeout/tunnel_active_timeout.test.py | 100 - tests/gold_tests/tls/allow-plain.test.py | 106 -- .../tls/exit_on_cert_load_fail.test.py | 125 -- .../tls/proxy_protocol_addressless.test.py | 87 - tests/gold_tests/tls/ssl_key_dialog.test.py | 99 - .../tls/ssl_multicert_loader.test.py | 163 -- tests/gold_tests/tls/tls.test.py | 90 - tests/gold_tests/tls/tls_0rtt_server.test.py | 261 --- .../tls/tls_accept_timeout_crash.test.py | 81 - .../tls/tls_async_handshake.test.py | 82 - tests/gold_tests/tls/tls_bad_alpn.test.py | 81 - tests/gold_tests/tls/tls_cert_comp.test.py | 176 -- .../tls/tls_cert_comp_cache.test.py | 183 -- .../tls/tls_check_cert_select_plugin.test.py | 184 -- .../tls/tls_check_cert_selection.test.py | 126 -- .../tls_check_cert_selection_reload.test.py | 124 -- .../tls/tls_check_dual_cert_selection.test.py | 162 -- .../tls_check_dual_cert_selection2.test.py | 190 -- ...s_check_dual_cert_selection_plugin.test.py | 200 -- .../tls/tls_client_alpn_configuration.test.py | 164 -- tests/gold_tests/tls/tls_client_cert.test.py | 294 --- tests/gold_tests/tls/tls_client_cert2.test.py | 209 -- .../tls/tls_client_cert2_plugin.test.py | 198 -- .../tls/tls_client_cert_override.test.py | 149 -- .../tls_client_cert_override_plugin.test.py | 227 --- .../tls/tls_client_cert_plugin.test.py | 283 --- .../gold_tests/tls/tls_client_verify.test.py | 240 --- .../gold_tests/tls/tls_client_verify2.test.py | 187 -- .../gold_tests/tls/tls_client_verify3.test.py | 146 -- .../tls/tls_client_versions.test.py | 121 -- .../tls/tls_client_versions_minmax.test.py | 137 -- tests/gold_tests/tls/tls_engine_abort.test.py | 120 -- tests/gold_tests/tls/tls_flow_control.test.py | 116 -- .../tls/tls_forward_nonhttp.test.py | 83 - .../tls/tls_h2_cipher_suite.test.py | 94 - .../tls/tls_hooks_client_verify.test.py | 121 -- tests/gold_tests/tls/tls_hooks_verify.test.py | 113 -- tests/gold_tests/tls/tls_keepalive.test.py | 115 -- tests/gold_tests/tls/tls_ocsp.test.py | 84 - .../tls/tls_origin_open_failed.test.py | 78 - .../tls/tls_origin_post_abort.test.py | 114 -- .../tls/tls_origin_session_reuse.test.py | 170 -- .../tls/tls_partial_blind_tunnel.test.py | 103 - tests/gold_tests/tls/tls_record_size.test.py | 123 -- .../tls/tls_reload_under_load.test.py | 100 - .../gold_tests/tls/tls_renegotiation.test.py | 129 -- .../tls/tls_renegotiation_allowed.test.py | 145 -- .../tls/tls_secret_update_default.test.py | 234 --- .../tls/tls_session_key_logging.test.py | 102 - .../gold_tests/tls/tls_session_reuse.test.py | 160 -- tests/gold_tests/tls/tls_sni_groups.test.py | 113 -- .../tls/tls_sni_host_policy.test.py | 306 --- tests/gold_tests/tls/tls_sni_ip_allow.test.py | 184 -- .../tls/tls_sni_parent_failover.test.py | 174 -- tests/gold_tests/tls/tls_sni_ticket.test.py | 277 --- .../gold_tests/tls/tls_sni_with_port.test.py | 212 --- .../tls/tls_sni_yaml_reload.test.py | 123 -- tests/gold_tests/tls/tls_ticket.test.py | 121 -- tests/gold_tests/tls/tls_tunnel.test.py | 405 ---- .../gold_tests/tls/tls_tunnel_forward.test.py | 156 -- tests/gold_tests/tls/tls_verify.test.py | 165 -- tests/gold_tests/tls/tls_verify2.test.py | 161 -- tests/gold_tests/tls/tls_verify3.test.py | 156 -- tests/gold_tests/tls/tls_verify4.test.py | 188 -- tests/gold_tests/tls/tls_verify_base.test.py | 140 -- .../tls/tls_verify_ca_override.test.py | 141 -- .../tls/tls_verify_not_pristine.test.py | 110 -- .../tls/tls_verify_override.test.py | 277 --- .../tls/tls_verify_override_base.test.py | 264 --- .../tls/tls_verify_override_sni.test.py | 159 -- tests/gold_tests/tls_hooks/tls_hooks.test.py | 80 - .../gold_tests/tls_hooks/tls_hooks10.test.py | 68 - .../gold_tests/tls_hooks/tls_hooks11.test.py | 75 - .../gold_tests/tls_hooks/tls_hooks12.test.py | 68 - .../gold_tests/tls_hooks/tls_hooks13.test.py | 68 - .../gold_tests/tls_hooks/tls_hooks14.test.py | 69 - .../gold_tests/tls_hooks/tls_hooks15.test.py | 68 - .../gold_tests/tls_hooks/tls_hooks16.test.py | 76 - .../gold_tests/tls_hooks/tls_hooks17.test.py | 76 - .../gold_tests/tls_hooks/tls_hooks18.test.py | 79 - tests/gold_tests/tls_hooks/tls_hooks2.test.py | 74 - tests/gold_tests/tls_hooks/tls_hooks3.test.py | 74 - tests/gold_tests/tls_hooks/tls_hooks4.test.py | 82 - tests/gold_tests/tls_hooks/tls_hooks6.test.py | 82 - tests/gold_tests/tls_hooks/tls_hooks7.test.py | 77 - tests/gold_tests/tls_hooks/tls_hooks8.test.py | 77 - tests/gold_tests/tls_hooks/tls_hooks9.test.py | 73 - .../tls_hooks_close_while_parked.test.py | 88 - .../convert_plugin_config.test.py | 68 - .../convert_ssl_multicert.test.py | 60 - .../convert_storage/convert_storage.test.py | 68 - .../traffic_ctl/remap_inc/remap_inc.test.py | 60 - .../show_ssl_multicert.test.py | 91 - .../traffic_ctl_config_output.test.py | 141 -- .../traffic_ctl_config_reload.test.py | 192 -- .../traffic_ctl_server_debug.test.py | 80 - .../traffic_ctl_server_output.test.py | 65 - .../traffic_ctl_set_read_only.test.py | 113 -- .../tunnel/tunnel_transform.test.py | 198 -- tests/gold_tests/tunnel/txn_type.test.py | 177 -- tests/gold_tests/uds/uds_socket_perm.test.py | 57 - tests/gold_tests/url/uri.test.py | 35 - tests/pyproject.toml | 11 +- tests/pytest_tests/test_config.py | 27 +- tests/pytest_tests/test_process.py | 4 +- tests/pytest_tests/test_runner.py | 158 ++ tests/pytest_tests/test_services.py | 109 ++ tests/serial_tests.txt | 12 +- tests/tools/mock_origin.py | 4 +- tests/tools/plugins/CMakeLists.txt | 54 +- tests/tools/plugins/test_hooks.cc | 2 +- .../config.py | 26 +- .../min_cfg/ip_allow.yaml | 0 .../min_cfg/readme.txt | 0 .../min_cfg/storage.yaml | 0 tests/uranium_testkit/plugin.py | 297 +++ .../process.py | 2 +- .../replay.py | 22 +- tests/uranium_testkit/runner.py | 479 +++++ .../runtime.py | 46 +- tests/uranium_testkit/scenario.py | 1684 +++++++++++++++++ tests/uranium_testkit/services.py | 124 ++ .../at_headers/plugins/CMakeLists.txt | 2 +- .../at_headers/plugins/at_header_probe.cc | 0 .../at_headers/replay/at_headers.test.yaml | 2 +- .../ats_probe/ats_probe.bt | 0 .../ats_probe/ats_probe.replay.yaml | 0 .../ats_probe/cache_dir_probe.bt | 0 .../uranium_tests/ats_probe/test_ats_probe.py | 131 ++ .../ats_probe/test_cache_dir_probe.py | 141 ++ .../bad_http_fmt/client.gold | 0 .../bad_http_fmt/method-server.sh | 0 .../bad_http_fmt/server.gold | 0 .../bad_http_fmt/test_bad_http_fmt.py | 207 ++ .../basic/.gitignore | 0 .../basic/test_basic.py} | 21 +- .../basic/test_config.py} | 19 +- tests/uranium_tests/basic/test_copy_config.py | 69 + .../uranium_tests/basic/test_copy_config2.py | 64 + tests/uranium_tests/basic/test_deny0.py | 133 ++ tests/uranium_tests/bigobj/test_bigobj.py | 168 ++ .../body_factory/config/remap.config | 0 .../data/www.customplugin204.test_get.txt | 0 .../data/www.customtemplate204.test_get.txt | 0 .../data/www.default204.test_get.txt | 0 .../data/www.default304.test_get.txt | 0 .../data/www.example.test_get_200.txt | 0 .../data/www.example.test_get_304.txt | 0 .../data/www.example.test_head.txt | 0 .../data/www.example.test_head_200.txt | 0 .../gold/http-204-custom-plugin.gold | 0 .../body_factory/gold/http-204-custom.gold | 0 .../body_factory/gold/http-204.gold | 0 .../body_factory/gold/http-304.gold | 0 .../body_factory/gold/http-get-200.gold | 0 .../body_factory/gold/http-get-304.gold | 0 .../body_factory/gold/http-head-200.gold | 0 .../gold/http-head-no-origin.gold | 0 .../body_factory/test_http204_response.py | 107 ++ .../test_http204_response_plugin.py | 72 + .../body_factory/test_http304_response.py | 71 + .../body_factory/test_http_head_no_origin.py | 63 + .../body_factory/test_http_with_origin.py | 124 ++ .../cache/cache-write-lock-fail-write.conf | 0 .../cache/cache_volume_defaults.test.yaml | 2 +- .../cache/cache_volume_features.test.yaml | 2 +- .../gold/background_fill_0_stderr_H.gold | 0 .../gold/background_fill_1_stderr_H.gold | 0 .../gold/background_fill_2_stderr_H.gold | 0 .../cache/gold/background_fill_3_stdout.gold | 0 .../gold/cache_shm_state_after_shutdown.gold | 0 .../cache/gold/hit_default-1.gold | 0 .../cache/gold/hit_default0.gold | 0 .../cache/gold/hit_gen1.gold | 0 .../cache/gold/hit_gen2.gold | 0 .../cache/gold/miss_default-1.gold | 0 .../cache/gold/miss_default0.gold | 0 .../cache/gold/miss_gen1.gold | 0 .../cache/gold/miss_gen2.gold | 0 .../negative-revalidating-enabled-access.gold | 0 .../cache/gold/serve_stale_dns_fail.gold | 0 .../cache/gold/storage_metrics_0_stdout.gold | 0 .../cache/gold/storage_metrics_1_stdout.gold | 0 .../cache/gold/storage_metrics_2_stdout.gold | 0 .../cache/gold/storage_metrics_3_stdout.gold | 0 .../cache/gold/two_alternates_evicted.gold | 0 .../alternate-caching-quality.test.yaml | 2 +- .../alternate-caching-update-size.test.yaml | 2 +- .../cache/replay/auth-s-maxage.test.yaml | 4 +- .../cache/replay/bg_fill.yaml | 0 .../replay/cache-control-basic.test.yaml | 4 +- .../replay/cache-control-max-age.test.yaml | 4 +- .../replay/cache-control-pragma.test.yaml | 4 +- .../replay/cache-control-s-maxage.test.yaml | 4 +- .../cache/replay/cache-exact-url.test.yaml | 2 +- .../replay/cache-heuristic-status.test.yaml | 2 +- .../replay/cache-range-response.test.yaml | 2 +- .../replay/cache-read-retry-stale.test.yaml | 2 +- .../cache/replay/cache-read-retry.test.yaml | 2 +- .../replay/cache-shm-dir-invalid.replay.yaml | 0 .../replay/cache-shm-fast-restart.replay.yaml | 0 .../cache-write-lock-stale-retry.test.yaml | 2 +- .../cache-write-lock-stale-serve.test.yaml | 2 +- .../cache-write-retry-callback.test.yaml | 2 +- .../conditional-get-cache-hit.test.yaml | 2 +- .../cookie-all-but-text-with-excp.test.yaml | 4 +- .../replay/cookie-all-but-text.test.yaml | 4 +- .../replay/cookie-bypass-cache.test.yaml | 4 +- .../replay/cookie-cache-img-only.test.yaml | 4 +- .../cache/replay/cookie-default.test.yaml | 4 +- .../cache/replay/delete_cached.test.yaml | 4 +- .../cache/replay/get_then_post.replay.yaml | 0 .../replay/head_with_get_cached.test.yaml | 4 +- ...ost_down_range_recursion_prime.replay.yaml | 0 ...ost_down_range_recursion_range.replay.yaml | 0 .../replay/ignore_authentication.test.yaml | 4 +- .../replay/max-stale-age-percent.test.yaml | 117 ++ ...ive-caching-300-second-timeout.replay.yaml | 0 .../negative-caching-customized.test.yaml | 4 +- .../replay/negative-caching-default.test.yaml | 4 +- .../negative-caching-disabled.test.yaml | 4 +- .../negative-caching-malformed-cc.test.yaml | 4 +- .../negative-caching-no-timeout.replay.yaml | 0 .../negative-caching-timeout.replay.yaml | 0 .../negative-caching-ttl-in-cache.replay.yaml | 0 .../negative-revalidating-disabled.test.yaml | 4 +- .../negative-revalidating-enabled.test.yaml | 4 +- .../negative-revalidating-list.test.yaml | 4 +- .../post_with_post_caching_disabled.test.yaml | 4 +- .../post_with_post_caching_enabled.test.yaml | 4 +- .../post_with_post_caching_override.test.yaml | 4 +- .../cache/replay/proxy_serve_stale.test.yaml | 2 +- .../proxy_serve_stale_origin_down.test.yaml | 2 +- .../request-cache-control-default.test.yaml | 4 +- ...quest-cache-control-honor-client.test.yaml | 4 +- .../response-cache-control-default.test.yaml | 4 +- .../response-cache-control-ignored.test.yaml | 4 +- .../replay/targeted-cache-control.test.yaml | 4 +- .../replay/varied_transactions.test.yaml | 2 +- .../cache/shm_poke.py | 2 +- .../cache/test_background_fill.py | 208 ++ .../cache/test_cache_config_reload.py | 89 + .../cache/test_cache_corrupt_recursive_uaf.py | 149 ++ .../cache/test_cache_generation_clear.py | 122 ++ .../cache/test_cache_generation_disjoint.py | 122 ++ .../cache/test_cache_shm_bad_disk_dropped.py | 251 +++ .../cache/test_cache_shm_concurrent_attach.py | 204 ++ .../test_cache_shm_control_size_mismatch.py | 222 +++ .../cache/test_cache_shm_dir_invalid.py | 261 +++ .../cache/test_cache_shm_fast_restart.py | 272 +++ .../cache/test_cache_shm_purge_on_disable.py | 253 +++ .../cache/test_cache_shm_schema_mismatch.py | 226 +++ .../cache/test_cache_shm_storage_mismatch.py | 229 +++ .../cache/test_cache_shm_unclean_shutdown.py | 205 ++ .../cache/test_cache_write_lock_contention.py | 193 ++ .../cache/test_disjoint_wait_for_cache.py | 125 ++ .../cache/test_host_down_range_recursion.py | 101 + .../cache/test_negative_caching.py | 146 ++ .../cache/test_proxy_serve_stale_dns_fail.py | 102 + .../cache/test_storage_metrics.py | 148 ++ .../chunked_encoding/CMakeLists.txt | 0 .../chunk_extension_client.py | 0 .../chunked_encoding/chunk_trailer_client.py | 0 .../chunked_encoding_h2_server.py | 0 .../gold/chunked_GET_200.gold | 0 .../gold/chunked_GET_200_uds.gold | 0 .../gold/chunked_POST_200.gold | 0 .../gold/h2_chunked_POST_200.gold | 0 .../gold/verifier_client_chunked.gold | 0 .../chunk_extension_quoted_string.replay.yaml | 0 .../replays/chunk_trailer_bare_lf.replay.yaml | 0 .../replays/chunked.replay.yaml | 0 .../replays/chunked_h1_flow_control.test.yaml | 2 +- .../replays/chunked_h2_flow_control.test.yaml | 2 +- .../replays/chunked_in_http_1_0.replay.yaml | 0 .../replays/chunked_not_last.test.yaml | 2 +- .../chunked_passthru_flow_control.test.yaml | 2 +- .../chunked_trailer_dropped.replay.yaml | 0 .../chunked_trailer_proxied.replay.yaml | 0 .../replays/large_chunked.test.yaml | 4 +- .../malformed_chunked_header.replay.yaml | 0 .../chunked_encoding/server4.sh | 0 .../chunked_encoding/smuggle-client.c | 0 .../test_bad_chunked_encoding.py | 247 +++ .../test_chunk_extension_quoted_string.py | 104 + .../test_chunk_trailer_bare_lf.py | 106 ++ .../chunked_encoding/test_chunked_encoding.py | 283 +++ .../test_chunked_encoding_disabled.py | 94 + .../test_chunked_encoding_h2.py | 142 ++ .../exempt_lists/exempt_all.yaml | 0 .../exempt_lists/exempt_localhost.yaml | 0 .../exempt_lists/no_localhost.yaml | 0 .../http2_slow_origins.replay.yaml | 0 .../http_slow_origins.replay.yaml | 0 .../https_slow_origins.replay.yaml | 0 .../test_per_client_connection_max.py | 267 +++ .../test_verify_global_plugin.py | 162 ++ .../test_verify_remap_plugin.py | 140 ++ .../test_config_destroy_thread.py | 98 + .../connect/gold/connect_0_stderr.gold | 0 .../connect/gold/connect_access.gold | 0 .../connect/gold/metrics.gold | 0 .../connect/malformed_h2_request_client.py | 0 .../connect/replays/connect.replay.yaml | 0 .../connect/replays/connect_h2.replay.yaml | 0 .../replays/connect_handshake.replay.yaml | 0 .../connect_parent_error_body.replay.yaml | 0 .../h2_malformed_request_logging.replay.yaml | 0 tests/uranium_tests/connect/test_connect.py | 273 +++ .../connect/test_connect_handshake.py | 84 + .../connect/test_connect_parent_error_body.py | 91 + .../test_h2_malformed_request_logging.py | 221 +++ .../gold/connect_down_policy_4_error_log.gold | 0 .../replay/connect_down_policy_4.test.yaml | 4 +- .../replay/inactive_timeout.replay.yaml | 0 .../test_connect_down_policy.py | 132 ++ .../cont_schedule/entire_pool.py | 0 .../gold/schedule_every_on_entire_pool.gold | 0 .../gold/schedule_every_on_pool.gold | 0 .../gold/schedule_every_on_thread.gold | 0 .../gold/schedule_on_entire_pool.gold | 0 .../cont_schedule/gold/schedule_on_pool.gold | 0 .../gold/schedule_on_thread.gold | 0 .../cont_schedule/gold/thread_affinity.gold | 0 .../test_schedule_every_on_entire_pool.py | 85 + .../test_schedule_every_on_pool.py | 83 + .../test_schedule_every_on_thread.py | 83 + .../test_schedule_on_entire_pool.py | 85 + .../cont_schedule/test_schedule_on_pool.py | 70 + .../cont_schedule/test_schedule_on_thread.py | 70 + .../cont_schedule/test_thread_affinity.py | 70 + .../continuations/plugins/CMakeLists.txt | 2 +- .../plugins/session_id_verify.cc | 0 .../continuations/test_double.py | 154 ++ .../continuations/test_double_h2.py | 174 ++ .../continuations/test_openclose.py | 161 ++ .../continuations/test_openclose_h2.py | 178 ++ .../continuations/test_session_id.py | 135 ++ .../cripts/cripts_bundle.test.yaml | 4 +- .../cripts/files/basic.cript | 0 .../cripts/files/bundle_headers.cript | 0 .../cripts/files/direct_headers.cript | 0 .../cripts/files/query_copy.cript | 0 .../cripts/files/server_bundle_headers.cript | 0 .../cripts/gold/basic_cript.gold | 0 .../cripts/gold/certs_cript.gold | 0 tests/uranium_tests/cripts/test_cripts.py | 165 ++ ...ect_attempts_rr_max_retries_error_log.gold | 0 .../connect_attempts_rr_no_error_log.gold | 0 ...connect_attempts_rr_retries_error_log.gold | 0 ...attempts_single_max_retries_error_log.gold | 0 .../dns/gold/test_case_0_stderr.gold | 0 .../dns/gold/test_case_1_stderr.gold | 0 .../dns/hosts_file | 0 .../connect_attempts_rr_max_retries.test.yaml | 4 +- .../connect_attempts_rr_no_retry.test.yaml | 4 +- .../connect_attempts_rr_retries.test.yaml | 4 +- ...nect_attempts_single_max_retries.test.yaml | 4 +- .../replay/multiple_host_requests.replay.yaml | 0 .../dns/replay/reverse_lookup.replay.yaml | 0 .../dns/replay/server_down.replay.yaml | 0 .../dns/replay/server_error.replay.yaml | 0 .../dns/replay/single_transaction.replay.yaml | 0 .../dns/test_dns_down_nameserver.py | 206 ++ tests/uranium_tests/dns/test_dns_host_down.py | 114 ++ .../dns/test_dns_reverse_lookup.py | 105 + tests/uranium_tests/dns/test_dns_ttl.py | 163 ++ tests/uranium_tests/dns/test_splitdns.py | 96 + .../uranium_tests/dns/test_splitdns_reload.py | 99 + .../early_hints/early_hints_server.py | 0 .../early_hints/test_early_hints.py | 193 ++ .../forward_proxy/forward_proxy.replay.yaml | 0 .../forward_proxy/test_forward_proxy.py | 134 ++ .../h2/clients/h2_extension_settings.py | 0 .../h2/clients/h2_max_active_streams.py | 0 .../h2/clients/h2_max_settings_per_minute.py | 0 .../h2/clients/h2_settings_ack_stall.py | 0 .../h2/clients/h2empty_data_frame.py | 0 .../h2/expect_100_continue.yaml | 0 .../h2/gold/active_timeout.gold | 0 .../h2/gold/bigfile.gold | 0 .../h2/gold/chunked.gold | 0 .../h2/gold/h2spec_stdout.gold | 0 .../h2/gold/http-request-method-metrics.gold | 0 .../h2/gold/http2_8_stdout.gold | 0 .../h2/gold/http2_9_stderr.gold | 0 .../h2/gold/http2_9_stdout.gold | 0 .../h2/gold/httpbin_0_stderr.gold | 0 .../h2/gold/httpbin_0_stdout.gold | 0 .../h2/gold/httpbin_1_stderr.gold | 0 .../h2/gold/httpbin_1_stdout.gold | 0 .../h2/gold/httpbin_2_stderr.gold | 0 .../h2/gold/httpbin_2_stdout.gold | 0 .../h2/gold/httpbin_3_stderr.gold | 0 .../h2/gold/httpbin_3_stdout.gold | 0 .../h2/gold/httpbin_access.gold | 0 .../h2/gold/log.gold | 0 .../h2/gold/nghttp_0_stdout.gold | 0 .../h2/gold/nghttp_1_stdout.gold | 0 .../h2/gold/nghttp_ts_stderr.gold | 0 .../h2/gold/post_chunked.gold | 0 .../h2/gold/priority_0_stderr.gold | 0 .../h2/gold/priority_0_stdout.gold | 0 .../h2/gold/remap-200.gold | 0 .../h2/gold/replay.gold | 0 .../h2/gold/server_after_headers.gold | 0 .../h2/grpc/grpc_client.py | 0 .../h2/grpc/grpc_server.py | 0 .../h2/grpc/simple.proto | 0 tests/uranium_tests/h2/grpc/test_grpc.py | 183 ++ .../h2/h1_trailer_client.py | 0 .../h2/h2_origin_trailers_h1.replay.yaml | 0 .../h2/h2active_timeout.py | 0 .../h2/h2client.py | 0 .../h2/h2get_with_body.yaml | 0 .../h2/http2_close_connection.yaml | 0 .../h2/http2_flow_control.replay.yaml | 0 .../h2/http2_flow_control_chunked.replay.yaml | 0 ...ient_reset_keeps_session_reading.test.yaml | 2 +- .../http2_concurrent_streams.replay.yaml | 0 ...p2_max_active_streams_advisory.replay.yaml | 0 ...tp2_max_active_streams_enforce.replay.yaml | 0 .../http2_settings_ack_stall.replay.yaml | 0 .../http2_txn_start_read_gate.replay.yaml | 0 .../h2/replay_h2_origin_cert_reverify.yaml | 0 .../replay_h2origin/h1-client-h2-origin.yaml | 0 .../h2/replay_h2origin/h2-origin.yaml | 0 .../http2_rst_stream_client_after_data.yaml | 0 ...http2_rst_stream_client_after_headers.yaml | 0 ...http2_rst_stream_server_after_headers.yaml | 0 .../h2/rules/graceful_shutdown.conf | 0 .../h2/rules/huge_resp_hdrs.conf | 0 .../h2/test_h2_origin_cert_reverify.py | 99 + .../h2/test_h2_origin_trailers_h1.py | 113 ++ tests/uranium_tests/h2/test_h2disable.py | 117 ++ .../h2/test_h2disable_no_accept_threads.py | 117 ++ tests/uranium_tests/h2/test_h2enable.py | 118 ++ .../h2/test_h2enable_no_accept_threads.py | 117 ++ .../uranium_tests/h2/test_h2get_with_body.py | 92 + tests/uranium_tests/h2/test_h2origin.py | 147 ++ .../h2/test_h2origin_single_thread.py | 113 ++ tests/uranium_tests/h2/test_h2spec.py | 95 + tests/uranium_tests/h2/test_http2.py | 308 +++ .../h2/test_http2_close_connection.py | 91 + .../h2/test_http2_concurrent_streams.py | 83 + .../h2/test_http2_crlf_header_validation.py | 118 ++ .../h2/test_http2_empty_data_frame.py | 96 + .../h2/test_http2_flow_control.py | 448 +++++ .../h2/test_http2_max_active_streams.py | 105 + tests/uranium_tests/h2/test_http2_priority.py | 107 ++ .../uranium_tests/h2/test_http2_rst_stream.py | 191 ++ .../h2/test_http2_txn_start_read_gate.py | 84 + .../h2/test_http2_write_threshold.py | 163 ++ tests/uranium_tests/h2/test_httpbin.py | 157 ++ tests/uranium_tests/h2/test_nghttp.py | 118 ++ .../h2/trickle_client.py | 0 .../h2/trickle_server.py | 0 .../h3/go_h3_client/go.mod | 0 .../h3/go_h3_client/go.sum | 0 .../h3/go_h3_client/main.go | 0 .../h3/h3_session_ticket.sh | 0 .../h3/py_h3_client/h3_client.py | 0 .../h3/replays/h3_active_timeout.test.yaml | 2 +- .../h3/replays/h3_flow_control.test.yaml | 2 +- .../h3/replays/h3_proxy_verifier.test.yaml | 2 +- .../h3_server_for_go_client.replay.yaml | 0 .../h3_server_for_python_client.replay.yaml | 0 .../h3/replays/h3_sni.replay.yaml | 0 .../h3/replays/h3_stream_lifetime.test.yaml | 2 +- tests/uranium_tests/h3/test_h3_curl.py | 162 ++ tests/uranium_tests/h3/test_h3_go_client.py | 148 ++ .../uranium_tests/h3/test_h3_python_client.py | 150 ++ tests/uranium_tests/h3/test_h3_range_cache.py | 159 ++ .../h3/test_h3_session_ticket.py | 128 ++ tests/uranium_tests/h3/test_h3_sni_check.py | 151 ++ .../headers/cache_and_error_nobody.gold | 0 ...cache_and_req_body-hit-stale-206-etag.gold | 0 .../cache_and_req_body-hit-stale-206.gold | 0 .../cache_and_req_body-hit-stale-INM.gold | 0 .../headers/cache_and_req_body-hit-stale.gold | 0 .../headers/cache_and_req_body-hit.gold | 0 .../headers/cache_and_req_body-hit_close.gold | 0 .../headers/cache_and_req_body-miss.gold | 0 .../cache_and_req_nobody-hit-stale.gold | 0 .../clients/oversized_field_h2_client.py | 4 +- .../headers/data/www.http408.test.txt | 0 .../headers/data/www.passthrough.test_get.txt | 0 .../headers/data/www.redirect0.test_get.txt | 0 .../headers/data/www.redirect301.test_get.txt | 0 .../headers/data/www.redirect302.test_get.txt | 0 .../headers/data/www.redirect307.test_get.txt | 0 .../headers/data/www.redirect308.test_get.txt | 0 .../headers/forwarded-observer.py | 0 .../headers/forwarded.gold | 0 .../general-connection-failure-502.gold | 0 .../headers/gold/bad_good_request.gold | 0 .../headers/gold/bad_good_request_header.gold | 0 .../headers/gold/bad_good_request_http1.gold | 0 .../headers/gold/bad_method.gold | 0 .../headers/gold/bad_protocol_number.gold | 0 .../headers/gold/bad_te_value.gold | 0 .../headers/gold/field_name_space.gold | 0 .../gold/invalid_character_in_te_value.gold | 0 .../headers/hsts.200.gold | 0 .../headers/hsts.404.gold | 0 .../headers/http408.gold | 0 .../headers/maps.reg | 0 .../headers/normalize_ae.gold | 0 .../headers/normalize_ae_observer.py | 0 .../headers/passthrough_get.gold | 0 .../headers/redirect0_get.gold | 0 .../headers/redirect301_get.gold | 0 .../headers/redirect302_get.gold | 0 .../headers/redirect307_get.gold | 0 .../headers/redirect308_get.gold | 0 .../headers/replay/oversized_field.test.yaml | 2 +- .../replay/oversized_field_h2.replay.yaml | 0 .../headers/replays/accept_webp.test.yaml | 2 +- .../headers/replays/cache-test.test.yaml | 2 +- .../replays/cached_ims_range.test.yaml | 2 +- .../replays/domain-blacklist-30x.test.yaml | 2 +- .../replays/hopbyhop_connection.test.yaml | 2 +- .../headers/replays/hsts.test.yaml | 2 +- .../replays/invalid_range_request.test.yaml | 2 +- .../replays/malformed_host_port.test.yaml | 2 +- ...ormalized_ae_varied_transactions.test.yaml | 2 +- .../headers/replays/range.test.yaml | 2 +- .../headers/replays/range_transform.test.yaml | 2 +- .../rewrite_rules/header_rewrite_rules_0.conf | 0 .../header_rewrite_rules_301.conf | 0 .../header_rewrite_rules_302.conf | 0 .../header_rewrite_rules_307.conf | 0 .../header_rewrite_rules_308.conf | 0 .../headers/syntax.200.gold | 0 .../headers/syntax.400.gold | 0 .../headers/test_cache_and_req_body.py | 180 ++ .../headers/test_field_name_space.py | 74 + tests/uranium_tests/headers/test_forwarded.py | 307 +++ .../test_general_connection_failure_502.py | 67 + .../headers/test_good_request_after_bad.py | 213 +++ tests/uranium_tests/headers/test_http408.py | 81 + .../headers/test_normalize_ae.py | 216 +++ .../headers/test_oversized_field_h2.py | 162 ++ tests/uranium_tests/headers/test_syntax.py | 123 ++ tests/uranium_tests/headers/test_via.py | 178 ++ .../headers/via-observer.py | 0 .../headers/via.gold | 0 .../headers/via_h3.gold | 0 .../headers/via_uds.gold | 0 .../ip_allow/gold/200.gold | 0 .../ip_allow/gold/403.gold | 0 .../ip_allow/gold/403_h2.gold | 0 .../ip_allow/gold/log.gold | 0 .../ip_allow_subjects_overflow.test.yaml | 2 +- .../replay/ip_allow_subjects_valid.test.yaml | 2 +- .../ip_allow/replays/h3.replay.yaml | 0 .../replays/http_proxy_protocol.replay.yaml | 0 .../replays/https_categories_all.replay.yaml | 0 .../https_categories_external.replay.yaml | 0 ...ttps_categories_external_remap.replay.yaml | 0 .../https_categories_internal.replay.yaml | 0 .../https_categories_server.replay.yaml | 0 .../https_multiple_methods.replay.yaml | 0 .../ip_allow/run_sed.sh | 0 .../ip_allow/test_connect_destination_acl.py | 191 ++ tests/uranium_tests/ip_allow/test_ip_allow.py | 387 ++++ .../test_ip_allow_reload_triggered.py | 240 +++ .../ip_allow/test_ip_category.py | 342 ++++ .../json/admin_config_set_records_req.json | 0 .../admin_detached_config_reload_req.json | 0 .../json/admin_host_set_status_req.json | 0 .../json/admin_lookup_records_req_1.json | 0 .../admin_lookup_records_req_invalid_rec.json | 0 .../json/admin_lookup_records_req_metric.json | 0 .../admin_lookup_records_req_multiple.json | 0 .../json/admin_plugin_send_basic_msg_req.json | 0 .../admin_storage_x_device_status_req.json | 0 .../jsonrpc/json/method_call_no_params.json | 0 .../jsonrpc/plugins/CMakeLists.txt | 2 +- .../plugins/jsonrpc_plugin_handler_test.cc | 0 .../jsonrpc/test_basic_plugin_handler.py | 196 ++ .../jsonrpc/test_config_reload_dedup.py | 181 ++ .../jsonrpc/test_config_reload_failures.py | 430 +++++ .../jsonrpc/test_config_reload_full_smoke.py | 187 ++ .../test_config_reload_reserve_subtask.py | 155 ++ .../jsonrpc/test_config_reload_rpc.py | 482 +++++ .../jsonrpc/test_config_reload_tracking.py | 303 +++ .../jsonrpc/test_jsonrpc_api_schema.py | 239 +++ .../lib/ats_urtest.py} | 0 .../lib}/jsonrpc.py | 0 tests/uranium_tests/lib/ports.py | 51 + .../lib}/verifier_common.py | 0 .../logging/all_headers_sanitizer.py | 0 .../logging/all_headers_sanitizer.sh | 0 .../logging/gold/binary_log_v3_ascii.gold | 0 .../logging/gold/binary_log_v3_json.gold | 0 .../logging/gold/custom.gold | 0 .../logging/gold/custom_uds.gold | 0 .../logging/gold/field-json-test.gold | 0 .../logging/gold/field-test.gold | 0 .../logging/gold/filter-test.gold | 0 .../logging/gold/pqsi-pqsp.gold | 0 .../logging/gold/test_all_headers.gold | 0 .../logging/gold/test_all_headers_uds.gold | 0 .../logging/log-filter.replays.yaml | 0 .../logging/new_log_flds_observer.py | 0 .../logging/pipe_buffer_is_larger_than.py | 0 .../logging/rename_qlog.sh | 0 .../logging/replay/basic1.replay.yaml | 0 .../replay/log-debug-client-ip-http.test.yaml | 2 +- .../log-debug-client-ip-http2.test.yaml | 2 +- .../log-debug-client-ip-http3.test.yaml | 2 +- .../log-debug-client-ip-https.test.yaml | 2 +- .../logging/sigusr2_rotate_custom_log.sh | 0 .../logging/sigusr2_rotate_diags.sh | 0 .../uranium_tests/logging/test_all_headers.py | 136 ++ .../logging/test_binary_log_v3.py | 190 ++ .../uranium_tests/logging/test_custom_log.py | 113 ++ tests/uranium_tests/logging/test_log_field.py | 142 ++ .../logging/test_log_field_json.py | 141 ++ .../logging/test_log_filenames.py | 277 +++ .../uranium_tests/logging/test_log_filter.py | 121 ++ .../logging/test_log_milestone_fields.py | 195 ++ .../uranium_tests/logging/test_log_mstsms.py | 138 ++ tests/uranium_tests/logging/test_log_pipe.py | 206 ++ .../logging/test_log_plugin_init.py | 59 + .../logging/test_log_retention.py | 508 +++++ .../logging/test_new_log_flds.py | 142 ++ tests/uranium_tests/logging/test_pqsi_pqsp.py | 110 ++ .../uranium_tests/logging/test_qlog_quiche.py | 147 ++ tests/uranium_tests/logging/test_sigusr2.py | 183 ++ .../logging/ts_process_handler.py | 0 .../logging/verify_milestone_fields.py | 0 .../test_parent_config_dest_ip.py | 146 ++ .../next_hop/strategies_ch/body.gold | 0 .../strategies_ch/test_strategies_ch.py | 160 ++ .../next_hop/strategies_ch/trace.gold | 0 .../next_hop/strategies_ch2/body.gold | 0 .../strategies_ch2/test_strategies_ch2.py | 176 ++ .../next_hop/strategies_ch2/trace.gold | 0 .../strategies_ch5/test_strategies_ch5.py | 174 ++ .../next_hop/strategies_ch5/trace.gold | 0 .../next_hop/strategies_stale/body.gold | 0 .../strategies_stale/test_strategies_stale.py | 163 ++ .../next_hop/zzz_strategies_peer/body.gold | 0 .../test_zzz_strategies_peer.py | 198 ++ .../next_hop/zzz_strategies_peer/trace.gold | 0 .../next_hop/zzz_strategies_peer2/body.gold | 0 .../test_zzz_strategies_peer2.py | 190 ++ .../next_hop/zzz_strategies_peer2/trace.gold | 0 .../gold/null_transform-200.gold | 0 .../gold/null_transform-tag.gold | 0 .../null_transform/test_null_transform.py | 86 + .../origin_connection/gold/two_200_ok.gold | 0 .../gold/two_503_congested.gold | 0 .../minimum_keep_alive.test.yaml | 2 +- .../per_server_metric_enabled.replay.yaml | 0 .../slow_servers.replay.yaml | 0 .../test_per_server_connection_max.py | 203 ++ .../test_per_server_metric_enabled.py | 132 ++ .../test_parent_config_reload.py | 98 + .../parent_retry_failure_stays_down.test.yaml | 2 +- .../parent_retry_non_retryable_post.test.yaml | 2 +- .../parent_retry_success_restores.test.yaml | 2 +- .../test_consistent_hash_ring_walk.py | 162 ++ .../parent_proxy/test_parent_retry.py | 66 + .../pipeline/desync_client.py | 0 .../pipeline/desync_client_miss.py | 0 .../pipeline/desync_server.py | 0 .../pipeline/noop_keepalive_client.py | 0 .../pipeline/pipeline_client.py | 0 .../pipeline/pipeline_server.py | 0 .../pipeline/request_framing_client.py | 0 .../pipeline/request_framing_server.py | 0 .../test_delete_maxforwards_body_desync.py | 109 ++ .../pipeline/test_delete_maxfwd_noop_drain.py | 108 ++ .../pipeline/test_noop_keepalive.py | 111 ++ tests/uranium_tests/pipeline/test_pipeline.py | 272 +++ .../pluginTest/TSVConnFd/CMakeLists.txt | 2 +- .../pluginTest/TSVConnFd/TSVConnFd.cc | 0 .../pluginTest/TSVConnFd/test_TSVConnFd.py | 94 + .../access_control/etc/hmac_keys.txt | 0 .../replays/access_control.replay.yaml | 0 .../access_control/test_access_control.py | 102 + .../block_errors/test_block_errors.py | 197 ++ .../pluginTest/cache_fill/test_cache_fill.py | 261 +++ .../replay/cache_promote.replay.yaml.tmpl | 0 .../cache_promote/test_cache_promote.py | 96 + .../gold/full.stderr.gold | 0 .../gold/inner.stderr.gold | 0 .../gold/last.stderr.gold | 0 .../cache_range_requests/reason.conf | 0 .../test_cache_range_requests.py | 390 ++++ ...range_requests_cache_complete_responses.py | 420 ++++ .../test_cache_range_requests_cachekey.py | 185 ++ ...st_cache_range_requests_cachekey_global.py | 166 ++ .../test_cache_range_requests_ident.py | 371 ++++ .../cert_update/gold/client-cert-after.gold | 0 .../cert_update/gold/client-cert-pre.gold | 0 .../cert_update/gold/server-cert-after.gold | 0 .../cert_update/gold/server-cert-pre.gold | 0 .../pluginTest/cert_update/gold/update.gold | 0 .../pluginTest/cert_update/ssl/client1.pem | 0 .../pluginTest/cert_update/ssl/client2.pem | 0 .../pluginTest/cert_update/ssl/server1.pem | 0 .../pluginTest/cert_update/ssl/server2.pem | 0 .../cert_update/test_cert_update.py | 170 ++ .../pluginTest/certifier/certs/ca-serial.txt | 0 .../pluginTest/certifier/certs/ca.cert | 0 .../pluginTest/certifier/certs/ca.key | 0 .../replays/https-no-sni.replay.yaml | 0 .../replays/https-path-traversal.replay.yaml | 0 .../replays/https-two-sessions.replay.yaml | 0 .../certifier/replays/https.replay.yaml | 0 .../pluginTest/certifier/test_certifier.py | 307 +++ .../client_context_dump/ssl/one.com.pem | 0 .../client_context_dump/ssl/two.com.pem | 0 .../test_client_context_dump.py | 102 + .../cache_control_aggregation.gold | 0 .../combo_handler_files/max_age_zero.gold | 0 .../combo_handler_files/tr1.gold | 0 .../combo_handler_files/tr2.gold | 0 .../combo_handler_files/tr3.gold | 0 .../pluginTest/combo_handler/ctwl.txt | 0 .../combo_handler/test_combo_handler.py | 195 ++ .../pluginTest/compress/compress.config | 0 .../pluginTest/compress/compress.gold | 0 .../pluginTest/compress/compress2.config | 0 .../pluginTest/compress/compress3.config | 0 .../compress/compress_100_continue_origin.py | 0 .../pluginTest/compress/compress_observer.py | 2 +- .../pluginTest/compress/compress_userver.gold | 0 .../pluginTest/compress/compress_vary.gold | 0 .../etc/cache-true-no-compression.config | 0 .../cache-true-remove-accept-encoding.config | 0 .../etc/cache-true-remove-range.config | 0 .../compress/etc/compress-cache-false.config | 0 .../compress/etc/ignore-params-false.config | 0 .../compress/etc/ignore-params-true.config | 0 .../pluginTest/compress/greplog.sh | 0 .../replay/compress-and-range.replay.yaml | 0 .../compress-content-type-params.replay.yaml | 0 .../pluginTest/compress/test_compress.py | 343 ++++ .../test_compress_cache_untransformed.py | 137 ++ .../test_compress_content_type_params.py | 86 + .../compress/test_compress_range.py | 92 + .../conf_remap/etc/negative_caching_list.yaml | 0 .../conf_remap/replay/conf_remap.replay.yaml | 0 .../pluginTest/conf_remap/test_conf_remap.py | 91 + .../cookie_remap/configs/bucketconfig.txt | 0 .../cookie_remap/configs/collapseconfig.txt | 0 .../cookie_remap/configs/connectorconfig.txt | 0 ...disable_pristine_host_hdr_config_false.txt | 0 .../disable_pristine_host_hdr_config_true.txt | 0 .../cookie_remap/configs/existsconfig.txt | 0 .../cookie_remap/configs/matchconfig.txt | 0 .../cookie_remap/configs/matchuriconfig.txt | 0 .../cookie_remap/configs/notexistsconfig.txt | 0 .../cookie_remap/configs/pcollapseconfig.txt | 0 .../configs/psubstituteconfig.txt | 0 .../cookie_remap/configs/regexconfig.txt | 0 .../cookie_remap/configs/statusconfig.txt | 0 .../cookie_remap/configs/subcookie.txt | 0 .../cookie_remap/configs/substituteconfig.txt | 0 ...isable_pristine_host_hdr_false.replay.yaml | 0 ...e_host_hdr_server_canary_false.replay.yaml | 0 ...ne_host_hdr_server_canary_true.replay.yaml | 0 ...ristine_host_hdr_server_stable.replay.yaml | 0 ...disable_pristine_host_hdr_true.replay.yaml | 0 .../cookie_remap/gold/collapseslashes.gold | 0 .../cookie_remap/gold/doesntexistcookie.gold | 0 .../cookie_remap/gold/existscookie.gold | 0 .../cookie_remap/gold/matchcookie.gold | 0 .../cookie_remap/gold/matchcookie2.gold | 0 .../cookie_remap/gold/matchelsestatus.gold | 0 .../cookie_remap/gold/matchstatus.gold | 0 .../cookie_remap/gold/psubstitute.gold | 0 .../cookie_remap/gold/regexdoesntmatch.gold | 0 .../cookie_remap/gold/regexmatches.gold | 0 .../cookie_remap/gold/substitute.gold | 0 .../cookie_remap/gold/wontmatchcookie.gold | 0 .../cookie_remap/gold/wontmatchcookie2.gold | 0 .../cookie_remap/test_bucketcookie.py | 130 ++ .../cookie_remap/test_collapseslashes.py | 99 + .../pluginTest/cookie_remap/test_connector.py | 130 ++ .../test_disable_pristine_host_hdr.py | 150 ++ .../cookie_remap/test_existscookie.py | 130 ++ .../cookie_remap/test_matchcookie.py | 130 ++ .../pluginTest/cookie_remap/test_matchuri.py | 129 ++ .../cookie_remap/test_notexistscookie.py | 129 ++ .../cookie_remap/test_pcollapseslashes.py | 100 + .../cookie_remap/test_psubstitute.py | 164 ++ .../cookie_remap/test_regexcookie.py | 130 ++ .../pluginTest/cookie_remap/test_setstatus.py | 98 + .../pluginTest/cookie_remap/test_subcookie.py | 132 ++ .../cookie_remap/test_substitute.py | 154 ++ .../pluginTest/cppapi/test_cppapi.py | 52 + .../pluginTest/crash_test/CMakeLists.txt | 2 +- .../pluginTest/crash_test/crash_test.cc | 0 .../pluginTest/crash_test/test_crash_test.py | 122 ++ .../escalate/escalate_failover.replay.yaml | 0 ...calate_failover_server_default.replay.yaml | 0 ...calate_failover_server_non_get.replay.yaml | 0 .../escalate_non_get_methods.replay.yaml | 0 .../escalate/escalate_original.replay.yaml | 0 ...calate_original_server_default.replay.yaml | 0 ...calate_original_server_non_get.replay.yaml | 0 .../pluginTest/escalate/test_escalate.py | 315 +++ .../esi/esi_nested_html_comment.replay.yaml | 0 ...esi_nested_html_comment_reject.replay.yaml | 0 .../esi/esi_nested_include.replay.yaml | 0 .../esi/esi_ssrf_validate.replay.yaml | 0 .../esi/gold/empty_response_body.gold | 0 .../pluginTest/esi/gold/esi_body.gold | 0 .../pluginTest/esi/gold/esi_gzipped.gold | 0 .../pluginTest/esi/gold/esi_headers.gold | 0 .../esi/gold/esi_private_headers.gold | 0 .../esi/gold/nested_include_body.gold | 0 .../uranium_tests/pluginTest/esi/test_esi.py | 399 ++++ .../pluginTest/esi/test_esi_304.py | 203 ++ .../esi/test_esi_nested_html_comment.py | 174 ++ .../pluginTest/esi/test_esi_nested_include.py | 125 ++ .../esi/test_esi_request_size_cap.py | 112 ++ .../pluginTest/esi/test_esi_ssrf_validate.py | 134 ++ .../config/filter_body_request_block.yaml | 0 .../config/filter_body_request_header.yaml | 0 .../config/filter_body_request_log.yaml | 0 .../config/filter_body_response_block.yaml | 0 .../config/filter_body_response_header.yaml | 0 .../config/filter_body_response_log.yaml | 0 .../filter_body/filter_body.test.yaml | 2 +- .../header_rewrite-set_body_from_200.gold | 0 ...eader_rewrite-set_body_from_conn_fail.gold | 0 ...ader_rewrite-set_body_from_remap_fail.gold | 0 .../header_rewrite-set_body_from_success.gold | 0 .../gold/header_rewrite-tag.gold | 0 .../gold/plugin-status-test.gold | 0 .../gold/set-redirect-glob.gold | 0 .../header_rewrite_bundle.test.yaml | 4 +- .../header_rewrite_post_remap.test.yaml | 2 +- .../pluginTest/header_rewrite/post_remap.conf | 0 .../header_rewrite/rules/complex_logics.conf | 0 .../rules/glob_set_redirect.conf | 0 .../header_rewrite/rules/implicit_hook.conf | 0 .../header_rewrite/rules/nested_ifs.conf | 0 .../header_rewrite/rules/query_sub_key.conf | 0 .../header_rewrite/rules/regex_tests.conf | 0 .../pluginTest/header_rewrite/rules/rule.conf | 0 .../rules/rule_add_cache_result_header.conf | 0 .../header_rewrite/rules/rule_cidr.conf | 0 .../header_rewrite/rules/rule_client.conf | 0 .../rules/rule_cond_method.conf | 0 .../header_rewrite/rules/rule_cookie.conf | 0 .../rules/rule_effective_address.conf | 0 .../header_rewrite/rules/rule_empty_body.conf | 0 .../header_rewrite/rules/rule_l_value.conf | 0 .../rules/rule_server_conditions.conf | 0 .../rules/rule_session_vars.conf | 0 .../rules/rule_set_body_from_plugin.conf | 0 .../rules/rule_set_body_from_remap.conf | 0 .../rules/rule_set_body_status.conf | 0 .../rule_set_header_after_ssn_txn_count.conf | 0 .../header_rewrite/rules/run_plugin.conf | 0 .../header_rewrite/rules/set_redirect.conf | 0 .../rules/set_status_in_if.conf | 0 .../test_header_rewrite_bad_run_plugin.py | 178 ++ .../test_header_rewrite_set_body_from.py | 224 +++ .../test_header_rewrite_url_glob.py | 85 + .../pluginTest/healthchecks/acme | 0 .../pluginTest/healthchecks/acme-ssl | 0 .../healthchecks/test_healthchecks.py | 235 +++ .../ja3_fingerprint_global.replay.yaml | 0 .../ja3_fingerprint_remap.replay.yaml | 0 .../modify-incoming-client.gold | 0 .../modify-incoming-proxy.gold | 0 .../ja3_fingerprint/modify-sent-client.gold | 0 .../modify-sent-proxy-global.gold | 0 .../modify-sent-proxy-remap.gold | 0 .../ja3_fingerprint/test_ja3_fingerprint.py | 228 +++ .../ja4_fingerprint.replay.yaml | 0 .../ja4_fingerprint_basic_server.replay.yaml | 0 .../ja4_fingerprint/test_ja4_fingerprint.py | 226 +++ .../jax_fingerprint_all_methods.replay.yaml | 0 .../jax_fingerprint_append.replay.yaml | 0 .../jax_fingerprint_global.replay.yaml | 0 .../jax_fingerprint_global_h2.replay.yaml | 0 ...fingerprint_hybrid_servernames.replay.yaml | 0 .../jax_fingerprint_keep.replay.yaml | 0 .../jax_fingerprint_remap.replay.yaml | 0 .../jax_fingerprint_remap_tls.replay.yaml | 0 .../jax_fingerprint_servernames.replay.yaml | 0 .../jax_fingerprint_tls.replay.yaml | 0 .../jax_fingerprint/test_jax_fingerprint.py | 528 ++++++ .../pluginTest/lua/client_hook.lua | 0 .../pluginTest/lua/fetch_ipv6_cliaddr.lua | 0 .../pluginTest/lua/global.lua | 0 .../pluginTest/lua/global_shutdown.lua | 0 .../pluginTest/lua/gold/lifecycle.gold | 0 .../lua/gold/lua_proxy_protocol.gold | 0 .../pluginTest/lua/gold/metrics.gold | 0 .../pluginTest/lua/header_table.lua | 0 .../pluginTest/lua/hello.lua | 0 .../pluginTest/lua/lifecycle_stats.sh | 0 .../pluginTest/lua/metrics.sh | 0 .../pluginTest/lua/proxy_protocol.lua | 0 .../pluginTest/lua/remap_after_hook.lua | 0 .../pluginTest/lua/set_error_response.lua | 0 .../lua/set_error_response.replay.yaml | 0 .../pluginTest/lua/tags.lua | 0 .../pluginTest/lua/tags.sh | 0 .../pluginTest/lua/test_lua_client_hook.py | 78 + .../pluginTest/lua/test_lua_debug_tags.py | 72 + .../lua/test_lua_fetch_ipv6_cliaddr.py | 80 + .../lua/test_lua_global_shutdown.py | 86 + .../pluginTest/lua/test_lua_header_table.py | 62 + .../pluginTest/lua/test_lua_proxy_protocol.py | 103 + .../lua/test_lua_remap_after_hook.py | 76 + .../lua/test_lua_set_error_response.py | 118 ++ .../pluginTest/lua/test_lua_states_stats.py | 117 ++ .../pluginTest/lua/test_lua_verified_addr.py | 101 + .../pluginTest/lua/test_lua_watermark.py | 78 + .../pluginTest/lua/verified_addr.lua | 0 .../pluginTest/lua/watermark.lua | 0 .../money_trace/gold/global-log.gold | 0 .../money_trace/gold/remap-log.gold | 0 .../money_trace/test_money_trace.py | 229 +++ .../money_trace/test_money_trace_global.py | 132 ++ .../replays/multiplexer_copy.replay.yaml | 0 .../multiplexer_copy_skip_post.replay.yaml | 0 ...multiplexer_invalid_chunk_copy.replay.yaml | 0 ...iplexer_invalid_chunk_original.replay.yaml | 0 .../replays/multiplexer_original.replay.yaml | 0 ...multiplexer_original_skip_post.replay.yaml | 0 .../multiplexer/test_multiplexer.py | 343 ++++ .../gold/origin_server_auth_parsing_ts.gold | 0 .../origin_server_auth_parsing_ts_uds 2.gold | 0 .../origin_server_auth_parsing_ts_uds.gold | 0 .../rules/s3_url_encoding.test_input | 0 .../rules/v4-parse-test.test_input | 0 .../test_origin_server_auth.py | 156 ++ .../test_s3_url_encoding.py | 217 +++ .../packet_mark/test_packet_mark.py | 196 ++ .../per_plugin_metrics/rules/global.conf | 0 .../test_per_plugin_metrics.py | 132 ++ .../plugin_yaml/test_plugin_yaml.py | 113 ++ .../polite_hook_wait/CMakeLists.txt | 2 +- .../pluginTest/polite_hook_wait/curl.gold | 0 .../polite_hook_wait/polite_hook_wait.cc | 0 .../polite_hook_wait/test_polite_hook_wait.py | 92 + .../pluginTest/prefetch/header_rewrite.conf | 0 .../pluginTest/prefetch/prefetch_bignum.gold | 0 .../pluginTest/prefetch/prefetch_cmcd0.gold | 0 .../pluginTest/prefetch/prefetch_cmcd1.gold | 0 .../prefetch/prefetch_optional_group.gold | 0 .../prefetch/prefetch_overflow.gold | 0 .../prefetch_query_path_traversal.test.yaml | 2 +- .../pluginTest/prefetch/prefetch_simple.gold | 0 .../test_prefetch_bad_count_refused.py | 74 + .../test_prefetch_bad_pattern_refused.py | 75 + .../prefetch/test_prefetch_bignum.py | 103 + .../pluginTest/prefetch/test_prefetch_cmcd.py | 311 +++ .../test_prefetch_empty_replacement.py | 111 ++ .../prefetch/test_prefetch_optional_group.py | 116 ++ .../prefetch/test_prefetch_overflow.py | 96 + .../prefetch/test_prefetch_simple.py | 96 + .../rate_limit/concurrent_reject.sh | 0 .../rate_limit/independent_limiters.sh | 0 .../rate_limit/queue_bypass_regression.sh | 0 .../pluginTest/rate_limit/queue_drain.sh | 0 .../rate_limit_sni_expiry_client.sh | 0 .../rate_limit/rate_limit_sni_queue_client.sh | 0 .../rate_limit_sni_reject_client.sh | 0 .../pluginTest/rate_limit/retry_after.sh | 0 .../pluginTest/rate_limit/sequential_pass.sh | 0 .../pluginTest/rate_limit/test_rate_limit.py | 153 ++ .../rate_limit/test_rate_limit_iprep.py | 141 ++ .../rate_limit/test_rate_limit_sni.py | 142 ++ .../rate_limit/test_rate_limit_sni_expiry.py | 107 ++ .../rate_limit/test_rate_limit_sni_queue.py | 108 ++ .../rate_limit/test_rate_limit_sni_reject.py | 103 + .../redirect_limit/test_redirect_limit.py | 155 ++ .../regex_remap/gold/regex_remap_crash.gold | 0 .../gold/regex_remap_redirect.gold | 0 .../regex_remap/gold/regex_remap_simple.gold | 0 .../regex_remap/gold/regex_remap_smoke.gold | 0 .../regex_remap/replay/yts-2819.replay.json | 0 .../regex_remap/test_regex_remap.py | 273 +++ .../regex_revalidate/gold/metrics.gold | 0 .../regex_revalidate/gold/metrics_miss.gold | 0 .../pluginTest/regex_revalidate/metrics.sh | 0 .../regex_revalidate/metrics_miss.sh | 0 .../regex_revalidate/test_regex_revalidate.py | 297 +++ .../test_regex_revalidate_miss.py | 271 +++ .../test_regex_revalidate_state.py | 160 ++ .../pluginTest/remap_stats/gold/metrics.gold | 0 .../remap_stats/gold/metrics_post.gold | 0 .../pluginTest/remap_stats/metrics.sh | 0 .../pluginTest/remap_stats/metrics_post.sh | 0 .../remap_stats/test_remap_stats.py | 87 + .../remap_stats/test_remap_stats_post.py | 87 + .../gold/server_push_preload_0_stdout.gold | 0 .../test_server_push_preload.py | 125 ++ .../pluginTest/slice/gold/aa.gold | 0 .../pluginTest/slice/gold/aaa.gold | 0 .../pluginTest/slice/gold/bb.gold | 0 .../pluginTest/slice/gold/bbb.gold | 0 .../slice/gold/slice_200.stderr.gold | 0 .../slice/gold/slice_206.stderr.gold | 0 .../slice/gold/slice_crr_ident.gold | 0 .../slice/gold/slice_first.stderr.gold | 0 .../pluginTest/slice/gold/slice_ident.gold | 0 .../slice/gold/slice_last.stderr.gold | 0 .../slice/gold/slice_mid.stderr.gold | 0 .../slice/gold_error/contents.stderr.gold | 0 .../slice/replay/slice_long_etag.test.yaml | 2 +- .../slice/replay/slice_purge.replay.yaml | 0 .../slice_purge_gaps_client.replay.yaml | 0 .../slice_purge_gaps_server.replay.yaml | 0 .../replay/slice_purge_no_ref.replay.yaml | 0 .../slice/replay/slice_purge_ref.replay.yaml | 0 .../slice/replay/slice_range.replay.yaml | 0 .../slice_stale_generation_client.replay.yaml | 0 .../slice_stale_generation_server.replay.yaml | 0 .../slice/rules/purge_block_failure.conf | 0 .../pluginTest/slice/shrink_origin.py | 0 .../pluginTest/slice/test_slice.py | 206 ++ .../slice/test_slice_conditional.py | 179 ++ .../slice/test_slice_content_shrink.py | 116 ++ .../pluginTest/slice/test_slice_crr_ident.py | 241 +++ .../pluginTest/slice/test_slice_error.py | 341 ++++ .../pluginTest/slice/test_slice_ident.py | 208 ++ .../pluginTest/slice/test_slice_prefetch.py | 236 +++ .../pluginTest/slice/test_slice_purge.py | 115 ++ .../pluginTest/slice/test_slice_purge_gaps.py | 366 ++++ .../pluginTest/slice/test_slice_regex.py | 168 ++ .../pluginTest/slice/test_slice_rm_range.py | 89 + .../slice/test_slice_selfhealing.py | 449 +++++ .../slice/test_slice_stale_generation.py | 345 ++++ .../pluginTest/sslheaders/observer.py | 0 .../pluginTest/sslheaders/sslheaders.gold | 0 .../pluginTest/sslheaders/test_sslheaders.py | 99 + .../stale_response_max_memory.replay.yaml | 0 .../stale_response_no_default.replay.yaml | 0 .../stale_response_with_defaults.replay.yaml | 0 .../stale_response_with_force_sie.replay.yaml | 0 .../stale_response_with_force_swr.replay.yaml | 0 .../stale_response/test_stale_response.py | 206 ++ .../pluginTest/statichit/empty.txt | 0 .../pluginTest/statichit/small_body.txt | 0 .../statichit/statichit.replay.yaml | 0 .../pluginTest/statichit/story_16.json | 0 .../pluginTest/statichit/test_statichit.py | 133 ++ .../gold/stats_over_http_csv_stderr.gold | 0 .../gold/stats_over_http_json_stderr.gold | 0 .../stats_over_http_prometheus_stderr.gold | 0 ...over_http_prometheus_v2_accept_stderr.gold | 0 .../stats_over_http_prometheus_v2_stderr.gold | 0 .../prometheus_stats_ingester.py | 0 .../stats_over_http/test_stats_over_http.py | 422 +++++ .../pluginTest/stek_share/server_list.yaml | 0 .../pluginTest/stek_share/ssl/self_signed.crt | 0 .../pluginTest/stek_share/ssl/self_signed.key | 0 .../pluginTest/stek_share/test_stek_share.py | 354 ++++ .../strategies/test_strategies_plugins.py | 432 +++++ .../pluginTest/test_hooks/200.gold | 0 .../pluginTest/test_hooks/clean.sh | 0 .../pluginTest/test_hooks/hook_add.gold | 0 .../pluginTest/test_hooks/log.gold | 0 .../pluginTest/test_hooks/log_uds.gold | 0 .../test_hooks/replay/body_buffer.test.yaml | 2 +- .../replay/zero_post_copy_size.test.yaml | 2 +- .../pluginTest/test_hooks/ssn_delay.gold | 0 .../pluginTest/test_hooks/test_hook_add.py | 79 + .../test_hooks/test_ssn_start_delay_hook.py | 81 + .../pluginTest/test_hooks/test_test_hooks.py | 126 ++ .../traffic_dump/gold/200_bob_no_sni.gold | 0 .../pluginTest/traffic_dump/gold/200_get.gold | 0 .../gold/200_get_sensitive_field.gold | 0 .../traffic_dump/gold/200_http10.gold | 0 .../traffic_dump/gold/200_post.gold | 0 .../traffic_dump/gold/200_sni_bob.gold | 0 .../traffic_dump/gold/200_sni_dave.gold | 0 .../gold/4_byte_response_body.gold | 0 .../traffic_dump/gold/explicit_target.gold | 0 .../traffic_dump/gold/post_with_body.gold | 0 .../traffic_dump/gold/two_transactions.gold | 0 .../pluginTest/traffic_dump/replay/http3.yaml | 0 .../traffic_dump/replay/response_body.yaml | 0 .../traffic_dump/replay/traffic_dump.yaml | 0 .../replay/traffic_dump_ip_filter_server.yaml | 0 .../replay/traffic_dump_server.yaml | 0 .../traffic_dump/replay/various_sni.yaml | 0 .../pluginTest/traffic_dump/ssl/server.key | 0 .../pluginTest/traffic_dump/ssl/server.pem | 0 .../traffic_dump/ssl/server_combined.pem | 0 .../traffic_dump/ssl/signed-foo.key | 0 .../traffic_dump/ssl/signed-foo.pem | 0 .../pluginTest/traffic_dump/ssl/signer.pem | 0 .../traffic_dump/test_traffic_dump.py | 451 +++++ .../traffic_dump/test_traffic_dump_http3.py | 163 ++ .../test_traffic_dump_ip_filter.py | 135 ++ .../test_traffic_dump_response_body.py | 166 ++ .../test_traffic_dump_sni_filter.py | 149 ++ .../pluginTest/traffic_dump/verify_replay.py | 0 .../transform/test_transaction_data_sink.py | 127 ++ .../transaction-with-body.replays.yaml | 0 .../pluginTest/tsapi/CMakeLists.txt | 12 +- .../pluginTest/tsapi/hrw_verified_addr.conf | 0 .../pluginTest/tsapi/log.gold | 0 .../pluginTest/tsapi/log_uds.gold | 0 .../pluginTest/tsapi/test_TSHttpSsnInfo.cc | 2 +- .../tsapi/test_TSHttpSsnInfo_curl0.gold | 0 .../tsapi/test_TSHttpSsnInfo_nghttp0.gold | 0 .../tsapi/test_TSHttpSsnInfo_plugin_log.gold | 0 .../tsapi/test_TSHttpTxnServerAddrSet.cc | 0 .../test_TSHttpTxnServerAddrSet.replay.yaml | 0 .../test_TSHttpTxnServerAddrSet_retry.cc | 0 .../tsapi/test_TSHttpTxnVerifiedAddr.cc | 0 .../pluginTest/tsapi/test_TSVConnPPInfo.cc | 2 +- .../tsapi/test_TSVConnPPInfo_curl0.gold | 0 .../tsapi/test_TSVConnPPInfo_curl1.gold | 0 .../tsapi/test_TSVConnPPInfo_plugin_log.gold | 0 .../tsapi/test_test_TSHttpSsnInfo.py | 134 ++ .../tsapi/test_test_TSHttpTxnServerAddrSet.py | 112 ++ .../test_test_TSHttpTxnServerAddrSet_retry.py | 111 ++ .../tsapi/test_test_TSHttpTxnVerifiedAddr.py | 86 + .../tsapi/test_test_TSVConnPPInfo.py | 133 ++ .../pluginTest/tsapi/test_tsapi.cc | 2 +- .../pluginTest/tsapi/test_tsapi.py | 135 ++ .../txn_box/basic/basic.replay.yaml | 0 .../pluginTest/txn_box/basic/cmp.replay.yaml | 0 .../txn_box/basic/ip-addr.replay.yaml | 0 .../pluginTest/txn_box/basic/mod.replay.yaml | 0 .../pluginTest/txn_box/basic/multi-cfg.1.yaml | 0 .../pluginTest/txn_box/basic/multi-cfg.2.yaml | 0 .../txn_box/basic/multi-cfg.replay.yaml | 0 .../txn_box/basic/redirect.replay.yaml | 0 .../txn_box/basic/reply.replay.yaml | 0 .../pluginTest/txn_box/basic/rxp.replay.yaml | 0 .../pluginTest/txn_box/basic/stat.replay.yaml | 0 .../txn_box/basic/test_txn_box_basic.py | 65 + .../txn_box/basic/test_txn_box_cmp.py | 47 + .../txn_box/basic/test_txn_box_ip_addr.py | 55 + .../txn_box/basic/test_txn_box_mod.py | 63 + .../txn_box/basic/test_txn_box_multi_cfg.py | 64 + .../txn_box/basic/test_txn_box_redirect.py | 73 + .../txn_box/basic/test_txn_box_reply.py | 68 + .../txn_box/basic/test_txn_box_rxp.py | 58 + .../txn_box/basic/test_txn_box_stat.py | 81 + .../txn_box/basic/test_txn_box_tls.py | 83 + .../txn_box/basic/test_txn_box_tls_cert.py | 88 + .../txn_box/basic/test_txn_box_tuple.py | 50 + .../txn_box/basic/test_txn_box_txn_debug.py | 88 + .../txn_box/basic/test_txn_box_txn_error.py | 78 + .../txn_box/basic/test_txn_box_txn_open_1.py | 62 + .../txn_box/basic/test_txn_box_txn_open_2.py | 67 + .../txn_box/basic/test_txn_box_txn_open_3.py | 62 + .../txn_box/basic/test_txn_box_with.py | 58 + .../txn_box/basic/tls-cert.replay.yaml | 0 .../pluginTest/txn_box/basic/tls.replay.yaml | 0 .../txn_box/basic/tuple.replay.yaml | 0 .../txn_box/basic/txn-debug.replay.yaml | 0 .../txn_box/basic/txn-error.replay.yaml | 0 .../txn_box/basic/txn_open_1.replay.yaml | 0 .../txn_box/basic/txn_open_2.replay.yaml | 0 .../txn_box/basic/txn_open_3.replay.yaml | 0 .../pluginTest/txn_box/basic/with.replay.yaml | 0 .../txn_box/ct_header/ct_header.replay.yaml | 0 .../ct_header/test_txn_box_ct_header.py | 86 + .../example/accept-encoding.replay.yaml | 0 .../example/test_txn_box_accept_encoding.py | 61 + .../txn_box/prod/cors-origin.replay.yaml | 0 .../txn_box/prod/cors-referrer.replay.yaml | 0 .../pluginTest/txn_box/prod/ip-acl.csv | 0 .../txn_box/prod/ip-acl.replay.yaml | 0 .../txn_box/prod/mTLS-alpha.replay.yaml | 0 .../txn_box/prod/mTLS-bravo.replay.yaml | 0 .../pluginTest/txn_box/prod/mTLS.replay.yaml | 0 .../pluginTest/txn_box/prod/mTLS.txnbox.yaml | 0 .../txn_box/prod/query-delete.replay.yaml | 0 .../pluginTest/txn_box/prod/query.replay.yaml | 0 .../txn_box/prod/stanley.replay.yaml | 0 .../txn_box/prod/test_txn_box_cors_origin.py | 66 + .../prod/test_txn_box_cors_referrer.py | 66 + .../txn_box/prod/test_txn_box_ip_acl.py | 71 + .../txn_box/prod/test_txn_box_mTLS.py | 97 + .../txn_box/prod/test_txn_box_query.py | 72 + .../prod/test_txn_box_query_delete_filter.py | 67 + .../prod/test_txn_box_query_delete_regex.py | 67 + .../txn_box/prod/test_txn_box_stanley.py | 72 + .../txn_box/prod/test_txn_box_vznith_1.py | 68 + .../txn_box/prod/test_txn_box_yts_3489.py | 73 + .../txn_box/prod/vznith-1.replay.yaml | 0 .../txn_box/prod/yts-3489.replay.yaml | 0 .../txn_box/ramp/multi-ramp-1.cfg.yaml | 0 .../txn_box/ramp/multi-ramp-2.cfg.yaml | 0 .../txn_box/ramp/multi-ramp-3.cfg.yaml | 0 .../txn_box/ramp/multi-ramp-4.cfg.yaml | 0 .../txn_box/ramp/multi-ramp.replay.yaml | 0 .../txn_box/ramp/multi_ramp_common.py | 0 .../pluginTest/txn_box/ramp/ramp.logging.yaml | 0 .../pluginTest/txn_box/ramp/ramp.replay.yaml | 0 .../txn_box/ramp/test_txn_box_multi_ramp_1.py | 67 + .../txn_box/ramp/test_txn_box_multi_ramp_2.py | 66 + .../txn_box/ramp/test_txn_box_multi_ramp_3.py | 66 + .../txn_box/ramp/test_txn_box_ramp.py | 78 + .../txn_box/remap/remap-base.replay.yaml | 0 .../txn_box/remap/test_txn_box_remap_base.py | 90 + .../txn_box/smoke/smoke-2.replay.yaml | 0 .../txn_box/smoke/smoke.replay.yaml | 0 .../txn_box/smoke/test_txn_box_smoke.py | 59 + .../txn_box/smoke/test_txn_box_smoke_2.py | 60 + .../pluginTest/txn_box/ssl/alpha-ca.key | 0 .../pluginTest/txn_box/ssl/alpha-ca.pem | 0 .../pluginTest/txn_box/ssl/alpha-signed.cert | 0 .../pluginTest/txn_box/ssl/alpha-signed.key | 0 .../pluginTest/txn_box/ssl/alpha-signed.pem | 0 .../pluginTest/txn_box/ssl/bravo-ca.key | 0 .../pluginTest/txn_box/ssl/bravo-ca.pem | 0 .../pluginTest/txn_box/ssl/bravo-signed.cert | 0 .../pluginTest/txn_box/ssl/bravo-signed.key | 0 .../pluginTest/txn_box/ssl/bravo-signed.pem | 0 .../pluginTest/txn_box/ssl/ca-bundle.pem | 0 .../pluginTest/txn_box/ssl/charlie-ca.key | 0 .../pluginTest/txn_box/ssl/charlie-ca.pem | 0 .../txn_box/ssl/charlie-signed.cert | 0 .../pluginTest/txn_box/ssl/charlie-signed.key | 0 .../pluginTest/txn_box/ssl/charlie-signed.pem | 0 .../pluginTest/txn_box/ssl/pv-server.cert | 0 .../pluginTest/txn_box/ssl/server.key | 0 .../pluginTest/txn_box/ssl/server.pem | 0 .../static_file/static_file.replay.yaml | 0 .../txn_box/static_file/static_file.txt | 0 .../static_file/test_txn_box_static_file.py | 73 + .../txn_box/static_file/unreadable.txt | 0 .../pluginTest/uri_signing/config.json | 0 .../pluginTest/uri_signing/gold/200.gold | 0 .../pluginTest/uri_signing/gold/403.gold | 0 .../pluginTest/uri_signing/run_sign.sh | 0 .../pluginTest/uri_signing/signer.json | 0 .../uri_signing/test_uri_signing.py | 273 +++ .../pluginTest/url_sig/run_sign.sh | 0 .../pluginTest/url_sig/test_url_sig.py | 426 +++++ .../pluginTest/url_sig/url_sig.all.config | 0 .../pluginTest/url_sig/url_sig.config | 0 .../pluginTest/url_sig/url_sig.gold | 0 .../replay/webp_chunked_cap.replay.yaml | 0 .../test_webp_transform_cache_refused.py | 111 ++ .../test_webp_transform_chunked_cap.py | 95 + .../test_webp_transform_decode_limit.py | 138 ++ .../test_webp_transform_max_buffer_size.py | 129 ++ .../test_webp_transform_size_cap.py | 143 ++ .../webp_transform_invalid_input.test.yaml | 2 +- .../pluginTest/xdebug/x_cache_info/none.in | 0 .../pluginTest/xdebug/x_cache_info/one.in | 0 .../pluginTest/xdebug/x_cache_info/out.gold | 0 .../xdebug/x_cache_info/test_x_cache_info.py | 91 + .../pluginTest/xdebug/x_cache_info/three.in | 0 .../pluginTest/xdebug/x_cache_info/two.in | 0 .../pluginTest/xdebug/x_effective_url/four.in | 0 .../pluginTest/xdebug/x_effective_url/none.in | 0 .../pluginTest/xdebug/x_effective_url/one.in | 0 .../xdebug/x_effective_url/out.gold | 0 .../x_effective_url/test_x_effective_url.py | 107 ++ .../xdebug/x_effective_url/three.in | 0 .../pluginTest/xdebug/x_effective_url/two.in | 0 .../pluginTest/xdebug/x_probe/test_x_probe.py | 101 + .../xdebug/x_probe/x_probe.replay.yaml | 0 .../x_probe_full_json/gold/jq_escaped.gold | 0 .../xdebug/x_probe_full_json/gold/jq_hex.gold | 0 .../x_probe_full_json/gold/jq_nobody.gold | 0 .../test_x_probe_full_json.py | 182 ++ .../x_probe_full_json.replay.yaml | 0 .../slow-body-server.sh | 0 .../test_x_probe_full_json_slow_origin.py | 102 + .../verify_no_loop.sh | 0 .../pluginTest/xdebug/x_remap/four.in | 0 .../pluginTest/xdebug/x_remap/fwd1.in | 0 .../pluginTest/xdebug/x_remap/fwd2.in | 0 .../pluginTest/xdebug/x_remap/fwd3.in | 0 .../pluginTest/xdebug/x_remap/fwd4.in | 0 .../pluginTest/xdebug/x_remap/fwd5.in | 0 .../pluginTest/xdebug/x_remap/none.in | 0 .../pluginTest/xdebug/x_remap/one.in | 0 .../pluginTest/xdebug/x_remap/out.gold | 0 .../pluginTest/xdebug/x_remap/test_x_remap.py | 102 + .../pluginTest/xdebug/x_remap/three.in | 0 .../pluginTest/xdebug/x_remap/two.in | 0 .../xdebug/x_remap/x_remap-observer.py | 0 .../pluginTest/xdebug/x_remap/x_remap.gold | 0 .../x_remap_long_url.test.yaml | 2 +- .../post/delay_client.sh | 0 .../post/delay_client2.sh | 0 .../post/delay_client3.sh | 0 .../post/expect_client.py | 0 .../post/gold/post-h1.gold | 0 .../post/gold/post-h2.gold | 0 .../post/http_utils.py | 0 .../post/replay/expect-continue.replay.yaml | 0 .../post/replay/post-continue.replay.yaml | 0 .../post/replay/post-no-cl.test.yaml | 2 +- tests/uranium_tests/post/test_expect_tests.py | 107 ++ .../uranium_tests/post/test_post_continue.py | 301 +++ .../post/test_post_early_return.py | 179 ++ .../test_simple_post_valid_buffer_check.py | 72 + .../post_slow_server/check.sh | 0 ..._slow_server_max_requests_in_0_stderr.gold | 0 ..._slow_server_max_requests_in_0_stdout.gold | 0 .../post_slow_server/server.sh | 0 .../post_slow_server/test_post_slow_server.py | 93 + .../test_post_slow_server_max_requests_in.py | 93 + .../proxy_protocol/gold/access-cp.gold | 0 .../proxy_protocol/gold/access-nocp.gold | 0 .../proxy_protocol_allowlist.replay.yaml | 0 .../replay/proxy_protocol_in.replay.yaml | 0 .../replay/proxy_protocol_out.replay.yaml | 0 .../proxy_protocol/test_proxy_protocol.py | 354 ++++ .../qmux/go_qmux_client/go.mod | 0 .../qmux/go_qmux_client/go.sum | 0 .../qmux/go_qmux_client/main.go | 0 .../qmux/go_qmux_client/qmux_compat.go | 0 .../qmux/qmux.replay.yaml | 0 .../uranium_tests/qmux/test_qmux_go_client.py | 147 ++ .../records/gold/full_records.yaml | 1 + .../records/gold/no_newline.yaml | 0 .../records/gold/records.yaml.cold_test0.gold | 0 .../records/gold/records.yaml.cold_test2.gold | 0 .../records/gold/records.yaml.cold_test4.gold | 0 .../records/gold/records.yaml.cold_test5.gold | 0 .../records/gold/renamed_records.gold | 0 .../records/gold/renamed_records.yaml | 0 .../records/legacy_config/full_records.config | 1 + .../records/legacy_config/no_newline.config | 0 .../records/legacy_config/old_records.config | 0 .../records/legacy_config/override_map.config | 0 .../legacy_config/override_value.config | 0 .../records/test_records_config_to_yaml.py | 88 + .../test_records_runroot_precedence.py | 185 ++ .../records/test_records_yaml.py | 225 +++ .../records/test_traffic_ctl_cold_config.py | 115 ++ .../records/test_ts_max_records_param.py | 77 + .../redirect/.gitignore | 0 .../gold/number_of_redirections_0.gold | 0 .../gold/number_of_redirections_1.gold | 0 .../gold/number_of_redirections_2.gold | 0 .../redirect/gold/redirect.gold | 0 .../redirect/gold/redirect_log.gold | 0 .../redirect/gold/redirect_post.gold | 0 .../redirect/gold/redirect_stale.gold | 0 .../redirect/replay/redirect_srv1_replay.yaml | 0 .../redirect/replay/redirect_srv2_replay.yaml | 0 .../redirect/replay/redirect_srv3_replay.yaml | 0 ...direct_to_same_origin_on_cache.replay.yaml | 0 .../redirect/test_number_of_redirects.py | 119 ++ tests/uranium_tests/redirect/test_redirect.py | 205 ++ .../redirect/test_redirect_actions.py | 287 +++ .../redirect/test_redirect_post.py | 112 ++ .../redirect/test_redirect_stale.py | 118 ++ .../test_redirect_to_same_origin_on_cache.py | 160 ++ .../redirect/wait_for_log.sh | 0 .../redirect/zone.json | 0 .../remap/all_acl_combinations.py | 0 .../remap/base.replay.yaml | 0 .../remap/deactivate_ip_allow.py | 0 .../remap/deny_head_post.replay.yaml | 0 .../remap/gold/200OK_test.gold | 0 .../remap/gold/200OK_test_uds.gold | 0 .../remap/gold/lookupTest.gold | 0 .../remap/gold/map-with-recv-port-ip.gold | 0 .../remap/gold/map-with-recv-port-unix.gold | 0 .../remap/gold/remap-200.gold | 0 .../remap/gold/remap-404.gold | 0 .../remap/gold/remap-DNS-200.gold | 0 .../remap/gold/remap-DNS-ipv6-200.gold | 0 .../remap/gold/remap-hitATS-404.gold | 0 .../remap/gold/remap-https-200.gold | 0 .../remap/gold/remap-https-200_2.gold | 0 .../remap/gold/remap-https-200_3.gold | 0 .../remap/gold/remap-ip-resolve.gold | 0 .../remap/gold/remap-redirect.gold | 0 .../remap/gold/remap-referer-hit.gold | 0 .../remap/gold/remap-referer-miss.gold | 0 .../remap/gold/remap-ws-metrics-uds.gold | 0 .../remap/gold/remap-ws-metrics.gold | 0 .../remap/gold/remap-ws-upgrade-400.gold | 0 .../remap/gold/remap-ws-upgrade.gold | 0 .../remap/gold/remap-zero-200.gold | 0 .../remap/gold/remap2-200.gold | 0 .../remap/reload_1.replay.yaml | 0 .../remap/reload_2.replay.yaml | 0 .../remap/reload_3.replay.yaml | 0 .../remap/reload_4.replay.yaml | 0 .../remap/reload_server.replay.yaml | 0 .../remap/remap_acl_all_allowed.replay.yaml | 0 .../remap/remap_acl_all_denied.replay.yaml | 0 .../remap/remap_acl_get_allowed.replay.yaml | 0 .../remap_acl_get_post_allowed.replay.yaml | 0 .../remap_acl_get_post_allowed_pp.replay.yaml | 0 .../remap_acl_get_post_denied.replay.yaml | 0 .../remap/replay/regex_map_anchor.test.yaml | 2 +- .../remap/test_basic_conf_remap_yaml.py | 217 +++ .../remap/test_conf_remap_float.py | 64 + .../remap/test_map_with_recv_port.py | 115 ++ tests/uranium_tests/remap/test_regex_map.py | 84 + tests/uranium_tests/remap/test_remap_acl.py | 643 +++++++ tests/uranium_tests/remap/test_remap_http.py | 190 ++ tests/uranium_tests/remap/test_remap_https.py | 148 ++ .../remap/test_remap_ip_resolve.py | 94 + .../remap/test_remap_load_empty_failure.py | 63 + .../remap/test_remap_load_empty_success.py | 54 + .../remap/test_remap_load_missing_failure.py | 50 + .../remap/test_remap_load_missing_success.py | 54 + .../uranium_tests/remap/test_remap_reload.py | 129 ++ tests/uranium_tests/remap/test_remap_ws.py | 148 ++ .../remap_yaml/all_acl_combinations_yaml.py | 0 .../remap_yaml/base.replay.yaml | 0 .../remap_yaml/deactivate_ip_allow_yaml.py | 0 .../remap_yaml/deny_head_post.replay.yaml | 0 .../remap_yaml/gold/200OK_test.gold | 0 .../remap_yaml/gold/200OK_test_uds.gold | 0 .../remap_yaml/gold/lookupTest.gold | 0 .../gold/map-with-recv-port-ip.gold | 0 .../gold/map-with-recv-port-unix.gold | 0 .../remap_yaml/gold/remap-200.gold | 0 .../remap_yaml/gold/remap-404.gold | 0 .../remap_yaml/gold/remap-DNS-200.gold | 0 .../remap_yaml/gold/remap-DNS-ipv6-200.gold | 0 .../remap_yaml/gold/remap-hitATS-404.gold | 0 .../remap_yaml/gold/remap-https-200.gold | 0 .../remap_yaml/gold/remap-https-200_2.gold | 0 .../remap_yaml/gold/remap-https-200_3.gold | 0 .../remap_yaml/gold/remap-ip-resolve.gold | 0 .../remap_yaml/gold/remap-redirect.gold | 0 .../remap_yaml/gold/remap-referer-hit.gold | 0 .../remap_yaml/gold/remap-referer-miss.gold | 0 .../remap_yaml/gold/remap-ws-metrics-uds.gold | 0 .../remap_yaml/gold/remap-ws-metrics.gold | 0 .../remap_yaml/gold/remap-ws-upgrade-400.gold | 0 .../remap_yaml/gold/remap-ws-upgrade.gold | 0 .../remap_yaml/gold/remap-zero-200.gold | 0 .../remap_yaml/gold/remap2-200.gold | 0 .../remap_yaml/reload_1.replay.yaml | 0 .../remap_yaml/reload_2.replay.yaml | 0 .../remap_yaml/reload_3.replay.yaml | 0 .../remap_yaml/reload_4.replay.yaml | 0 .../remap_yaml/reload_server.replay.yaml | 0 .../remap_acl_all_allowed.replay.yaml | 0 .../remap_acl_all_denied.replay.yaml | 0 .../remap_acl_get_allowed.replay.yaml | 0 .../remap_acl_get_post_allowed.replay.yaml | 0 .../remap_acl_get_post_allowed_pp.replay.yaml | 0 .../remap_acl_get_post_denied.replay.yaml | 0 .../test_basic_conf_remap_yaml_yaml.py | 227 +++ .../remap_yaml/test_conf_remap_float_yaml.py | 74 + .../test_map_with_recv_port_yaml.py | 120 ++ .../remap_yaml/test_regex_map_yaml.py | 90 + .../remap_yaml/test_remap_acl_yaml.py | 661 +++++++ .../remap_yaml/test_remap_http_yaml.py | 224 +++ .../remap_yaml/test_remap_https_yaml.py | 165 ++ .../remap_yaml/test_remap_ip_resolve_yaml.py | 110 ++ .../test_remap_load_empty_failure_yaml.py | 63 + .../test_remap_load_empty_success_yaml.py | 54 + .../test_remap_load_missing_failure_yaml.py | 50 + .../test_remap_load_missing_success_yaml.py | 54 + .../remap_yaml/test_remap_reload_yaml.py | 175 ++ .../remap_yaml/test_remap_ws_yaml.py | 157 ++ .../runroot/test_runroot_error.py | 79 + .../runroot/test_runroot_init.py | 99 + .../runroot/test_runroot_remove.py | 91 + .../uranium_tests/runroot/test_runroot_use.py | 86 + .../runroot/test_runroot_verify.py | 86 + .../session_sharing/gold/200.gold | 0 .../session_sharing/test_session_match.py | 143 ++ .../test_crashlog_no_false_positive.py | 94 + .../uranium_tests/shutdown/test_emergency.py | 84 + tests/uranium_tests/shutdown/test_fatal.py | 83 + .../slow_post/__init__.py | 0 .../slow_post/gold/200.gold | 0 .../slow_post/quick_server.py | 0 .../slow_post/slow_post_client.py | 0 .../slow_post/slow_post_clients.py | 0 .../slow_post/test_quick_server.py | 153 ++ .../slow_post/test_secrets/aaa-signed.key | 0 .../slow_post/test_secrets/aaa-signed.pem | 0 .../slow_post/test_server_abort.py | 78 + .../uranium_tests/slow_post/test_slow_post.py | 103 + .../statistics/abort_client.py | 0 .../replay/metric_response_429.test.yaml | 2 +- .../statistics/test_metric_response_000.py | 130 ++ .../thread_config/check_threads.py | 0 .../thread_config/test_thread_config.py | 273 +++ .../timeout/.gitignore | 0 .../timeout/CMakeLists.txt | 0 .../timeout/case1.sh | 2 +- .../timeout/create_request.sh | 0 .../timeout/delay-server.sh | 0 .../client_default_inactivity_timeout.gold | 0 .../gold/quic_no_activity_timeout.gold | 0 .../timeout/http2_incomplete_header_client.py | 0 .../default_inactivity_timeout.replay.yaml | 0 ...ttp2_incomplete_header_timeout.replay.yaml | 0 .../http2_no_activity_timeout.replay.yaml | 0 .../quic_no_activity_timeout.replay.yaml | 0 .../timeout/setupnetns.sh | 0 .../timeout/slow_server.yaml | 0 .../timeout/slow_server_uds.yaml | 0 .../timeout/ssl-delay-server.cc | 0 .../timeout/ssl/cert.crt | 0 .../timeout/ssl/private-key.key | 0 .../timeout/test_accept_timeout.py | 97 + .../timeout/test_active_timeout.py | 96 + .../timeout/test_conn_timeout.py | 120 ++ .../test_default_inactivity_timeout.py | 122 ++ .../test_http2_incomplete_header_timeout.py | 226 +++ .../timeout/test_http2_no_activity_timeout.py | 176 ++ .../timeout/test_inactive_client_timeout.py | 99 + .../timeout/test_inactive_timeout.py | 88 + .../timeout/test_quic_no_activity_timeout.py | 154 ++ .../timeout/test_quic_poll_timeout.py | 96 + .../timeout/test_tls_conn_timeout.py | 140 ++ .../timeout/test_tunnel_active_timeout.py | 121 ++ .../timeout/time_client.sh | 0 .../timeout/tunnel_timeout_client.py | 0 .../tls/CMakeLists.txt | 0 .../tls/early_h1_get.txt | 0 .../tls/early_h1_post.txt | 0 .../tls/early_h2_get.txt | Bin .../tls/early_h2_multi1.txt | Bin .../tls/early_h2_multi2.txt | Bin .../tls/early_h2_post.txt | Bin .../tls/file.ticket | 0 .../tls/gold/accesslog.gold | 0 .../tls/gold/clientcert-accesslog.gold | 0 .../tls/gold/proxycert-accesslog.gold | 0 .../tls/gold/proxycert2-accesslog.gold | 0 .../tls/gold/ssl-post.gold | 0 .../tls-partial-blind-tunnel-metrics.gold | 0 .../tls/gold/tls-tunnel-forward-metrics.gold | 0 .../tls/gold/tls-tunnel-metrics.gold | 0 .../tls/h2_early_decode.py | 2 +- .../tls/h2_early_gen.py | 0 .../tls/proxy_protocol_client.py | 0 .../tls/receive_split_client_hello.py | 0 .../tls/replay/allow-plain.replay.yaml | 0 .../tls/replay/ip_allow.replay.yaml | 0 .../tls/replay/ip_allow_proxy.replay.yaml | 0 .../tls/replay/ip_allow_tunnel.replay.yaml | 0 .../outbound_sni_server_name_plain.test.yaml | 2 +- .../replay/tls_cert_compression.replay.yaml | 0 .../tls_cert_compression_cache.replay.yaml | 0 .../tls/split_client_hello.py | 0 .../tls/ssl-post.c | 0 .../tls/ssl/aaa-ca.key | 0 .../tls/ssl/aaa-ca.pem | 0 .../tls/ssl/aaa-signed.key | 0 .../tls/ssl/aaa-signed.pem | 0 .../tls/ssl/bbb-ca.key | 0 .../tls/ssl/bbb-ca.pem | 0 .../tls/ssl/bbb-signed.key | 0 .../tls/ssl/bbb-signed.pem | 0 .../tls/ssl/ca.ocsp.key | 0 .../tls/ssl/ca.ocsp.pem | 0 .../tls/ssl/ccc-ca.key | 0 .../tls/ssl/ccc-ca.pem | 0 .../tls/ssl/ccc-signed.key | 0 .../tls/ssl/ccc-signed.pem | 0 .../tls/ssl/combined-ec.pem | 0 .../tls/ssl/combined.pem | 0 .../tls/ssl/combo-signed-foo.pem | 0 .../tls/ssl/combo.pem | 0 .../tls/ssl/gen_client_certs.sh | 0 .../tls/ssl/gen_foobar_certs.sh | 0 .../tls/ssl/ocsp_response.der | Bin .../tls/ssl/openssl.cnf | 0 .../tls/ssl/passphrase.key | 0 .../tls/ssl/passphrase.pem | 0 .../tls/ssl/passphrase2.key | 0 .../tls/ssl/passphrase2.pem | 0 .../tls/ssl/responder.ocsp.key | 0 .../tls/ssl/responder.ocsp.pem | 0 .../tls/ssl/server-bar.key | 0 .../tls/ssl/server-bar.pem | 0 .../tls/ssl/server-foo.key | 0 .../tls/ssl/server-foo.pem | 0 .../tls/ssl/server.key | 0 .../tls/ssl/server.ocsp.key | 0 .../tls/ssl/server.ocsp.pem | 0 .../tls/ssl/server.pem | 0 .../tls/ssl/signed-bar.key | 0 .../tls/ssl/signed-bar.pem | 0 .../tls/ssl/signed-bob-bar.pem | 0 .../tls/ssl/signed-bob-foo.pem | 0 .../tls/ssl/signed-foo-ec.key | 0 .../tls/ssl/signed-foo-ec.pem | 0 .../tls/ssl/signed-foo.key | 0 .../tls/ssl/signed-foo.pem | 0 .../tls/ssl/signed-san-ec.key | 0 .../tls/ssl/signed-san-ec.pem | 0 .../tls/ssl/signed-san.key | 0 .../tls/ssl/signed-san.pem | 0 .../tls/ssl/signed-wild.key | 0 .../tls/ssl/signed-wild.pem | 0 .../tls/ssl/signed2-bar.pem | 0 .../tls/ssl/signed2-foo.pem | 0 .../tls/ssl/signer.key | 0 .../tls/ssl/signer.pem | 0 .../tls/ssl/signer2.key | 0 .../tls/ssl/signer2.pem | 0 .../tls/test-0rtt-s_client.py | 0 .../tls/test-nc-s_client.sh | 0 tests/uranium_tests/tls/test_allow_plain.py | 127 ++ .../tls/test_exit_on_cert_load_fail.py | 144 ++ .../tls/test_proxy_protocol_addressless.py | 108 ++ .../uranium_tests/tls/test_ssl_key_dialog.py | 120 ++ .../tls/test_ssl_multicert_loader.py | 186 ++ tests/uranium_tests/tls/test_tls.py | 111 ++ .../uranium_tests/tls/test_tls_0rtt_server.py | 290 +++ .../tls/test_tls_accept_timeout_crash.py | 102 + .../tls/test_tls_async_handshake.py | 103 + tests/uranium_tests/tls/test_tls_bad_alpn.py | 102 + tests/uranium_tests/tls/test_tls_cert_comp.py | 197 ++ .../tls/test_tls_cert_comp_cache.py | 203 ++ .../tls/test_tls_check_cert_select_plugin.py | 207 ++ .../tls/test_tls_check_cert_selection.py | 147 ++ .../test_tls_check_cert_selection_reload.py | 149 ++ .../tls/test_tls_check_dual_cert_selection.py | 183 ++ .../test_tls_check_dual_cert_selection2.py | 211 +++ ...st_tls_check_dual_cert_selection_plugin.py | 226 +++ .../tls/test_tls_client_alpn_configuration.py | 183 ++ .../uranium_tests/tls/test_tls_client_cert.py | 315 +++ .../tls/test_tls_client_cert2.py | 230 +++ .../tls/test_tls_client_cert2_plugin.py | 219 +++ .../tls/test_tls_client_cert_override.py | 170 ++ .../test_tls_client_cert_override_plugin.py | 248 +++ .../tls/test_tls_client_cert_plugin.py | 304 +++ .../tls/test_tls_client_verify.py | 262 +++ .../tls/test_tls_client_verify2.py | 208 ++ .../tls/test_tls_client_verify3.py | 171 ++ .../tls/test_tls_client_versions.py | 146 ++ .../tls/test_tls_client_versions_minmax.py | 162 ++ .../tls/test_tls_engine_abort.py | 141 ++ .../tls/test_tls_flow_control.py | 135 ++ .../tls/test_tls_forward_nonhttp.py | 104 + .../tls/test_tls_h2_cipher_suite.py | 115 ++ .../tls/test_tls_hooks_client_verify.py | 144 ++ .../tls/test_tls_hooks_verify.py | 135 ++ tests/uranium_tests/tls/test_tls_keepalive.py | 138 ++ tests/uranium_tests/tls/test_tls_ocsp.py | 105 + .../tls/test_tls_origin_open_failed.py | 98 + .../tls/test_tls_origin_post_abort.py | 133 ++ .../tls/test_tls_origin_session_reuse.py | 195 ++ .../tls/test_tls_partial_blind_tunnel.py | 129 ++ .../uranium_tests/tls/test_tls_record_size.py | 142 ++ .../tls/test_tls_reload_under_load.py | 119 ++ .../tls/test_tls_renegotiation.py | 152 ++ .../tls/test_tls_renegotiation_allowed.py | 164 ++ .../tls/test_tls_secret_update_default.py | 255 +++ .../tls/test_tls_session_key_logging.py | 121 ++ .../tls/test_tls_session_reuse.py | 179 ++ .../uranium_tests/tls/test_tls_sni_groups.py | 135 ++ .../tls/test_tls_sni_host_policy.py | 331 ++++ .../tls/test_tls_sni_ip_allow.py | 204 ++ .../tls/test_tls_sni_parent_failover.py | 203 ++ .../uranium_tests/tls/test_tls_sni_ticket.py | 299 +++ .../tls/test_tls_sni_with_port.py | 229 +++ .../tls/test_tls_sni_yaml_reload.py | 144 ++ tests/uranium_tests/tls/test_tls_ticket.py | 140 ++ tests/uranium_tests/tls/test_tls_tunnel.py | 435 +++++ .../tls/test_tls_tunnel_forward.py | 187 ++ tests/uranium_tests/tls/test_tls_verify.py | 186 ++ tests/uranium_tests/tls/test_tls_verify2.py | 183 ++ tests/uranium_tests/tls/test_tls_verify3.py | 178 ++ tests/uranium_tests/tls/test_tls_verify4.py | 209 ++ .../uranium_tests/tls/test_tls_verify_base.py | 161 ++ .../tls/test_tls_verify_ca_override.py | 160 ++ .../tls/test_tls_verify_not_pristine.py | 131 ++ .../tls/test_tls_verify_override.py | 298 +++ .../tls/test_tls_verify_override_base.py | 285 +++ .../tls/test_tls_verify_override_sni.py | 180 ++ .../tls/tls_accept_timeout_crash.replay.yaml | 0 .../tls_client_alpn_configuration.replay.yaml | 0 .../tls/tls_engine_abort.py | 0 .../tls/tls_flow_control_client.py | 0 .../tls/tls_post_abort_client.py | 0 .../tls/tls_post_abort_origin.py | 0 .../tls/tls_record_size_client.py | 0 .../tls/tls_reload_under_load_client.py | 0 .../tls/tls_renegotiation_allowed_client.py | 0 .../tls/tls_renegotiation_client.py | 0 .../tls/tls_session_key_logging.replay.yaml | 0 .../tls/tls_sni_with_port.replay.yaml | 0 .../tls_hooks/gold/cert-1.gold | 0 .../tls_hooks/gold/client-hello-1.gold | 0 .../tls_hooks/gold/preaccept-1.gold | 0 .../tls_hooks/gold/sni-1.gold | 0 .../tls_hooks/gold/ts-cert-1-im-2.gold | 0 .../tls_hooks/gold/ts-cert-1.gold | 0 .../tls_hooks/gold/ts-cert-2.gold | 0 .../tls_hooks/gold/ts-cert-im-1.gold | 0 .../tls_hooks/gold/ts-client-hello-1.gold | 0 .../tls_hooks/gold/ts-client-hello-2.gold | 0 .../gold/ts-client-hello-delayed-1.gold | 0 .../tls_hooks/gold/ts-close-out-close.gold | 0 .../tls_hooks/gold/ts-out-delay-start-2.gold | 0 .../tls_hooks/gold/ts-out-start-close-2.gold | 0 .../tls_hooks/gold/ts-preaccept-1.gold | 0 .../tls_hooks/gold/ts-preaccept-2.gold | 0 .../ts-preaccept-delayed-1-immdate-2.gold | 0 .../gold/ts-preaccept-delayed-1.gold | 0 .../gold/ts-preaccept1-sni1-cert1.gold | 0 .../tls_hooks/gold/ts-sni-1.gold | 0 .../tls_hooks/gold/ts-sni-2.gold | 0 .../uranium_tests/tls_hooks/test_tls_hooks.py | 101 + .../tls_hooks/test_tls_hooks10.py | 89 + .../tls_hooks/test_tls_hooks11.py | 96 + .../tls_hooks/test_tls_hooks12.py | 89 + .../tls_hooks/test_tls_hooks13.py | 89 + .../tls_hooks/test_tls_hooks14.py | 90 + .../tls_hooks/test_tls_hooks15.py | 89 + .../tls_hooks/test_tls_hooks16.py | 97 + .../tls_hooks/test_tls_hooks17.py | 97 + .../tls_hooks/test_tls_hooks18.py | 100 + .../tls_hooks/test_tls_hooks2.py | 95 + .../tls_hooks/test_tls_hooks3.py | 95 + .../tls_hooks/test_tls_hooks4.py | 104 + .../tls_hooks/test_tls_hooks6.py | 103 + .../tls_hooks/test_tls_hooks7.py | 98 + .../tls_hooks/test_tls_hooks8.py | 98 + .../tls_hooks/test_tls_hooks9.py | 94 + .../test_tls_hooks_close_while_parked.py | 110 ++ .../convert_plugin_config/gold/basic.yaml | 0 .../convert_plugin_config/gold/commented.yaml | 0 .../convert_plugin_config/gold/quoted.yaml | 0 .../gold/skip_disabled.yaml | 0 .../legacy_config/basic.config | 0 .../legacy_config/commented.config | 0 .../legacy_config/quoted.config | 0 .../test_convert_plugin_config.py | 89 + .../convert_ssl_multicert/gold/basic.yaml | 0 .../convert_ssl_multicert/gold/full.yaml | 0 .../convert_ssl_multicert/gold/quoted.yaml | 0 .../legacy_config/basic.config | 0 .../legacy_config/full.config | 0 .../legacy_config/quoted.config | 0 .../test_convert_ssl_multicert.py | 81 + .../convert_storage/gold/basic.yaml | 0 .../convert_storage/gold/exclusive.yaml | 0 .../convert_storage/gold/no_volumes.yaml | 0 .../legacy_config/basic.storage.config | 0 .../legacy_config/basic.volume.config | 0 .../legacy_config/exclusive.storage.config | 0 .../legacy_config/exclusive.volume.config | 0 .../legacy_config/no_volumes.storage.config | 0 .../convert_storage/test_convert_storage.py | 89 + .../traffic_ctl/gold/describe.gold | 0 .../traffic_ctl/gold/diff.gold | 0 .../traffic_ctl/gold/diff_yaml.gold | 0 .../traffic_ctl/gold/match.gold | 0 .../traffic_ctl/gold/t1_yaml.gold | 0 .../traffic_ctl/gold/t2_yaml.gold | 0 .../traffic_ctl/gold/t3_yaml.gold | 0 .../traffic_ctl/gold/t4_yaml.gold | 0 .../traffic_ctl/remap_inc/test_remap_inc.py | 81 + .../show_ssl_multicert/gold/show_json.gold | 0 .../show_ssl_multicert/gold/show_yaml.gold | 0 .../test_show_ssl_multicert.py | 110 ++ .../test_traffic_ctl_config_output.py | 162 ++ .../test_traffic_ctl_config_reload.py | 215 +++ .../test_traffic_ctl_server_debug.py | 101 + .../test_traffic_ctl_server_output.py | 86 + .../test_traffic_ctl_set_read_only.py | 130 ++ .../traffic_ctl/traffic_ctl_test_utils.py | 0 .../tunnel/dumb_proxy.py | 0 .../tunnel/test_tunnel_transform.py | 215 +++ tests/uranium_tests/tunnel/test_txn_type.py | 197 ++ .../uranium_tests/uds/test_uds_socket_perm.py | 76 + tests/uranium_tests/url/test_uri.py | 56 + .../url/uri.replay.yaml | 0 .../basic/txn_box_cmp.test.py => urtest.sh} | 12 +- .../txn_box_ip-addr.test.py => urtest.sh.in} | 31 +- tests/uv.lock | 1233 ++++++++++++ 2247 files changed, 82186 insertions(+), 73273 deletions(-) rename doc/developer-guide/testing/{autests.en.rst => uranium-tests.en.rst} (75%) delete mode 100644 tests/ats_testkit/plugin.py delete mode 100755 tests/autest-parallel.py.in delete mode 100755 tests/autest.sh delete mode 100755 tests/autest.sh.in delete mode 100644 tests/gold_tests/ats_probe/ats_probe.test.py delete mode 100644 tests/gold_tests/ats_probe/cache_dir_probe.test.py delete mode 100644 tests/gold_tests/autest-site/.editorconfig delete mode 100644 tests/gold_tests/autest-site/ats_replay.test.ext delete mode 100644 tests/gold_tests/autest-site/cli_tools.test.ext delete mode 100644 tests/gold_tests/autest-site/conditions.test.ext delete mode 100644 tests/gold_tests/autest-site/config_reload.test.ext delete mode 100644 tests/gold_tests/autest-site/curl.test.ext delete mode 100644 tests/gold_tests/autest-site/curl_header.test.ext delete mode 100644 tests/gold_tests/autest-site/httpbin.test.ext delete mode 100644 tests/gold_tests/autest-site/init.cli.ext delete mode 100755 tests/gold_tests/autest-site/ip.test.ext delete mode 100644 tests/gold_tests/autest-site/jsonrpc_client.test.ext delete mode 100644 tests/gold_tests/autest-site/microDNS.test.ext delete mode 100644 tests/gold_tests/autest-site/microserver.test.ext delete mode 100644 tests/gold_tests/autest-site/ordered_set_queue.py delete mode 100644 tests/gold_tests/autest-site/ports.py delete mode 100644 tests/gold_tests/autest-site/readme.md delete mode 100644 tests/gold_tests/autest-site/setup.cli.ext delete mode 100644 tests/gold_tests/autest-site/traffic_replay.test.ext delete mode 100755 tests/gold_tests/autest-site/trafficserver.test.ext delete mode 100644 tests/gold_tests/autest-site/trafficserver_plugins.test.ext delete mode 100755 tests/gold_tests/autest-site/txn_box.test.ext delete mode 100755 tests/gold_tests/autest-site/verifier_client.test.ext delete mode 100755 tests/gold_tests/autest-site/verifier_server.test.ext delete mode 100644 tests/gold_tests/autest-site/when.test.ext delete mode 100644 tests/gold_tests/bad_http_fmt/bad_http_fmt.test.py delete mode 100644 tests/gold_tests/basic/config.test.py delete mode 100644 tests/gold_tests/basic/copy_config.test.py delete mode 100644 tests/gold_tests/basic/copy_config2.test.py delete mode 100644 tests/gold_tests/basic/deny0.test.py delete mode 100644 tests/gold_tests/bigobj/bigobj.test.py delete mode 100644 tests/gold_tests/body_factory/http204_response.test.py delete mode 100644 tests/gold_tests/body_factory/http204_response_plugin.test.py delete mode 100644 tests/gold_tests/body_factory/http304_response.test.py delete mode 100644 tests/gold_tests/body_factory/http_head_no_origin.test.py delete mode 100644 tests/gold_tests/body_factory/http_with_origin.test.py delete mode 100644 tests/gold_tests/cache/background_fill.test.py delete mode 100644 tests/gold_tests/cache/cache-generation-clear.test.py delete mode 100644 tests/gold_tests/cache/cache-generation-disjoint.test.py delete mode 100644 tests/gold_tests/cache/cache-write-lock-contention.test.py delete mode 100644 tests/gold_tests/cache/cache_config_reload.test.py delete mode 100644 tests/gold_tests/cache/cache_corrupt_recursive_uaf.test.py delete mode 100644 tests/gold_tests/cache/cache_shm_bad_disk_dropped.test.py delete mode 100644 tests/gold_tests/cache/cache_shm_concurrent_attach.test.py delete mode 100644 tests/gold_tests/cache/cache_shm_control_size_mismatch.test.py delete mode 100644 tests/gold_tests/cache/cache_shm_dir_invalid.test.py delete mode 100644 tests/gold_tests/cache/cache_shm_fast_restart.test.py delete mode 100644 tests/gold_tests/cache/cache_shm_purge_on_disable.test.py delete mode 100644 tests/gold_tests/cache/cache_shm_schema_mismatch.test.py delete mode 100644 tests/gold_tests/cache/cache_shm_storage_mismatch.test.py delete mode 100644 tests/gold_tests/cache/cache_shm_unclean_shutdown.test.py delete mode 100644 tests/gold_tests/cache/disjoint-wait-for-cache.test.py delete mode 100644 tests/gold_tests/cache/host_down_range_recursion.test.py delete mode 100644 tests/gold_tests/cache/negative-caching.test.py delete mode 100644 tests/gold_tests/cache/proxy_serve_stale_dns_fail.test.py delete mode 100644 tests/gold_tests/cache/storage-metrics.test.py delete mode 100644 tests/gold_tests/chunked_encoding/bad_chunked_encoding.test.py delete mode 100644 tests/gold_tests/chunked_encoding/chunk_extension_quoted_string.test.py delete mode 100644 tests/gold_tests/chunked_encoding/chunk_trailer_bare_lf.test.py delete mode 100644 tests/gold_tests/chunked_encoding/chunked_encoding.test.py delete mode 100644 tests/gold_tests/chunked_encoding/chunked_encoding_disabled.test.py delete mode 100644 tests/gold_tests/chunked_encoding/chunked_encoding_h2.test.py delete mode 100644 tests/gold_tests/client_connection/per_client_connection_max.test.py delete mode 100644 tests/gold_tests/command_argument/verify_global_plugin.test.py delete mode 100644 tests/gold_tests/command_argument/verify_remap_plugin.test.py delete mode 100644 tests/gold_tests/config_processor/config_destroy_thread.test.py delete mode 100644 tests/gold_tests/connect/connect.test.py delete mode 100644 tests/gold_tests/connect/connect_handshake.test.py delete mode 100644 tests/gold_tests/connect/connect_parent_error_body.test.py delete mode 100644 tests/gold_tests/connect/h2_malformed_request_logging.test.py delete mode 100644 tests/gold_tests/connect_down_policy/connect_down_policy.test.py delete mode 100644 tests/gold_tests/cont_schedule/schedule_every_on_entire_pool.test.py delete mode 100644 tests/gold_tests/cont_schedule/schedule_every_on_pool.test.py delete mode 100644 tests/gold_tests/cont_schedule/schedule_every_on_thread.test.py delete mode 100644 tests/gold_tests/cont_schedule/schedule_on_entire_pool.test.py delete mode 100644 tests/gold_tests/cont_schedule/schedule_on_pool.test.py delete mode 100644 tests/gold_tests/cont_schedule/schedule_on_thread.test.py delete mode 100644 tests/gold_tests/cont_schedule/thread_affinity.test.py delete mode 100644 tests/gold_tests/continuations/double.test.py delete mode 100644 tests/gold_tests/continuations/double_h2.test.py delete mode 100644 tests/gold_tests/continuations/openclose.test.py delete mode 100644 tests/gold_tests/continuations/openclose_h2.test.py delete mode 100644 tests/gold_tests/continuations/session_id.test.py delete mode 100644 tests/gold_tests/cripts/cripts.test.py delete mode 100644 tests/gold_tests/dns/dns_down_nameserver.test.py delete mode 100644 tests/gold_tests/dns/dns_host_down.test.py delete mode 100644 tests/gold_tests/dns/dns_reverse_lookup.test.py delete mode 100644 tests/gold_tests/dns/dns_ttl.test.py delete mode 100644 tests/gold_tests/dns/splitdns.test.py delete mode 100644 tests/gold_tests/dns/splitdns_reload.test.py delete mode 100644 tests/gold_tests/early_hints/early_hints.test.py delete mode 100644 tests/gold_tests/forward_proxy/forward_proxy.test.py delete mode 100644 tests/gold_tests/h2/grpc/grpc.test.py delete mode 100644 tests/gold_tests/h2/h2_origin_cert_reverify.test.py delete mode 100644 tests/gold_tests/h2/h2_origin_trailers_h1.test.py delete mode 100644 tests/gold_tests/h2/h2disable.test.py delete mode 100644 tests/gold_tests/h2/h2disable_no_accept_threads.test.py delete mode 100644 tests/gold_tests/h2/h2enable.test.py delete mode 100644 tests/gold_tests/h2/h2enable_no_accept_threads.test.py delete mode 100644 tests/gold_tests/h2/h2get_with_body.test.py delete mode 100644 tests/gold_tests/h2/h2origin.test.py delete mode 100644 tests/gold_tests/h2/h2origin_single_thread.test.py delete mode 100644 tests/gold_tests/h2/h2spec.test.py delete mode 100644 tests/gold_tests/h2/http2.test.py delete mode 100644 tests/gold_tests/h2/http2_close_connection.test.py delete mode 100644 tests/gold_tests/h2/http2_concurrent_streams.test.py delete mode 100644 tests/gold_tests/h2/http2_crlf_header_validation.test.py delete mode 100644 tests/gold_tests/h2/http2_empty_data_frame.test.py delete mode 100644 tests/gold_tests/h2/http2_flow_control.test.py delete mode 100644 tests/gold_tests/h2/http2_max_active_streams.test.py delete mode 100644 tests/gold_tests/h2/http2_priority.test.py delete mode 100644 tests/gold_tests/h2/http2_rst_stream.test.py delete mode 100644 tests/gold_tests/h2/http2_txn_start_read_gate.test.py delete mode 100644 tests/gold_tests/h2/http2_write_threshold.test.py delete mode 100644 tests/gold_tests/h2/httpbin.test.py delete mode 100644 tests/gold_tests/h2/nghttp.test.py delete mode 100644 tests/gold_tests/h3/h3_curl.test.py delete mode 100644 tests/gold_tests/h3/h3_go_client.test.py delete mode 100644 tests/gold_tests/h3/h3_python_client.test.py delete mode 100644 tests/gold_tests/h3/h3_range_cache.test.py delete mode 100644 tests/gold_tests/h3/h3_session_ticket.test.py delete mode 100644 tests/gold_tests/h3/h3_sni_check.test.py delete mode 100644 tests/gold_tests/headers/cache_and_req_body.test.py delete mode 100644 tests/gold_tests/headers/field_name_space.test.py delete mode 100644 tests/gold_tests/headers/forwarded.test.py delete mode 100644 tests/gold_tests/headers/general-connection-failure-502.test.py delete mode 100644 tests/gold_tests/headers/good_request_after_bad.test.py delete mode 100644 tests/gold_tests/headers/http408.test.py delete mode 100644 tests/gold_tests/headers/normalize_ae.test.py delete mode 100644 tests/gold_tests/headers/oversized_field_h2.test.py delete mode 100644 tests/gold_tests/headers/syntax.test.py delete mode 100644 tests/gold_tests/headers/via.test.py delete mode 100644 tests/gold_tests/ip_allow/connect_destination_acl.test.py delete mode 100644 tests/gold_tests/ip_allow/ip_allow.test.py delete mode 100644 tests/gold_tests/ip_allow/ip_allow_reload_triggered.test.py delete mode 100644 tests/gold_tests/ip_allow/ip_category.test.py delete mode 100644 tests/gold_tests/jsonrpc/basic_plugin_handler.test.py delete mode 100644 tests/gold_tests/jsonrpc/config_reload_dedup.test.py delete mode 100644 tests/gold_tests/jsonrpc/config_reload_failures.test.py delete mode 100644 tests/gold_tests/jsonrpc/config_reload_full_smoke.test.py delete mode 100644 tests/gold_tests/jsonrpc/config_reload_reserve_subtask.test.py delete mode 100644 tests/gold_tests/jsonrpc/config_reload_rpc.test.py delete mode 100644 tests/gold_tests/jsonrpc/config_reload_tracking.test.py delete mode 100644 tests/gold_tests/jsonrpc/jsonrpc_api_schema.test.py delete mode 100644 tests/gold_tests/logging/all_headers.test.py delete mode 100644 tests/gold_tests/logging/binary_log_v3.test.py delete mode 100644 tests/gold_tests/logging/custom-log.test.py delete mode 100644 tests/gold_tests/logging/log-field-json.test.py delete mode 100644 tests/gold_tests/logging/log-field.test.py delete mode 100644 tests/gold_tests/logging/log-filenames.test.py delete mode 100644 tests/gold_tests/logging/log-filter.test.py delete mode 100644 tests/gold_tests/logging/log-milestone-fields.test.py delete mode 100644 tests/gold_tests/logging/log-mstsms.test.py delete mode 100644 tests/gold_tests/logging/log_pipe.test.py delete mode 100644 tests/gold_tests/logging/log_plugin_init.test.py delete mode 100644 tests/gold_tests/logging/log_retention.test.py delete mode 100644 tests/gold_tests/logging/new_log_flds.test.py delete mode 100644 tests/gold_tests/logging/pqsi-pqsp.test.py delete mode 100644 tests/gold_tests/logging/qlog_quiche.test.py delete mode 100644 tests/gold_tests/logging/sigusr2.test.py delete mode 100644 tests/gold_tests/next_hop/parent_config/parent_config_dest_ip.test.py delete mode 100644 tests/gold_tests/next_hop/strategies_ch/strategies_ch.test.py delete mode 100644 tests/gold_tests/next_hop/strategies_ch2/strategies_ch2.test.py delete mode 100644 tests/gold_tests/next_hop/strategies_ch5/strategies_ch5.test.py delete mode 100644 tests/gold_tests/next_hop/strategies_stale/strategies_stale.test.py delete mode 100644 tests/gold_tests/next_hop/zzz_strategies_peer/zzz_strategies_peer.test.py delete mode 100644 tests/gold_tests/next_hop/zzz_strategies_peer2/zzz_strategies_peer2.test.py delete mode 100644 tests/gold_tests/null_transform/null_transform.test.py delete mode 100644 tests/gold_tests/origin_connection/per_server_connection_max.test.py delete mode 100644 tests/gold_tests/origin_connection/per_server_metric_enabled.test.py delete mode 100644 tests/gold_tests/parent_config/parent_config_reload.test.py delete mode 100644 tests/gold_tests/parent_proxy/consistent_hash_ring_walk.test.py delete mode 100644 tests/gold_tests/parent_proxy/parent-retry.test.py delete mode 100644 tests/gold_tests/pipeline/delete_maxforwards_body_desync.test.py delete mode 100644 tests/gold_tests/pipeline/delete_maxfwd_noop_drain.test.py delete mode 100644 tests/gold_tests/pipeline/noop_keepalive.test.py delete mode 100644 tests/gold_tests/pipeline/pipeline.test.py delete mode 100644 tests/gold_tests/pluginTest/TSVConnFd/TSVConnFd.test.py delete mode 100644 tests/gold_tests/pluginTest/access_control/access_control.test.py delete mode 100644 tests/gold_tests/pluginTest/block_errors/block_errors.test.py delete mode 100644 tests/gold_tests/pluginTest/cache_fill/cache_fill.test.py delete mode 100644 tests/gold_tests/pluginTest/cache_promote/cache_promote.test.py delete mode 100644 tests/gold_tests/pluginTest/cache_range_requests/cache_range_requests.test.py delete mode 100644 tests/gold_tests/pluginTest/cache_range_requests/cache_range_requests_cache_complete_responses.test.py delete mode 100644 tests/gold_tests/pluginTest/cache_range_requests/cache_range_requests_cachekey.test.py delete mode 100644 tests/gold_tests/pluginTest/cache_range_requests/cache_range_requests_cachekey_global.test.py delete mode 100644 tests/gold_tests/pluginTest/cache_range_requests/cache_range_requests_ident.test.py delete mode 100644 tests/gold_tests/pluginTest/cert_update/cert_update.test.py delete mode 100644 tests/gold_tests/pluginTest/certifier/certifier.test.py delete mode 100644 tests/gold_tests/pluginTest/client_context_dump/client_context_dump.test.py delete mode 100644 tests/gold_tests/pluginTest/combo_handler/combo_handler.test.py delete mode 100644 tests/gold_tests/pluginTest/compress/compress-cache-untransformed.test.py delete mode 100644 tests/gold_tests/pluginTest/compress/compress-content-type-params.test.py delete mode 100644 tests/gold_tests/pluginTest/compress/compress-range.test.py delete mode 100644 tests/gold_tests/pluginTest/compress/compress.test.py delete mode 100644 tests/gold_tests/pluginTest/conf_remap/conf_remap.test.py delete mode 100644 tests/gold_tests/pluginTest/cookie_remap/bucketcookie.test.py delete mode 100644 tests/gold_tests/pluginTest/cookie_remap/collapseslashes.test.py delete mode 100644 tests/gold_tests/pluginTest/cookie_remap/connector.test.py delete mode 100644 tests/gold_tests/pluginTest/cookie_remap/disable_pristine_host_hdr.test.py delete mode 100644 tests/gold_tests/pluginTest/cookie_remap/existscookie.test.py delete mode 100644 tests/gold_tests/pluginTest/cookie_remap/matchcookie.test.py delete mode 100644 tests/gold_tests/pluginTest/cookie_remap/matchuri.test.py delete mode 100644 tests/gold_tests/pluginTest/cookie_remap/notexistscookie.test.py delete mode 100644 tests/gold_tests/pluginTest/cookie_remap/pcollapseslashes.test.py delete mode 100644 tests/gold_tests/pluginTest/cookie_remap/psubstitute.test.py delete mode 100644 tests/gold_tests/pluginTest/cookie_remap/regexcookie.test.py delete mode 100644 tests/gold_tests/pluginTest/cookie_remap/setstatus.test.py delete mode 100644 tests/gold_tests/pluginTest/cookie_remap/subcookie.test.py delete mode 100644 tests/gold_tests/pluginTest/cookie_remap/substitute.test.py delete mode 100644 tests/gold_tests/pluginTest/crash_test/crash_test.test.py delete mode 100644 tests/gold_tests/pluginTest/escalate/escalate.test.py delete mode 100644 tests/gold_tests/pluginTest/esi/esi.test.py delete mode 100644 tests/gold_tests/pluginTest/esi/esi_304.test.py delete mode 100644 tests/gold_tests/pluginTest/esi/esi_nested_html_comment.test.py delete mode 100644 tests/gold_tests/pluginTest/esi/esi_nested_include.test.py delete mode 100644 tests/gold_tests/pluginTest/esi/esi_request_size_cap.test.py delete mode 100644 tests/gold_tests/pluginTest/esi/esi_ssrf_validate.test.py delete mode 100644 tests/gold_tests/pluginTest/header_rewrite/header_rewrite_bad_run_plugin.test.py delete mode 100644 tests/gold_tests/pluginTest/header_rewrite/header_rewrite_set_body_from.test.py delete mode 100644 tests/gold_tests/pluginTest/header_rewrite/header_rewrite_url_glob.test.py delete mode 100644 tests/gold_tests/pluginTest/healthchecks/healthchecks.test.py delete mode 100644 tests/gold_tests/pluginTest/ja3_fingerprint/ja3_fingerprint.test.py delete mode 100644 tests/gold_tests/pluginTest/ja4_fingerprint/ja4_fingerprint.test.py delete mode 100644 tests/gold_tests/pluginTest/jax_fingerprint/jax_fingerprint.test.py delete mode 100644 tests/gold_tests/pluginTest/lua/lua_client_hook.test.py delete mode 100644 tests/gold_tests/pluginTest/lua/lua_debug_tags.test.py delete mode 100644 tests/gold_tests/pluginTest/lua/lua_fetch_ipv6_cliaddr.test.py delete mode 100644 tests/gold_tests/pluginTest/lua/lua_global_shutdown.test.py delete mode 100644 tests/gold_tests/pluginTest/lua/lua_header_table.test.py delete mode 100644 tests/gold_tests/pluginTest/lua/lua_proxy_protocol.test.py delete mode 100644 tests/gold_tests/pluginTest/lua/lua_remap_after_hook.test.py delete mode 100644 tests/gold_tests/pluginTest/lua/lua_set_error_response.test.py delete mode 100644 tests/gold_tests/pluginTest/lua/lua_states_stats.test.py delete mode 100644 tests/gold_tests/pluginTest/lua/lua_verified_addr.test.py delete mode 100644 tests/gold_tests/pluginTest/lua/lua_watermark.test.py delete mode 100644 tests/gold_tests/pluginTest/money_trace/money_trace.test.py delete mode 100644 tests/gold_tests/pluginTest/money_trace/money_trace_global.test.py delete mode 100644 tests/gold_tests/pluginTest/multiplexer/multiplexer.test.py delete mode 100644 tests/gold_tests/pluginTest/origin_server_auth/origin_server_auth.test.py delete mode 100644 tests/gold_tests/pluginTest/origin_server_auth/s3_url_encoding.test.py delete mode 100644 tests/gold_tests/pluginTest/packet_mark/packet_mark.test.py delete mode 100644 tests/gold_tests/pluginTest/per_plugin_metrics/per_plugin_metrics.test.py delete mode 100644 tests/gold_tests/pluginTest/plugin_yaml/plugin_yaml.test.py delete mode 100644 tests/gold_tests/pluginTest/polite_hook_wait/polite_hook_wait.test.py delete mode 100644 tests/gold_tests/pluginTest/prefetch/prefetch_bad_count_refused.test.py delete mode 100644 tests/gold_tests/pluginTest/prefetch/prefetch_bad_pattern_refused.test.py delete mode 100644 tests/gold_tests/pluginTest/prefetch/prefetch_bignum.test.py delete mode 100644 tests/gold_tests/pluginTest/prefetch/prefetch_cmcd.test.py delete mode 100644 tests/gold_tests/pluginTest/prefetch/prefetch_empty_replacement.test.py delete mode 100644 tests/gold_tests/pluginTest/prefetch/prefetch_optional_group.test.py delete mode 100644 tests/gold_tests/pluginTest/prefetch/prefetch_overflow.test.py delete mode 100644 tests/gold_tests/pluginTest/prefetch/prefetch_simple.test.py delete mode 100644 tests/gold_tests/pluginTest/rate_limit/rate_limit.test.py delete mode 100644 tests/gold_tests/pluginTest/rate_limit/rate_limit_iprep.test.py delete mode 100644 tests/gold_tests/pluginTest/rate_limit/rate_limit_sni.test.py delete mode 100644 tests/gold_tests/pluginTest/rate_limit/rate_limit_sni_expiry.test.py delete mode 100644 tests/gold_tests/pluginTest/rate_limit/rate_limit_sni_queue.test.py delete mode 100644 tests/gold_tests/pluginTest/rate_limit/rate_limit_sni_reject.test.py delete mode 100644 tests/gold_tests/pluginTest/redirect_limit/redirect_limit.test.py delete mode 100644 tests/gold_tests/pluginTest/regex_remap/regex_remap.test.py delete mode 100644 tests/gold_tests/pluginTest/regex_revalidate/regex_revalidate.test.py delete mode 100644 tests/gold_tests/pluginTest/regex_revalidate/regex_revalidate_miss.test.py delete mode 100644 tests/gold_tests/pluginTest/regex_revalidate/regex_revalidate_state.test.py delete mode 100644 tests/gold_tests/pluginTest/remap_stats/remap_stats.test.py delete mode 100644 tests/gold_tests/pluginTest/remap_stats/remap_stats_post.test.py delete mode 100644 tests/gold_tests/pluginTest/server_push_preload/server_push_preload.test.py delete mode 100644 tests/gold_tests/pluginTest/slice/slice.test.py delete mode 100644 tests/gold_tests/pluginTest/slice/slice_conditional.test.py delete mode 100644 tests/gold_tests/pluginTest/slice/slice_content_shrink.test.py delete mode 100644 tests/gold_tests/pluginTest/slice/slice_crr_ident.test.py delete mode 100644 tests/gold_tests/pluginTest/slice/slice_error.test.py delete mode 100644 tests/gold_tests/pluginTest/slice/slice_ident.test.py delete mode 100644 tests/gold_tests/pluginTest/slice/slice_prefetch.test.py delete mode 100644 tests/gold_tests/pluginTest/slice/slice_purge.test.py delete mode 100644 tests/gold_tests/pluginTest/slice/slice_purge_gaps.test.py delete mode 100644 tests/gold_tests/pluginTest/slice/slice_regex.test.py delete mode 100644 tests/gold_tests/pluginTest/slice/slice_rm_range.test.py delete mode 100644 tests/gold_tests/pluginTest/slice/slice_selfhealing.test.py delete mode 100644 tests/gold_tests/pluginTest/slice/slice_stale_generation.test.py delete mode 100644 tests/gold_tests/pluginTest/sslheaders/sslheaders.test.py delete mode 100644 tests/gold_tests/pluginTest/stale_response/stale_response.test.py delete mode 100644 tests/gold_tests/pluginTest/statichit/statichit.test.py delete mode 100644 tests/gold_tests/pluginTest/stats_over_http/stats_over_http.test.py delete mode 100644 tests/gold_tests/pluginTest/stek_share/stek_share.test.py delete mode 100644 tests/gold_tests/pluginTest/strategies/strategies_plugins.test.py delete mode 100644 tests/gold_tests/pluginTest/test_hooks/hook_add.test.py delete mode 100644 tests/gold_tests/pluginTest/test_hooks/ssn_start_delay_hook.test.py delete mode 100644 tests/gold_tests/pluginTest/test_hooks/test_hooks.test.py delete mode 100644 tests/gold_tests/pluginTest/traffic_dump/traffic_dump.test.py delete mode 100644 tests/gold_tests/pluginTest/traffic_dump/traffic_dump_http3.test.py delete mode 100644 tests/gold_tests/pluginTest/traffic_dump/traffic_dump_ip_filter.test.py delete mode 100644 tests/gold_tests/pluginTest/traffic_dump/traffic_dump_response_body.test.py delete mode 100644 tests/gold_tests/pluginTest/traffic_dump/traffic_dump_sni_filter.test.py delete mode 100644 tests/gold_tests/pluginTest/transform/transaction_data_sink.test.py delete mode 100644 tests/gold_tests/pluginTest/tsapi/test_TSHttpSsnInfo.test.py delete mode 100644 tests/gold_tests/pluginTest/tsapi/test_TSHttpTxnServerAddrSet.test.py delete mode 100644 tests/gold_tests/pluginTest/tsapi/test_TSHttpTxnServerAddrSet_retry.test.py delete mode 100644 tests/gold_tests/pluginTest/tsapi/test_TSHttpTxnVerifiedAddr.test.py delete mode 100644 tests/gold_tests/pluginTest/tsapi/test_TSVConnPPInfo.test.py delete mode 100644 tests/gold_tests/pluginTest/tsapi/tsapi.test.py delete mode 100644 tests/gold_tests/pluginTest/txn_box/basic/txn_box_basic.test.py delete mode 100644 tests/gold_tests/pluginTest/txn_box/basic/txn_box_mod.test.py delete mode 100644 tests/gold_tests/pluginTest/txn_box/basic/txn_box_multi-cfg.test.py delete mode 100644 tests/gold_tests/pluginTest/txn_box/basic/txn_box_redirect.test.py delete mode 100644 tests/gold_tests/pluginTest/txn_box/basic/txn_box_reply.test.py delete mode 100644 tests/gold_tests/pluginTest/txn_box/basic/txn_box_rxp.test.py delete mode 100644 tests/gold_tests/pluginTest/txn_box/basic/txn_box_stat.test.py delete mode 100644 tests/gold_tests/pluginTest/txn_box/basic/txn_box_tls-cert.test.py delete mode 100644 tests/gold_tests/pluginTest/txn_box/basic/txn_box_tls.test.py delete mode 100644 tests/gold_tests/pluginTest/txn_box/basic/txn_box_tuple.test.py delete mode 100644 tests/gold_tests/pluginTest/txn_box/basic/txn_box_txn-debug.test.py delete mode 100644 tests/gold_tests/pluginTest/txn_box/basic/txn_box_txn-error.test.py delete mode 100644 tests/gold_tests/pluginTest/txn_box/basic/txn_box_txn_open_1.test.py delete mode 100644 tests/gold_tests/pluginTest/txn_box/basic/txn_box_txn_open_2.test.py delete mode 100644 tests/gold_tests/pluginTest/txn_box/basic/txn_box_txn_open_3.test.py delete mode 100644 tests/gold_tests/pluginTest/txn_box/basic/txn_box_with.test.py delete mode 100644 tests/gold_tests/pluginTest/txn_box/ct_header/txn_box_ct_header.test.py delete mode 100644 tests/gold_tests/pluginTest/txn_box/example/txn_box_accept-encoding.test.py delete mode 100644 tests/gold_tests/pluginTest/txn_box/prod/txn_box_cors-origin.test.py delete mode 100644 tests/gold_tests/pluginTest/txn_box/prod/txn_box_cors-referrer.test.py delete mode 100644 tests/gold_tests/pluginTest/txn_box/prod/txn_box_ip-acl.test.py delete mode 100644 tests/gold_tests/pluginTest/txn_box/prod/txn_box_mTLS.test.py delete mode 100644 tests/gold_tests/pluginTest/txn_box/prod/txn_box_query-delete-filter.test.py delete mode 100644 tests/gold_tests/pluginTest/txn_box/prod/txn_box_query-delete-regex.test.py delete mode 100644 tests/gold_tests/pluginTest/txn_box/prod/txn_box_query.test.py delete mode 100644 tests/gold_tests/pluginTest/txn_box/prod/txn_box_stanley.test.py delete mode 100644 tests/gold_tests/pluginTest/txn_box/prod/txn_box_vznith-1.test.py delete mode 100644 tests/gold_tests/pluginTest/txn_box/prod/txn_box_yts-3489.test.py delete mode 100644 tests/gold_tests/pluginTest/txn_box/ramp/txn_box_multi-ramp-1.test.py delete mode 100644 tests/gold_tests/pluginTest/txn_box/ramp/txn_box_multi-ramp-2.test.py delete mode 100644 tests/gold_tests/pluginTest/txn_box/ramp/txn_box_multi-ramp-3.test.py delete mode 100644 tests/gold_tests/pluginTest/txn_box/ramp/txn_box_ramp.test.py delete mode 100644 tests/gold_tests/pluginTest/txn_box/remap/txn_box_remap-base.test.py delete mode 100644 tests/gold_tests/pluginTest/txn_box/smoke/txn_box_smoke-2.test.py delete mode 100644 tests/gold_tests/pluginTest/txn_box/smoke/txn_box_smoke.test.py delete mode 100644 tests/gold_tests/pluginTest/txn_box/static_file/txn_box_static_file.test.py delete mode 100644 tests/gold_tests/pluginTest/uri_signing/uri_signing.test.py delete mode 100644 tests/gold_tests/pluginTest/url_sig/url_sig.test.py delete mode 100644 tests/gold_tests/pluginTest/webp_transform/webp_transform_cache_refused.test.py delete mode 100644 tests/gold_tests/pluginTest/webp_transform/webp_transform_chunked_cap.test.py delete mode 100644 tests/gold_tests/pluginTest/webp_transform/webp_transform_decode_limit.test.py delete mode 100644 tests/gold_tests/pluginTest/webp_transform/webp_transform_max_buffer_size.test.py delete mode 100644 tests/gold_tests/pluginTest/webp_transform/webp_transform_size_cap.test.py delete mode 100644 tests/gold_tests/pluginTest/xdebug/x_cache_info/x_cache_info.test.py delete mode 100644 tests/gold_tests/pluginTest/xdebug/x_effective_url/x_effective_url.test.py delete mode 100644 tests/gold_tests/pluginTest/xdebug/x_probe/x_probe.test.py delete mode 100644 tests/gold_tests/pluginTest/xdebug/x_probe_full_json/x_probe_full_json.test.py delete mode 100644 tests/gold_tests/pluginTest/xdebug/x_probe_full_json_slow_origin/x_probe_full_json_slow_origin.test.py delete mode 100644 tests/gold_tests/pluginTest/xdebug/x_remap/x_remap.test.py delete mode 100644 tests/gold_tests/post/expect_tests.test.py delete mode 100644 tests/gold_tests/post/post-continue.test.py delete mode 100644 tests/gold_tests/post/post-early-return.test.py delete mode 100644 tests/gold_tests/post/simple-post-valid-buffer-check.test.py delete mode 100644 tests/gold_tests/post_slow_server/post_slow_server.test.py delete mode 100644 tests/gold_tests/post_slow_server/post_slow_server_max_requests_in.test.py delete mode 100644 tests/gold_tests/proxy_protocol/proxy_protocol.test.py delete mode 100644 tests/gold_tests/qmux/qmux_go_client.test.py delete mode 100644 tests/gold_tests/records/records_config_to_yaml.test.py delete mode 100644 tests/gold_tests/records/records_runroot_precedence.test.py delete mode 100644 tests/gold_tests/records/records_yaml.test.py delete mode 100644 tests/gold_tests/records/traffic_ctl_cold_config.test.py delete mode 100644 tests/gold_tests/records/ts_max_records_param.test.py delete mode 100644 tests/gold_tests/redirect/number_of_redirects.test.py delete mode 100644 tests/gold_tests/redirect/redirect.test.py delete mode 100644 tests/gold_tests/redirect/redirect_actions.test.py delete mode 100644 tests/gold_tests/redirect/redirect_post.test.py delete mode 100644 tests/gold_tests/redirect/redirect_stale.test.py delete mode 100644 tests/gold_tests/redirect/redirect_to_same_origin_on_cache.test.py delete mode 100644 tests/gold_tests/remap/basic_conf_remap_yaml.test.py delete mode 100644 tests/gold_tests/remap/conf_remap_float.test.py delete mode 100644 tests/gold_tests/remap/map_with_recv_port.test.py delete mode 100644 tests/gold_tests/remap/regex_map.test.py delete mode 100644 tests/gold_tests/remap/remap_acl.test.py delete mode 100644 tests/gold_tests/remap/remap_http.test.py delete mode 100644 tests/gold_tests/remap/remap_https.test.py delete mode 100644 tests/gold_tests/remap/remap_ip_resolve.test.py delete mode 100644 tests/gold_tests/remap/remap_load_empty_failure.test.py delete mode 100644 tests/gold_tests/remap/remap_load_empty_success.test.py delete mode 100644 tests/gold_tests/remap/remap_load_missing_failure.test.py delete mode 100644 tests/gold_tests/remap/remap_load_missing_success.test.py delete mode 100644 tests/gold_tests/remap/remap_reload.test.py delete mode 100644 tests/gold_tests/remap/remap_ws.test.py delete mode 100644 tests/gold_tests/remap_yaml/basic_conf_remap_yaml_yaml.test.py delete mode 100644 tests/gold_tests/remap_yaml/conf_remap_float_yaml.test.py delete mode 100644 tests/gold_tests/remap_yaml/map_with_recv_port_yaml.test.py delete mode 100644 tests/gold_tests/remap_yaml/regex_map_yaml.test.py delete mode 100644 tests/gold_tests/remap_yaml/remap_acl_yaml.test.py delete mode 100644 tests/gold_tests/remap_yaml/remap_http_yaml.test.py delete mode 100644 tests/gold_tests/remap_yaml/remap_https_yaml.test.py delete mode 100644 tests/gold_tests/remap_yaml/remap_ip_resolve_yaml.test.py delete mode 100644 tests/gold_tests/remap_yaml/remap_load_empty_failure_yaml.test.py delete mode 100644 tests/gold_tests/remap_yaml/remap_load_empty_success_yaml.test.py delete mode 100644 tests/gold_tests/remap_yaml/remap_load_missing_failure_yaml.test.py delete mode 100644 tests/gold_tests/remap_yaml/remap_load_missing_success_yaml.test.py delete mode 100644 tests/gold_tests/remap_yaml/remap_reload_yaml.test.py delete mode 100644 tests/gold_tests/remap_yaml/remap_ws_yaml.test.py delete mode 100644 tests/gold_tests/runroot/runroot_error.test.py delete mode 100644 tests/gold_tests/runroot/runroot_init.test.py delete mode 100644 tests/gold_tests/runroot/runroot_remove.test.py delete mode 100644 tests/gold_tests/runroot/runroot_use.test.py delete mode 100644 tests/gold_tests/runroot/runroot_verify.test.py delete mode 100644 tests/gold_tests/session_sharing/session_match.test.py delete mode 100644 tests/gold_tests/shutdown/crashlog_no_false_positive.test.py delete mode 100644 tests/gold_tests/shutdown/emergency.test.py delete mode 100644 tests/gold_tests/shutdown/fatal.test.py delete mode 100644 tests/gold_tests/slow_post/quick_server.test.py delete mode 100644 tests/gold_tests/slow_post/server_abort.test.py delete mode 100644 tests/gold_tests/slow_post/slow_post.test.py delete mode 100644 tests/gold_tests/statistics/metric_response_000.test.py delete mode 100644 tests/gold_tests/thread_config/thread_config.test.py delete mode 100644 tests/gold_tests/timeout/accept_timeout.test.py delete mode 100644 tests/gold_tests/timeout/active_timeout.test.py delete mode 100644 tests/gold_tests/timeout/conn_timeout.test.py delete mode 100644 tests/gold_tests/timeout/default_inactivity_timeout.test.py delete mode 100644 tests/gold_tests/timeout/http2_incomplete_header_timeout.test.py delete mode 100644 tests/gold_tests/timeout/http2_no_activity_timeout.test.py delete mode 100644 tests/gold_tests/timeout/inactive_client_timeout.test.py delete mode 100644 tests/gold_tests/timeout/inactive_timeout.test.py delete mode 100644 tests/gold_tests/timeout/quic_no_activity_timeout.test.py delete mode 100644 tests/gold_tests/timeout/quic_poll_timeout.test.py delete mode 100644 tests/gold_tests/timeout/tls_conn_timeout.test.py delete mode 100644 tests/gold_tests/timeout/tunnel_active_timeout.test.py delete mode 100644 tests/gold_tests/tls/allow-plain.test.py delete mode 100644 tests/gold_tests/tls/exit_on_cert_load_fail.test.py delete mode 100644 tests/gold_tests/tls/proxy_protocol_addressless.test.py delete mode 100644 tests/gold_tests/tls/ssl_key_dialog.test.py delete mode 100644 tests/gold_tests/tls/ssl_multicert_loader.test.py delete mode 100644 tests/gold_tests/tls/tls.test.py delete mode 100644 tests/gold_tests/tls/tls_0rtt_server.test.py delete mode 100644 tests/gold_tests/tls/tls_accept_timeout_crash.test.py delete mode 100644 tests/gold_tests/tls/tls_async_handshake.test.py delete mode 100644 tests/gold_tests/tls/tls_bad_alpn.test.py delete mode 100644 tests/gold_tests/tls/tls_cert_comp.test.py delete mode 100644 tests/gold_tests/tls/tls_cert_comp_cache.test.py delete mode 100644 tests/gold_tests/tls/tls_check_cert_select_plugin.test.py delete mode 100644 tests/gold_tests/tls/tls_check_cert_selection.test.py delete mode 100644 tests/gold_tests/tls/tls_check_cert_selection_reload.test.py delete mode 100644 tests/gold_tests/tls/tls_check_dual_cert_selection.test.py delete mode 100644 tests/gold_tests/tls/tls_check_dual_cert_selection2.test.py delete mode 100644 tests/gold_tests/tls/tls_check_dual_cert_selection_plugin.test.py delete mode 100644 tests/gold_tests/tls/tls_client_alpn_configuration.test.py delete mode 100644 tests/gold_tests/tls/tls_client_cert.test.py delete mode 100644 tests/gold_tests/tls/tls_client_cert2.test.py delete mode 100644 tests/gold_tests/tls/tls_client_cert2_plugin.test.py delete mode 100644 tests/gold_tests/tls/tls_client_cert_override.test.py delete mode 100644 tests/gold_tests/tls/tls_client_cert_override_plugin.test.py delete mode 100644 tests/gold_tests/tls/tls_client_cert_plugin.test.py delete mode 100644 tests/gold_tests/tls/tls_client_verify.test.py delete mode 100644 tests/gold_tests/tls/tls_client_verify2.test.py delete mode 100644 tests/gold_tests/tls/tls_client_verify3.test.py delete mode 100644 tests/gold_tests/tls/tls_client_versions.test.py delete mode 100644 tests/gold_tests/tls/tls_client_versions_minmax.test.py delete mode 100644 tests/gold_tests/tls/tls_engine_abort.test.py delete mode 100644 tests/gold_tests/tls/tls_flow_control.test.py delete mode 100644 tests/gold_tests/tls/tls_forward_nonhttp.test.py delete mode 100644 tests/gold_tests/tls/tls_h2_cipher_suite.test.py delete mode 100644 tests/gold_tests/tls/tls_hooks_client_verify.test.py delete mode 100644 tests/gold_tests/tls/tls_hooks_verify.test.py delete mode 100644 tests/gold_tests/tls/tls_keepalive.test.py delete mode 100644 tests/gold_tests/tls/tls_ocsp.test.py delete mode 100644 tests/gold_tests/tls/tls_origin_open_failed.test.py delete mode 100644 tests/gold_tests/tls/tls_origin_post_abort.test.py delete mode 100644 tests/gold_tests/tls/tls_origin_session_reuse.test.py delete mode 100644 tests/gold_tests/tls/tls_partial_blind_tunnel.test.py delete mode 100644 tests/gold_tests/tls/tls_record_size.test.py delete mode 100644 tests/gold_tests/tls/tls_reload_under_load.test.py delete mode 100644 tests/gold_tests/tls/tls_renegotiation.test.py delete mode 100644 tests/gold_tests/tls/tls_renegotiation_allowed.test.py delete mode 100644 tests/gold_tests/tls/tls_secret_update_default.test.py delete mode 100644 tests/gold_tests/tls/tls_session_key_logging.test.py delete mode 100644 tests/gold_tests/tls/tls_session_reuse.test.py delete mode 100644 tests/gold_tests/tls/tls_sni_groups.test.py delete mode 100644 tests/gold_tests/tls/tls_sni_host_policy.test.py delete mode 100644 tests/gold_tests/tls/tls_sni_ip_allow.test.py delete mode 100644 tests/gold_tests/tls/tls_sni_parent_failover.test.py delete mode 100644 tests/gold_tests/tls/tls_sni_ticket.test.py delete mode 100644 tests/gold_tests/tls/tls_sni_with_port.test.py delete mode 100644 tests/gold_tests/tls/tls_sni_yaml_reload.test.py delete mode 100644 tests/gold_tests/tls/tls_ticket.test.py delete mode 100644 tests/gold_tests/tls/tls_tunnel.test.py delete mode 100644 tests/gold_tests/tls/tls_tunnel_forward.test.py delete mode 100644 tests/gold_tests/tls/tls_verify.test.py delete mode 100644 tests/gold_tests/tls/tls_verify2.test.py delete mode 100644 tests/gold_tests/tls/tls_verify3.test.py delete mode 100644 tests/gold_tests/tls/tls_verify4.test.py delete mode 100644 tests/gold_tests/tls/tls_verify_base.test.py delete mode 100644 tests/gold_tests/tls/tls_verify_ca_override.test.py delete mode 100644 tests/gold_tests/tls/tls_verify_not_pristine.test.py delete mode 100644 tests/gold_tests/tls/tls_verify_override.test.py delete mode 100644 tests/gold_tests/tls/tls_verify_override_base.test.py delete mode 100644 tests/gold_tests/tls/tls_verify_override_sni.test.py delete mode 100644 tests/gold_tests/tls_hooks/tls_hooks.test.py delete mode 100644 tests/gold_tests/tls_hooks/tls_hooks10.test.py delete mode 100644 tests/gold_tests/tls_hooks/tls_hooks11.test.py delete mode 100644 tests/gold_tests/tls_hooks/tls_hooks12.test.py delete mode 100644 tests/gold_tests/tls_hooks/tls_hooks13.test.py delete mode 100644 tests/gold_tests/tls_hooks/tls_hooks14.test.py delete mode 100644 tests/gold_tests/tls_hooks/tls_hooks15.test.py delete mode 100644 tests/gold_tests/tls_hooks/tls_hooks16.test.py delete mode 100644 tests/gold_tests/tls_hooks/tls_hooks17.test.py delete mode 100644 tests/gold_tests/tls_hooks/tls_hooks18.test.py delete mode 100644 tests/gold_tests/tls_hooks/tls_hooks2.test.py delete mode 100644 tests/gold_tests/tls_hooks/tls_hooks3.test.py delete mode 100644 tests/gold_tests/tls_hooks/tls_hooks4.test.py delete mode 100644 tests/gold_tests/tls_hooks/tls_hooks6.test.py delete mode 100644 tests/gold_tests/tls_hooks/tls_hooks7.test.py delete mode 100644 tests/gold_tests/tls_hooks/tls_hooks8.test.py delete mode 100644 tests/gold_tests/tls_hooks/tls_hooks9.test.py delete mode 100644 tests/gold_tests/tls_hooks/tls_hooks_close_while_parked.test.py delete mode 100644 tests/gold_tests/traffic_ctl/convert_plugin_config/convert_plugin_config.test.py delete mode 100644 tests/gold_tests/traffic_ctl/convert_ssl_multicert/convert_ssl_multicert.test.py delete mode 100644 tests/gold_tests/traffic_ctl/convert_storage/convert_storage.test.py delete mode 100644 tests/gold_tests/traffic_ctl/remap_inc/remap_inc.test.py delete mode 100644 tests/gold_tests/traffic_ctl/show_ssl_multicert/show_ssl_multicert.test.py delete mode 100644 tests/gold_tests/traffic_ctl/traffic_ctl_config_output.test.py delete mode 100644 tests/gold_tests/traffic_ctl/traffic_ctl_config_reload.test.py delete mode 100644 tests/gold_tests/traffic_ctl/traffic_ctl_server_debug.test.py delete mode 100644 tests/gold_tests/traffic_ctl/traffic_ctl_server_output.test.py delete mode 100644 tests/gold_tests/traffic_ctl/traffic_ctl_set_read_only.test.py delete mode 100644 tests/gold_tests/tunnel/tunnel_transform.test.py delete mode 100644 tests/gold_tests/tunnel/txn_type.test.py delete mode 100644 tests/gold_tests/uds/uds_socket_perm.test.py delete mode 100644 tests/gold_tests/url/uri.test.py create mode 100644 tests/pytest_tests/test_runner.py create mode 100644 tests/pytest_tests/test_services.py rename tests/{ats_testkit => uranium_testkit}/config.py (84%) rename tests/{gold_tests/autest-site => uranium_testkit}/min_cfg/ip_allow.yaml (100%) rename tests/{gold_tests/autest-site => uranium_testkit}/min_cfg/readme.txt (100%) rename tests/{gold_tests/autest-site => uranium_testkit}/min_cfg/storage.yaml (100%) create mode 100644 tests/uranium_testkit/plugin.py rename tests/{ats_testkit => uranium_testkit}/process.py (98%) rename tests/{ats_testkit => uranium_testkit}/replay.py (97%) create mode 100644 tests/uranium_testkit/runner.py rename tests/{ats_testkit => uranium_testkit}/runtime.py (82%) create mode 100644 tests/uranium_testkit/scenario.py create mode 100644 tests/uranium_testkit/services.py rename tests/{gold_tests => uranium_tests}/at_headers/plugins/CMakeLists.txt (93%) rename tests/{gold_tests => uranium_tests}/at_headers/plugins/at_header_probe.cc (100%) rename tests/{gold_tests => uranium_tests}/at_headers/replay/at_headers.test.yaml (99%) rename tests/{gold_tests => uranium_tests}/ats_probe/ats_probe.bt (100%) rename tests/{gold_tests => uranium_tests}/ats_probe/ats_probe.replay.yaml (100%) rename tests/{gold_tests => uranium_tests}/ats_probe/cache_dir_probe.bt (100%) create mode 100644 tests/uranium_tests/ats_probe/test_ats_probe.py create mode 100644 tests/uranium_tests/ats_probe/test_cache_dir_probe.py rename tests/{gold_tests => uranium_tests}/bad_http_fmt/client.gold (100%) rename tests/{gold_tests => uranium_tests}/bad_http_fmt/method-server.sh (100%) rename tests/{gold_tests => uranium_tests}/bad_http_fmt/server.gold (100%) create mode 100644 tests/uranium_tests/bad_http_fmt/test_bad_http_fmt.py rename tests/{gold_tests => uranium_tests}/basic/.gitignore (100%) rename tests/{gold_tests/basic/basic.test.py => uranium_tests/basic/test_basic.py} (68%) rename tests/{gold_tests/pluginTest/cppapi/cppapi.test.py => uranium_tests/basic/test_config.py} (65%) create mode 100644 tests/uranium_tests/basic/test_copy_config.py create mode 100644 tests/uranium_tests/basic/test_copy_config2.py create mode 100644 tests/uranium_tests/basic/test_deny0.py create mode 100644 tests/uranium_tests/bigobj/test_bigobj.py rename tests/{gold_tests => uranium_tests}/body_factory/config/remap.config (100%) rename tests/{gold_tests => uranium_tests}/body_factory/data/www.customplugin204.test_get.txt (100%) rename tests/{gold_tests => uranium_tests}/body_factory/data/www.customtemplate204.test_get.txt (100%) rename tests/{gold_tests => uranium_tests}/body_factory/data/www.default204.test_get.txt (100%) rename tests/{gold_tests => uranium_tests}/body_factory/data/www.default304.test_get.txt (100%) rename tests/{gold_tests => uranium_tests}/body_factory/data/www.example.test_get_200.txt (100%) rename tests/{gold_tests => uranium_tests}/body_factory/data/www.example.test_get_304.txt (100%) rename tests/{gold_tests => uranium_tests}/body_factory/data/www.example.test_head.txt (100%) rename tests/{gold_tests => uranium_tests}/body_factory/data/www.example.test_head_200.txt (100%) rename tests/{gold_tests => uranium_tests}/body_factory/gold/http-204-custom-plugin.gold (100%) rename tests/{gold_tests => uranium_tests}/body_factory/gold/http-204-custom.gold (100%) rename tests/{gold_tests => uranium_tests}/body_factory/gold/http-204.gold (100%) rename tests/{gold_tests => uranium_tests}/body_factory/gold/http-304.gold (100%) rename tests/{gold_tests => uranium_tests}/body_factory/gold/http-get-200.gold (100%) rename tests/{gold_tests => uranium_tests}/body_factory/gold/http-get-304.gold (100%) rename tests/{gold_tests => uranium_tests}/body_factory/gold/http-head-200.gold (100%) rename tests/{gold_tests => uranium_tests}/body_factory/gold/http-head-no-origin.gold (100%) create mode 100644 tests/uranium_tests/body_factory/test_http204_response.py create mode 100644 tests/uranium_tests/body_factory/test_http204_response_plugin.py create mode 100644 tests/uranium_tests/body_factory/test_http304_response.py create mode 100644 tests/uranium_tests/body_factory/test_http_head_no_origin.py create mode 100644 tests/uranium_tests/body_factory/test_http_with_origin.py rename tests/{gold_tests => uranium_tests}/cache/cache-write-lock-fail-write.conf (100%) rename tests/{gold_tests => uranium_tests}/cache/cache_volume_defaults.test.yaml (99%) rename tests/{gold_tests => uranium_tests}/cache/cache_volume_features.test.yaml (99%) rename tests/{gold_tests => uranium_tests}/cache/gold/background_fill_0_stderr_H.gold (100%) rename tests/{gold_tests => uranium_tests}/cache/gold/background_fill_1_stderr_H.gold (100%) rename tests/{gold_tests => uranium_tests}/cache/gold/background_fill_2_stderr_H.gold (100%) rename tests/{gold_tests => uranium_tests}/cache/gold/background_fill_3_stdout.gold (100%) rename tests/{gold_tests => uranium_tests}/cache/gold/cache_shm_state_after_shutdown.gold (100%) rename tests/{gold_tests => uranium_tests}/cache/gold/hit_default-1.gold (100%) rename tests/{gold_tests => uranium_tests}/cache/gold/hit_default0.gold (100%) rename tests/{gold_tests => uranium_tests}/cache/gold/hit_gen1.gold (100%) rename tests/{gold_tests => uranium_tests}/cache/gold/hit_gen2.gold (100%) rename tests/{gold_tests => uranium_tests}/cache/gold/miss_default-1.gold (100%) rename tests/{gold_tests => uranium_tests}/cache/gold/miss_default0.gold (100%) rename tests/{gold_tests => uranium_tests}/cache/gold/miss_gen1.gold (100%) rename tests/{gold_tests => uranium_tests}/cache/gold/miss_gen2.gold (100%) rename tests/{gold_tests => uranium_tests}/cache/gold/negative-revalidating-enabled-access.gold (100%) rename tests/{gold_tests => uranium_tests}/cache/gold/serve_stale_dns_fail.gold (100%) rename tests/{gold_tests => uranium_tests}/cache/gold/storage_metrics_0_stdout.gold (100%) rename tests/{gold_tests => uranium_tests}/cache/gold/storage_metrics_1_stdout.gold (100%) rename tests/{gold_tests => uranium_tests}/cache/gold/storage_metrics_2_stdout.gold (100%) rename tests/{gold_tests => uranium_tests}/cache/gold/storage_metrics_3_stdout.gold (100%) rename tests/{gold_tests => uranium_tests}/cache/gold/two_alternates_evicted.gold (100%) rename tests/{gold_tests => uranium_tests}/cache/replay/alternate-caching-quality.test.yaml (99%) rename tests/{gold_tests => uranium_tests}/cache/replay/alternate-caching-update-size.test.yaml (99%) rename tests/{gold_tests => uranium_tests}/cache/replay/auth-s-maxage.test.yaml (99%) rename tests/{gold_tests => uranium_tests}/cache/replay/bg_fill.yaml (100%) rename tests/{gold_tests => uranium_tests}/cache/replay/cache-control-basic.test.yaml (99%) rename tests/{gold_tests => uranium_tests}/cache/replay/cache-control-max-age.test.yaml (99%) rename tests/{gold_tests => uranium_tests}/cache/replay/cache-control-pragma.test.yaml (98%) rename tests/{gold_tests => uranium_tests}/cache/replay/cache-control-s-maxage.test.yaml (99%) rename tests/{gold_tests => uranium_tests}/cache/replay/cache-exact-url.test.yaml (99%) rename tests/{gold_tests => uranium_tests}/cache/replay/cache-heuristic-status.test.yaml (99%) rename tests/{gold_tests => uranium_tests}/cache/replay/cache-range-response.test.yaml (99%) rename tests/{gold_tests => uranium_tests}/cache/replay/cache-read-retry-stale.test.yaml (99%) rename tests/{gold_tests => uranium_tests}/cache/replay/cache-read-retry.test.yaml (99%) rename tests/{gold_tests => uranium_tests}/cache/replay/cache-shm-dir-invalid.replay.yaml (100%) rename tests/{gold_tests => uranium_tests}/cache/replay/cache-shm-fast-restart.replay.yaml (100%) rename tests/{gold_tests => uranium_tests}/cache/replay/cache-write-lock-stale-retry.test.yaml (99%) rename tests/{gold_tests => uranium_tests}/cache/replay/cache-write-lock-stale-serve.test.yaml (99%) rename tests/{gold_tests => uranium_tests}/cache/replay/cache-write-retry-callback.test.yaml (99%) rename tests/{gold_tests => uranium_tests}/cache/replay/conditional-get-cache-hit.test.yaml (99%) rename tests/{gold_tests => uranium_tests}/cache/replay/cookie-all-but-text-with-excp.test.yaml (99%) rename tests/{gold_tests => uranium_tests}/cache/replay/cookie-all-but-text.test.yaml (99%) rename tests/{gold_tests => uranium_tests}/cache/replay/cookie-bypass-cache.test.yaml (99%) rename tests/{gold_tests => uranium_tests}/cache/replay/cookie-cache-img-only.test.yaml (99%) rename tests/{gold_tests => uranium_tests}/cache/replay/cookie-default.test.yaml (99%) rename tests/{gold_tests => uranium_tests}/cache/replay/delete_cached.test.yaml (98%) rename tests/{gold_tests => uranium_tests}/cache/replay/get_then_post.replay.yaml (100%) rename tests/{gold_tests => uranium_tests}/cache/replay/head_with_get_cached.test.yaml (99%) rename tests/{gold_tests => uranium_tests}/cache/replay/host_down_range_recursion_prime.replay.yaml (100%) rename tests/{gold_tests => uranium_tests}/cache/replay/host_down_range_recursion_range.replay.yaml (100%) rename tests/{gold_tests => uranium_tests}/cache/replay/ignore_authentication.test.yaml (98%) create mode 100644 tests/uranium_tests/cache/replay/max-stale-age-percent.test.yaml rename tests/{gold_tests => uranium_tests}/cache/replay/negative-caching-300-second-timeout.replay.yaml (100%) rename tests/{gold_tests => uranium_tests}/cache/replay/negative-caching-customized.test.yaml (98%) rename tests/{gold_tests => uranium_tests}/cache/replay/negative-caching-default.test.yaml (99%) rename tests/{gold_tests => uranium_tests}/cache/replay/negative-caching-disabled.test.yaml (99%) rename tests/{gold_tests => uranium_tests}/cache/replay/negative-caching-malformed-cc.test.yaml (99%) rename tests/{gold_tests => uranium_tests}/cache/replay/negative-caching-no-timeout.replay.yaml (100%) rename tests/{gold_tests => uranium_tests}/cache/replay/negative-caching-timeout.replay.yaml (100%) rename tests/{gold_tests => uranium_tests}/cache/replay/negative-caching-ttl-in-cache.replay.yaml (100%) rename tests/{gold_tests => uranium_tests}/cache/replay/negative-revalidating-disabled.test.yaml (98%) rename tests/{gold_tests => uranium_tests}/cache/replay/negative-revalidating-enabled.test.yaml (99%) rename tests/{gold_tests => uranium_tests}/cache/replay/negative-revalidating-list.test.yaml (98%) rename tests/{gold_tests => uranium_tests}/cache/replay/post_with_post_caching_disabled.test.yaml (99%) rename tests/{gold_tests => uranium_tests}/cache/replay/post_with_post_caching_enabled.test.yaml (99%) rename tests/{gold_tests => uranium_tests}/cache/replay/post_with_post_caching_override.test.yaml (98%) rename tests/{gold_tests => uranium_tests}/cache/replay/proxy_serve_stale.test.yaml (99%) rename tests/{gold_tests => uranium_tests}/cache/replay/proxy_serve_stale_origin_down.test.yaml (99%) rename tests/{gold_tests => uranium_tests}/cache/replay/request-cache-control-default.test.yaml (98%) rename tests/{gold_tests => uranium_tests}/cache/replay/request-cache-control-honor-client.test.yaml (99%) rename tests/{gold_tests => uranium_tests}/cache/replay/response-cache-control-default.test.yaml (98%) rename tests/{gold_tests => uranium_tests}/cache/replay/response-cache-control-ignored.test.yaml (99%) rename tests/{gold_tests => uranium_tests}/cache/replay/targeted-cache-control.test.yaml (99%) rename tests/{gold_tests => uranium_tests}/cache/replay/varied_transactions.test.yaml (99%) rename tests/{gold_tests => uranium_tests}/cache/shm_poke.py (99%) create mode 100644 tests/uranium_tests/cache/test_background_fill.py create mode 100644 tests/uranium_tests/cache/test_cache_config_reload.py create mode 100644 tests/uranium_tests/cache/test_cache_corrupt_recursive_uaf.py create mode 100644 tests/uranium_tests/cache/test_cache_generation_clear.py create mode 100644 tests/uranium_tests/cache/test_cache_generation_disjoint.py create mode 100644 tests/uranium_tests/cache/test_cache_shm_bad_disk_dropped.py create mode 100644 tests/uranium_tests/cache/test_cache_shm_concurrent_attach.py create mode 100644 tests/uranium_tests/cache/test_cache_shm_control_size_mismatch.py create mode 100644 tests/uranium_tests/cache/test_cache_shm_dir_invalid.py create mode 100644 tests/uranium_tests/cache/test_cache_shm_fast_restart.py create mode 100644 tests/uranium_tests/cache/test_cache_shm_purge_on_disable.py create mode 100644 tests/uranium_tests/cache/test_cache_shm_schema_mismatch.py create mode 100644 tests/uranium_tests/cache/test_cache_shm_storage_mismatch.py create mode 100644 tests/uranium_tests/cache/test_cache_shm_unclean_shutdown.py create mode 100644 tests/uranium_tests/cache/test_cache_write_lock_contention.py create mode 100644 tests/uranium_tests/cache/test_disjoint_wait_for_cache.py create mode 100644 tests/uranium_tests/cache/test_host_down_range_recursion.py create mode 100644 tests/uranium_tests/cache/test_negative_caching.py create mode 100644 tests/uranium_tests/cache/test_proxy_serve_stale_dns_fail.py create mode 100644 tests/uranium_tests/cache/test_storage_metrics.py rename tests/{gold_tests => uranium_tests}/chunked_encoding/CMakeLists.txt (100%) rename tests/{gold_tests => uranium_tests}/chunked_encoding/chunk_extension_client.py (100%) rename tests/{gold_tests => uranium_tests}/chunked_encoding/chunk_trailer_client.py (100%) rename tests/{gold_tests => uranium_tests}/chunked_encoding/chunked_encoding_h2_server.py (100%) rename tests/{gold_tests => uranium_tests}/chunked_encoding/gold/chunked_GET_200.gold (100%) rename tests/{gold_tests => uranium_tests}/chunked_encoding/gold/chunked_GET_200_uds.gold (100%) rename tests/{gold_tests => uranium_tests}/chunked_encoding/gold/chunked_POST_200.gold (100%) rename tests/{gold_tests => uranium_tests}/chunked_encoding/gold/h2_chunked_POST_200.gold (100%) rename tests/{gold_tests => uranium_tests}/chunked_encoding/gold/verifier_client_chunked.gold (100%) rename tests/{gold_tests => uranium_tests}/chunked_encoding/replays/chunk_extension_quoted_string.replay.yaml (100%) rename tests/{gold_tests => uranium_tests}/chunked_encoding/replays/chunk_trailer_bare_lf.replay.yaml (100%) rename tests/{gold_tests => uranium_tests}/chunked_encoding/replays/chunked.replay.yaml (100%) rename tests/{gold_tests => uranium_tests}/chunked_encoding/replays/chunked_h1_flow_control.test.yaml (99%) rename tests/{gold_tests => uranium_tests}/chunked_encoding/replays/chunked_h2_flow_control.test.yaml (99%) rename tests/{gold_tests => uranium_tests}/chunked_encoding/replays/chunked_in_http_1_0.replay.yaml (100%) rename tests/{gold_tests => uranium_tests}/chunked_encoding/replays/chunked_not_last.test.yaml (99%) rename tests/{gold_tests => uranium_tests}/chunked_encoding/replays/chunked_passthru_flow_control.test.yaml (99%) rename tests/{gold_tests => uranium_tests}/chunked_encoding/replays/chunked_trailer_dropped.replay.yaml (100%) rename tests/{gold_tests => uranium_tests}/chunked_encoding/replays/chunked_trailer_proxied.replay.yaml (100%) rename tests/{gold_tests => uranium_tests}/chunked_encoding/replays/large_chunked.test.yaml (98%) rename tests/{gold_tests => uranium_tests}/chunked_encoding/replays/malformed_chunked_header.replay.yaml (100%) rename tests/{gold_tests => uranium_tests}/chunked_encoding/server4.sh (100%) rename tests/{gold_tests => uranium_tests}/chunked_encoding/smuggle-client.c (100%) create mode 100644 tests/uranium_tests/chunked_encoding/test_bad_chunked_encoding.py create mode 100644 tests/uranium_tests/chunked_encoding/test_chunk_extension_quoted_string.py create mode 100644 tests/uranium_tests/chunked_encoding/test_chunk_trailer_bare_lf.py create mode 100644 tests/uranium_tests/chunked_encoding/test_chunked_encoding.py create mode 100644 tests/uranium_tests/chunked_encoding/test_chunked_encoding_disabled.py create mode 100644 tests/uranium_tests/chunked_encoding/test_chunked_encoding_h2.py rename tests/{gold_tests => uranium_tests}/client_connection/exempt_lists/exempt_all.yaml (100%) rename tests/{gold_tests => uranium_tests}/client_connection/exempt_lists/exempt_localhost.yaml (100%) rename tests/{gold_tests => uranium_tests}/client_connection/exempt_lists/no_localhost.yaml (100%) rename tests/{gold_tests => uranium_tests}/client_connection/http2_slow_origins.replay.yaml (100%) rename tests/{gold_tests => uranium_tests}/client_connection/http_slow_origins.replay.yaml (100%) rename tests/{gold_tests => uranium_tests}/client_connection/https_slow_origins.replay.yaml (100%) create mode 100644 tests/uranium_tests/client_connection/test_per_client_connection_max.py create mode 100644 tests/uranium_tests/command_argument/test_verify_global_plugin.py create mode 100644 tests/uranium_tests/command_argument/test_verify_remap_plugin.py create mode 100644 tests/uranium_tests/config_processor/test_config_destroy_thread.py rename tests/{gold_tests => uranium_tests}/connect/gold/connect_0_stderr.gold (100%) rename tests/{gold_tests => uranium_tests}/connect/gold/connect_access.gold (100%) rename tests/{gold_tests => uranium_tests}/connect/gold/metrics.gold (100%) rename tests/{gold_tests => uranium_tests}/connect/malformed_h2_request_client.py (100%) rename tests/{gold_tests => uranium_tests}/connect/replays/connect.replay.yaml (100%) rename tests/{gold_tests => uranium_tests}/connect/replays/connect_h2.replay.yaml (100%) rename tests/{gold_tests => uranium_tests}/connect/replays/connect_handshake.replay.yaml (100%) rename tests/{gold_tests => uranium_tests}/connect/replays/connect_parent_error_body.replay.yaml (100%) rename tests/{gold_tests => uranium_tests}/connect/replays/h2_malformed_request_logging.replay.yaml (100%) create mode 100644 tests/uranium_tests/connect/test_connect.py create mode 100644 tests/uranium_tests/connect/test_connect_handshake.py create mode 100644 tests/uranium_tests/connect/test_connect_parent_error_body.py create mode 100644 tests/uranium_tests/connect/test_h2_malformed_request_logging.py rename tests/{gold_tests => uranium_tests}/connect_down_policy/gold/connect_down_policy_4_error_log.gold (100%) rename tests/{gold_tests => uranium_tests}/connect_down_policy/replay/connect_down_policy_4.test.yaml (98%) rename tests/{gold_tests => uranium_tests}/connect_down_policy/replay/inactive_timeout.replay.yaml (100%) create mode 100644 tests/uranium_tests/connect_down_policy/test_connect_down_policy.py rename tests/{gold_tests => uranium_tests}/cont_schedule/entire_pool.py (100%) rename tests/{gold_tests => uranium_tests}/cont_schedule/gold/schedule_every_on_entire_pool.gold (100%) rename tests/{gold_tests => uranium_tests}/cont_schedule/gold/schedule_every_on_pool.gold (100%) rename tests/{gold_tests => uranium_tests}/cont_schedule/gold/schedule_every_on_thread.gold (100%) rename tests/{gold_tests => uranium_tests}/cont_schedule/gold/schedule_on_entire_pool.gold (100%) rename tests/{gold_tests => uranium_tests}/cont_schedule/gold/schedule_on_pool.gold (100%) rename tests/{gold_tests => uranium_tests}/cont_schedule/gold/schedule_on_thread.gold (100%) rename tests/{gold_tests => uranium_tests}/cont_schedule/gold/thread_affinity.gold (100%) create mode 100644 tests/uranium_tests/cont_schedule/test_schedule_every_on_entire_pool.py create mode 100644 tests/uranium_tests/cont_schedule/test_schedule_every_on_pool.py create mode 100644 tests/uranium_tests/cont_schedule/test_schedule_every_on_thread.py create mode 100644 tests/uranium_tests/cont_schedule/test_schedule_on_entire_pool.py create mode 100644 tests/uranium_tests/cont_schedule/test_schedule_on_pool.py create mode 100644 tests/uranium_tests/cont_schedule/test_schedule_on_thread.py create mode 100644 tests/uranium_tests/cont_schedule/test_thread_affinity.py rename tests/{gold_tests => uranium_tests}/continuations/plugins/CMakeLists.txt (93%) rename tests/{gold_tests => uranium_tests}/continuations/plugins/session_id_verify.cc (100%) create mode 100644 tests/uranium_tests/continuations/test_double.py create mode 100644 tests/uranium_tests/continuations/test_double_h2.py create mode 100644 tests/uranium_tests/continuations/test_openclose.py create mode 100644 tests/uranium_tests/continuations/test_openclose_h2.py create mode 100644 tests/uranium_tests/continuations/test_session_id.py rename tests/{gold_tests => uranium_tests}/cripts/cripts_bundle.test.yaml (99%) rename tests/{gold_tests => uranium_tests}/cripts/files/basic.cript (100%) rename tests/{gold_tests => uranium_tests}/cripts/files/bundle_headers.cript (100%) rename tests/{gold_tests => uranium_tests}/cripts/files/direct_headers.cript (100%) rename tests/{gold_tests => uranium_tests}/cripts/files/query_copy.cript (100%) rename tests/{gold_tests => uranium_tests}/cripts/files/server_bundle_headers.cript (100%) rename tests/{gold_tests => uranium_tests}/cripts/gold/basic_cript.gold (100%) rename tests/{gold_tests => uranium_tests}/cripts/gold/certs_cript.gold (100%) create mode 100644 tests/uranium_tests/cripts/test_cripts.py rename tests/{gold_tests => uranium_tests}/dns/gold/connect_attempts_rr_max_retries_error_log.gold (100%) rename tests/{gold_tests => uranium_tests}/dns/gold/connect_attempts_rr_no_error_log.gold (100%) rename tests/{gold_tests => uranium_tests}/dns/gold/connect_attempts_rr_retries_error_log.gold (100%) rename tests/{gold_tests => uranium_tests}/dns/gold/connect_attempts_single_max_retries_error_log.gold (100%) rename tests/{gold_tests => uranium_tests}/dns/gold/test_case_0_stderr.gold (100%) rename tests/{gold_tests => uranium_tests}/dns/gold/test_case_1_stderr.gold (100%) rename tests/{gold_tests => uranium_tests}/dns/hosts_file (100%) rename tests/{gold_tests => uranium_tests}/dns/replay/connect_attempts_rr_max_retries.test.yaml (98%) rename tests/{gold_tests => uranium_tests}/dns/replay/connect_attempts_rr_no_retry.test.yaml (98%) rename tests/{gold_tests => uranium_tests}/dns/replay/connect_attempts_rr_retries.test.yaml (98%) rename tests/{gold_tests => uranium_tests}/dns/replay/connect_attempts_single_max_retries.test.yaml (98%) rename tests/{gold_tests => uranium_tests}/dns/replay/multiple_host_requests.replay.yaml (100%) rename tests/{gold_tests => uranium_tests}/dns/replay/reverse_lookup.replay.yaml (100%) rename tests/{gold_tests => uranium_tests}/dns/replay/server_down.replay.yaml (100%) rename tests/{gold_tests => uranium_tests}/dns/replay/server_error.replay.yaml (100%) rename tests/{gold_tests => uranium_tests}/dns/replay/single_transaction.replay.yaml (100%) create mode 100644 tests/uranium_tests/dns/test_dns_down_nameserver.py create mode 100644 tests/uranium_tests/dns/test_dns_host_down.py create mode 100644 tests/uranium_tests/dns/test_dns_reverse_lookup.py create mode 100644 tests/uranium_tests/dns/test_dns_ttl.py create mode 100644 tests/uranium_tests/dns/test_splitdns.py create mode 100644 tests/uranium_tests/dns/test_splitdns_reload.py rename tests/{gold_tests => uranium_tests}/early_hints/early_hints_server.py (100%) create mode 100644 tests/uranium_tests/early_hints/test_early_hints.py rename tests/{gold_tests => uranium_tests}/forward_proxy/forward_proxy.replay.yaml (100%) create mode 100644 tests/uranium_tests/forward_proxy/test_forward_proxy.py rename tests/{gold_tests => uranium_tests}/h2/clients/h2_extension_settings.py (100%) rename tests/{gold_tests => uranium_tests}/h2/clients/h2_max_active_streams.py (100%) rename tests/{gold_tests => uranium_tests}/h2/clients/h2_max_settings_per_minute.py (100%) rename tests/{gold_tests => uranium_tests}/h2/clients/h2_settings_ack_stall.py (100%) rename tests/{gold_tests => uranium_tests}/h2/clients/h2empty_data_frame.py (100%) rename tests/{gold_tests => uranium_tests}/h2/expect_100_continue.yaml (100%) rename tests/{gold_tests => uranium_tests}/h2/gold/active_timeout.gold (100%) rename tests/{gold_tests => uranium_tests}/h2/gold/bigfile.gold (100%) rename tests/{gold_tests => uranium_tests}/h2/gold/chunked.gold (100%) rename tests/{gold_tests => uranium_tests}/h2/gold/h2spec_stdout.gold (100%) rename tests/{gold_tests => uranium_tests}/h2/gold/http-request-method-metrics.gold (100%) rename tests/{gold_tests => uranium_tests}/h2/gold/http2_8_stdout.gold (100%) rename tests/{gold_tests => uranium_tests}/h2/gold/http2_9_stderr.gold (100%) rename tests/{gold_tests => uranium_tests}/h2/gold/http2_9_stdout.gold (100%) rename tests/{gold_tests => uranium_tests}/h2/gold/httpbin_0_stderr.gold (100%) rename tests/{gold_tests => uranium_tests}/h2/gold/httpbin_0_stdout.gold (100%) rename tests/{gold_tests => uranium_tests}/h2/gold/httpbin_1_stderr.gold (100%) rename tests/{gold_tests => uranium_tests}/h2/gold/httpbin_1_stdout.gold (100%) rename tests/{gold_tests => uranium_tests}/h2/gold/httpbin_2_stderr.gold (100%) rename tests/{gold_tests => uranium_tests}/h2/gold/httpbin_2_stdout.gold (100%) rename tests/{gold_tests => uranium_tests}/h2/gold/httpbin_3_stderr.gold (100%) rename tests/{gold_tests => uranium_tests}/h2/gold/httpbin_3_stdout.gold (100%) rename tests/{gold_tests => uranium_tests}/h2/gold/httpbin_access.gold (100%) rename tests/{gold_tests => uranium_tests}/h2/gold/log.gold (100%) rename tests/{gold_tests => uranium_tests}/h2/gold/nghttp_0_stdout.gold (100%) rename tests/{gold_tests => uranium_tests}/h2/gold/nghttp_1_stdout.gold (100%) rename tests/{gold_tests => uranium_tests}/h2/gold/nghttp_ts_stderr.gold (100%) rename tests/{gold_tests => uranium_tests}/h2/gold/post_chunked.gold (100%) rename tests/{gold_tests => uranium_tests}/h2/gold/priority_0_stderr.gold (100%) rename tests/{gold_tests => uranium_tests}/h2/gold/priority_0_stdout.gold (100%) rename tests/{gold_tests => uranium_tests}/h2/gold/remap-200.gold (100%) rename tests/{gold_tests => uranium_tests}/h2/gold/replay.gold (100%) rename tests/{gold_tests => uranium_tests}/h2/gold/server_after_headers.gold (100%) rename tests/{gold_tests => uranium_tests}/h2/grpc/grpc_client.py (100%) rename tests/{gold_tests => uranium_tests}/h2/grpc/grpc_server.py (100%) rename tests/{gold_tests => uranium_tests}/h2/grpc/simple.proto (100%) create mode 100644 tests/uranium_tests/h2/grpc/test_grpc.py rename tests/{gold_tests => uranium_tests}/h2/h1_trailer_client.py (100%) rename tests/{gold_tests => uranium_tests}/h2/h2_origin_trailers_h1.replay.yaml (100%) rename tests/{gold_tests => uranium_tests}/h2/h2active_timeout.py (100%) rename tests/{gold_tests => uranium_tests}/h2/h2client.py (100%) rename tests/{gold_tests => uranium_tests}/h2/h2get_with_body.yaml (100%) rename tests/{gold_tests => uranium_tests}/h2/http2_close_connection.yaml (100%) rename tests/{gold_tests => uranium_tests}/h2/http2_flow_control.replay.yaml (100%) rename tests/{gold_tests => uranium_tests}/h2/http2_flow_control_chunked.replay.yaml (100%) rename tests/{gold_tests => uranium_tests}/h2/replay/http2_client_reset_keeps_session_reading.test.yaml (99%) rename tests/{gold_tests => uranium_tests}/h2/replay/http2_concurrent_streams.replay.yaml (100%) rename tests/{gold_tests => uranium_tests}/h2/replay/http2_max_active_streams_advisory.replay.yaml (100%) rename tests/{gold_tests => uranium_tests}/h2/replay/http2_max_active_streams_enforce.replay.yaml (100%) rename tests/{gold_tests => uranium_tests}/h2/replay/http2_settings_ack_stall.replay.yaml (100%) rename tests/{gold_tests => uranium_tests}/h2/replay/http2_txn_start_read_gate.replay.yaml (100%) rename tests/{gold_tests => uranium_tests}/h2/replay_h2_origin_cert_reverify.yaml (100%) rename tests/{gold_tests => uranium_tests}/h2/replay_h2origin/h1-client-h2-origin.yaml (100%) rename tests/{gold_tests => uranium_tests}/h2/replay_h2origin/h2-origin.yaml (100%) rename tests/{gold_tests => uranium_tests}/h2/replay_rst_stream/http2_rst_stream_client_after_data.yaml (100%) rename tests/{gold_tests => uranium_tests}/h2/replay_rst_stream/http2_rst_stream_client_after_headers.yaml (100%) rename tests/{gold_tests => uranium_tests}/h2/replay_rst_stream/http2_rst_stream_server_after_headers.yaml (100%) rename tests/{gold_tests => uranium_tests}/h2/rules/graceful_shutdown.conf (100%) rename tests/{gold_tests => uranium_tests}/h2/rules/huge_resp_hdrs.conf (100%) create mode 100644 tests/uranium_tests/h2/test_h2_origin_cert_reverify.py create mode 100644 tests/uranium_tests/h2/test_h2_origin_trailers_h1.py create mode 100644 tests/uranium_tests/h2/test_h2disable.py create mode 100644 tests/uranium_tests/h2/test_h2disable_no_accept_threads.py create mode 100644 tests/uranium_tests/h2/test_h2enable.py create mode 100644 tests/uranium_tests/h2/test_h2enable_no_accept_threads.py create mode 100644 tests/uranium_tests/h2/test_h2get_with_body.py create mode 100644 tests/uranium_tests/h2/test_h2origin.py create mode 100644 tests/uranium_tests/h2/test_h2origin_single_thread.py create mode 100644 tests/uranium_tests/h2/test_h2spec.py create mode 100644 tests/uranium_tests/h2/test_http2.py create mode 100644 tests/uranium_tests/h2/test_http2_close_connection.py create mode 100644 tests/uranium_tests/h2/test_http2_concurrent_streams.py create mode 100644 tests/uranium_tests/h2/test_http2_crlf_header_validation.py create mode 100644 tests/uranium_tests/h2/test_http2_empty_data_frame.py create mode 100644 tests/uranium_tests/h2/test_http2_flow_control.py create mode 100644 tests/uranium_tests/h2/test_http2_max_active_streams.py create mode 100644 tests/uranium_tests/h2/test_http2_priority.py create mode 100644 tests/uranium_tests/h2/test_http2_rst_stream.py create mode 100644 tests/uranium_tests/h2/test_http2_txn_start_read_gate.py create mode 100644 tests/uranium_tests/h2/test_http2_write_threshold.py create mode 100644 tests/uranium_tests/h2/test_httpbin.py create mode 100644 tests/uranium_tests/h2/test_nghttp.py rename tests/{gold_tests => uranium_tests}/h2/trickle_client.py (100%) rename tests/{gold_tests => uranium_tests}/h2/trickle_server.py (100%) rename tests/{gold_tests => uranium_tests}/h3/go_h3_client/go.mod (100%) rename tests/{gold_tests => uranium_tests}/h3/go_h3_client/go.sum (100%) rename tests/{gold_tests => uranium_tests}/h3/go_h3_client/main.go (100%) rename tests/{gold_tests => uranium_tests}/h3/h3_session_ticket.sh (100%) rename tests/{gold_tests => uranium_tests}/h3/py_h3_client/h3_client.py (100%) rename tests/{gold_tests => uranium_tests}/h3/replays/h3_active_timeout.test.yaml (99%) rename tests/{gold_tests => uranium_tests}/h3/replays/h3_flow_control.test.yaml (99%) rename tests/{gold_tests => uranium_tests}/h3/replays/h3_proxy_verifier.test.yaml (99%) rename tests/{gold_tests => uranium_tests}/h3/replays/h3_server_for_go_client.replay.yaml (100%) rename tests/{gold_tests => uranium_tests}/h3/replays/h3_server_for_python_client.replay.yaml (100%) rename tests/{gold_tests => uranium_tests}/h3/replays/h3_sni.replay.yaml (100%) rename tests/{gold_tests => uranium_tests}/h3/replays/h3_stream_lifetime.test.yaml (99%) create mode 100644 tests/uranium_tests/h3/test_h3_curl.py create mode 100644 tests/uranium_tests/h3/test_h3_go_client.py create mode 100644 tests/uranium_tests/h3/test_h3_python_client.py create mode 100644 tests/uranium_tests/h3/test_h3_range_cache.py create mode 100644 tests/uranium_tests/h3/test_h3_session_ticket.py create mode 100644 tests/uranium_tests/h3/test_h3_sni_check.py rename tests/{gold_tests => uranium_tests}/headers/cache_and_error_nobody.gold (100%) rename tests/{gold_tests => uranium_tests}/headers/cache_and_req_body-hit-stale-206-etag.gold (100%) rename tests/{gold_tests => uranium_tests}/headers/cache_and_req_body-hit-stale-206.gold (100%) rename tests/{gold_tests => uranium_tests}/headers/cache_and_req_body-hit-stale-INM.gold (100%) rename tests/{gold_tests => uranium_tests}/headers/cache_and_req_body-hit-stale.gold (100%) rename tests/{gold_tests => uranium_tests}/headers/cache_and_req_body-hit.gold (100%) rename tests/{gold_tests => uranium_tests}/headers/cache_and_req_body-hit_close.gold (100%) rename tests/{gold_tests => uranium_tests}/headers/cache_and_req_body-miss.gold (100%) rename tests/{gold_tests => uranium_tests}/headers/cache_and_req_nobody-hit-stale.gold (100%) rename tests/{gold_tests => uranium_tests}/headers/clients/oversized_field_h2_client.py (97%) rename tests/{gold_tests => uranium_tests}/headers/data/www.http408.test.txt (100%) rename tests/{gold_tests => uranium_tests}/headers/data/www.passthrough.test_get.txt (100%) rename tests/{gold_tests => uranium_tests}/headers/data/www.redirect0.test_get.txt (100%) rename tests/{gold_tests => uranium_tests}/headers/data/www.redirect301.test_get.txt (100%) rename tests/{gold_tests => uranium_tests}/headers/data/www.redirect302.test_get.txt (100%) rename tests/{gold_tests => uranium_tests}/headers/data/www.redirect307.test_get.txt (100%) rename tests/{gold_tests => uranium_tests}/headers/data/www.redirect308.test_get.txt (100%) rename tests/{gold_tests => uranium_tests}/headers/forwarded-observer.py (100%) rename tests/{gold_tests => uranium_tests}/headers/forwarded.gold (100%) rename tests/{gold_tests => uranium_tests}/headers/general-connection-failure-502.gold (100%) rename tests/{gold_tests => uranium_tests}/headers/gold/bad_good_request.gold (100%) rename tests/{gold_tests => uranium_tests}/headers/gold/bad_good_request_header.gold (100%) rename tests/{gold_tests => uranium_tests}/headers/gold/bad_good_request_http1.gold (100%) rename tests/{gold_tests => uranium_tests}/headers/gold/bad_method.gold (100%) rename tests/{gold_tests => uranium_tests}/headers/gold/bad_protocol_number.gold (100%) rename tests/{gold_tests => uranium_tests}/headers/gold/bad_te_value.gold (100%) rename tests/{gold_tests => uranium_tests}/headers/gold/field_name_space.gold (100%) rename tests/{gold_tests => uranium_tests}/headers/gold/invalid_character_in_te_value.gold (100%) rename tests/{gold_tests => uranium_tests}/headers/hsts.200.gold (100%) rename tests/{gold_tests => uranium_tests}/headers/hsts.404.gold (100%) rename tests/{gold_tests => uranium_tests}/headers/http408.gold (100%) rename tests/{gold_tests => uranium_tests}/headers/maps.reg (100%) rename tests/{gold_tests => uranium_tests}/headers/normalize_ae.gold (100%) rename tests/{gold_tests => uranium_tests}/headers/normalize_ae_observer.py (100%) rename tests/{gold_tests => uranium_tests}/headers/passthrough_get.gold (100%) rename tests/{gold_tests => uranium_tests}/headers/redirect0_get.gold (100%) rename tests/{gold_tests => uranium_tests}/headers/redirect301_get.gold (100%) rename tests/{gold_tests => uranium_tests}/headers/redirect302_get.gold (100%) rename tests/{gold_tests => uranium_tests}/headers/redirect307_get.gold (100%) rename tests/{gold_tests => uranium_tests}/headers/redirect308_get.gold (100%) rename tests/{gold_tests => uranium_tests}/headers/replay/oversized_field.test.yaml (99%) rename tests/{gold_tests => uranium_tests}/headers/replay/oversized_field_h2.replay.yaml (100%) rename tests/{gold_tests => uranium_tests}/headers/replays/accept_webp.test.yaml (99%) rename tests/{gold_tests => uranium_tests}/headers/replays/cache-test.test.yaml (99%) rename tests/{gold_tests => uranium_tests}/headers/replays/cached_ims_range.test.yaml (99%) rename tests/{gold_tests => uranium_tests}/headers/replays/domain-blacklist-30x.test.yaml (99%) rename tests/{gold_tests => uranium_tests}/headers/replays/hopbyhop_connection.test.yaml (99%) rename tests/{gold_tests => uranium_tests}/headers/replays/hsts.test.yaml (99%) rename tests/{gold_tests => uranium_tests}/headers/replays/invalid_range_request.test.yaml (99%) rename tests/{gold_tests => uranium_tests}/headers/replays/malformed_host_port.test.yaml (99%) rename tests/{gold_tests => uranium_tests}/headers/replays/normalized_ae_varied_transactions.test.yaml (99%) rename tests/{gold_tests => uranium_tests}/headers/replays/range.test.yaml (99%) rename tests/{gold_tests => uranium_tests}/headers/replays/range_transform.test.yaml (99%) rename tests/{gold_tests => uranium_tests}/headers/rewrite_rules/header_rewrite_rules_0.conf (100%) rename tests/{gold_tests => uranium_tests}/headers/rewrite_rules/header_rewrite_rules_301.conf (100%) rename tests/{gold_tests => uranium_tests}/headers/rewrite_rules/header_rewrite_rules_302.conf (100%) rename tests/{gold_tests => uranium_tests}/headers/rewrite_rules/header_rewrite_rules_307.conf (100%) rename tests/{gold_tests => uranium_tests}/headers/rewrite_rules/header_rewrite_rules_308.conf (100%) rename tests/{gold_tests => uranium_tests}/headers/syntax.200.gold (100%) rename tests/{gold_tests => uranium_tests}/headers/syntax.400.gold (100%) create mode 100644 tests/uranium_tests/headers/test_cache_and_req_body.py create mode 100644 tests/uranium_tests/headers/test_field_name_space.py create mode 100644 tests/uranium_tests/headers/test_forwarded.py create mode 100644 tests/uranium_tests/headers/test_general_connection_failure_502.py create mode 100644 tests/uranium_tests/headers/test_good_request_after_bad.py create mode 100644 tests/uranium_tests/headers/test_http408.py create mode 100644 tests/uranium_tests/headers/test_normalize_ae.py create mode 100644 tests/uranium_tests/headers/test_oversized_field_h2.py create mode 100644 tests/uranium_tests/headers/test_syntax.py create mode 100644 tests/uranium_tests/headers/test_via.py rename tests/{gold_tests => uranium_tests}/headers/via-observer.py (100%) rename tests/{gold_tests => uranium_tests}/headers/via.gold (100%) rename tests/{gold_tests => uranium_tests}/headers/via_h3.gold (100%) rename tests/{gold_tests => uranium_tests}/headers/via_uds.gold (100%) rename tests/{gold_tests => uranium_tests}/ip_allow/gold/200.gold (100%) rename tests/{gold_tests => uranium_tests}/ip_allow/gold/403.gold (100%) rename tests/{gold_tests => uranium_tests}/ip_allow/gold/403_h2.gold (100%) rename tests/{gold_tests => uranium_tests}/ip_allow/gold/log.gold (100%) rename tests/{gold_tests => uranium_tests}/ip_allow/replay/ip_allow_subjects_overflow.test.yaml (99%) rename tests/{gold_tests => uranium_tests}/ip_allow/replay/ip_allow_subjects_valid.test.yaml (99%) rename tests/{gold_tests => uranium_tests}/ip_allow/replays/h3.replay.yaml (100%) rename tests/{gold_tests => uranium_tests}/ip_allow/replays/http_proxy_protocol.replay.yaml (100%) rename tests/{gold_tests => uranium_tests}/ip_allow/replays/https_categories_all.replay.yaml (100%) rename tests/{gold_tests => uranium_tests}/ip_allow/replays/https_categories_external.replay.yaml (100%) rename tests/{gold_tests => uranium_tests}/ip_allow/replays/https_categories_external_remap.replay.yaml (100%) rename tests/{gold_tests => uranium_tests}/ip_allow/replays/https_categories_internal.replay.yaml (100%) rename tests/{gold_tests => uranium_tests}/ip_allow/replays/https_categories_server.replay.yaml (100%) rename tests/{gold_tests => uranium_tests}/ip_allow/replays/https_multiple_methods.replay.yaml (100%) rename tests/{gold_tests => uranium_tests}/ip_allow/run_sed.sh (100%) create mode 100644 tests/uranium_tests/ip_allow/test_connect_destination_acl.py create mode 100644 tests/uranium_tests/ip_allow/test_ip_allow.py create mode 100644 tests/uranium_tests/ip_allow/test_ip_allow_reload_triggered.py create mode 100644 tests/uranium_tests/ip_allow/test_ip_category.py rename tests/{gold_tests => uranium_tests}/jsonrpc/json/admin_config_set_records_req.json (100%) rename tests/{gold_tests => uranium_tests}/jsonrpc/json/admin_detached_config_reload_req.json (100%) rename tests/{gold_tests => uranium_tests}/jsonrpc/json/admin_host_set_status_req.json (100%) rename tests/{gold_tests => uranium_tests}/jsonrpc/json/admin_lookup_records_req_1.json (100%) rename tests/{gold_tests => uranium_tests}/jsonrpc/json/admin_lookup_records_req_invalid_rec.json (100%) rename tests/{gold_tests => uranium_tests}/jsonrpc/json/admin_lookup_records_req_metric.json (100%) rename tests/{gold_tests => uranium_tests}/jsonrpc/json/admin_lookup_records_req_multiple.json (100%) rename tests/{gold_tests => uranium_tests}/jsonrpc/json/admin_plugin_send_basic_msg_req.json (100%) rename tests/{gold_tests => uranium_tests}/jsonrpc/json/admin_storage_x_device_status_req.json (100%) rename tests/{gold_tests => uranium_tests}/jsonrpc/json/method_call_no_params.json (100%) rename tests/{gold_tests => uranium_tests}/jsonrpc/plugins/CMakeLists.txt (93%) rename tests/{gold_tests => uranium_tests}/jsonrpc/plugins/jsonrpc_plugin_handler_test.cc (100%) create mode 100644 tests/uranium_tests/jsonrpc/test_basic_plugin_handler.py create mode 100644 tests/uranium_tests/jsonrpc/test_config_reload_dedup.py create mode 100644 tests/uranium_tests/jsonrpc/test_config_reload_failures.py create mode 100644 tests/uranium_tests/jsonrpc/test_config_reload_full_smoke.py create mode 100644 tests/uranium_tests/jsonrpc/test_config_reload_reserve_subtask.py create mode 100644 tests/uranium_tests/jsonrpc/test_config_reload_rpc.py create mode 100644 tests/uranium_tests/jsonrpc/test_config_reload_tracking.py create mode 100644 tests/uranium_tests/jsonrpc/test_jsonrpc_api_schema.py rename tests/{gold_tests/lib/ats_autest.py => uranium_tests/lib/ats_urtest.py} (100%) rename tests/{gold_tests/autest-site => uranium_tests/lib}/jsonrpc.py (100%) create mode 100644 tests/uranium_tests/lib/ports.py rename tests/{gold_tests/autest-site => uranium_tests/lib}/verifier_common.py (100%) rename tests/{gold_tests => uranium_tests}/logging/all_headers_sanitizer.py (100%) rename tests/{gold_tests => uranium_tests}/logging/all_headers_sanitizer.sh (100%) rename tests/{gold_tests => uranium_tests}/logging/gold/binary_log_v3_ascii.gold (100%) rename tests/{gold_tests => uranium_tests}/logging/gold/binary_log_v3_json.gold (100%) rename tests/{gold_tests => uranium_tests}/logging/gold/custom.gold (100%) rename tests/{gold_tests => uranium_tests}/logging/gold/custom_uds.gold (100%) rename tests/{gold_tests => uranium_tests}/logging/gold/field-json-test.gold (100%) rename tests/{gold_tests => uranium_tests}/logging/gold/field-test.gold (100%) rename tests/{gold_tests => uranium_tests}/logging/gold/filter-test.gold (100%) rename tests/{gold_tests => uranium_tests}/logging/gold/pqsi-pqsp.gold (100%) rename tests/{gold_tests => uranium_tests}/logging/gold/test_all_headers.gold (100%) rename tests/{gold_tests => uranium_tests}/logging/gold/test_all_headers_uds.gold (100%) rename tests/{gold_tests => uranium_tests}/logging/log-filter.replays.yaml (100%) rename tests/{gold_tests => uranium_tests}/logging/new_log_flds_observer.py (100%) rename tests/{gold_tests => uranium_tests}/logging/pipe_buffer_is_larger_than.py (100%) rename tests/{gold_tests => uranium_tests}/logging/rename_qlog.sh (100%) rename tests/{gold_tests => uranium_tests}/logging/replay/basic1.replay.yaml (100%) rename tests/{gold_tests => uranium_tests}/logging/replay/log-debug-client-ip-http.test.yaml (99%) rename tests/{gold_tests => uranium_tests}/logging/replay/log-debug-client-ip-http2.test.yaml (99%) rename tests/{gold_tests => uranium_tests}/logging/replay/log-debug-client-ip-http3.test.yaml (99%) rename tests/{gold_tests => uranium_tests}/logging/replay/log-debug-client-ip-https.test.yaml (99%) rename tests/{gold_tests => uranium_tests}/logging/sigusr2_rotate_custom_log.sh (100%) rename tests/{gold_tests => uranium_tests}/logging/sigusr2_rotate_diags.sh (100%) create mode 100644 tests/uranium_tests/logging/test_all_headers.py create mode 100644 tests/uranium_tests/logging/test_binary_log_v3.py create mode 100644 tests/uranium_tests/logging/test_custom_log.py create mode 100644 tests/uranium_tests/logging/test_log_field.py create mode 100644 tests/uranium_tests/logging/test_log_field_json.py create mode 100644 tests/uranium_tests/logging/test_log_filenames.py create mode 100644 tests/uranium_tests/logging/test_log_filter.py create mode 100644 tests/uranium_tests/logging/test_log_milestone_fields.py create mode 100644 tests/uranium_tests/logging/test_log_mstsms.py create mode 100644 tests/uranium_tests/logging/test_log_pipe.py create mode 100644 tests/uranium_tests/logging/test_log_plugin_init.py create mode 100644 tests/uranium_tests/logging/test_log_retention.py create mode 100644 tests/uranium_tests/logging/test_new_log_flds.py create mode 100644 tests/uranium_tests/logging/test_pqsi_pqsp.py create mode 100644 tests/uranium_tests/logging/test_qlog_quiche.py create mode 100644 tests/uranium_tests/logging/test_sigusr2.py rename tests/{gold_tests => uranium_tests}/logging/ts_process_handler.py (100%) rename tests/{gold_tests => uranium_tests}/logging/verify_milestone_fields.py (100%) create mode 100644 tests/uranium_tests/next_hop/parent_config/test_parent_config_dest_ip.py rename tests/{gold_tests => uranium_tests}/next_hop/strategies_ch/body.gold (100%) create mode 100644 tests/uranium_tests/next_hop/strategies_ch/test_strategies_ch.py rename tests/{gold_tests => uranium_tests}/next_hop/strategies_ch/trace.gold (100%) rename tests/{gold_tests => uranium_tests}/next_hop/strategies_ch2/body.gold (100%) create mode 100644 tests/uranium_tests/next_hop/strategies_ch2/test_strategies_ch2.py rename tests/{gold_tests => uranium_tests}/next_hop/strategies_ch2/trace.gold (100%) create mode 100644 tests/uranium_tests/next_hop/strategies_ch5/test_strategies_ch5.py rename tests/{gold_tests => uranium_tests}/next_hop/strategies_ch5/trace.gold (100%) rename tests/{gold_tests => uranium_tests}/next_hop/strategies_stale/body.gold (100%) create mode 100644 tests/uranium_tests/next_hop/strategies_stale/test_strategies_stale.py rename tests/{gold_tests => uranium_tests}/next_hop/zzz_strategies_peer/body.gold (100%) create mode 100644 tests/uranium_tests/next_hop/zzz_strategies_peer/test_zzz_strategies_peer.py rename tests/{gold_tests => uranium_tests}/next_hop/zzz_strategies_peer/trace.gold (100%) rename tests/{gold_tests => uranium_tests}/next_hop/zzz_strategies_peer2/body.gold (100%) create mode 100644 tests/uranium_tests/next_hop/zzz_strategies_peer2/test_zzz_strategies_peer2.py rename tests/{gold_tests => uranium_tests}/next_hop/zzz_strategies_peer2/trace.gold (100%) rename tests/{gold_tests => uranium_tests}/null_transform/gold/null_transform-200.gold (100%) rename tests/{gold_tests => uranium_tests}/null_transform/gold/null_transform-tag.gold (100%) create mode 100644 tests/uranium_tests/null_transform/test_null_transform.py rename tests/{gold_tests => uranium_tests}/origin_connection/gold/two_200_ok.gold (100%) rename tests/{gold_tests => uranium_tests}/origin_connection/gold/two_503_congested.gold (100%) rename tests/{gold_tests => uranium_tests}/origin_connection/minimum_keep_alive.test.yaml (99%) rename tests/{gold_tests => uranium_tests}/origin_connection/per_server_metric_enabled.replay.yaml (100%) rename tests/{gold_tests => uranium_tests}/origin_connection/slow_servers.replay.yaml (100%) create mode 100644 tests/uranium_tests/origin_connection/test_per_server_connection_max.py create mode 100644 tests/uranium_tests/origin_connection/test_per_server_metric_enabled.py create mode 100644 tests/uranium_tests/parent_config/test_parent_config_reload.py rename tests/{gold_tests => uranium_tests}/parent_proxy/replays/parent_retry_failure_stays_down.test.yaml (99%) rename tests/{gold_tests => uranium_tests}/parent_proxy/replays/parent_retry_non_retryable_post.test.yaml (99%) rename tests/{gold_tests => uranium_tests}/parent_proxy/replays/parent_retry_success_restores.test.yaml (99%) create mode 100644 tests/uranium_tests/parent_proxy/test_consistent_hash_ring_walk.py create mode 100644 tests/uranium_tests/parent_proxy/test_parent_retry.py rename tests/{gold_tests => uranium_tests}/pipeline/desync_client.py (100%) rename tests/{gold_tests => uranium_tests}/pipeline/desync_client_miss.py (100%) rename tests/{gold_tests => uranium_tests}/pipeline/desync_server.py (100%) rename tests/{gold_tests => uranium_tests}/pipeline/noop_keepalive_client.py (100%) rename tests/{gold_tests => uranium_tests}/pipeline/pipeline_client.py (100%) rename tests/{gold_tests => uranium_tests}/pipeline/pipeline_server.py (100%) rename tests/{gold_tests => uranium_tests}/pipeline/request_framing_client.py (100%) rename tests/{gold_tests => uranium_tests}/pipeline/request_framing_server.py (100%) create mode 100644 tests/uranium_tests/pipeline/test_delete_maxforwards_body_desync.py create mode 100644 tests/uranium_tests/pipeline/test_delete_maxfwd_noop_drain.py create mode 100644 tests/uranium_tests/pipeline/test_noop_keepalive.py create mode 100644 tests/uranium_tests/pipeline/test_pipeline.py rename tests/{gold_tests => uranium_tests}/pluginTest/TSVConnFd/CMakeLists.txt (95%) rename tests/{gold_tests => uranium_tests}/pluginTest/TSVConnFd/TSVConnFd.cc (100%) create mode 100644 tests/uranium_tests/pluginTest/TSVConnFd/test_TSVConnFd.py rename tests/{gold_tests => uranium_tests}/pluginTest/access_control/etc/hmac_keys.txt (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/access_control/replays/access_control.replay.yaml (100%) create mode 100644 tests/uranium_tests/pluginTest/access_control/test_access_control.py create mode 100644 tests/uranium_tests/pluginTest/block_errors/test_block_errors.py create mode 100644 tests/uranium_tests/pluginTest/cache_fill/test_cache_fill.py rename tests/{gold_tests => uranium_tests}/pluginTest/cache_promote/replay/cache_promote.replay.yaml.tmpl (100%) create mode 100644 tests/uranium_tests/pluginTest/cache_promote/test_cache_promote.py rename tests/{gold_tests => uranium_tests}/pluginTest/cache_range_requests/gold/full.stderr.gold (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/cache_range_requests/gold/inner.stderr.gold (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/cache_range_requests/gold/last.stderr.gold (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/cache_range_requests/reason.conf (100%) create mode 100644 tests/uranium_tests/pluginTest/cache_range_requests/test_cache_range_requests.py create mode 100644 tests/uranium_tests/pluginTest/cache_range_requests/test_cache_range_requests_cache_complete_responses.py create mode 100644 tests/uranium_tests/pluginTest/cache_range_requests/test_cache_range_requests_cachekey.py create mode 100644 tests/uranium_tests/pluginTest/cache_range_requests/test_cache_range_requests_cachekey_global.py create mode 100644 tests/uranium_tests/pluginTest/cache_range_requests/test_cache_range_requests_ident.py rename tests/{gold_tests => uranium_tests}/pluginTest/cert_update/gold/client-cert-after.gold (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/cert_update/gold/client-cert-pre.gold (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/cert_update/gold/server-cert-after.gold (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/cert_update/gold/server-cert-pre.gold (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/cert_update/gold/update.gold (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/cert_update/ssl/client1.pem (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/cert_update/ssl/client2.pem (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/cert_update/ssl/server1.pem (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/cert_update/ssl/server2.pem (100%) create mode 100644 tests/uranium_tests/pluginTest/cert_update/test_cert_update.py rename tests/{gold_tests => uranium_tests}/pluginTest/certifier/certs/ca-serial.txt (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/certifier/certs/ca.cert (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/certifier/certs/ca.key (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/certifier/replays/https-no-sni.replay.yaml (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/certifier/replays/https-path-traversal.replay.yaml (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/certifier/replays/https-two-sessions.replay.yaml (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/certifier/replays/https.replay.yaml (100%) create mode 100644 tests/uranium_tests/pluginTest/certifier/test_certifier.py rename tests/{gold_tests => uranium_tests}/pluginTest/client_context_dump/ssl/one.com.pem (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/client_context_dump/ssl/two.com.pem (100%) create mode 100644 tests/uranium_tests/pluginTest/client_context_dump/test_client_context_dump.py rename tests/{gold_tests => uranium_tests}/pluginTest/combo_handler/combo_handler_files/cache_control_aggregation.gold (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/combo_handler/combo_handler_files/max_age_zero.gold (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/combo_handler/combo_handler_files/tr1.gold (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/combo_handler/combo_handler_files/tr2.gold (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/combo_handler/combo_handler_files/tr3.gold (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/combo_handler/ctwl.txt (100%) create mode 100644 tests/uranium_tests/pluginTest/combo_handler/test_combo_handler.py rename tests/{gold_tests => uranium_tests}/pluginTest/compress/compress.config (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/compress/compress.gold (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/compress/compress2.config (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/compress/compress3.config (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/compress/compress_100_continue_origin.py (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/compress/compress_observer.py (94%) rename tests/{gold_tests => uranium_tests}/pluginTest/compress/compress_userver.gold (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/compress/compress_vary.gold (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/compress/etc/cache-true-no-compression.config (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/compress/etc/cache-true-remove-accept-encoding.config (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/compress/etc/cache-true-remove-range.config (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/compress/etc/compress-cache-false.config (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/compress/etc/ignore-params-false.config (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/compress/etc/ignore-params-true.config (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/compress/greplog.sh (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/compress/replay/compress-and-range.replay.yaml (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/compress/replay/compress-content-type-params.replay.yaml (100%) create mode 100644 tests/uranium_tests/pluginTest/compress/test_compress.py create mode 100644 tests/uranium_tests/pluginTest/compress/test_compress_cache_untransformed.py create mode 100644 tests/uranium_tests/pluginTest/compress/test_compress_content_type_params.py create mode 100644 tests/uranium_tests/pluginTest/compress/test_compress_range.py rename tests/{gold_tests => uranium_tests}/pluginTest/conf_remap/etc/negative_caching_list.yaml (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/conf_remap/replay/conf_remap.replay.yaml (100%) create mode 100644 tests/uranium_tests/pluginTest/conf_remap/test_conf_remap.py rename tests/{gold_tests => uranium_tests}/pluginTest/cookie_remap/configs/bucketconfig.txt (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/cookie_remap/configs/collapseconfig.txt (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/cookie_remap/configs/connectorconfig.txt (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/cookie_remap/configs/disable_pristine_host_hdr_config_false.txt (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/cookie_remap/configs/disable_pristine_host_hdr_config_true.txt (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/cookie_remap/configs/existsconfig.txt (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/cookie_remap/configs/matchconfig.txt (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/cookie_remap/configs/matchuriconfig.txt (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/cookie_remap/configs/notexistsconfig.txt (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/cookie_remap/configs/pcollapseconfig.txt (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/cookie_remap/configs/psubstituteconfig.txt (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/cookie_remap/configs/regexconfig.txt (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/cookie_remap/configs/statusconfig.txt (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/cookie_remap/configs/subcookie.txt (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/cookie_remap/configs/substituteconfig.txt (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/cookie_remap/disable_pristine_host_hdr_false.replay.yaml (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/cookie_remap/disable_pristine_host_hdr_server_canary_false.replay.yaml (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/cookie_remap/disable_pristine_host_hdr_server_canary_true.replay.yaml (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/cookie_remap/disable_pristine_host_hdr_server_stable.replay.yaml (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/cookie_remap/disable_pristine_host_hdr_true.replay.yaml (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/cookie_remap/gold/collapseslashes.gold (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/cookie_remap/gold/doesntexistcookie.gold (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/cookie_remap/gold/existscookie.gold (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/cookie_remap/gold/matchcookie.gold (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/cookie_remap/gold/matchcookie2.gold (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/cookie_remap/gold/matchelsestatus.gold (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/cookie_remap/gold/matchstatus.gold (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/cookie_remap/gold/psubstitute.gold (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/cookie_remap/gold/regexdoesntmatch.gold (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/cookie_remap/gold/regexmatches.gold (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/cookie_remap/gold/substitute.gold (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/cookie_remap/gold/wontmatchcookie.gold (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/cookie_remap/gold/wontmatchcookie2.gold (100%) create mode 100644 tests/uranium_tests/pluginTest/cookie_remap/test_bucketcookie.py create mode 100644 tests/uranium_tests/pluginTest/cookie_remap/test_collapseslashes.py create mode 100644 tests/uranium_tests/pluginTest/cookie_remap/test_connector.py create mode 100644 tests/uranium_tests/pluginTest/cookie_remap/test_disable_pristine_host_hdr.py create mode 100644 tests/uranium_tests/pluginTest/cookie_remap/test_existscookie.py create mode 100644 tests/uranium_tests/pluginTest/cookie_remap/test_matchcookie.py create mode 100644 tests/uranium_tests/pluginTest/cookie_remap/test_matchuri.py create mode 100644 tests/uranium_tests/pluginTest/cookie_remap/test_notexistscookie.py create mode 100644 tests/uranium_tests/pluginTest/cookie_remap/test_pcollapseslashes.py create mode 100644 tests/uranium_tests/pluginTest/cookie_remap/test_psubstitute.py create mode 100644 tests/uranium_tests/pluginTest/cookie_remap/test_regexcookie.py create mode 100644 tests/uranium_tests/pluginTest/cookie_remap/test_setstatus.py create mode 100644 tests/uranium_tests/pluginTest/cookie_remap/test_subcookie.py create mode 100644 tests/uranium_tests/pluginTest/cookie_remap/test_substitute.py create mode 100644 tests/uranium_tests/pluginTest/cppapi/test_cppapi.py rename tests/{gold_tests => uranium_tests}/pluginTest/crash_test/CMakeLists.txt (95%) rename tests/{gold_tests => uranium_tests}/pluginTest/crash_test/crash_test.cc (100%) create mode 100644 tests/uranium_tests/pluginTest/crash_test/test_crash_test.py rename tests/{gold_tests => uranium_tests}/pluginTest/escalate/escalate_failover.replay.yaml (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/escalate/escalate_failover_server_default.replay.yaml (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/escalate/escalate_failover_server_non_get.replay.yaml (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/escalate/escalate_non_get_methods.replay.yaml (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/escalate/escalate_original.replay.yaml (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/escalate/escalate_original_server_default.replay.yaml (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/escalate/escalate_original_server_non_get.replay.yaml (100%) create mode 100644 tests/uranium_tests/pluginTest/escalate/test_escalate.py rename tests/{gold_tests => uranium_tests}/pluginTest/esi/esi_nested_html_comment.replay.yaml (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/esi/esi_nested_html_comment_reject.replay.yaml (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/esi/esi_nested_include.replay.yaml (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/esi/esi_ssrf_validate.replay.yaml (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/esi/gold/empty_response_body.gold (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/esi/gold/esi_body.gold (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/esi/gold/esi_gzipped.gold (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/esi/gold/esi_headers.gold (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/esi/gold/esi_private_headers.gold (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/esi/gold/nested_include_body.gold (100%) create mode 100644 tests/uranium_tests/pluginTest/esi/test_esi.py create mode 100644 tests/uranium_tests/pluginTest/esi/test_esi_304.py create mode 100644 tests/uranium_tests/pluginTest/esi/test_esi_nested_html_comment.py create mode 100644 tests/uranium_tests/pluginTest/esi/test_esi_nested_include.py create mode 100644 tests/uranium_tests/pluginTest/esi/test_esi_request_size_cap.py create mode 100644 tests/uranium_tests/pluginTest/esi/test_esi_ssrf_validate.py rename tests/{gold_tests => uranium_tests}/pluginTest/filter_body/config/filter_body_request_block.yaml (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/filter_body/config/filter_body_request_header.yaml (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/filter_body/config/filter_body_request_log.yaml (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/filter_body/config/filter_body_response_block.yaml (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/filter_body/config/filter_body_response_header.yaml (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/filter_body/config/filter_body_response_log.yaml (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/filter_body/filter_body.test.yaml (99%) rename tests/{gold_tests => uranium_tests}/pluginTest/header_rewrite/gold/header_rewrite-set_body_from_200.gold (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/header_rewrite/gold/header_rewrite-set_body_from_conn_fail.gold (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/header_rewrite/gold/header_rewrite-set_body_from_remap_fail.gold (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/header_rewrite/gold/header_rewrite-set_body_from_success.gold (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/header_rewrite/gold/header_rewrite-tag.gold (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/header_rewrite/gold/plugin-status-test.gold (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/header_rewrite/gold/set-redirect-glob.gold (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/header_rewrite/header_rewrite_bundle.test.yaml (99%) rename tests/{gold_tests => uranium_tests}/pluginTest/header_rewrite/header_rewrite_post_remap.test.yaml (99%) rename tests/{gold_tests => uranium_tests}/pluginTest/header_rewrite/post_remap.conf (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/header_rewrite/rules/complex_logics.conf (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/header_rewrite/rules/glob_set_redirect.conf (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/header_rewrite/rules/implicit_hook.conf (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/header_rewrite/rules/nested_ifs.conf (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/header_rewrite/rules/query_sub_key.conf (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/header_rewrite/rules/regex_tests.conf (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/header_rewrite/rules/rule.conf (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/header_rewrite/rules/rule_add_cache_result_header.conf (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/header_rewrite/rules/rule_cidr.conf (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/header_rewrite/rules/rule_client.conf (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/header_rewrite/rules/rule_cond_method.conf (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/header_rewrite/rules/rule_cookie.conf (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/header_rewrite/rules/rule_effective_address.conf (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/header_rewrite/rules/rule_empty_body.conf (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/header_rewrite/rules/rule_l_value.conf (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/header_rewrite/rules/rule_server_conditions.conf (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/header_rewrite/rules/rule_session_vars.conf (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/header_rewrite/rules/rule_set_body_from_plugin.conf (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/header_rewrite/rules/rule_set_body_from_remap.conf (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/header_rewrite/rules/rule_set_body_status.conf (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/header_rewrite/rules/rule_set_header_after_ssn_txn_count.conf (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/header_rewrite/rules/run_plugin.conf (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/header_rewrite/rules/set_redirect.conf (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/header_rewrite/rules/set_status_in_if.conf (100%) create mode 100644 tests/uranium_tests/pluginTest/header_rewrite/test_header_rewrite_bad_run_plugin.py create mode 100644 tests/uranium_tests/pluginTest/header_rewrite/test_header_rewrite_set_body_from.py create mode 100644 tests/uranium_tests/pluginTest/header_rewrite/test_header_rewrite_url_glob.py rename tests/{gold_tests => uranium_tests}/pluginTest/healthchecks/acme (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/healthchecks/acme-ssl (100%) create mode 100644 tests/uranium_tests/pluginTest/healthchecks/test_healthchecks.py rename tests/{gold_tests => uranium_tests}/pluginTest/ja3_fingerprint/ja3_fingerprint_global.replay.yaml (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/ja3_fingerprint/ja3_fingerprint_remap.replay.yaml (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/ja3_fingerprint/modify-incoming-client.gold (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/ja3_fingerprint/modify-incoming-proxy.gold (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/ja3_fingerprint/modify-sent-client.gold (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/ja3_fingerprint/modify-sent-proxy-global.gold (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/ja3_fingerprint/modify-sent-proxy-remap.gold (100%) create mode 100644 tests/uranium_tests/pluginTest/ja3_fingerprint/test_ja3_fingerprint.py rename tests/{gold_tests => uranium_tests}/pluginTest/ja4_fingerprint/ja4_fingerprint.replay.yaml (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/ja4_fingerprint/ja4_fingerprint_basic_server.replay.yaml (100%) create mode 100644 tests/uranium_tests/pluginTest/ja4_fingerprint/test_ja4_fingerprint.py rename tests/{gold_tests => uranium_tests}/pluginTest/jax_fingerprint/jax_fingerprint_all_methods.replay.yaml (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/jax_fingerprint/jax_fingerprint_append.replay.yaml (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/jax_fingerprint/jax_fingerprint_global.replay.yaml (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/jax_fingerprint/jax_fingerprint_global_h2.replay.yaml (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/jax_fingerprint/jax_fingerprint_hybrid_servernames.replay.yaml (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/jax_fingerprint/jax_fingerprint_keep.replay.yaml (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/jax_fingerprint/jax_fingerprint_remap.replay.yaml (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/jax_fingerprint/jax_fingerprint_remap_tls.replay.yaml (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/jax_fingerprint/jax_fingerprint_servernames.replay.yaml (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/jax_fingerprint/jax_fingerprint_tls.replay.yaml (100%) create mode 100644 tests/uranium_tests/pluginTest/jax_fingerprint/test_jax_fingerprint.py rename tests/{gold_tests => uranium_tests}/pluginTest/lua/client_hook.lua (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/lua/fetch_ipv6_cliaddr.lua (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/lua/global.lua (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/lua/global_shutdown.lua (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/lua/gold/lifecycle.gold (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/lua/gold/lua_proxy_protocol.gold (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/lua/gold/metrics.gold (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/lua/header_table.lua (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/lua/hello.lua (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/lua/lifecycle_stats.sh (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/lua/metrics.sh (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/lua/proxy_protocol.lua (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/lua/remap_after_hook.lua (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/lua/set_error_response.lua (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/lua/set_error_response.replay.yaml (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/lua/tags.lua (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/lua/tags.sh (100%) create mode 100644 tests/uranium_tests/pluginTest/lua/test_lua_client_hook.py create mode 100644 tests/uranium_tests/pluginTest/lua/test_lua_debug_tags.py create mode 100644 tests/uranium_tests/pluginTest/lua/test_lua_fetch_ipv6_cliaddr.py create mode 100644 tests/uranium_tests/pluginTest/lua/test_lua_global_shutdown.py create mode 100644 tests/uranium_tests/pluginTest/lua/test_lua_header_table.py create mode 100644 tests/uranium_tests/pluginTest/lua/test_lua_proxy_protocol.py create mode 100644 tests/uranium_tests/pluginTest/lua/test_lua_remap_after_hook.py create mode 100644 tests/uranium_tests/pluginTest/lua/test_lua_set_error_response.py create mode 100644 tests/uranium_tests/pluginTest/lua/test_lua_states_stats.py create mode 100644 tests/uranium_tests/pluginTest/lua/test_lua_verified_addr.py create mode 100644 tests/uranium_tests/pluginTest/lua/test_lua_watermark.py rename tests/{gold_tests => uranium_tests}/pluginTest/lua/verified_addr.lua (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/lua/watermark.lua (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/money_trace/gold/global-log.gold (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/money_trace/gold/remap-log.gold (100%) create mode 100644 tests/uranium_tests/pluginTest/money_trace/test_money_trace.py create mode 100644 tests/uranium_tests/pluginTest/money_trace/test_money_trace_global.py rename tests/{gold_tests => uranium_tests}/pluginTest/multiplexer/replays/multiplexer_copy.replay.yaml (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/multiplexer/replays/multiplexer_copy_skip_post.replay.yaml (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/multiplexer/replays/multiplexer_invalid_chunk_copy.replay.yaml (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/multiplexer/replays/multiplexer_invalid_chunk_original.replay.yaml (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/multiplexer/replays/multiplexer_original.replay.yaml (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/multiplexer/replays/multiplexer_original_skip_post.replay.yaml (100%) create mode 100644 tests/uranium_tests/pluginTest/multiplexer/test_multiplexer.py rename tests/{gold_tests => uranium_tests}/pluginTest/origin_server_auth/gold/origin_server_auth_parsing_ts.gold (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/origin_server_auth/gold/origin_server_auth_parsing_ts_uds 2.gold (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/origin_server_auth/gold/origin_server_auth_parsing_ts_uds.gold (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/origin_server_auth/rules/s3_url_encoding.test_input (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/origin_server_auth/rules/v4-parse-test.test_input (100%) create mode 100644 tests/uranium_tests/pluginTest/origin_server_auth/test_origin_server_auth.py create mode 100644 tests/uranium_tests/pluginTest/origin_server_auth/test_s3_url_encoding.py create mode 100644 tests/uranium_tests/pluginTest/packet_mark/test_packet_mark.py rename tests/{gold_tests => uranium_tests}/pluginTest/per_plugin_metrics/rules/global.conf (100%) create mode 100644 tests/uranium_tests/pluginTest/per_plugin_metrics/test_per_plugin_metrics.py create mode 100644 tests/uranium_tests/pluginTest/plugin_yaml/test_plugin_yaml.py rename tests/{gold_tests => uranium_tests}/pluginTest/polite_hook_wait/CMakeLists.txt (93%) rename tests/{gold_tests => uranium_tests}/pluginTest/polite_hook_wait/curl.gold (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/polite_hook_wait/polite_hook_wait.cc (100%) create mode 100644 tests/uranium_tests/pluginTest/polite_hook_wait/test_polite_hook_wait.py rename tests/{gold_tests => uranium_tests}/pluginTest/prefetch/header_rewrite.conf (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/prefetch/prefetch_bignum.gold (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/prefetch/prefetch_cmcd0.gold (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/prefetch/prefetch_cmcd1.gold (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/prefetch/prefetch_optional_group.gold (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/prefetch/prefetch_overflow.gold (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/prefetch/prefetch_query_path_traversal.test.yaml (99%) rename tests/{gold_tests => uranium_tests}/pluginTest/prefetch/prefetch_simple.gold (100%) create mode 100644 tests/uranium_tests/pluginTest/prefetch/test_prefetch_bad_count_refused.py create mode 100644 tests/uranium_tests/pluginTest/prefetch/test_prefetch_bad_pattern_refused.py create mode 100644 tests/uranium_tests/pluginTest/prefetch/test_prefetch_bignum.py create mode 100644 tests/uranium_tests/pluginTest/prefetch/test_prefetch_cmcd.py create mode 100644 tests/uranium_tests/pluginTest/prefetch/test_prefetch_empty_replacement.py create mode 100644 tests/uranium_tests/pluginTest/prefetch/test_prefetch_optional_group.py create mode 100644 tests/uranium_tests/pluginTest/prefetch/test_prefetch_overflow.py create mode 100644 tests/uranium_tests/pluginTest/prefetch/test_prefetch_simple.py rename tests/{gold_tests => uranium_tests}/pluginTest/rate_limit/concurrent_reject.sh (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/rate_limit/independent_limiters.sh (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/rate_limit/queue_bypass_regression.sh (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/rate_limit/queue_drain.sh (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/rate_limit/rate_limit_sni_expiry_client.sh (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/rate_limit/rate_limit_sni_queue_client.sh (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/rate_limit/rate_limit_sni_reject_client.sh (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/rate_limit/retry_after.sh (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/rate_limit/sequential_pass.sh (100%) create mode 100644 tests/uranium_tests/pluginTest/rate_limit/test_rate_limit.py create mode 100644 tests/uranium_tests/pluginTest/rate_limit/test_rate_limit_iprep.py create mode 100644 tests/uranium_tests/pluginTest/rate_limit/test_rate_limit_sni.py create mode 100644 tests/uranium_tests/pluginTest/rate_limit/test_rate_limit_sni_expiry.py create mode 100644 tests/uranium_tests/pluginTest/rate_limit/test_rate_limit_sni_queue.py create mode 100644 tests/uranium_tests/pluginTest/rate_limit/test_rate_limit_sni_reject.py create mode 100644 tests/uranium_tests/pluginTest/redirect_limit/test_redirect_limit.py rename tests/{gold_tests => uranium_tests}/pluginTest/regex_remap/gold/regex_remap_crash.gold (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/regex_remap/gold/regex_remap_redirect.gold (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/regex_remap/gold/regex_remap_simple.gold (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/regex_remap/gold/regex_remap_smoke.gold (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/regex_remap/replay/yts-2819.replay.json (100%) create mode 100644 tests/uranium_tests/pluginTest/regex_remap/test_regex_remap.py rename tests/{gold_tests => uranium_tests}/pluginTest/regex_revalidate/gold/metrics.gold (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/regex_revalidate/gold/metrics_miss.gold (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/regex_revalidate/metrics.sh (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/regex_revalidate/metrics_miss.sh (100%) create mode 100644 tests/uranium_tests/pluginTest/regex_revalidate/test_regex_revalidate.py create mode 100644 tests/uranium_tests/pluginTest/regex_revalidate/test_regex_revalidate_miss.py create mode 100644 tests/uranium_tests/pluginTest/regex_revalidate/test_regex_revalidate_state.py rename tests/{gold_tests => uranium_tests}/pluginTest/remap_stats/gold/metrics.gold (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/remap_stats/gold/metrics_post.gold (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/remap_stats/metrics.sh (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/remap_stats/metrics_post.sh (100%) create mode 100644 tests/uranium_tests/pluginTest/remap_stats/test_remap_stats.py create mode 100644 tests/uranium_tests/pluginTest/remap_stats/test_remap_stats_post.py rename tests/{gold_tests => uranium_tests}/pluginTest/server_push_preload/gold/server_push_preload_0_stdout.gold (100%) create mode 100644 tests/uranium_tests/pluginTest/server_push_preload/test_server_push_preload.py rename tests/{gold_tests => uranium_tests}/pluginTest/slice/gold/aa.gold (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/slice/gold/aaa.gold (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/slice/gold/bb.gold (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/slice/gold/bbb.gold (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/slice/gold/slice_200.stderr.gold (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/slice/gold/slice_206.stderr.gold (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/slice/gold/slice_crr_ident.gold (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/slice/gold/slice_first.stderr.gold (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/slice/gold/slice_ident.gold (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/slice/gold/slice_last.stderr.gold (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/slice/gold/slice_mid.stderr.gold (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/slice/gold_error/contents.stderr.gold (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/slice/replay/slice_long_etag.test.yaml (99%) rename tests/{gold_tests => uranium_tests}/pluginTest/slice/replay/slice_purge.replay.yaml (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/slice/replay/slice_purge_gaps_client.replay.yaml (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/slice/replay/slice_purge_gaps_server.replay.yaml (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/slice/replay/slice_purge_no_ref.replay.yaml (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/slice/replay/slice_purge_ref.replay.yaml (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/slice/replay/slice_range.replay.yaml (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/slice/replay/slice_stale_generation_client.replay.yaml (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/slice/replay/slice_stale_generation_server.replay.yaml (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/slice/rules/purge_block_failure.conf (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/slice/shrink_origin.py (100%) create mode 100644 tests/uranium_tests/pluginTest/slice/test_slice.py create mode 100644 tests/uranium_tests/pluginTest/slice/test_slice_conditional.py create mode 100644 tests/uranium_tests/pluginTest/slice/test_slice_content_shrink.py create mode 100644 tests/uranium_tests/pluginTest/slice/test_slice_crr_ident.py create mode 100644 tests/uranium_tests/pluginTest/slice/test_slice_error.py create mode 100644 tests/uranium_tests/pluginTest/slice/test_slice_ident.py create mode 100644 tests/uranium_tests/pluginTest/slice/test_slice_prefetch.py create mode 100644 tests/uranium_tests/pluginTest/slice/test_slice_purge.py create mode 100644 tests/uranium_tests/pluginTest/slice/test_slice_purge_gaps.py create mode 100644 tests/uranium_tests/pluginTest/slice/test_slice_regex.py create mode 100644 tests/uranium_tests/pluginTest/slice/test_slice_rm_range.py create mode 100644 tests/uranium_tests/pluginTest/slice/test_slice_selfhealing.py create mode 100644 tests/uranium_tests/pluginTest/slice/test_slice_stale_generation.py rename tests/{gold_tests => uranium_tests}/pluginTest/sslheaders/observer.py (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/sslheaders/sslheaders.gold (100%) create mode 100644 tests/uranium_tests/pluginTest/sslheaders/test_sslheaders.py rename tests/{gold_tests => uranium_tests}/pluginTest/stale_response/stale_response_max_memory.replay.yaml (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/stale_response/stale_response_no_default.replay.yaml (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/stale_response/stale_response_with_defaults.replay.yaml (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/stale_response/stale_response_with_force_sie.replay.yaml (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/stale_response/stale_response_with_force_swr.replay.yaml (100%) create mode 100644 tests/uranium_tests/pluginTest/stale_response/test_stale_response.py rename tests/{gold_tests => uranium_tests}/pluginTest/statichit/empty.txt (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/statichit/small_body.txt (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/statichit/statichit.replay.yaml (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/statichit/story_16.json (100%) create mode 100644 tests/uranium_tests/pluginTest/statichit/test_statichit.py rename tests/{gold_tests => uranium_tests}/pluginTest/stats_over_http/gold/stats_over_http_csv_stderr.gold (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/stats_over_http/gold/stats_over_http_json_stderr.gold (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/stats_over_http/gold/stats_over_http_prometheus_stderr.gold (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/stats_over_http/gold/stats_over_http_prometheus_v2_accept_stderr.gold (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/stats_over_http/gold/stats_over_http_prometheus_v2_stderr.gold (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/stats_over_http/prometheus_stats_ingester.py (100%) create mode 100644 tests/uranium_tests/pluginTest/stats_over_http/test_stats_over_http.py rename tests/{gold_tests => uranium_tests}/pluginTest/stek_share/server_list.yaml (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/stek_share/ssl/self_signed.crt (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/stek_share/ssl/self_signed.key (100%) create mode 100644 tests/uranium_tests/pluginTest/stek_share/test_stek_share.py create mode 100644 tests/uranium_tests/pluginTest/strategies/test_strategies_plugins.py rename tests/{gold_tests => uranium_tests}/pluginTest/test_hooks/200.gold (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/test_hooks/clean.sh (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/test_hooks/hook_add.gold (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/test_hooks/log.gold (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/test_hooks/log_uds.gold (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/test_hooks/replay/body_buffer.test.yaml (99%) rename tests/{gold_tests => uranium_tests}/pluginTest/test_hooks/replay/zero_post_copy_size.test.yaml (99%) rename tests/{gold_tests => uranium_tests}/pluginTest/test_hooks/ssn_delay.gold (100%) create mode 100644 tests/uranium_tests/pluginTest/test_hooks/test_hook_add.py create mode 100644 tests/uranium_tests/pluginTest/test_hooks/test_ssn_start_delay_hook.py create mode 100644 tests/uranium_tests/pluginTest/test_hooks/test_test_hooks.py rename tests/{gold_tests => uranium_tests}/pluginTest/traffic_dump/gold/200_bob_no_sni.gold (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/traffic_dump/gold/200_get.gold (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/traffic_dump/gold/200_get_sensitive_field.gold (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/traffic_dump/gold/200_http10.gold (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/traffic_dump/gold/200_post.gold (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/traffic_dump/gold/200_sni_bob.gold (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/traffic_dump/gold/200_sni_dave.gold (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/traffic_dump/gold/4_byte_response_body.gold (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/traffic_dump/gold/explicit_target.gold (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/traffic_dump/gold/post_with_body.gold (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/traffic_dump/gold/two_transactions.gold (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/traffic_dump/replay/http3.yaml (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/traffic_dump/replay/response_body.yaml (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/traffic_dump/replay/traffic_dump.yaml (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/traffic_dump/replay/traffic_dump_ip_filter_server.yaml (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/traffic_dump/replay/traffic_dump_server.yaml (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/traffic_dump/replay/various_sni.yaml (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/traffic_dump/ssl/server.key (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/traffic_dump/ssl/server.pem (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/traffic_dump/ssl/server_combined.pem (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/traffic_dump/ssl/signed-foo.key (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/traffic_dump/ssl/signed-foo.pem (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/traffic_dump/ssl/signer.pem (100%) create mode 100644 tests/uranium_tests/pluginTest/traffic_dump/test_traffic_dump.py create mode 100644 tests/uranium_tests/pluginTest/traffic_dump/test_traffic_dump_http3.py create mode 100644 tests/uranium_tests/pluginTest/traffic_dump/test_traffic_dump_ip_filter.py create mode 100644 tests/uranium_tests/pluginTest/traffic_dump/test_traffic_dump_response_body.py create mode 100644 tests/uranium_tests/pluginTest/traffic_dump/test_traffic_dump_sni_filter.py rename tests/{gold_tests => uranium_tests}/pluginTest/traffic_dump/verify_replay.py (100%) create mode 100644 tests/uranium_tests/pluginTest/transform/test_transaction_data_sink.py rename tests/{gold_tests => uranium_tests}/pluginTest/transform/transaction-with-body.replays.yaml (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/tsapi/CMakeLists.txt (71%) rename tests/{gold_tests => uranium_tests}/pluginTest/tsapi/hrw_verified_addr.conf (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/tsapi/log.gold (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/tsapi/log_uds.gold (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/tsapi/test_TSHttpSsnInfo.cc (97%) rename tests/{gold_tests => uranium_tests}/pluginTest/tsapi/test_TSHttpSsnInfo_curl0.gold (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/tsapi/test_TSHttpSsnInfo_nghttp0.gold (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/tsapi/test_TSHttpSsnInfo_plugin_log.gold (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/tsapi/test_TSHttpTxnServerAddrSet.cc (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/tsapi/test_TSHttpTxnServerAddrSet.replay.yaml (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/tsapi/test_TSHttpTxnServerAddrSet_retry.cc (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/tsapi/test_TSHttpTxnVerifiedAddr.cc (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/tsapi/test_TSVConnPPInfo.cc (96%) rename tests/{gold_tests => uranium_tests}/pluginTest/tsapi/test_TSVConnPPInfo_curl0.gold (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/tsapi/test_TSVConnPPInfo_curl1.gold (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/tsapi/test_TSVConnPPInfo_plugin_log.gold (100%) create mode 100644 tests/uranium_tests/pluginTest/tsapi/test_test_TSHttpSsnInfo.py create mode 100644 tests/uranium_tests/pluginTest/tsapi/test_test_TSHttpTxnServerAddrSet.py create mode 100644 tests/uranium_tests/pluginTest/tsapi/test_test_TSHttpTxnServerAddrSet_retry.py create mode 100644 tests/uranium_tests/pluginTest/tsapi/test_test_TSHttpTxnVerifiedAddr.py create mode 100644 tests/uranium_tests/pluginTest/tsapi/test_test_TSVConnPPInfo.py rename tests/{gold_tests => uranium_tests}/pluginTest/tsapi/test_tsapi.cc (98%) create mode 100644 tests/uranium_tests/pluginTest/tsapi/test_tsapi.py rename tests/{gold_tests => uranium_tests}/pluginTest/txn_box/basic/basic.replay.yaml (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/txn_box/basic/cmp.replay.yaml (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/txn_box/basic/ip-addr.replay.yaml (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/txn_box/basic/mod.replay.yaml (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/txn_box/basic/multi-cfg.1.yaml (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/txn_box/basic/multi-cfg.2.yaml (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/txn_box/basic/multi-cfg.replay.yaml (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/txn_box/basic/redirect.replay.yaml (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/txn_box/basic/reply.replay.yaml (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/txn_box/basic/rxp.replay.yaml (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/txn_box/basic/stat.replay.yaml (100%) create mode 100644 tests/uranium_tests/pluginTest/txn_box/basic/test_txn_box_basic.py create mode 100644 tests/uranium_tests/pluginTest/txn_box/basic/test_txn_box_cmp.py create mode 100644 tests/uranium_tests/pluginTest/txn_box/basic/test_txn_box_ip_addr.py create mode 100644 tests/uranium_tests/pluginTest/txn_box/basic/test_txn_box_mod.py create mode 100644 tests/uranium_tests/pluginTest/txn_box/basic/test_txn_box_multi_cfg.py create mode 100644 tests/uranium_tests/pluginTest/txn_box/basic/test_txn_box_redirect.py create mode 100644 tests/uranium_tests/pluginTest/txn_box/basic/test_txn_box_reply.py create mode 100644 tests/uranium_tests/pluginTest/txn_box/basic/test_txn_box_rxp.py create mode 100644 tests/uranium_tests/pluginTest/txn_box/basic/test_txn_box_stat.py create mode 100644 tests/uranium_tests/pluginTest/txn_box/basic/test_txn_box_tls.py create mode 100644 tests/uranium_tests/pluginTest/txn_box/basic/test_txn_box_tls_cert.py create mode 100644 tests/uranium_tests/pluginTest/txn_box/basic/test_txn_box_tuple.py create mode 100644 tests/uranium_tests/pluginTest/txn_box/basic/test_txn_box_txn_debug.py create mode 100644 tests/uranium_tests/pluginTest/txn_box/basic/test_txn_box_txn_error.py create mode 100644 tests/uranium_tests/pluginTest/txn_box/basic/test_txn_box_txn_open_1.py create mode 100644 tests/uranium_tests/pluginTest/txn_box/basic/test_txn_box_txn_open_2.py create mode 100644 tests/uranium_tests/pluginTest/txn_box/basic/test_txn_box_txn_open_3.py create mode 100644 tests/uranium_tests/pluginTest/txn_box/basic/test_txn_box_with.py rename tests/{gold_tests => uranium_tests}/pluginTest/txn_box/basic/tls-cert.replay.yaml (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/txn_box/basic/tls.replay.yaml (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/txn_box/basic/tuple.replay.yaml (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/txn_box/basic/txn-debug.replay.yaml (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/txn_box/basic/txn-error.replay.yaml (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/txn_box/basic/txn_open_1.replay.yaml (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/txn_box/basic/txn_open_2.replay.yaml (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/txn_box/basic/txn_open_3.replay.yaml (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/txn_box/basic/with.replay.yaml (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/txn_box/ct_header/ct_header.replay.yaml (100%) create mode 100644 tests/uranium_tests/pluginTest/txn_box/ct_header/test_txn_box_ct_header.py rename tests/{gold_tests => uranium_tests}/pluginTest/txn_box/example/accept-encoding.replay.yaml (100%) create mode 100644 tests/uranium_tests/pluginTest/txn_box/example/test_txn_box_accept_encoding.py rename tests/{gold_tests => uranium_tests}/pluginTest/txn_box/prod/cors-origin.replay.yaml (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/txn_box/prod/cors-referrer.replay.yaml (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/txn_box/prod/ip-acl.csv (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/txn_box/prod/ip-acl.replay.yaml (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/txn_box/prod/mTLS-alpha.replay.yaml (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/txn_box/prod/mTLS-bravo.replay.yaml (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/txn_box/prod/mTLS.replay.yaml (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/txn_box/prod/mTLS.txnbox.yaml (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/txn_box/prod/query-delete.replay.yaml (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/txn_box/prod/query.replay.yaml (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/txn_box/prod/stanley.replay.yaml (100%) create mode 100644 tests/uranium_tests/pluginTest/txn_box/prod/test_txn_box_cors_origin.py create mode 100644 tests/uranium_tests/pluginTest/txn_box/prod/test_txn_box_cors_referrer.py create mode 100644 tests/uranium_tests/pluginTest/txn_box/prod/test_txn_box_ip_acl.py create mode 100644 tests/uranium_tests/pluginTest/txn_box/prod/test_txn_box_mTLS.py create mode 100644 tests/uranium_tests/pluginTest/txn_box/prod/test_txn_box_query.py create mode 100644 tests/uranium_tests/pluginTest/txn_box/prod/test_txn_box_query_delete_filter.py create mode 100644 tests/uranium_tests/pluginTest/txn_box/prod/test_txn_box_query_delete_regex.py create mode 100644 tests/uranium_tests/pluginTest/txn_box/prod/test_txn_box_stanley.py create mode 100644 tests/uranium_tests/pluginTest/txn_box/prod/test_txn_box_vznith_1.py create mode 100644 tests/uranium_tests/pluginTest/txn_box/prod/test_txn_box_yts_3489.py rename tests/{gold_tests => uranium_tests}/pluginTest/txn_box/prod/vznith-1.replay.yaml (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/txn_box/prod/yts-3489.replay.yaml (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/txn_box/ramp/multi-ramp-1.cfg.yaml (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/txn_box/ramp/multi-ramp-2.cfg.yaml (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/txn_box/ramp/multi-ramp-3.cfg.yaml (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/txn_box/ramp/multi-ramp-4.cfg.yaml (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/txn_box/ramp/multi-ramp.replay.yaml (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/txn_box/ramp/multi_ramp_common.py (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/txn_box/ramp/ramp.logging.yaml (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/txn_box/ramp/ramp.replay.yaml (100%) create mode 100644 tests/uranium_tests/pluginTest/txn_box/ramp/test_txn_box_multi_ramp_1.py create mode 100644 tests/uranium_tests/pluginTest/txn_box/ramp/test_txn_box_multi_ramp_2.py create mode 100644 tests/uranium_tests/pluginTest/txn_box/ramp/test_txn_box_multi_ramp_3.py create mode 100644 tests/uranium_tests/pluginTest/txn_box/ramp/test_txn_box_ramp.py rename tests/{gold_tests => uranium_tests}/pluginTest/txn_box/remap/remap-base.replay.yaml (100%) create mode 100644 tests/uranium_tests/pluginTest/txn_box/remap/test_txn_box_remap_base.py rename tests/{gold_tests => uranium_tests}/pluginTest/txn_box/smoke/smoke-2.replay.yaml (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/txn_box/smoke/smoke.replay.yaml (100%) create mode 100644 tests/uranium_tests/pluginTest/txn_box/smoke/test_txn_box_smoke.py create mode 100644 tests/uranium_tests/pluginTest/txn_box/smoke/test_txn_box_smoke_2.py rename tests/{gold_tests => uranium_tests}/pluginTest/txn_box/ssl/alpha-ca.key (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/txn_box/ssl/alpha-ca.pem (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/txn_box/ssl/alpha-signed.cert (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/txn_box/ssl/alpha-signed.key (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/txn_box/ssl/alpha-signed.pem (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/txn_box/ssl/bravo-ca.key (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/txn_box/ssl/bravo-ca.pem (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/txn_box/ssl/bravo-signed.cert (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/txn_box/ssl/bravo-signed.key (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/txn_box/ssl/bravo-signed.pem (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/txn_box/ssl/ca-bundle.pem (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/txn_box/ssl/charlie-ca.key (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/txn_box/ssl/charlie-ca.pem (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/txn_box/ssl/charlie-signed.cert (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/txn_box/ssl/charlie-signed.key (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/txn_box/ssl/charlie-signed.pem (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/txn_box/ssl/pv-server.cert (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/txn_box/ssl/server.key (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/txn_box/ssl/server.pem (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/txn_box/static_file/static_file.replay.yaml (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/txn_box/static_file/static_file.txt (100%) create mode 100644 tests/uranium_tests/pluginTest/txn_box/static_file/test_txn_box_static_file.py rename tests/{gold_tests => uranium_tests}/pluginTest/txn_box/static_file/unreadable.txt (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/uri_signing/config.json (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/uri_signing/gold/200.gold (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/uri_signing/gold/403.gold (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/uri_signing/run_sign.sh (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/uri_signing/signer.json (100%) create mode 100644 tests/uranium_tests/pluginTest/uri_signing/test_uri_signing.py rename tests/{gold_tests => uranium_tests}/pluginTest/url_sig/run_sign.sh (100%) create mode 100644 tests/uranium_tests/pluginTest/url_sig/test_url_sig.py rename tests/{gold_tests => uranium_tests}/pluginTest/url_sig/url_sig.all.config (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/url_sig/url_sig.config (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/url_sig/url_sig.gold (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/webp_transform/replay/webp_chunked_cap.replay.yaml (100%) create mode 100644 tests/uranium_tests/pluginTest/webp_transform/test_webp_transform_cache_refused.py create mode 100644 tests/uranium_tests/pluginTest/webp_transform/test_webp_transform_chunked_cap.py create mode 100644 tests/uranium_tests/pluginTest/webp_transform/test_webp_transform_decode_limit.py create mode 100644 tests/uranium_tests/pluginTest/webp_transform/test_webp_transform_max_buffer_size.py create mode 100644 tests/uranium_tests/pluginTest/webp_transform/test_webp_transform_size_cap.py rename tests/{gold_tests => uranium_tests}/pluginTest/webp_transform/webp_transform_invalid_input.test.yaml (99%) rename tests/{gold_tests => uranium_tests}/pluginTest/xdebug/x_cache_info/none.in (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/xdebug/x_cache_info/one.in (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/xdebug/x_cache_info/out.gold (100%) create mode 100644 tests/uranium_tests/pluginTest/xdebug/x_cache_info/test_x_cache_info.py rename tests/{gold_tests => uranium_tests}/pluginTest/xdebug/x_cache_info/three.in (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/xdebug/x_cache_info/two.in (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/xdebug/x_effective_url/four.in (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/xdebug/x_effective_url/none.in (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/xdebug/x_effective_url/one.in (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/xdebug/x_effective_url/out.gold (100%) create mode 100644 tests/uranium_tests/pluginTest/xdebug/x_effective_url/test_x_effective_url.py rename tests/{gold_tests => uranium_tests}/pluginTest/xdebug/x_effective_url/three.in (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/xdebug/x_effective_url/two.in (100%) create mode 100644 tests/uranium_tests/pluginTest/xdebug/x_probe/test_x_probe.py rename tests/{gold_tests => uranium_tests}/pluginTest/xdebug/x_probe/x_probe.replay.yaml (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/xdebug/x_probe_full_json/gold/jq_escaped.gold (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/xdebug/x_probe_full_json/gold/jq_hex.gold (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/xdebug/x_probe_full_json/gold/jq_nobody.gold (100%) create mode 100644 tests/uranium_tests/pluginTest/xdebug/x_probe_full_json/test_x_probe_full_json.py rename tests/{gold_tests => uranium_tests}/pluginTest/xdebug/x_probe_full_json/x_probe_full_json.replay.yaml (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/xdebug/x_probe_full_json_slow_origin/slow-body-server.sh (100%) create mode 100644 tests/uranium_tests/pluginTest/xdebug/x_probe_full_json_slow_origin/test_x_probe_full_json_slow_origin.py rename tests/{gold_tests => uranium_tests}/pluginTest/xdebug/x_probe_full_json_slow_origin/verify_no_loop.sh (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/xdebug/x_remap/four.in (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/xdebug/x_remap/fwd1.in (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/xdebug/x_remap/fwd2.in (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/xdebug/x_remap/fwd3.in (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/xdebug/x_remap/fwd4.in (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/xdebug/x_remap/fwd5.in (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/xdebug/x_remap/none.in (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/xdebug/x_remap/one.in (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/xdebug/x_remap/out.gold (100%) create mode 100644 tests/uranium_tests/pluginTest/xdebug/x_remap/test_x_remap.py rename tests/{gold_tests => uranium_tests}/pluginTest/xdebug/x_remap/three.in (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/xdebug/x_remap/two.in (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/xdebug/x_remap/x_remap-observer.py (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/xdebug/x_remap/x_remap.gold (100%) rename tests/{gold_tests => uranium_tests}/pluginTest/xdebug/x_remap_long_url/x_remap_long_url.test.yaml (99%) rename tests/{gold_tests => uranium_tests}/post/delay_client.sh (100%) rename tests/{gold_tests => uranium_tests}/post/delay_client2.sh (100%) rename tests/{gold_tests => uranium_tests}/post/delay_client3.sh (100%) rename tests/{gold_tests => uranium_tests}/post/expect_client.py (100%) rename tests/{gold_tests => uranium_tests}/post/gold/post-h1.gold (100%) rename tests/{gold_tests => uranium_tests}/post/gold/post-h2.gold (100%) rename tests/{gold_tests => uranium_tests}/post/http_utils.py (100%) rename tests/{gold_tests => uranium_tests}/post/replay/expect-continue.replay.yaml (100%) rename tests/{gold_tests => uranium_tests}/post/replay/post-continue.replay.yaml (100%) rename tests/{gold_tests => uranium_tests}/post/replay/post-no-cl.test.yaml (99%) create mode 100644 tests/uranium_tests/post/test_expect_tests.py create mode 100644 tests/uranium_tests/post/test_post_continue.py create mode 100644 tests/uranium_tests/post/test_post_early_return.py create mode 100644 tests/uranium_tests/post/test_simple_post_valid_buffer_check.py rename tests/{gold_tests => uranium_tests}/post_slow_server/check.sh (100%) rename tests/{gold_tests => uranium_tests}/post_slow_server/gold/post_slow_server_max_requests_in_0_stderr.gold (100%) rename tests/{gold_tests => uranium_tests}/post_slow_server/gold/post_slow_server_max_requests_in_0_stdout.gold (100%) rename tests/{gold_tests => uranium_tests}/post_slow_server/server.sh (100%) create mode 100644 tests/uranium_tests/post_slow_server/test_post_slow_server.py create mode 100644 tests/uranium_tests/post_slow_server/test_post_slow_server_max_requests_in.py rename tests/{gold_tests => uranium_tests}/proxy_protocol/gold/access-cp.gold (100%) rename tests/{gold_tests => uranium_tests}/proxy_protocol/gold/access-nocp.gold (100%) rename tests/{gold_tests => uranium_tests}/proxy_protocol/replay/proxy_protocol_allowlist.replay.yaml (100%) rename tests/{gold_tests => uranium_tests}/proxy_protocol/replay/proxy_protocol_in.replay.yaml (100%) rename tests/{gold_tests => uranium_tests}/proxy_protocol/replay/proxy_protocol_out.replay.yaml (100%) create mode 100644 tests/uranium_tests/proxy_protocol/test_proxy_protocol.py rename tests/{gold_tests => uranium_tests}/qmux/go_qmux_client/go.mod (100%) rename tests/{gold_tests => uranium_tests}/qmux/go_qmux_client/go.sum (100%) rename tests/{gold_tests => uranium_tests}/qmux/go_qmux_client/main.go (100%) rename tests/{gold_tests => uranium_tests}/qmux/go_qmux_client/qmux_compat.go (100%) rename tests/{gold_tests => uranium_tests}/qmux/qmux.replay.yaml (100%) create mode 100644 tests/uranium_tests/qmux/test_qmux_go_client.py rename tests/{gold_tests => uranium_tests}/records/gold/full_records.yaml (99%) rename tests/{gold_tests => uranium_tests}/records/gold/no_newline.yaml (100%) rename tests/{gold_tests => uranium_tests}/records/gold/records.yaml.cold_test0.gold (100%) rename tests/{gold_tests => uranium_tests}/records/gold/records.yaml.cold_test2.gold (100%) rename tests/{gold_tests => uranium_tests}/records/gold/records.yaml.cold_test4.gold (100%) rename tests/{gold_tests => uranium_tests}/records/gold/records.yaml.cold_test5.gold (100%) rename tests/{gold_tests => uranium_tests}/records/gold/renamed_records.gold (100%) rename tests/{gold_tests => uranium_tests}/records/gold/renamed_records.yaml (100%) rename tests/{gold_tests => uranium_tests}/records/legacy_config/full_records.config (99%) rename tests/{gold_tests => uranium_tests}/records/legacy_config/no_newline.config (100%) rename tests/{gold_tests => uranium_tests}/records/legacy_config/old_records.config (100%) rename tests/{gold_tests => uranium_tests}/records/legacy_config/override_map.config (100%) rename tests/{gold_tests => uranium_tests}/records/legacy_config/override_value.config (100%) create mode 100644 tests/uranium_tests/records/test_records_config_to_yaml.py create mode 100644 tests/uranium_tests/records/test_records_runroot_precedence.py create mode 100644 tests/uranium_tests/records/test_records_yaml.py create mode 100644 tests/uranium_tests/records/test_traffic_ctl_cold_config.py create mode 100644 tests/uranium_tests/records/test_ts_max_records_param.py rename tests/{gold_tests => uranium_tests}/redirect/.gitignore (100%) rename tests/{gold_tests => uranium_tests}/redirect/gold/number_of_redirections_0.gold (100%) rename tests/{gold_tests => uranium_tests}/redirect/gold/number_of_redirections_1.gold (100%) rename tests/{gold_tests => uranium_tests}/redirect/gold/number_of_redirections_2.gold (100%) rename tests/{gold_tests => uranium_tests}/redirect/gold/redirect.gold (100%) rename tests/{gold_tests => uranium_tests}/redirect/gold/redirect_log.gold (100%) rename tests/{gold_tests => uranium_tests}/redirect/gold/redirect_post.gold (100%) rename tests/{gold_tests => uranium_tests}/redirect/gold/redirect_stale.gold (100%) rename tests/{gold_tests => uranium_tests}/redirect/replay/redirect_srv1_replay.yaml (100%) rename tests/{gold_tests => uranium_tests}/redirect/replay/redirect_srv2_replay.yaml (100%) rename tests/{gold_tests => uranium_tests}/redirect/replay/redirect_srv3_replay.yaml (100%) rename tests/{gold_tests => uranium_tests}/redirect/replay/redirect_to_same_origin_on_cache.replay.yaml (100%) create mode 100644 tests/uranium_tests/redirect/test_number_of_redirects.py create mode 100644 tests/uranium_tests/redirect/test_redirect.py create mode 100644 tests/uranium_tests/redirect/test_redirect_actions.py create mode 100644 tests/uranium_tests/redirect/test_redirect_post.py create mode 100644 tests/uranium_tests/redirect/test_redirect_stale.py create mode 100644 tests/uranium_tests/redirect/test_redirect_to_same_origin_on_cache.py rename tests/{gold_tests => uranium_tests}/redirect/wait_for_log.sh (100%) rename tests/{gold_tests => uranium_tests}/redirect/zone.json (100%) rename tests/{gold_tests => uranium_tests}/remap/all_acl_combinations.py (100%) rename tests/{gold_tests => uranium_tests}/remap/base.replay.yaml (100%) rename tests/{gold_tests => uranium_tests}/remap/deactivate_ip_allow.py (100%) rename tests/{gold_tests => uranium_tests}/remap/deny_head_post.replay.yaml (100%) rename tests/{gold_tests => uranium_tests}/remap/gold/200OK_test.gold (100%) rename tests/{gold_tests => uranium_tests}/remap/gold/200OK_test_uds.gold (100%) rename tests/{gold_tests => uranium_tests}/remap/gold/lookupTest.gold (100%) rename tests/{gold_tests => uranium_tests}/remap/gold/map-with-recv-port-ip.gold (100%) rename tests/{gold_tests => uranium_tests}/remap/gold/map-with-recv-port-unix.gold (100%) rename tests/{gold_tests => uranium_tests}/remap/gold/remap-200.gold (100%) rename tests/{gold_tests => uranium_tests}/remap/gold/remap-404.gold (100%) rename tests/{gold_tests => uranium_tests}/remap/gold/remap-DNS-200.gold (100%) rename tests/{gold_tests => uranium_tests}/remap/gold/remap-DNS-ipv6-200.gold (100%) rename tests/{gold_tests => uranium_tests}/remap/gold/remap-hitATS-404.gold (100%) rename tests/{gold_tests => uranium_tests}/remap/gold/remap-https-200.gold (100%) rename tests/{gold_tests => uranium_tests}/remap/gold/remap-https-200_2.gold (100%) rename tests/{gold_tests => uranium_tests}/remap/gold/remap-https-200_3.gold (100%) rename tests/{gold_tests => uranium_tests}/remap/gold/remap-ip-resolve.gold (100%) rename tests/{gold_tests => uranium_tests}/remap/gold/remap-redirect.gold (100%) rename tests/{gold_tests => uranium_tests}/remap/gold/remap-referer-hit.gold (100%) rename tests/{gold_tests => uranium_tests}/remap/gold/remap-referer-miss.gold (100%) rename tests/{gold_tests => uranium_tests}/remap/gold/remap-ws-metrics-uds.gold (100%) rename tests/{gold_tests => uranium_tests}/remap/gold/remap-ws-metrics.gold (100%) rename tests/{gold_tests => uranium_tests}/remap/gold/remap-ws-upgrade-400.gold (100%) rename tests/{gold_tests => uranium_tests}/remap/gold/remap-ws-upgrade.gold (100%) rename tests/{gold_tests => uranium_tests}/remap/gold/remap-zero-200.gold (100%) rename tests/{gold_tests => uranium_tests}/remap/gold/remap2-200.gold (100%) rename tests/{gold_tests => uranium_tests}/remap/reload_1.replay.yaml (100%) rename tests/{gold_tests => uranium_tests}/remap/reload_2.replay.yaml (100%) rename tests/{gold_tests => uranium_tests}/remap/reload_3.replay.yaml (100%) rename tests/{gold_tests => uranium_tests}/remap/reload_4.replay.yaml (100%) rename tests/{gold_tests => uranium_tests}/remap/reload_server.replay.yaml (100%) rename tests/{gold_tests => uranium_tests}/remap/remap_acl_all_allowed.replay.yaml (100%) rename tests/{gold_tests => uranium_tests}/remap/remap_acl_all_denied.replay.yaml (100%) rename tests/{gold_tests => uranium_tests}/remap/remap_acl_get_allowed.replay.yaml (100%) rename tests/{gold_tests => uranium_tests}/remap/remap_acl_get_post_allowed.replay.yaml (100%) rename tests/{gold_tests => uranium_tests}/remap/remap_acl_get_post_allowed_pp.replay.yaml (100%) rename tests/{gold_tests => uranium_tests}/remap/remap_acl_get_post_denied.replay.yaml (100%) rename tests/{gold_tests => uranium_tests}/remap/replay/regex_map_anchor.test.yaml (99%) create mode 100644 tests/uranium_tests/remap/test_basic_conf_remap_yaml.py create mode 100644 tests/uranium_tests/remap/test_conf_remap_float.py create mode 100644 tests/uranium_tests/remap/test_map_with_recv_port.py create mode 100644 tests/uranium_tests/remap/test_regex_map.py create mode 100644 tests/uranium_tests/remap/test_remap_acl.py create mode 100644 tests/uranium_tests/remap/test_remap_http.py create mode 100644 tests/uranium_tests/remap/test_remap_https.py create mode 100644 tests/uranium_tests/remap/test_remap_ip_resolve.py create mode 100644 tests/uranium_tests/remap/test_remap_load_empty_failure.py create mode 100644 tests/uranium_tests/remap/test_remap_load_empty_success.py create mode 100644 tests/uranium_tests/remap/test_remap_load_missing_failure.py create mode 100644 tests/uranium_tests/remap/test_remap_load_missing_success.py create mode 100644 tests/uranium_tests/remap/test_remap_reload.py create mode 100644 tests/uranium_tests/remap/test_remap_ws.py rename tests/{gold_tests => uranium_tests}/remap_yaml/all_acl_combinations_yaml.py (100%) rename tests/{gold_tests => uranium_tests}/remap_yaml/base.replay.yaml (100%) rename tests/{gold_tests => uranium_tests}/remap_yaml/deactivate_ip_allow_yaml.py (100%) rename tests/{gold_tests => uranium_tests}/remap_yaml/deny_head_post.replay.yaml (100%) rename tests/{gold_tests => uranium_tests}/remap_yaml/gold/200OK_test.gold (100%) rename tests/{gold_tests => uranium_tests}/remap_yaml/gold/200OK_test_uds.gold (100%) rename tests/{gold_tests => uranium_tests}/remap_yaml/gold/lookupTest.gold (100%) rename tests/{gold_tests => uranium_tests}/remap_yaml/gold/map-with-recv-port-ip.gold (100%) rename tests/{gold_tests => uranium_tests}/remap_yaml/gold/map-with-recv-port-unix.gold (100%) rename tests/{gold_tests => uranium_tests}/remap_yaml/gold/remap-200.gold (100%) rename tests/{gold_tests => uranium_tests}/remap_yaml/gold/remap-404.gold (100%) rename tests/{gold_tests => uranium_tests}/remap_yaml/gold/remap-DNS-200.gold (100%) rename tests/{gold_tests => uranium_tests}/remap_yaml/gold/remap-DNS-ipv6-200.gold (100%) rename tests/{gold_tests => uranium_tests}/remap_yaml/gold/remap-hitATS-404.gold (100%) rename tests/{gold_tests => uranium_tests}/remap_yaml/gold/remap-https-200.gold (100%) rename tests/{gold_tests => uranium_tests}/remap_yaml/gold/remap-https-200_2.gold (100%) rename tests/{gold_tests => uranium_tests}/remap_yaml/gold/remap-https-200_3.gold (100%) rename tests/{gold_tests => uranium_tests}/remap_yaml/gold/remap-ip-resolve.gold (100%) rename tests/{gold_tests => uranium_tests}/remap_yaml/gold/remap-redirect.gold (100%) rename tests/{gold_tests => uranium_tests}/remap_yaml/gold/remap-referer-hit.gold (100%) rename tests/{gold_tests => uranium_tests}/remap_yaml/gold/remap-referer-miss.gold (100%) rename tests/{gold_tests => uranium_tests}/remap_yaml/gold/remap-ws-metrics-uds.gold (100%) rename tests/{gold_tests => uranium_tests}/remap_yaml/gold/remap-ws-metrics.gold (100%) rename tests/{gold_tests => uranium_tests}/remap_yaml/gold/remap-ws-upgrade-400.gold (100%) rename tests/{gold_tests => uranium_tests}/remap_yaml/gold/remap-ws-upgrade.gold (100%) rename tests/{gold_tests => uranium_tests}/remap_yaml/gold/remap-zero-200.gold (100%) rename tests/{gold_tests => uranium_tests}/remap_yaml/gold/remap2-200.gold (100%) rename tests/{gold_tests => uranium_tests}/remap_yaml/reload_1.replay.yaml (100%) rename tests/{gold_tests => uranium_tests}/remap_yaml/reload_2.replay.yaml (100%) rename tests/{gold_tests => uranium_tests}/remap_yaml/reload_3.replay.yaml (100%) rename tests/{gold_tests => uranium_tests}/remap_yaml/reload_4.replay.yaml (100%) rename tests/{gold_tests => uranium_tests}/remap_yaml/reload_server.replay.yaml (100%) rename tests/{gold_tests => uranium_tests}/remap_yaml/remap_acl_all_allowed.replay.yaml (100%) rename tests/{gold_tests => uranium_tests}/remap_yaml/remap_acl_all_denied.replay.yaml (100%) rename tests/{gold_tests => uranium_tests}/remap_yaml/remap_acl_get_allowed.replay.yaml (100%) rename tests/{gold_tests => uranium_tests}/remap_yaml/remap_acl_get_post_allowed.replay.yaml (100%) rename tests/{gold_tests => uranium_tests}/remap_yaml/remap_acl_get_post_allowed_pp.replay.yaml (100%) rename tests/{gold_tests => uranium_tests}/remap_yaml/remap_acl_get_post_denied.replay.yaml (100%) create mode 100644 tests/uranium_tests/remap_yaml/test_basic_conf_remap_yaml_yaml.py create mode 100644 tests/uranium_tests/remap_yaml/test_conf_remap_float_yaml.py create mode 100644 tests/uranium_tests/remap_yaml/test_map_with_recv_port_yaml.py create mode 100644 tests/uranium_tests/remap_yaml/test_regex_map_yaml.py create mode 100644 tests/uranium_tests/remap_yaml/test_remap_acl_yaml.py create mode 100644 tests/uranium_tests/remap_yaml/test_remap_http_yaml.py create mode 100644 tests/uranium_tests/remap_yaml/test_remap_https_yaml.py create mode 100644 tests/uranium_tests/remap_yaml/test_remap_ip_resolve_yaml.py create mode 100644 tests/uranium_tests/remap_yaml/test_remap_load_empty_failure_yaml.py create mode 100644 tests/uranium_tests/remap_yaml/test_remap_load_empty_success_yaml.py create mode 100644 tests/uranium_tests/remap_yaml/test_remap_load_missing_failure_yaml.py create mode 100644 tests/uranium_tests/remap_yaml/test_remap_load_missing_success_yaml.py create mode 100644 tests/uranium_tests/remap_yaml/test_remap_reload_yaml.py create mode 100644 tests/uranium_tests/remap_yaml/test_remap_ws_yaml.py create mode 100644 tests/uranium_tests/runroot/test_runroot_error.py create mode 100644 tests/uranium_tests/runroot/test_runroot_init.py create mode 100644 tests/uranium_tests/runroot/test_runroot_remove.py create mode 100644 tests/uranium_tests/runroot/test_runroot_use.py create mode 100644 tests/uranium_tests/runroot/test_runroot_verify.py rename tests/{gold_tests => uranium_tests}/session_sharing/gold/200.gold (100%) create mode 100644 tests/uranium_tests/session_sharing/test_session_match.py create mode 100644 tests/uranium_tests/shutdown/test_crashlog_no_false_positive.py create mode 100644 tests/uranium_tests/shutdown/test_emergency.py create mode 100644 tests/uranium_tests/shutdown/test_fatal.py rename tests/{gold_tests => uranium_tests}/slow_post/__init__.py (100%) rename tests/{gold_tests => uranium_tests}/slow_post/gold/200.gold (100%) rename tests/{gold_tests => uranium_tests}/slow_post/quick_server.py (100%) rename tests/{gold_tests => uranium_tests}/slow_post/slow_post_client.py (100%) rename tests/{gold_tests => uranium_tests}/slow_post/slow_post_clients.py (100%) create mode 100644 tests/uranium_tests/slow_post/test_quick_server.py rename tests/{gold_tests => uranium_tests}/slow_post/test_secrets/aaa-signed.key (100%) rename tests/{gold_tests => uranium_tests}/slow_post/test_secrets/aaa-signed.pem (100%) create mode 100644 tests/uranium_tests/slow_post/test_server_abort.py create mode 100644 tests/uranium_tests/slow_post/test_slow_post.py rename tests/{gold_tests => uranium_tests}/statistics/abort_client.py (100%) rename tests/{gold_tests => uranium_tests}/statistics/replay/metric_response_429.test.yaml (99%) create mode 100644 tests/uranium_tests/statistics/test_metric_response_000.py rename tests/{gold_tests => uranium_tests}/thread_config/check_threads.py (100%) create mode 100644 tests/uranium_tests/thread_config/test_thread_config.py rename tests/{gold_tests => uranium_tests}/timeout/.gitignore (100%) rename tests/{gold_tests => uranium_tests}/timeout/CMakeLists.txt (100%) rename tests/{gold_tests => uranium_tests}/timeout/case1.sh (96%) rename tests/{gold_tests => uranium_tests}/timeout/create_request.sh (100%) rename tests/{gold_tests => uranium_tests}/timeout/delay-server.sh (100%) rename tests/{gold_tests => uranium_tests}/timeout/gold/client_default_inactivity_timeout.gold (100%) rename tests/{gold_tests => uranium_tests}/timeout/gold/quic_no_activity_timeout.gold (100%) rename tests/{gold_tests => uranium_tests}/timeout/http2_incomplete_header_client.py (100%) rename tests/{gold_tests => uranium_tests}/timeout/replay/default_inactivity_timeout.replay.yaml (100%) rename tests/{gold_tests => uranium_tests}/timeout/replay/http2_incomplete_header_timeout.replay.yaml (100%) rename tests/{gold_tests => uranium_tests}/timeout/replay/http2_no_activity_timeout.replay.yaml (100%) rename tests/{gold_tests => uranium_tests}/timeout/replay/quic_no_activity_timeout.replay.yaml (100%) rename tests/{gold_tests => uranium_tests}/timeout/setupnetns.sh (100%) rename tests/{gold_tests => uranium_tests}/timeout/slow_server.yaml (100%) rename tests/{gold_tests => uranium_tests}/timeout/slow_server_uds.yaml (100%) rename tests/{gold_tests => uranium_tests}/timeout/ssl-delay-server.cc (100%) rename tests/{gold_tests => uranium_tests}/timeout/ssl/cert.crt (100%) rename tests/{gold_tests => uranium_tests}/timeout/ssl/private-key.key (100%) create mode 100644 tests/uranium_tests/timeout/test_accept_timeout.py create mode 100644 tests/uranium_tests/timeout/test_active_timeout.py create mode 100644 tests/uranium_tests/timeout/test_conn_timeout.py create mode 100644 tests/uranium_tests/timeout/test_default_inactivity_timeout.py create mode 100644 tests/uranium_tests/timeout/test_http2_incomplete_header_timeout.py create mode 100644 tests/uranium_tests/timeout/test_http2_no_activity_timeout.py create mode 100644 tests/uranium_tests/timeout/test_inactive_client_timeout.py create mode 100644 tests/uranium_tests/timeout/test_inactive_timeout.py create mode 100644 tests/uranium_tests/timeout/test_quic_no_activity_timeout.py create mode 100644 tests/uranium_tests/timeout/test_quic_poll_timeout.py create mode 100644 tests/uranium_tests/timeout/test_tls_conn_timeout.py create mode 100644 tests/uranium_tests/timeout/test_tunnel_active_timeout.py rename tests/{gold_tests => uranium_tests}/timeout/time_client.sh (100%) rename tests/{gold_tests => uranium_tests}/timeout/tunnel_timeout_client.py (100%) rename tests/{gold_tests => uranium_tests}/tls/CMakeLists.txt (100%) rename tests/{gold_tests => uranium_tests}/tls/early_h1_get.txt (100%) rename tests/{gold_tests => uranium_tests}/tls/early_h1_post.txt (100%) rename tests/{gold_tests => uranium_tests}/tls/early_h2_get.txt (100%) rename tests/{gold_tests => uranium_tests}/tls/early_h2_multi1.txt (100%) rename tests/{gold_tests => uranium_tests}/tls/early_h2_multi2.txt (100%) rename tests/{gold_tests => uranium_tests}/tls/early_h2_post.txt (100%) rename tests/{gold_tests => uranium_tests}/tls/file.ticket (100%) rename tests/{gold_tests => uranium_tests}/tls/gold/accesslog.gold (100%) rename tests/{gold_tests => uranium_tests}/tls/gold/clientcert-accesslog.gold (100%) rename tests/{gold_tests => uranium_tests}/tls/gold/proxycert-accesslog.gold (100%) rename tests/{gold_tests => uranium_tests}/tls/gold/proxycert2-accesslog.gold (100%) rename tests/{gold_tests => uranium_tests}/tls/gold/ssl-post.gold (100%) rename tests/{gold_tests => uranium_tests}/tls/gold/tls-partial-blind-tunnel-metrics.gold (100%) rename tests/{gold_tests => uranium_tests}/tls/gold/tls-tunnel-forward-metrics.gold (100%) rename tests/{gold_tests => uranium_tests}/tls/gold/tls-tunnel-metrics.gold (100%) rename tests/{gold_tests => uranium_tests}/tls/h2_early_decode.py (99%) rename tests/{gold_tests => uranium_tests}/tls/h2_early_gen.py (100%) rename tests/{gold_tests => uranium_tests}/tls/proxy_protocol_client.py (100%) rename tests/{gold_tests => uranium_tests}/tls/receive_split_client_hello.py (100%) rename tests/{gold_tests => uranium_tests}/tls/replay/allow-plain.replay.yaml (100%) rename tests/{gold_tests => uranium_tests}/tls/replay/ip_allow.replay.yaml (100%) rename tests/{gold_tests => uranium_tests}/tls/replay/ip_allow_proxy.replay.yaml (100%) rename tests/{gold_tests => uranium_tests}/tls/replay/ip_allow_tunnel.replay.yaml (100%) rename tests/{gold_tests => uranium_tests}/tls/replay/outbound_sni_server_name_plain.test.yaml (99%) rename tests/{gold_tests => uranium_tests}/tls/replay/tls_cert_compression.replay.yaml (100%) rename tests/{gold_tests => uranium_tests}/tls/replay/tls_cert_compression_cache.replay.yaml (100%) rename tests/{gold_tests => uranium_tests}/tls/split_client_hello.py (100%) rename tests/{gold_tests => uranium_tests}/tls/ssl-post.c (100%) rename tests/{gold_tests => uranium_tests}/tls/ssl/aaa-ca.key (100%) rename tests/{gold_tests => uranium_tests}/tls/ssl/aaa-ca.pem (100%) rename tests/{gold_tests => uranium_tests}/tls/ssl/aaa-signed.key (100%) rename tests/{gold_tests => uranium_tests}/tls/ssl/aaa-signed.pem (100%) rename tests/{gold_tests => uranium_tests}/tls/ssl/bbb-ca.key (100%) rename tests/{gold_tests => uranium_tests}/tls/ssl/bbb-ca.pem (100%) rename tests/{gold_tests => uranium_tests}/tls/ssl/bbb-signed.key (100%) rename tests/{gold_tests => uranium_tests}/tls/ssl/bbb-signed.pem (100%) rename tests/{gold_tests => uranium_tests}/tls/ssl/ca.ocsp.key (100%) rename tests/{gold_tests => uranium_tests}/tls/ssl/ca.ocsp.pem (100%) rename tests/{gold_tests => uranium_tests}/tls/ssl/ccc-ca.key (100%) rename tests/{gold_tests => uranium_tests}/tls/ssl/ccc-ca.pem (100%) rename tests/{gold_tests => uranium_tests}/tls/ssl/ccc-signed.key (100%) rename tests/{gold_tests => uranium_tests}/tls/ssl/ccc-signed.pem (100%) rename tests/{gold_tests => uranium_tests}/tls/ssl/combined-ec.pem (100%) rename tests/{gold_tests => uranium_tests}/tls/ssl/combined.pem (100%) rename tests/{gold_tests => uranium_tests}/tls/ssl/combo-signed-foo.pem (100%) rename tests/{gold_tests => uranium_tests}/tls/ssl/combo.pem (100%) rename tests/{gold_tests => uranium_tests}/tls/ssl/gen_client_certs.sh (100%) rename tests/{gold_tests => uranium_tests}/tls/ssl/gen_foobar_certs.sh (100%) rename tests/{gold_tests => uranium_tests}/tls/ssl/ocsp_response.der (100%) rename tests/{gold_tests => uranium_tests}/tls/ssl/openssl.cnf (100%) rename tests/{gold_tests => uranium_tests}/tls/ssl/passphrase.key (100%) rename tests/{gold_tests => uranium_tests}/tls/ssl/passphrase.pem (100%) rename tests/{gold_tests => uranium_tests}/tls/ssl/passphrase2.key (100%) rename tests/{gold_tests => uranium_tests}/tls/ssl/passphrase2.pem (100%) rename tests/{gold_tests => uranium_tests}/tls/ssl/responder.ocsp.key (100%) rename tests/{gold_tests => uranium_tests}/tls/ssl/responder.ocsp.pem (100%) rename tests/{gold_tests => uranium_tests}/tls/ssl/server-bar.key (100%) rename tests/{gold_tests => uranium_tests}/tls/ssl/server-bar.pem (100%) rename tests/{gold_tests => uranium_tests}/tls/ssl/server-foo.key (100%) rename tests/{gold_tests => uranium_tests}/tls/ssl/server-foo.pem (100%) rename tests/{gold_tests => uranium_tests}/tls/ssl/server.key (100%) rename tests/{gold_tests => uranium_tests}/tls/ssl/server.ocsp.key (100%) rename tests/{gold_tests => uranium_tests}/tls/ssl/server.ocsp.pem (100%) rename tests/{gold_tests => uranium_tests}/tls/ssl/server.pem (100%) rename tests/{gold_tests => uranium_tests}/tls/ssl/signed-bar.key (100%) rename tests/{gold_tests => uranium_tests}/tls/ssl/signed-bar.pem (100%) rename tests/{gold_tests => uranium_tests}/tls/ssl/signed-bob-bar.pem (100%) rename tests/{gold_tests => uranium_tests}/tls/ssl/signed-bob-foo.pem (100%) rename tests/{gold_tests => uranium_tests}/tls/ssl/signed-foo-ec.key (100%) rename tests/{gold_tests => uranium_tests}/tls/ssl/signed-foo-ec.pem (100%) rename tests/{gold_tests => uranium_tests}/tls/ssl/signed-foo.key (100%) rename tests/{gold_tests => uranium_tests}/tls/ssl/signed-foo.pem (100%) rename tests/{gold_tests => uranium_tests}/tls/ssl/signed-san-ec.key (100%) rename tests/{gold_tests => uranium_tests}/tls/ssl/signed-san-ec.pem (100%) rename tests/{gold_tests => uranium_tests}/tls/ssl/signed-san.key (100%) rename tests/{gold_tests => uranium_tests}/tls/ssl/signed-san.pem (100%) rename tests/{gold_tests => uranium_tests}/tls/ssl/signed-wild.key (100%) rename tests/{gold_tests => uranium_tests}/tls/ssl/signed-wild.pem (100%) rename tests/{gold_tests => uranium_tests}/tls/ssl/signed2-bar.pem (100%) rename tests/{gold_tests => uranium_tests}/tls/ssl/signed2-foo.pem (100%) rename tests/{gold_tests => uranium_tests}/tls/ssl/signer.key (100%) rename tests/{gold_tests => uranium_tests}/tls/ssl/signer.pem (100%) rename tests/{gold_tests => uranium_tests}/tls/ssl/signer2.key (100%) rename tests/{gold_tests => uranium_tests}/tls/ssl/signer2.pem (100%) rename tests/{gold_tests => uranium_tests}/tls/test-0rtt-s_client.py (100%) rename tests/{gold_tests => uranium_tests}/tls/test-nc-s_client.sh (100%) create mode 100644 tests/uranium_tests/tls/test_allow_plain.py create mode 100644 tests/uranium_tests/tls/test_exit_on_cert_load_fail.py create mode 100644 tests/uranium_tests/tls/test_proxy_protocol_addressless.py create mode 100644 tests/uranium_tests/tls/test_ssl_key_dialog.py create mode 100644 tests/uranium_tests/tls/test_ssl_multicert_loader.py create mode 100644 tests/uranium_tests/tls/test_tls.py create mode 100644 tests/uranium_tests/tls/test_tls_0rtt_server.py create mode 100644 tests/uranium_tests/tls/test_tls_accept_timeout_crash.py create mode 100644 tests/uranium_tests/tls/test_tls_async_handshake.py create mode 100644 tests/uranium_tests/tls/test_tls_bad_alpn.py create mode 100644 tests/uranium_tests/tls/test_tls_cert_comp.py create mode 100644 tests/uranium_tests/tls/test_tls_cert_comp_cache.py create mode 100644 tests/uranium_tests/tls/test_tls_check_cert_select_plugin.py create mode 100644 tests/uranium_tests/tls/test_tls_check_cert_selection.py create mode 100644 tests/uranium_tests/tls/test_tls_check_cert_selection_reload.py create mode 100644 tests/uranium_tests/tls/test_tls_check_dual_cert_selection.py create mode 100644 tests/uranium_tests/tls/test_tls_check_dual_cert_selection2.py create mode 100644 tests/uranium_tests/tls/test_tls_check_dual_cert_selection_plugin.py create mode 100644 tests/uranium_tests/tls/test_tls_client_alpn_configuration.py create mode 100644 tests/uranium_tests/tls/test_tls_client_cert.py create mode 100644 tests/uranium_tests/tls/test_tls_client_cert2.py create mode 100644 tests/uranium_tests/tls/test_tls_client_cert2_plugin.py create mode 100644 tests/uranium_tests/tls/test_tls_client_cert_override.py create mode 100644 tests/uranium_tests/tls/test_tls_client_cert_override_plugin.py create mode 100644 tests/uranium_tests/tls/test_tls_client_cert_plugin.py create mode 100644 tests/uranium_tests/tls/test_tls_client_verify.py create mode 100644 tests/uranium_tests/tls/test_tls_client_verify2.py create mode 100644 tests/uranium_tests/tls/test_tls_client_verify3.py create mode 100644 tests/uranium_tests/tls/test_tls_client_versions.py create mode 100644 tests/uranium_tests/tls/test_tls_client_versions_minmax.py create mode 100644 tests/uranium_tests/tls/test_tls_engine_abort.py create mode 100644 tests/uranium_tests/tls/test_tls_flow_control.py create mode 100644 tests/uranium_tests/tls/test_tls_forward_nonhttp.py create mode 100644 tests/uranium_tests/tls/test_tls_h2_cipher_suite.py create mode 100644 tests/uranium_tests/tls/test_tls_hooks_client_verify.py create mode 100644 tests/uranium_tests/tls/test_tls_hooks_verify.py create mode 100644 tests/uranium_tests/tls/test_tls_keepalive.py create mode 100644 tests/uranium_tests/tls/test_tls_ocsp.py create mode 100644 tests/uranium_tests/tls/test_tls_origin_open_failed.py create mode 100644 tests/uranium_tests/tls/test_tls_origin_post_abort.py create mode 100644 tests/uranium_tests/tls/test_tls_origin_session_reuse.py create mode 100644 tests/uranium_tests/tls/test_tls_partial_blind_tunnel.py create mode 100644 tests/uranium_tests/tls/test_tls_record_size.py create mode 100644 tests/uranium_tests/tls/test_tls_reload_under_load.py create mode 100644 tests/uranium_tests/tls/test_tls_renegotiation.py create mode 100644 tests/uranium_tests/tls/test_tls_renegotiation_allowed.py create mode 100644 tests/uranium_tests/tls/test_tls_secret_update_default.py create mode 100644 tests/uranium_tests/tls/test_tls_session_key_logging.py create mode 100644 tests/uranium_tests/tls/test_tls_session_reuse.py create mode 100644 tests/uranium_tests/tls/test_tls_sni_groups.py create mode 100644 tests/uranium_tests/tls/test_tls_sni_host_policy.py create mode 100644 tests/uranium_tests/tls/test_tls_sni_ip_allow.py create mode 100644 tests/uranium_tests/tls/test_tls_sni_parent_failover.py create mode 100644 tests/uranium_tests/tls/test_tls_sni_ticket.py create mode 100644 tests/uranium_tests/tls/test_tls_sni_with_port.py create mode 100644 tests/uranium_tests/tls/test_tls_sni_yaml_reload.py create mode 100644 tests/uranium_tests/tls/test_tls_ticket.py create mode 100644 tests/uranium_tests/tls/test_tls_tunnel.py create mode 100644 tests/uranium_tests/tls/test_tls_tunnel_forward.py create mode 100644 tests/uranium_tests/tls/test_tls_verify.py create mode 100644 tests/uranium_tests/tls/test_tls_verify2.py create mode 100644 tests/uranium_tests/tls/test_tls_verify3.py create mode 100644 tests/uranium_tests/tls/test_tls_verify4.py create mode 100644 tests/uranium_tests/tls/test_tls_verify_base.py create mode 100644 tests/uranium_tests/tls/test_tls_verify_ca_override.py create mode 100644 tests/uranium_tests/tls/test_tls_verify_not_pristine.py create mode 100644 tests/uranium_tests/tls/test_tls_verify_override.py create mode 100644 tests/uranium_tests/tls/test_tls_verify_override_base.py create mode 100644 tests/uranium_tests/tls/test_tls_verify_override_sni.py rename tests/{gold_tests => uranium_tests}/tls/tls_accept_timeout_crash.replay.yaml (100%) rename tests/{gold_tests => uranium_tests}/tls/tls_client_alpn_configuration.replay.yaml (100%) rename tests/{gold_tests => uranium_tests}/tls/tls_engine_abort.py (100%) rename tests/{gold_tests => uranium_tests}/tls/tls_flow_control_client.py (100%) rename tests/{gold_tests => uranium_tests}/tls/tls_post_abort_client.py (100%) rename tests/{gold_tests => uranium_tests}/tls/tls_post_abort_origin.py (100%) rename tests/{gold_tests => uranium_tests}/tls/tls_record_size_client.py (100%) rename tests/{gold_tests => uranium_tests}/tls/tls_reload_under_load_client.py (100%) rename tests/{gold_tests => uranium_tests}/tls/tls_renegotiation_allowed_client.py (100%) rename tests/{gold_tests => uranium_tests}/tls/tls_renegotiation_client.py (100%) rename tests/{gold_tests => uranium_tests}/tls/tls_session_key_logging.replay.yaml (100%) rename tests/{gold_tests => uranium_tests}/tls/tls_sni_with_port.replay.yaml (100%) rename tests/{gold_tests => uranium_tests}/tls_hooks/gold/cert-1.gold (100%) rename tests/{gold_tests => uranium_tests}/tls_hooks/gold/client-hello-1.gold (100%) rename tests/{gold_tests => uranium_tests}/tls_hooks/gold/preaccept-1.gold (100%) rename tests/{gold_tests => uranium_tests}/tls_hooks/gold/sni-1.gold (100%) rename tests/{gold_tests => uranium_tests}/tls_hooks/gold/ts-cert-1-im-2.gold (100%) rename tests/{gold_tests => uranium_tests}/tls_hooks/gold/ts-cert-1.gold (100%) rename tests/{gold_tests => uranium_tests}/tls_hooks/gold/ts-cert-2.gold (100%) rename tests/{gold_tests => uranium_tests}/tls_hooks/gold/ts-cert-im-1.gold (100%) rename tests/{gold_tests => uranium_tests}/tls_hooks/gold/ts-client-hello-1.gold (100%) rename tests/{gold_tests => uranium_tests}/tls_hooks/gold/ts-client-hello-2.gold (100%) rename tests/{gold_tests => uranium_tests}/tls_hooks/gold/ts-client-hello-delayed-1.gold (100%) rename tests/{gold_tests => uranium_tests}/tls_hooks/gold/ts-close-out-close.gold (100%) rename tests/{gold_tests => uranium_tests}/tls_hooks/gold/ts-out-delay-start-2.gold (100%) rename tests/{gold_tests => uranium_tests}/tls_hooks/gold/ts-out-start-close-2.gold (100%) rename tests/{gold_tests => uranium_tests}/tls_hooks/gold/ts-preaccept-1.gold (100%) rename tests/{gold_tests => uranium_tests}/tls_hooks/gold/ts-preaccept-2.gold (100%) rename tests/{gold_tests => uranium_tests}/tls_hooks/gold/ts-preaccept-delayed-1-immdate-2.gold (100%) rename tests/{gold_tests => uranium_tests}/tls_hooks/gold/ts-preaccept-delayed-1.gold (100%) rename tests/{gold_tests => uranium_tests}/tls_hooks/gold/ts-preaccept1-sni1-cert1.gold (100%) rename tests/{gold_tests => uranium_tests}/tls_hooks/gold/ts-sni-1.gold (100%) rename tests/{gold_tests => uranium_tests}/tls_hooks/gold/ts-sni-2.gold (100%) create mode 100644 tests/uranium_tests/tls_hooks/test_tls_hooks.py create mode 100644 tests/uranium_tests/tls_hooks/test_tls_hooks10.py create mode 100644 tests/uranium_tests/tls_hooks/test_tls_hooks11.py create mode 100644 tests/uranium_tests/tls_hooks/test_tls_hooks12.py create mode 100644 tests/uranium_tests/tls_hooks/test_tls_hooks13.py create mode 100644 tests/uranium_tests/tls_hooks/test_tls_hooks14.py create mode 100644 tests/uranium_tests/tls_hooks/test_tls_hooks15.py create mode 100644 tests/uranium_tests/tls_hooks/test_tls_hooks16.py create mode 100644 tests/uranium_tests/tls_hooks/test_tls_hooks17.py create mode 100644 tests/uranium_tests/tls_hooks/test_tls_hooks18.py create mode 100644 tests/uranium_tests/tls_hooks/test_tls_hooks2.py create mode 100644 tests/uranium_tests/tls_hooks/test_tls_hooks3.py create mode 100644 tests/uranium_tests/tls_hooks/test_tls_hooks4.py create mode 100644 tests/uranium_tests/tls_hooks/test_tls_hooks6.py create mode 100644 tests/uranium_tests/tls_hooks/test_tls_hooks7.py create mode 100644 tests/uranium_tests/tls_hooks/test_tls_hooks8.py create mode 100644 tests/uranium_tests/tls_hooks/test_tls_hooks9.py create mode 100644 tests/uranium_tests/tls_hooks/test_tls_hooks_close_while_parked.py rename tests/{gold_tests => uranium_tests}/traffic_ctl/convert_plugin_config/gold/basic.yaml (100%) rename tests/{gold_tests => uranium_tests}/traffic_ctl/convert_plugin_config/gold/commented.yaml (100%) rename tests/{gold_tests => uranium_tests}/traffic_ctl/convert_plugin_config/gold/quoted.yaml (100%) rename tests/{gold_tests => uranium_tests}/traffic_ctl/convert_plugin_config/gold/skip_disabled.yaml (100%) rename tests/{gold_tests => uranium_tests}/traffic_ctl/convert_plugin_config/legacy_config/basic.config (100%) rename tests/{gold_tests => uranium_tests}/traffic_ctl/convert_plugin_config/legacy_config/commented.config (100%) rename tests/{gold_tests => uranium_tests}/traffic_ctl/convert_plugin_config/legacy_config/quoted.config (100%) create mode 100644 tests/uranium_tests/traffic_ctl/convert_plugin_config/test_convert_plugin_config.py rename tests/{gold_tests => uranium_tests}/traffic_ctl/convert_ssl_multicert/gold/basic.yaml (100%) rename tests/{gold_tests => uranium_tests}/traffic_ctl/convert_ssl_multicert/gold/full.yaml (100%) rename tests/{gold_tests => uranium_tests}/traffic_ctl/convert_ssl_multicert/gold/quoted.yaml (100%) rename tests/{gold_tests => uranium_tests}/traffic_ctl/convert_ssl_multicert/legacy_config/basic.config (100%) rename tests/{gold_tests => uranium_tests}/traffic_ctl/convert_ssl_multicert/legacy_config/full.config (100%) rename tests/{gold_tests => uranium_tests}/traffic_ctl/convert_ssl_multicert/legacy_config/quoted.config (100%) create mode 100644 tests/uranium_tests/traffic_ctl/convert_ssl_multicert/test_convert_ssl_multicert.py rename tests/{gold_tests => uranium_tests}/traffic_ctl/convert_storage/gold/basic.yaml (100%) rename tests/{gold_tests => uranium_tests}/traffic_ctl/convert_storage/gold/exclusive.yaml (100%) rename tests/{gold_tests => uranium_tests}/traffic_ctl/convert_storage/gold/no_volumes.yaml (100%) rename tests/{gold_tests => uranium_tests}/traffic_ctl/convert_storage/legacy_config/basic.storage.config (100%) rename tests/{gold_tests => uranium_tests}/traffic_ctl/convert_storage/legacy_config/basic.volume.config (100%) rename tests/{gold_tests => uranium_tests}/traffic_ctl/convert_storage/legacy_config/exclusive.storage.config (100%) rename tests/{gold_tests => uranium_tests}/traffic_ctl/convert_storage/legacy_config/exclusive.volume.config (100%) rename tests/{gold_tests => uranium_tests}/traffic_ctl/convert_storage/legacy_config/no_volumes.storage.config (100%) create mode 100644 tests/uranium_tests/traffic_ctl/convert_storage/test_convert_storage.py rename tests/{gold_tests => uranium_tests}/traffic_ctl/gold/describe.gold (100%) rename tests/{gold_tests => uranium_tests}/traffic_ctl/gold/diff.gold (100%) rename tests/{gold_tests => uranium_tests}/traffic_ctl/gold/diff_yaml.gold (100%) rename tests/{gold_tests => uranium_tests}/traffic_ctl/gold/match.gold (100%) rename tests/{gold_tests => uranium_tests}/traffic_ctl/gold/t1_yaml.gold (100%) rename tests/{gold_tests => uranium_tests}/traffic_ctl/gold/t2_yaml.gold (100%) rename tests/{gold_tests => uranium_tests}/traffic_ctl/gold/t3_yaml.gold (100%) rename tests/{gold_tests => uranium_tests}/traffic_ctl/gold/t4_yaml.gold (100%) create mode 100644 tests/uranium_tests/traffic_ctl/remap_inc/test_remap_inc.py rename tests/{gold_tests => uranium_tests}/traffic_ctl/show_ssl_multicert/gold/show_json.gold (100%) rename tests/{gold_tests => uranium_tests}/traffic_ctl/show_ssl_multicert/gold/show_yaml.gold (100%) create mode 100644 tests/uranium_tests/traffic_ctl/show_ssl_multicert/test_show_ssl_multicert.py create mode 100644 tests/uranium_tests/traffic_ctl/test_traffic_ctl_config_output.py create mode 100644 tests/uranium_tests/traffic_ctl/test_traffic_ctl_config_reload.py create mode 100644 tests/uranium_tests/traffic_ctl/test_traffic_ctl_server_debug.py create mode 100644 tests/uranium_tests/traffic_ctl/test_traffic_ctl_server_output.py create mode 100644 tests/uranium_tests/traffic_ctl/test_traffic_ctl_set_read_only.py rename tests/{gold_tests => uranium_tests}/traffic_ctl/traffic_ctl_test_utils.py (100%) rename tests/{gold_tests => uranium_tests}/tunnel/dumb_proxy.py (100%) create mode 100644 tests/uranium_tests/tunnel/test_tunnel_transform.py create mode 100644 tests/uranium_tests/tunnel/test_txn_type.py create mode 100644 tests/uranium_tests/uds/test_uds_socket_perm.py create mode 100644 tests/uranium_tests/url/test_uri.py rename tests/{gold_tests => uranium_tests}/url/uri.replay.yaml (100%) rename tests/{gold_tests/pluginTest/txn_box/basic/txn_box_cmp.test.py => urtest.sh} (68%) mode change 100644 => 100755 rename tests/{gold_tests/pluginTest/txn_box/basic/txn_box_ip-addr.test.py => urtest.sh.in} (59%) mode change 100644 => 100755 create mode 100644 tests/uv.lock diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 92e09afb98e..f83c8a11fa1 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -313,7 +313,7 @@ trafficserver/ **Pattern discovery:** - Search for similar functionality in existing code - Check `include/ts/ts.h` for plugin API patterns -- Look at tests in `tests/gold_tests/` for usage examples +- Look at tests in `tests/uranium_tests/` for usage examples ### Code Organization @@ -362,7 +362,7 @@ plugins/my_plugin/ **When adding new functionality:** 1. Check if unit tests exist in same directory (Catch2) -2. Add integration tests in `tests/gold_tests/` (autest) +2. Add integration tests in `tests/uranium_tests/` (autest) 3. Prefer `Test.ATSReplayTest()` with `replay.yaml` format (Proxy Verifier). If `ATSReplayTest` doesn't fit, prefer organizing the test around a test class with separate functions for configuring the servers, ATS, client, etc. diff --git a/.gitignore b/.gitignore index c8d604a148e..460dd866223 100644 --- a/.gitignore +++ b/.gitignore @@ -202,10 +202,10 @@ cmake-build-* tests/env-test/ tests/proxy-verifier tests/Pipfile.lock -tests/gold_tests/bigobj/check_ramp -tests/gold_tests/bigobj/push_request -tests/gold_tests/chunked_encoding/smuggle-client -tests/gold_tests/tls/ssl-post +tests/uranium_tests/bigobj/check_ramp +tests/uranium_tests/bigobj/push_request +tests/uranium_tests/chunked_encoding/smuggle-client +tests/uranium_tests/tls/ssl-post src/iocore/cache/test_* src/iocore/cache/unit_tests/var/trafficserver/cache.db diff --git a/AGENTS.md b/AGENTS.md index 47580ba8e02..1b5dad65931 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -11,15 +11,15 @@ with a sophisticated plugin system. **Key Technologies:** - Language: C++20 - Build System: CMake (migrated from autotools in v10) -- Testing: Catch2 (unit tests) + AuTest Python framework (end-to-end tests) +- Testing: Catch2 (unit tests) + pytest (Uranium tests, with an AuTest compatibility backend) - Protocols: TLS, HTTP/1.1, HTTP/2, HTTP/3 (via Quiche) ## Project Structure Core sources live in `src/` (for example `src/proxy`, `src/iocore`, `src/traffic_server`). Public headers are in `include/`. Built-in plugins are -in `plugins/` and `plugins/experimental/`. End-to-end tests are in `tests/`, -especially `tests/gold_tests/`. Build system files are in `cmake/` plus the +in `plugins/` and `plugins/experimental/`. Uranium tests are in `tests/`, +especially `tests/uranium_tests/`. Build system files are in `cmake/` plus the top-level `CMakeLists.txt`, and docs are in `doc/`. Third party libraries that we include locally are in `lib/`. @@ -80,61 +80,63 @@ Unit tests are built into executables. Find the test binary and run it directly: ./build/src/tscore/test_tscore ``` -### End-to-End Tests (AuTest) +### Uranium Tests (pytest) -**Enable autests during configuration:** +**Enable Uranium tests during configuration:** ```bash -cmake -B build -DENABLE_AUTEST=ON +cmake -B build -DENABLE_URTEST=ON cmake --build build cmake --install build ``` -**Run all autests:** +**Run all Uranium tests:** ```bash -cmake --build build -t autest +cmake --build build -t urtest ``` **Run specific test(s):** ```bash cd build/tests -./autest.sh --sandbox /tmp/sbcodex --clean=none -f +./urtest.sh -f ``` For example, to run `cache-auth.test.py`: ```bash -./autest.sh --sandbox /tmp/sbcursor --clean=none -f cache-auth +./urtest.sh -f cache-auth ``` To run multiple tests efficiently, pass the -j option. ```bash cd build/tests -./autest.sh -j4 --sandbox /tmp/sbcodex --clean=none -f 'header_rewrite*' +./urtest.sh -j4 -f 'header_rewrite*' ``` -Most end-to-end test coverage is in `tests/gold_tests/`. The CI system uses the -Docker image `ci.trafficserver.apache.org/ats/fedora:43` (Fedora version updated -regularly). +Most Uranium test coverage is in `tests/uranium_tests/`. The CI system uses the +Docker image `ci.trafficserver.apache.org/ats/fedora:44` (Fedora version updated +regularly). The source-tree `tests/urtest.sh` defaults to this image. It runs +directly instead when it detects that it is already inside a Fedora 44 +container. Use `--run-in-docker` or `--no-run-in-docker` to override. -### Writing End-to-End Tests +### Writing Uranium Tests **New tests should normally be direct pytest replay tests.** Name the Proxy -Verifier replay `.test.yaml`; the file's `autest` section describes +Verifier replay `.test.yaml`; the file's `urtest` section describes DNS, server, client, and ATS setup, and pytest collects it without a companion -`.test.py` wrapper. Run these with `cmake --build build -t pytest-replay`. +`.test.py` wrapper. Run these with `cmake --build build -t urtest-replay`. If a direct replay test is not a good fit (say, the test needs a custom client), then organize the test around a test class with member functions that configure any servers, the ATS process, and the client. See -`tests/gold_tests/ats_probe/ats_probe.test.py` for an example of a test organized +`tests/uranium_tests/ats_probe/ats_probe.test.py` for an example of a test organized around a test class. -In autests, launch Python helpers with `{sys.executable}` rather than a +In compatibility tests, launch Python helpers with `{sys.executable}` rather than a hardcoded `python3`, so the test runs under the same interpreter the harness uses. -**For complete details on writing end-to-end tests, see:** -- `doc/developer-guide/testing/autests.en.rst` - Comprehensive end-to-end test guide +**For complete details on writing Uranium tests, see:** +- `doc/developer-guide/testing/uranium-tests.en.rst` - Comprehensive Uranium test guide - Proxy Verifier format: https://github.com/yahoo/proxy-verifier - AuTest framework: https://autestsuite.bitbucket.io/ diff --git a/CMakeLists.txt b/CMakeLists.txt index 0e8de39044e..efa9b31d34a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -161,8 +161,8 @@ option(ENABLE_DISK_FAILURE_TESTS "Build disk failure tests (enables AIO fault in if(ENABLE_DISK_FAILURE_TESTS) add_compile_definitions("AIO_FAULT_INJECTION") endif() -option(ENABLE_AUTEST "Setup autest (default OFF)") -option(ENABLE_AUTEST_UDS "Setup autest with curl using UDS (default OFF)") +option(ENABLE_URTEST "Set up Uranium tests (default OFF)") +option(ENABLE_URTEST_UDS "Set up Uranium tests with curl using UDS (default OFF)") option(ENABLE_BENCHMARKS "Build benchmarks (default OFF)") option(EXTERNAL_YAML_CPP "Use external yaml-cpp (default OFF)") option(EXTERNAL_LIBSWOC "Use external libswoc (default OFF)") @@ -772,7 +772,7 @@ check_struct_has_member("struct mptcp_info" mptcpi_subflows "linux/mptcp.h" HAVE # find resolv library if available find_package(resolv) -if(ENABLE_DOCS OR ENABLE_AUTEST) +if(ENABLE_DOCS OR ENABLE_URTEST) find_package(Python3 REQUIRED) find_program(UV uv REQUIRED) find_program(NETCAT_PROGRAM nc REQUIRED) @@ -788,7 +788,7 @@ if(ENABLE_DOCS) find_program(GRAPHVIZ_DOT dot REQUIRED) endif() -if(ENABLE_AUTEST) +if(ENABLE_URTEST) # Default the sandbox to /tmp to keep paths short. Unix domain socket paths # are limited to 108 characters and deep build directories (e.g. in home # directories) can exceed this limit, causing confusing test failures. A hash @@ -796,24 +796,26 @@ if(ENABLE_AUTEST) # deterministic across runs. string(MD5 _build_dir_hash "${CMAKE_BINARY_DIR}") string(SUBSTRING "${_build_dir_hash}" 0 8 _build_dir_hash) - set(AUTEST_SANDBOX - "/tmp/sb_${_build_dir_hash}" - CACHE STRING "Location for autest output (default /tmp/sb_)" + set(URTEST_SANDBOX + "/tmp/urtest_${_build_dir_hash}" + CACHE STRING "Location for Uranium test output (default /tmp/urtest_)" ) - set(AUTEST_OPTIONS - "" - CACHE STRING "Additional options for autest (default \"\")" - ) - set(PYTEST_OPTIONS + if(AUTEST_SANDBOX) + message(DEPRECATION "AUTEST_SANDBOX is deprecated; use URTEST_SANDBOX") + set(URTEST_SANDBOX "${AUTEST_SANDBOX}") + endif() + set(URTEST_OPTIONS "" - CACHE STRING "Additional options for pytest replay tests (default \"\")" + CACHE STRING "Additional options for Uranium tests (default \"\")" ) - # Also create AUTEST_OPTIONS_LIST from the string for use in cmake targets. - # This prevents cmake from escaping spaces in the arguments, which confuses - # the autest command. The original AUTEST_OPTIONS string is used in the - # autest.sh script. - separate_arguments(AUTEST_OPTIONS_LIST UNIX_COMMAND "${AUTEST_OPTIONS}") - separate_arguments(PYTEST_OPTIONS_LIST UNIX_COMMAND "${PYTEST_OPTIONS}") + if(AUTEST_OPTIONS) + message(DEPRECATION "AUTEST_OPTIONS is deprecated; use URTEST_OPTIONS") + string(PREPEND URTEST_OPTIONS "${AUTEST_OPTIONS} ") + endif() + if(PYTEST_OPTIONS) + message(DEPRECATION "PYTEST_OPTIONS is deprecated; use URTEST_OPTIONS") + string(APPEND URTEST_OPTIONS " ${PYTEST_OPTIONS}") + endif() file(READ "${CMAKE_SOURCE_DIR}/tests/proxy-verifier-version.txt" PROXY_VERIFIER_VERSION) string(STRIP "${PROXY_VERIFIER_VERSION}" PROXY_VERIFIER_VERSION) file(READ "${CMAKE_SOURCE_DIR}/tests/proxy-verifier-checksum.txt" PROXY_VERIFIER_SHA1) @@ -933,7 +935,7 @@ add_subdirectory(src/traffic_via) if(ENABLE_CRIPTS) add_subdirectory(src/cripts) endif() -if(ENABLE_AUTEST) +if(ENABLE_URTEST) add_subdirectory(tests) endif() if(ENABLE_FUZZING) diff --git a/CMakePresets.json b/CMakePresets.json index 68bbb2d4e4a..0f91e81030c 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -52,25 +52,29 @@ } }, { - "name": "autest", + "name": "urtest", + "displayName": "Uranium tests", + "description": "Build and run the ATS Uranium test suite", "inherits": ["default"], - "binaryDir": "${sourceDir}/build-autest", + "binaryDir": "${sourceDir}/build-urtest", "cacheVariables": { - "ENABLE_AUTEST": "ON", - "CMAKE_INSTALL_PREFIX": "/tmp/ts-autest", + "ENABLE_URTEST": "ON", + "CMAKE_INSTALL_PREFIX": "/tmp/ts-urtest", "BUILD_EXPERIMENTAL_PLUGINS": "ON", "ENABLE_WASM_WAMR": "OFF", "ENABLE_EXAMPLE": "ON" } }, { - "name": "autest-uds", + "name": "urtest-uds", + "displayName": "Uranium tests over Unix sockets", + "description": "Build and run the ATS Uranium test suite with curl over Unix sockets", "inherits": ["default"], - "binaryDir": "${sourceDir}/build-autest", + "binaryDir": "${sourceDir}/build-urtest", "cacheVariables": { - "ENABLE_AUTEST": "ON", - "ENABLE_AUTEST_UDS": "ON", - "CMAKE_INSTALL_PREFIX": "/tmp/ts-autest", + "ENABLE_URTEST": "ON", + "ENABLE_URTEST_UDS": "ON", + "CMAKE_INSTALL_PREFIX": "/tmp/ts-urtest", "BUILD_EXPERIMENTAL_PLUGINS": "ON", "ENABLE_EXAMPLE": "ON" } @@ -225,10 +229,10 @@ } }, { - "name": "ci-fedora-autest", - "displayName": "CI Fedora Autest", - "description": "CI Pipeline config for Fedora Linux (autest build)", - "inherits": ["ci-fedora", "autest"] + "name": "ci-fedora-urtest", + "displayName": "CI Fedora Uranium tests", + "description": "CI pipeline configuration for the ATS Uranium test suite", + "inherits": ["ci-fedora", "urtest"] }, { "name": "ci-freebsd", @@ -337,7 +341,7 @@ "displayName": "CI branch Quiche", "inherits": ["branch"], "cacheVariables": { - "ENABLE_AUTEST": "ON", + "ENABLE_URTEST": "ON", "nuraft_ROOT": "/opt/nuraft-boringssl", "OPENSSL_ROOT_DIR": "/opt/h3-tools-boringssl/boringssl", "quiche_ROOT": "/opt/h3-tools-boringssl/quiche", @@ -349,7 +353,7 @@ "displayName": "CI branch Quiche", "inherits": ["branch"], "cacheVariables": { - "ENABLE_AUTEST": "ON", + "ENABLE_URTEST": "ON", "nuraft_ROOT": "/opt", "OPENSSL_ROOT_DIR": "/opt/openssl-quic/", "quiche_ROOT": "/opt/quiche", @@ -365,22 +369,22 @@ } }, { - "name": "branch-autest", - "displayName": "CI branch autest", + "name": "branch-urtest", + "displayName": "CI branch Uranium tests", "inherits": ["branch"], "cacheVariables": { - "ENABLE_AUTEST": "ON", + "ENABLE_URTEST": "ON", "ENABLE_EXAMPLE": "ON", "ENABLE_CRIPTS": "ON" } }, { - "name": "branch-autest-uds", - "displayName": "CI branch autest", + "name": "branch-urtest-uds", + "displayName": "CI branch Uranium tests over Unix sockets", "inherits": ["branch"], "cacheVariables": { - "ENABLE_AUTEST": "ON", - "ENABLE_AUTEST_UDS": "ON", + "ENABLE_URTEST": "ON", + "ENABLE_URTEST_UDS": "ON", "ENABLE_EXAMPLE": "ON", "ENABLE_CRIPTS": "ON" } @@ -441,7 +445,7 @@ "name": "branch-coverage", "displayName": "CI branch coverage", "description": "Defaults for branch coverage builds", - "inherits": ["branch-autest"], + "inherits": ["branch-urtest"], "cacheVariables": { "CMAKE_CXX_FLAGS_DEBUG": "--coverage", "CMAKE_C_FLAGS_DEBUG": "--coverage" @@ -450,9 +454,9 @@ ], "buildPresets": [ { - "name": "autest", - "configurePreset": "autest", - "targets": ["autest"] + "name": "urtest", + "configurePreset": "urtest", + "targets": ["urtest"] } ] } diff --git a/ci/coverage b/ci/coverage index 97a83ffbbaf..f20e976b205 100755 --- a/ci/coverage +++ b/ci/coverage @@ -16,8 +16,8 @@ # See the License for the specific language governing permissions and # limitations under the License. -# Build ATS with gcov instrumentation, run the unit tests (and optionally -# autests), and produce a coverage report with gcovr. +# Build ATS with gcov instrumentation, run the unit and optional Uranium +# tests, and produce a coverage report with gcovr. # # Usage: # ci/coverage [extra cmake args...] @@ -27,8 +27,8 @@ # PREFIX install prefix (default: /tmp/ts-coverage) # NPROCS build parallelism (default: nproc) # GCOVR gcovr command (default: gcovr, falls back to uvx gcovr) -# AUTEST run autests too if set to a -f/--filter basename glob, -# e.g. AUTEST='tls_*' or AUTEST=all for the whole suite +# URTEST run Uranium tests too if set to a basename glob, +# e.g. URTEST='tls_*' or URTEST=all for the whole suite # # Requires: cmake, ninja or make, gcc/gcov, and gcovr (pip install gcovr). @@ -55,11 +55,11 @@ fi COVERAGE_FLAGS="-g -O0 --coverage -fprofile-update=atomic -fprofile-abs-path" configure() { - # Enabling autest pulls in Python3/uv/nc and the proxy-verifier toolchain at - # configure time, so only ask for it when AUTEST actually selects a run. - local autest_args=() - if [ -n "$AUTEST" ]; then - autest_args=(-DENABLE_AUTEST=ON) + # Enabling Uranium tests pulls in Python3/uv/nc and Proxy Verifier at + # configure time, so only ask for it when URTEST selects a run. + local urtest_args=() + if [ -n "$URTEST" ]; then + urtest_args=(-DENABLE_URTEST=ON) fi cmake -S "$SRCROOT" -B "$BUILDDIR" \ @@ -67,7 +67,7 @@ configure() { -DCMAKE_INSTALL_PREFIX="$PREFIX" \ -DCMAKE_COMPILE_WARNING_AS_ERROR=OFF \ -DBUILD_TESTING=ON \ - "${autest_args[@]}" \ + "${urtest_args[@]}" \ -DBUILD_EXPERIMENTAL_PLUGINS=ON \ -DENABLE_EXAMPLE=ON \ -DCMAKE_CXX_FLAGS_DEBUG="$COVERAGE_FLAGS" \ @@ -86,18 +86,16 @@ run_tests() { ctest --test-dir "$BUILDDIR" -j "$NPROCS" - if [ -n "$AUTEST" ]; then + if [ -n "$URTEST" ]; then local filter_args=() - if [ "$AUTEST" != all ]; then - # Quote the pattern: the parallel runner fnmatches it against each test's - # basename (e.g. 'tls_*'), so it must reach autest verbatim rather than be + if [ "$URTEST" != all ]; then + # Quote the pattern: the runner matches it against each test's basename + # (e.g. 'tls_*'), so it must reach urtest verbatim rather than be # glob-expanded by the shell first. - filter_args=(-f "$AUTEST") + filter_args=(-f "$URTEST") fi - # Match ctest's parallelism: -j routes autest.sh through autest-parallel.py, - # which already aims --sandbox at a per-build-tree path under /tmp, so - # concurrent coverage runs in separate build trees don't trample each other. - (cd "$BUILDDIR/tests" && ./autest.sh -j "$NPROCS" "${filter_args[@]}") + # Match ctest's parallelism while retaining a per-build-tree sandbox. + (cd "$BUILDDIR/tests" && ./urtest.sh -j "$NPROCS" "${filter_args[@]}") fi } diff --git a/ci/jenkins/bin/autest.sh b/ci/jenkins/bin/autest.sh index 551574b060a..e57ecb71266 100755 --- a/ci/jenkins/bin/autest.sh +++ b/ci/jenkins/bin/autest.sh @@ -120,7 +120,7 @@ if [ "x${pv_version}" != "x" ]; then fi ${AUTEST} \ - -D ./tests/gold_tests \ + -D ./tests/uranium_tests \ --sandbox "$SANDBOX" \ --ats-bin "${INSTALL}/bin" \ $PROXY_VERIFIER_ARGUMENT \ diff --git a/ci/rat-exclude.txt b/ci/rat-exclude.txt index a3271fb6a38..9f4d40b1d7d 100644 --- a/ci/rat-exclude.txt +++ b/ci/rat-exclude.txt @@ -76,8 +76,8 @@ blib/** **/systemtap/** **/swoc/** **/highway/** -tests/gold_tests/autest-site/min_cfg -tests/gold_tests/h2/rules/huge_resp_hdrs.conf +tests/uranium_tests/autest-site/min_cfg +tests/uranium_tests/h2/rules/huge_resp_hdrs.conf tools/http_load/** **/clang-tidy.conf build*/** diff --git a/ci/regression b/ci/regression index 1f83f6eeae3..9afb85c86f2 100755 --- a/ci/regression +++ b/ci/regression @@ -105,7 +105,7 @@ build() { regress() { ( cd $OBJROOT && $MAKE check ) && \ $DSTROOT/bin/traffic_server -k -K -R 1 && \ - $SRCROOT/tests/autest.sh --ats-bin=$DSTROOT/bin + $SRCROOT/tests/urtest.sh --ats-bin=$DSTROOT/bin } CC=${CC:-$(prog cc)} diff --git a/contrib/docker/ubuntu/noble/Dockerfile b/contrib/docker/ubuntu/noble/Dockerfile index d5c90227dfa..b5d1938dc82 100644 --- a/contrib/docker/ubuntu/noble/Dockerfile +++ b/contrib/docker/ubuntu/noble/Dockerfile @@ -209,7 +209,7 @@ RUN git clone --depth 1 -b ${ATS_VERSION} https://github.com/apache/trafficserve -DCMAKE_BUILD_TYPE=Release \ -DBUILD_TESTING=OFF \ -DBUILD_REGRESSION_TESTING=OFF \ - -DENABLE_AUTEST=OFF \ + -DENABLE_URTEST=OFF \ -DBUILD_EXPERIMENTAL_PLUGINS=ON \ -DENABLE_JEMALLOC=ON \ -DENABLE_MALLOC_ALLOCATOR=ON \ diff --git a/doc/admin-guide/files/records.yaml.en.rst b/doc/admin-guide/files/records.yaml.en.rst index 25c4bec22ef..12a1a4d46c2 100644 --- a/doc/admin-guide/files/records.yaml.en.rst +++ b/doc/admin-guide/files/records.yaml.en.rst @@ -2032,9 +2032,10 @@ Origin Server Connect Attempts :overridable: Specifies how long (in seconds) |TS| remembers that an origin server was unreachable. - During this window, if a stale cached response exists and its age is within the cached response's ``max-age`` plus - :ts:cv:`proxy.config.http.cache.max_stale_age`, |TS| serves the stale content directly - without attempting to contact the origin server. + During this window, if a stale cached response exists and its age is within the limits configured by + :ts:cv:`proxy.config.http.cache.max_stale_age` and + :ts:cv:`proxy.config.http.cache.max_stale_age_percent`, |TS| serves the stale content directly without attempting + to contact the origin server. .. ts:cv:: CONFIG proxy.config.http.uncacheable_requests_bypass_parent INT 1 :reloadable: @@ -2128,8 +2129,8 @@ Negative Response Caching current stale content is preserved and served. Note this is considered only on a revalidation of already cached content. A revalidation failure means a connection failure or a 50x response code. When considering replying with a stale response in these negative revalidating circumstances, - |TS| will respect the :ts:cv:`proxy.config.http.cache.max_stale_age` configuration and will not - use a cached response older than ``max_stale_age`` seconds plus ``max-age`` of cached content. + |TS| will respect the :ts:cv:`proxy.config.http.cache.max_stale_age` and + :ts:cv:`proxy.config.http.cache.max_stale_age_percent` limits. A value of ``0`` disables serving stale content and a value of ``1`` enables keeping and serving stale content if revalidation fails. @@ -2155,7 +2156,8 @@ Negative Response Caching behavior with regard to whether it considers a stale object to be fresh enough to serve out of cache when revalidation fails. As mentioned above in :ts:cv:`proxy.config.http.negative_revalidating_enabled`, - :ts:cv:`proxy.config.http.cache.max_stale_age` is used for that determination. + :ts:cv:`proxy.config.http.cache.max_stale_age` and + :ts:cv:`proxy.config.http.cache.max_stale_age_percent` are used for that determination. This configuration defaults to 1,800 seconds (30 minutes). @@ -2654,7 +2656,26 @@ Cache Control :reloadable: :overridable: - The maximum age in seconds allowed for a stale response before it cannot be cached. + The maximum number of seconds that a stale response can be served after its ``max-age`` freshness + lifetime has passed. + + When :ts:cv:`proxy.config.http.cache.max_stale_age_percent` is enabled, the smaller of the + absolute and percentage limits is used. + +.. ts:cv:: CONFIG proxy.config.http.cache.max_stale_age_percent INT 0 + :reloadable: + :overridable: + + Limits the stale response lifetime to a percentage of its ``max-age`` freshness lifetime. The + valid range is ``0`` to ``100``. A value of ``0`` disables the percentage limit. When enabled, + |TS| uses the smaller of this limit and + :ts:cv:`proxy.config.http.cache.max_stale_age`. + + For example, a value of ``25`` allows a response with a one-hour freshness lifetime to be served + stale for up to 15 minutes, provided that the absolute stale age limit is at least 15 minutes. + + Responses without ``max-age`` are controlled by :ts:cv:`proxy.config.http.cache.max_stale_age` + alone. .. ts:cv:: CONFIG proxy.config.http.cache.guaranteed_min_lifetime INT 0 :reloadable: @@ -3241,13 +3262,15 @@ Dynamic Content & Content Negotiation ===== ====================================================================== ``0`` Default. Disable cache and go to origin server. ``1`` Return a ``502`` error on a cache miss. - ``2`` Serve stale if object's age is under ``max-age`` + - :ts:cv:`proxy.config.http.cache.max_stale_age`. Otherwise, go to - origin server. + ``2`` Serve stale if the object's age is within the + :ts:cv:`proxy.config.http.cache.max_stale_age` and + :ts:cv:`proxy.config.http.cache.max_stale_age_percent` limits. + Otherwise, go to the origin server. ``3`` Return a ``502`` error on a cache miss or serve stale on a cache - revalidate if object's age is under ``max-age`` + - :ts:cv:`proxy.config.http.cache.max_stale_age`. Otherwise, go to - origin server. + revalidate if the object's age is within the + :ts:cv:`proxy.config.http.cache.max_stale_age` and + :ts:cv:`proxy.config.http.cache.max_stale_age_percent` limits. + Otherwise, go to the origin server. ``4`` Return a ``502`` error on either a cache miss or on a revalidation. ``5`` Retry Cache Read on a Cache Write Lock failure. This option together with :ts:cv:`proxy.config.cache.enable_read_while_writer` configuration diff --git a/doc/admin-guide/plugins/lua.en.rst b/doc/admin-guide/plugins/lua.en.rst index c5e7f784ce3..0716e89c115 100644 --- a/doc/admin-guide/plugins/lua.en.rst +++ b/doc/admin-guide/plugins/lua.en.rst @@ -4822,6 +4822,7 @@ Http config constants TS_LUA_CONFIG_BODY_FACTORY_RESPONSE_SUPPRESSION_MODE TS_LUA_CONFIG_HTTP_CACHE_POST_METHOD TS_LUA_CONFIG_HTTP_CACHE_TARGETED_CACHE_CONTROL_HEADERS + TS_LUA_CONFIG_HTTP_CACHE_MAX_STALE_AGE_PERCENT TS_LUA_CONFIG_LAST_ENTRY :ref:`TOP ` diff --git a/doc/admin-guide/plugins/txn_box/examples.en.rst b/doc/admin-guide/plugins/txn_box/examples.en.rst index a4d9ca8e335..445ca92248a 100755 --- a/doc/admin-guide/plugins/txn_box/examples.en.rst +++ b/doc/admin-guide/plugins/txn_box/examples.en.rst @@ -39,7 +39,7 @@ Goal Force all proxy requests to have a value for the "Accept-Encoding" field. If not already set, it should be set to "identity". -.. literalinclude:: ../../../../tests/gold_tests/pluginTest/txn_box/example/accept-encoding.replay.yaml +.. literalinclude:: ../../../../tests/uranium_tests/pluginTest/txn_box/example/accept-encoding.replay.yaml :start-after: doc.start :end-before: doc.end @@ -57,7 +57,7 @@ paths. The paths for the two hosts are identical, only the host for the request The simplest way to do this would be -.. literalinclude:: ../../../../tests/gold_tests/pluginTest/txn_box/ramp/multi-ramp-1.cfg.yaml +.. literalinclude:: ../../../../tests/uranium_tests/pluginTest/txn_box/ramp/multi-ramp-1.cfg.yaml :start-after: doc.start :end-before: doc.end @@ -72,7 +72,7 @@ This can be done in another way by generating the random value once and checking Given the no backtrack rule, this is challenging to do by checking the percentage first. Instead the use of tuples makes it possible to check both the value and the path together. -.. literalinclude:: ../../../../tests/gold_tests/pluginTest/txn_box/ramp/multi-ramp-2.cfg.yaml +.. literalinclude:: ../../../../tests/uranium_tests/pluginTest/txn_box/ramp/multi-ramp-2.cfg.yaml :start-after: doc.start :end-before: doc.end @@ -84,7 +84,7 @@ path. Because there is no nested :drtv:`with` there is no need to backtrack. It might be reasonable to split every path in to a different bucket to make adjusting the percentage easier. In that case the previous example could be changed to look like -.. literalinclude:: ../../../../tests/gold_tests/pluginTest/txn_box/ramp/multi-ramp-3.cfg.yaml +.. literalinclude:: ../../../../tests/uranium_tests/pluginTest/txn_box/ramp/multi-ramp-3.cfg.yaml :lines: 1-12,16-17 This style presumes the bucket action is identical for all buckets. If not, the previous style would @@ -104,7 +104,7 @@ Goal Example configuration -.. literalinclude:: ../../../../tests/gold_tests/pluginTest/txn_box/static_file/static_file.replay.yaml +.. literalinclude:: ../../../../tests/uranium_tests/pluginTest/txn_box/static_file/static_file.replay.yaml :start-after: doc-1--> :end-before: doc-1--< @@ -112,7 +112,7 @@ This checks on the upstream response. If the status is 404 (not found) and the p "security.txt" then change the response to a 200 and provide a hard wired default for the content. The text is retrieved via a YAML reference to an anchor. -.. literalinclude:: ../../../../tests/gold_tests/pluginTest/txn_box/static_file/static_file.replay.yaml +.. literalinclude:: ../../../../tests/uranium_tests/pluginTest/txn_box/static_file/static_file.replay.yaml :start-after: doc-secure-text--> :end-before: doc-secure-text--< @@ -123,7 +123,7 @@ the anchor and reference in the previous configuration, to make sure the exact s both cases. Note this is done during YAML parsing, not at runtime, and is identical to using literal strings in both cases. -.. literalinclude:: ../../../../tests/gold_tests/pluginTest/txn_box/static_file/static_file.replay.yaml +.. literalinclude:: ../../../../tests/uranium_tests/pluginTest/txn_box/static_file/static_file.replay.yaml :start-after: doc-proxy-rsp--> :end-before: doc-proxy-rsp--< @@ -147,14 +147,14 @@ that pushes default tokens to the file "/var/www/jwt/default-token.jwt", a text to load that file and check it for changes every 12 hours. If the file is missing, a special marker "N/A" that signals this problem to the upstream. -.. literalinclude:: ../../../../tests/gold_tests/pluginTest/txn_box/static_file/static_file.replay.yaml +.. literalinclude:: ../../../../tests/uranium_tests/pluginTest/txn_box/static_file/static_file.replay.yaml :start-after: doc-jwt--> :end-before: doc-jwt--< To use this, the proxy request is checked for the "Author-i-tay" field. If set it is passed through on the presumption it is a valid token. If not, then the default token is added. -.. literalinclude:: ../../../../tests/gold_tests/pluginTest/txn_box//static_file/static_file.replay.yaml +.. literalinclude:: ../../../../tests/uranium_tests/pluginTest/txn_box//static_file/static_file.replay.yaml :start-after: doc-jwt-apply--> :end-before: doc-jwt-apply--< @@ -172,7 +172,7 @@ discarded. If the parameter "ma=0" is present, then the base file name must have attached. If the parameter "mc=1" is present, then the base filename must have the string "_mac" attached. If both are present then both strings are added. -.. literalinclude:: ../../../../tests/gold_tests/pluginTest/txn_box/prod/vznith-1.replay.yaml +.. literalinclude:: ../../../../tests/uranium_tests/pluginTest/txn_box/prod/vznith-1.replay.yaml :start-after: doc-start :end-before: doc-end @@ -211,7 +211,7 @@ and The basic setup is to check the values on the client certificate and set the ``Authorization`` field if valid and remove it if not. -.. literalinclude:: ../../../../tests/gold_tests/pluginTest/txn_box/prod/mTLS.txnbox.yaml +.. literalinclude:: ../../../../tests/uranium_tests/pluginTest/txn_box/prod/mTLS.txnbox.yaml This checks two values - the issuer (authentication) and the subject field (authorization). diff --git a/doc/admin-guide/plugins/txn_box/user/DirectiveReference.en.rst b/doc/admin-guide/plugins/txn_box/user/DirectiveReference.en.rst index f53ae06aab7..e4652065431 100755 --- a/doc/admin-guide/plugins/txn_box/user/DirectiveReference.en.rst +++ b/doc/admin-guide/plugins/txn_box/user/DirectiveReference.en.rst @@ -379,7 +379,7 @@ Transaction This could be done using :drtv:`proxy-reply` and :drtv:`when` but is a common enough case to warrant a directive. - .. literalinclude:: ../../../../../tests/gold_tests/pluginTest/txn_box/basic/redirect.replay.yaml + .. literalinclude:: ../../../../../tests/uranium_tests/pluginTest/txn_box/basic/redirect.replay.yaml :start-after: doc-redirect-reply-form-< :end-before: doc-redirect-reply-form-> diff --git a/doc/admin-guide/plugins/txn_box/user/ModifierReference.en.rst b/doc/admin-guide/plugins/txn_box/user/ModifierReference.en.rst index 759a98cb32a..5425e5f65e7 100755 --- a/doc/admin-guide/plugins/txn_box/user/ModifierReference.en.rst +++ b/doc/admin-guide/plugins/txn_box/user/ModifierReference.en.rst @@ -118,7 +118,7 @@ Modifiers This can be used to append separated strings even on empty fields. For instance, to make sure the list of rock bands contains "Delain", it would be - .. literalinclude:: ../../../../../tests/gold_tests/pluginTest/txn_box/basic/mod.replay.yaml + .. literalinclude:: ../../../../../tests/uranium_tests/pluginTest/txn_box/basic/mod.replay.yaml :start-after: doc-concat-empty-< :end-before: doc-concat-empty-> @@ -213,13 +213,13 @@ Modifiers The following example shows how a particular field can be encoded. - .. literalinclude:: ../../../../../tests/gold_tests/pluginTest/txn_box/basic/mod.replay.yaml + .. literalinclude:: ../../../../../tests/uranium_tests/pluginTest/txn_box/basic/mod.replay.yaml :start-after: doc-url-encode-< :end-before: doc-url-encode-> Perform a redirect and apply the :mod:`url-encode` to :ex:`pre-remap-query` in combination with :mod:`concat`. - .. literalinclude:: ../../../../../tests/gold_tests/pluginTest/txn_box/basic/redirect.replay.yaml + .. literalinclude:: ../../../../../tests/uranium_tests/pluginTest/txn_box/basic/redirect.replay.yaml :start-after: doc-redirect-url-encode-form-< :end-before: doc-redirect-url-encode-form-> @@ -232,13 +232,13 @@ Modifiers The following example shows how a particular field can be encoded. - .. literalinclude:: ../../../../../tests/gold_tests/pluginTest/txn_box/basic/mod.replay.yaml + .. literalinclude:: ../../../../../tests/uranium_tests/pluginTest/txn_box/basic/mod.replay.yaml :start-after: doc-url-decode-< :end-before: doc-url-decode-> Perform a redirect and apply the :mod:`url-decode` to :ex:`pre-remap-query` in combination with :mod:`concat`. - .. literalinclude:: ../../../../../tests/gold_tests/pluginTest/txn_box/basic/redirect.replay.yaml + .. literalinclude:: ../../../../../tests/uranium_tests/pluginTest/txn_box/basic/redirect.replay.yaml :start-after: doc-redirect-url-decode-form-< :end-before: doc-redirect-url-decode-form-> diff --git a/doc/appendices/command-line/traffic_ctl.en.rst b/doc/appendices/command-line/traffic_ctl.en.rst index 6d50dce4cac..0d4c7e19cb5 100644 --- a/doc/appendices/command-line/traffic_ctl.en.rst +++ b/doc/appendices/command-line/traffic_ctl.en.rst @@ -1507,8 +1507,8 @@ Configure Traffic Server to insert ``Via`` header in the response to the client # traffic_ctl config set proxy.config.http.insert_response_via_str 1 # traffic_ctl config reload -Autest -====== +Uranium Tests +================ Runroot needs to be configured in order to let `traffic_ctl` know where to find the socket. This is done by default and there is no change you have to do to interact with it, but make sure that you are not overriding the `dump_runroot=False` diff --git a/doc/developer-guide/api/functions/TSHttpHookAdd.en.rst b/doc/developer-guide/api/functions/TSHttpHookAdd.en.rst index e07ae270c27..cae53ceab97 100644 --- a/doc/developer-guide/api/functions/TSHttpHookAdd.en.rst +++ b/doc/developer-guide/api/functions/TSHttpHookAdd.en.rst @@ -146,7 +146,7 @@ transaction hooks:: } For more example code using hooks, see the test_hooks plugin in tests/tools/plugins (used by the test_hooks.test.py -Gold test). +Uranium test). See Also ======== diff --git a/doc/developer-guide/api/functions/TSHttpOverridableConfig.en.rst b/doc/developer-guide/api/functions/TSHttpOverridableConfig.en.rst index 43d98b40ad7..19e992d2599 100644 --- a/doc/developer-guide/api/functions/TSHttpOverridableConfig.en.rst +++ b/doc/developer-guide/api/functions/TSHttpOverridableConfig.en.rst @@ -199,6 +199,7 @@ TSOverridableConfigKey Value Confi :enumerator:`TS_CONFIG_BODY_FACTORY_RESPONSE_SUPPRESSION_MODE` :ts:cv:`proxy.config.body_factory.response_suppression_mode` :enumerator:`TS_CONFIG_HTTP_CACHE_POST_METHOD` :ts:cv:`proxy.config.http.cache.post_method` :enumerator:`TS_CONFIG_HTTP_CACHE_TARGETED_CACHE_CONTROL_HEADERS` :ts:cv:`proxy.config.http.cache.targeted_cache_control_headers` +:enumerator:`TS_CONFIG_HTTP_CACHE_MAX_STALE_AGE_PERCENT` :ts:cv:`proxy.config.http.cache.max_stale_age_percent` ======================================================================== ==================================================================== Examples diff --git a/doc/developer-guide/api/types/TSOverridableConfigKey.en.rst b/doc/developer-guide/api/types/TSOverridableConfigKey.en.rst index 4ce7c1712d6..d9ae419c296 100644 --- a/doc/developer-guide/api/types/TSOverridableConfigKey.en.rst +++ b/doc/developer-guide/api/types/TSOverridableConfigKey.en.rst @@ -166,6 +166,7 @@ Enumeration Members .. enumerator:: TS_CONFIG_HTTP_CACHE_IGNORE_QUERY .. enumerator:: TS_CONFIG_HTTP_CACHE_POST_METHOD .. enumerator:: TS_CONFIG_HTTP_CACHE_TARGETED_CACHE_CONTROL_HEADERS +.. enumerator:: TS_CONFIG_HTTP_CACHE_MAX_STALE_AGE_PERCENT Description diff --git a/doc/developer-guide/cache-architecture/shm-fast-restart.en.rst b/doc/developer-guide/cache-architecture/shm-fast-restart.en.rst index 14cb8f9f45b..dd5177ec93d 100644 --- a/doc/developer-guide/cache-architecture/shm-fast-restart.en.rst +++ b/doc/developer-guide/cache-architecture/shm-fast-restart.en.rst @@ -550,7 +550,7 @@ A stripe filled and cleanly shut down before the first periodic dir sync (:ts:cv:`proxy.config.cache.dir.sync_frequency`, 60 s by default) is exactly that case: skipping the shutdown write leaves ``sync_serial == 0`` on disk, and the next start that cannot use the segment finds an empty directory and silently -loses every object. The ``cache_shm_dir_invalid`` autest covers this. +loses every object. The ``cache_shm_dir_invalid`` Uranium test covers this. So the shutdown write stays. It costs what it cost before the feature existed, and it buys the guarantee that the fallback path is always recoverable. Only @@ -809,8 +809,8 @@ The pure trust-gate logic is unit-tested in storage-signature topology sensitivity, control-header round-trip, the macOS name-length limit, and the process-liveness check). -The end-to-end behavior is covered by autests in -:ts:git:`tests/gold_tests/cache/`, one scenario each: +The end-to-end behavior is covered by Uranium tests in +:ts:git:`tests/uranium_tests/cache/`, one scenario each: .. list-table:: :header-rows: 1 diff --git a/doc/developer-guide/config-reload-framework.en.rst b/doc/developer-guide/config-reload-framework.en.rst index e6e327d6701..0cb77fa1250 100644 --- a/doc/developer-guide/config-reload-framework.en.rst +++ b/doc/developer-guide/config-reload-framework.en.rst @@ -993,9 +993,9 @@ After registering a new handler: 7. Use :option:`traffic_ctl config status` ``--format json`` to inspect the raw :ref:`get_reload_config_status` response for automation testing. -**Autests** — the project includes autest helpers for config reload testing. +**Uranium tests** — the project includes compatibility helpers for config reload testing. -For **end-to-end tests** that trigger a reload via ``traffic_ctl`` and validate the result, use +For **Uranium tests** that trigger a reload via ``traffic_ctl`` and validate the result, use the :ref:`autest-config-reload` extension (``Test.AddConfigReload()``). This is the recommended approach for most reload tests. @@ -1004,11 +1004,11 @@ use ``AddJsonRPCClientRequest`` with ``Request.admin_config_reload()`` to trigge ``Testers.CustomJSONRPCResponse`` to validate responses programmatically. See the existing tests for examples: -- ``tests/gold_tests/jsonrpc/config_reload_tracking.test.py`` — token generation, status +- ``tests/uranium_tests/jsonrpc/config_reload_tracking.test.py`` — token generation, status queries, history, force reload, duplicate token rejection. -- ``tests/gold_tests/jsonrpc/config_reload_rpc.test.py`` — inline reload, multiple configs, +- ``tests/uranium_tests/jsonrpc/config_reload_rpc.test.py`` — inline reload, multiple configs, ``FileOnly`` rejection, large payloads. -- ``tests/gold_tests/jsonrpc/config_reload_failures.test.py`` — error handling, broken configs, +- ``tests/uranium_tests/jsonrpc/config_reload_failures.test.py`` — error handling, broken configs, handler failure reporting. diff --git a/doc/developer-guide/jsonrpc/jsonrpc-handler-development.en.rst b/doc/developer-guide/jsonrpc/jsonrpc-handler-development.en.rst index 4ed55c7f702..58c9588317f 100644 --- a/doc/developer-guide/jsonrpc/jsonrpc-handler-development.en.rst +++ b/doc/developer-guide/jsonrpc/jsonrpc-handler-development.en.rst @@ -467,13 +467,13 @@ For more information check the :func:`TSRPCRegister` for API details and :ref:` Unit test ========= -All new methods exposed through the RPC server can be tested using the jsonrpc autest extension. +All new methods exposed through the RPC server can be tested using the JSONRPC Uranium compatibility extension. jsonrpc_client.test.text ------------------------ This extension provides the ability to interact with the JSONRPC interface by using :program:`traffic_ctl` as a client. As a helper -for all new autest that needs to write and read jsonrpc message, there is also a new module `jsonrpc.py` which provides +for every new compatibility test that needs to write and read jsonrpc message, there is also a new module `jsonrpc.py` which provides a nice and easy interface to write methods and notifications. This extension also provides the facility to write custom jsonrpc validations. Please check some of the following examples: diff --git a/doc/developer-guide/testing/config-reload-ext.en.rst b/doc/developer-guide/testing/config-reload-ext.en.rst index 92e5ba15705..7d42d47f8c1 100644 --- a/doc/developer-guide/testing/config-reload-ext.en.rst +++ b/doc/developer-guide/testing/config-reload-ext.en.rst @@ -27,7 +27,7 @@ replace the legacy pattern of fire-and-forget ``traffic_ctl config reload`` followed by log grepping with a deterministic, structured approach. The extension is loaded automatically from -``tests/gold_tests/autest-site/config_reload.test.ext``. +``tests/uranium_tests/autest-site/config_reload.test.ext``. Why Use This Extension diff --git a/doc/developer-guide/testing/index.en.rst b/doc/developer-guide/testing/index.en.rst index d14c2542e5f..ad8a2a1790c 100644 --- a/doc/developer-guide/testing/index.en.rst +++ b/doc/developer-guide/testing/index.en.rst @@ -25,5 +25,5 @@ Testing Traffic Server .. toctree:: :maxdepth: 2 - autests.en + uranium-tests.en config-reload-ext.en diff --git a/doc/developer-guide/testing/autests.en.rst b/doc/developer-guide/testing/uranium-tests.en.rst similarity index 75% rename from doc/developer-guide/testing/autests.en.rst rename to doc/developer-guide/testing/uranium-tests.en.rst index fe8fe3ee8c5..c93f5a956c3 100644 --- a/doc/developer-guide/testing/autests.en.rst +++ b/doc/developer-guide/testing/uranium-tests.en.rst @@ -17,104 +17,144 @@ .. include:: ../../common.defs -.. _autest-writing: +.. _uranium-tests: -Writing End-to-End Tests -************************ +Writing Uranium Tests +********************* -|TS| uses Catch2 for unit tests and pytest plus AuTest for end-to-end tests. -Catch2 tests reside next to the associated source code. End-to-end tests are -under ``tests/gold_tests/``. New replay-driven tests are collected directly by -pytest; tests that need bespoke process orchestration continue to use AuTest. +|TS| uses Catch2 for unit tests and pytest for Uranium tests. Catch2 tests +reside next to the associated source code. Uranium tests are under +``tests/uranium_tests/``. Replay-driven tests execute natively. Existing bespoke +Python definitions are also pytest items, with AuTest retained as a pinned +compatibility backend while those definitions migrate. * For catch test framework documentation, see: https://github.com/catchorg/Catch2 * For AuTest framework documentation, see: https://autestsuite.bitbucket.io/index.html -This document focuses on end-to-end tests because Catch2 documentation is +This document focuses on Uranium tests because Catch2 documentation is available elsewhere. +Why "Uranium"? +=============== + +Uranium is enormously useful: as nuclear fuel, it powers homes with reliable, +low-carbon electricity. It also demands careful handling. The name is +deliberately tongue-in-cheek because these system tests have a similar dual +nature: they provide indispensable confidence in a complete |TS| deployment, +but can be so unstable as to be toxic when process lifecycles, timing, network +state, and artifacts are not carefully contained. The name is a reminder to +keep every test isolated, deterministic, and safe to run. + File Structure and Naming -========================== +========================= -The end-to-end runners use distinct filename conventions: +The Uranium test runners use distinct filename conventions: - Tests are placed in appropriate subdirectories under - ``tests/gold_tests/`` (e.g., ``cache/``, ``pluginTest/``, ``tls/``, + ``tests/uranium_tests/`` (e.g., ``cache/``, ``pluginTest/``, ``tls/``, etc.) - Replay-driven pytest files have a descriptive ``.test.yaml`` extension. The file is both the test registration and the Proxy Verifier replay. -- Bespoke AuTest files have a descriptive ``.test.py`` extension. Use these - only when the direct replay runner cannot express the required client, - server, or lifecycle. -- ``tests/gold_tests/autest-site`` is a special directory. AuTest, a general +- Bespoke compatibility files have a descriptive ``.test.py`` extension. + Pytest reports and schedules each as one item, then delegates its process + definition to the pinned AuTest backend. Do not add new compatibility files + when the direct replay runner can express the test. +- ``tests/uranium_tests/autest-site`` is a special directory. AuTest, a general testing framework, is extended to add domain specific support, |TS| in this case, via ``.test.ext`` extension files. The files in here customize the command line arguments recognized by the ``autest`` command, the functions availabe to the ``Test`` and ``TestRun`` AuTest objects, specific ``Process`` objects available to test, ``Skip`` conditions for individual tests, etc. -Running End-to-End Tests -======================== +Running Uranium Tests +===================== -Configure the build with ``-DENABLE_AUTEST=ON``. The ``autest`` build target -runs both direct pytest replays and the remaining AuTests, preserving the -existing CI entry point: +Configure the build with ``-DENABLE_URTEST=ON``. The ``urtest`` build target +runs the complete pytest-owned inventory: .. code-block:: bash - cmake --build build --target autest + cmake --build build --target urtest Run only the pytest replay suite with: .. code-block:: bash - cmake --build build --target pytest-replay + cmake --build build --target urtest-replay Pass pytest selection or concurrency options at configure time. For example: .. code-block:: bash - cmake -B build -DENABLE_AUTEST=ON -DPYTEST_OPTIONS="-n 4 -k cache-control" - cmake --build build --target pytest-replay + cmake -B build -DENABLE_URTEST=ON -DURTEST_OPTIONS="-n 4 -k cache-control" + cmake --build build --target urtest-replay -For a bespoke AuTest, use ``autest.sh`` directly: +Use ``urtest.sh`` directly for the shortest single-test workflow: .. code-block:: bash cmake --build build cmake --install build cd build/tests - ./autest.sh --sandbox /tmp/sbcursor --clean=none -f + ./urtest.sh -f For example, to run ``cache-auth.test.py``: .. code-block:: bash - ./autest.sh --sandbox /tmp/sbcursor --clean=none -f cache-auth + ./urtest.sh -f cache-auth + +The filter works for both ``.test.yaml`` and ``.test.py`` items and accepts +globs. Pytest's ``-k`` expressions are also available. To run tests in parallel, pass ``-j N`` where ``N`` is the number of worker processes. Each worker gets an isolated port range to avoid conflicts: .. code-block:: bash - ./autest.sh -j 10 --sandbox /tmp/sbcursor -f cache-auth -f cache-control + ./urtest.sh -j 10 --sandbox /tmp/sbcursor -f cache-auth -f cache-control + +Without ``-j``, tests run sequentially. Compatibility tests listed in +``tests/serial_tests.txt`` acquire an exclusive lock and never overlap other +Uranium test items. + +Container Execution +=================== + +The source-tree ``tests/urtest.sh`` command defaults to the official +``ci.trafficserver.apache.org/ats/fedora:44`` image. It configures an +incremental build under ``build-urtest-container`` and then runs pytest. Pass +``--run-in-docker`` to force this behavior or ``--no-run-in-docker`` to use the +current environment. A short ``/tmp`` sandbox avoids Unix socket path limits; +afterward, regular files are copied to ``build-urtest-container/sandbox`` on +the host. + +The default changes to direct execution only when both conditions are true: + +- A Docker, Podman, containerd, or Kubernetes marker indicates that the + process is containerized. +- ``/etc/os-release`` identifies Fedora 44. -Without ``-j``, tests run sequentially. +Thus Jenkins, which already launches the Fedora 44 image, does not attempt +nested Docker. The container launch matches CI with an init process, host +networking, and the ``SYS_PTRACE`` capability. It does not require +``--privileged`` and does not mount the host Docker socket. Recommended Approach: Direct Replay Tests ========================================== New tests should normally be a single ``.test.yaml`` file. Pytest recognizes -that suffix through the ATS testkit plugin, constructs isolated DNS, Proxy +that suffix through the Uranium testkit plugin, constructs isolated DNS, Proxy Verifier server, ATS, and Proxy Verifier client processes, and reports the file as one pytest item. ATS readiness is based on the fully-initialized log message, not merely an open port. The YAML format is parseable without executing Python and keeps the test -topology, ATS configuration, traffic, and validation together. Tests requiring -ad-hoc clients or servers can still use the generic AuTest syntax. +topology, ATS configuration, traffic, and validation together. Existing tests +requiring ad-hoc clients or servers remain supported through the ``.test.py`` +compatibility layer. -The traffic portion of the ``.test.yaml`` files specifies Proxy Verifier HTTP +The traffic portion of each ``.test.yaml`` file specifies Proxy Verifier HTTP traffic behavior and follows the replay and verification syntax described extensively in its project's README.md file here: https://github.com/yahoo/proxy-verifier @@ -140,10 +180,9 @@ hit. Replay File Structure ---------------------- -The replay file contains both the test configuration (in the ``autest`` YAML -node) and the traffic replay and verification specification (in the ``sessions`` -YAML node). The ``autest`` key retains its name for compatibility with existing -replay files. Here is an example: +The replay file contains both the Uranium test configuration (in the ``urtest`` +YAML node) and the traffic replay and verification specification (in the +``sessions`` YAML node). Here is an example: .. code-block:: yaml @@ -166,8 +205,8 @@ replay files. Here is an example: meta: version: "1.0" - # Configuration section for ATS pytest integration - autest: + # Configuration for the Uranium test runner + urtest: description: 'Test description for this scenario' # Optional (but typical) DNS configuration. @@ -318,10 +357,10 @@ replay files. Here is an example: Replay File Components ----------------------- -autest Configuration Section -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +The ``urtest`` Configuration Section +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -The ``autest`` section configures the test environment: +The ``urtest`` section configures the test environment: - **description** or **summary** (one required): A brief description of what this test scenario validates. This documents the intention for human readers diff --git a/example/plugins/c-api/txn_data_sink/txn_data_sink.cc b/example/plugins/c-api/txn_data_sink/txn_data_sink.cc index ff004bd41fd..7e7689934af 100644 --- a/example/plugins/c-api/txn_data_sink/txn_data_sink.cc +++ b/example/plugins/c-api/txn_data_sink/txn_data_sink.cc @@ -50,7 +50,7 @@ struct SinkData { /** The bytes for the response body streamed in from the sink. * * @note This example plugin buffers the body which is useful for the - * associated Autest. In most production scenarios the user will want to + * associated Uranium test. In most production scenarios the user will want to * interact with the body as a stream rather than buffering the entire body * for each transaction. */ @@ -59,7 +59,7 @@ struct SinkData { /** The bytes for the request body streamed in from the sink. * * @note This example plugin buffers the body which is useful for the - * associated Autest. In most production scenarios the user will want to + * associated Uranium test. In most production scenarios the user will want to * interact with the body as a stream rather than buffering the entire body * for each transaction. */ diff --git a/include/cripts/Configs.hpp b/include/cripts/Configs.hpp index 79460327e7d..013f84a9d92 100644 --- a/include/cripts/Configs.hpp +++ b/include/cripts/Configs.hpp @@ -101,6 +101,7 @@ class Proxy cripts::IntConfig max_open_write_retries{"proxy.config.http.cache.max_open_write_retries"}; cripts::IntConfig max_open_write_retry_timeout{"proxy.config.http.cache.max_open_write_retry_timeout"}; cripts::IntConfig max_stale_age{"proxy.config.http.cache.max_stale_age"}; + cripts::IntConfig max_stale_age_percent{"proxy.config.http.cache.max_stale_age_percent"}; cripts::IntConfig open_read_retry_time{"proxy.config.http.cache.open_read_retry_time"}; cripts::IntConfig open_write_fail_action{"proxy.config.http.cache.open_write_fail_action"}; diff --git a/include/proxy/http/HttpConfig.h b/include/proxy/http/HttpConfig.h index 5bf58f7d7d0..887ac1cb642 100644 --- a/include/proxy/http/HttpConfig.h +++ b/include/proxy/http/HttpConfig.h @@ -705,6 +705,7 @@ struct OverridableHttpConfigParams { MgmtInt cache_guaranteed_min_lifetime = 0; MgmtInt cache_guaranteed_max_lifetime = 31536000; MgmtInt cache_max_stale_age = 604800; + MgmtInt cache_max_stale_age_percent = 0; /////////////////////////////////////////////////// // connection variables. timeouts are in seconds // diff --git a/include/proxy/http/OverridableConfigDefs.h b/include/proxy/http/OverridableConfigDefs.h index bf00fc3def9..864461b1c8f 100644 --- a/include/proxy/http/OverridableConfigDefs.h +++ b/include/proxy/http/OverridableConfigDefs.h @@ -252,6 +252,7 @@ X(HTTP_NEGATIVE_REVALIDATING_LIST, negative_revalidating_list, "proxy.config.http.negative_revalidating_list", STRING, HttpStatusCodeList_Conv) \ X(HTTP_CACHE_POST_METHOD, cache_post_method, "proxy.config.http.cache.post_method", INT, GENERIC) \ X(HTTP_CACHE_TARGETED_CACHE_CONTROL_HEADERS, targeted_cache_control_headers, "proxy.config.http.cache.targeted_cache_control_headers", STRING, TargetedCacheControlHeaders_Conv) \ - X(SSL_CLIENT_CA_CERT_PATH, ssl_client_ca_cert_path, "proxy.config.ssl.client.CA.cert.path", STRING, NONE) + X(SSL_CLIENT_CA_CERT_PATH, ssl_client_ca_cert_path, "proxy.config.ssl.client.CA.cert.path", STRING, NONE) \ + X(HTTP_CACHE_MAX_STALE_AGE_PERCENT, cache_max_stale_age_percent, "proxy.config.http.cache.max_stale_age_percent", INT, GENERIC) // clang-format on diff --git a/include/ts/apidefs.h.in b/include/ts/apidefs.h.in index f34ffdb1a3f..8e110942b01 100644 --- a/include/ts/apidefs.h.in +++ b/include/ts/apidefs.h.in @@ -918,6 +918,7 @@ enum TSOverridableConfigKey { TS_CONFIG_HTTP_CACHE_POST_METHOD, TS_CONFIG_HTTP_CACHE_TARGETED_CACHE_CONTROL_HEADERS, TS_CONFIG_SSL_CLIENT_CA_CERT_PATH, + TS_CONFIG_HTTP_CACHE_MAX_STALE_AGE_PERCENT, TS_CONFIG_LAST_ENTRY, }; diff --git a/plugins/experimental/jax_fingerprint/README b/plugins/experimental/jax_fingerprint/README index ca9502e0834..0692fa8624d 100644 --- a/plugins/experimental/jax_fingerprint/README +++ b/plugins/experimental/jax_fingerprint/README @@ -139,6 +139,6 @@ Unit tests vs. AuTest algorithm logic. Built when BUILD_TESTING is on; run via ctest or the test_jax binary directly. - * End-to-end AuTests live under `tests/gold_tests/pluginTest/jax_fingerprint/` + * End-to-end AuTests live under `tests/uranium_tests/pluginTest/jax_fingerprint/` and exercise the plugin via Proxy Verifier replay yamls. They require a full install (traffic_server, traffic_layout, the plugin .so). diff --git a/src/iocore/cache/CacheRead.cc b/src/iocore/cache/CacheRead.cc index 5c60a68d700..8249bbc45d6 100644 --- a/src/iocore/cache/CacheRead.cc +++ b/src/iocore/cache/CacheRead.cc @@ -60,7 +60,7 @@ thread_local int t_read_recursive = 0; // openReadStartEarliest into the recursive Lread path. When an inner recursion // level falls into free_CacheVC the outer frame then touches the recursion // counter through the freed `this`; reaching the depth limit is not required to -// trigger it. Used by the autest reproducer to observe the recursive-read +// trigger it. Used by the Uranium test reproducer to observe the recursive-read // use-after-free (ASan build) on the unfixed sources. #if TS_HAS_TESTS static bool const test_force_corrupt_doc = std::getenv("ATS_TEST_FORCE_CORRUPT_DOC") != nullptr; diff --git a/src/iocore/net/unit_tests/test_OCSPStapling.cc b/src/iocore/net/unit_tests/test_OCSPStapling.cc index dbeb71e6cb1..d71a69c320c 100644 --- a/src/iocore/net/unit_tests/test_OCSPStapling.cc +++ b/src/iocore/net/unit_tests/test_OCSPStapling.cc @@ -41,7 +41,7 @@ namespace { -constexpr char OCSP_TEST_SSL_DIR[] = _XSTR(LIBINKNET_UNIT_TEST_DIR) "/../../../../tests/gold_tests/tls/ssl"; +constexpr char OCSP_TEST_SSL_DIR[] = _XSTR(LIBINKNET_UNIT_TEST_DIR) "/../../../../tests/uranium_tests/tls/ssl"; struct BioDeleter { void diff --git a/src/proxy/http/HttpConfig.cc b/src/proxy/http/HttpConfig.cc index f0b1b535c32..08a3ed23633 100644 --- a/src/proxy/http/HttpConfig.cc +++ b/src/proxy/http/HttpConfig.cc @@ -1109,6 +1109,7 @@ HttpConfig::startup() HttpEstablishStaticConfigLongLong(c.oride.cache_guaranteed_max_lifetime, "proxy.config.http.cache.guaranteed_max_lifetime"); HttpEstablishStaticConfigLongLong(c.oride.cache_max_stale_age, "proxy.config.http.cache.max_stale_age"); + HttpEstablishStaticConfigLongLong(c.oride.cache_max_stale_age_percent, "proxy.config.http.cache.max_stale_age_percent"); HttpEstablishStaticConfigByte(c.oride.srv_enabled, "proxy.config.srv_enabled"); @@ -1456,7 +1457,8 @@ HttpConfig::reconfigure() params->oride.cache_guaranteed_min_lifetime = m_master.oride.cache_guaranteed_min_lifetime; params->oride.cache_guaranteed_max_lifetime = m_master.oride.cache_guaranteed_max_lifetime; - params->oride.cache_max_stale_age = m_master.oride.cache_max_stale_age; + params->oride.cache_max_stale_age = m_master.oride.cache_max_stale_age; + params->oride.cache_max_stale_age_percent = m_master.oride.cache_max_stale_age_percent; params->oride.srv_enabled = m_master.oride.srv_enabled; diff --git a/src/proxy/http/HttpTransact.cc b/src/proxy/http/HttpTransact.cc index 79e9fc5fa26..e638ed14a4c 100644 --- a/src/proxy/http/HttpTransact.cc +++ b/src/proxy/http/HttpTransact.cc @@ -6396,8 +6396,20 @@ HttpTransact::is_stale_cache_response_returnable(State *s) time_t current_age = HttpTransactCache::calculate_document_age(s->cache_info.object_read->request_sent_time_get(), s->cache_info.object_read->response_received_time_get(), cached_response, cached_response->get_date(), s->current.now); + + MgmtInt max_age = get_max_age(cached_response); + MgmtInt max_stale_age = s->txn_conf->cache_max_stale_age; + MgmtInt max_stale_percentage = + std::clamp(s->txn_conf->cache_max_stale_age_percent, static_cast(0), static_cast(100)); + + if (max_stale_percentage > 0 && max_age >= 0) { + MgmtInt percent_max_stale_age = max_age * max_stale_percentage / 100; + + max_stale_age = std::min(max_stale_age, percent_max_stale_age); + } + // Negative age is overflow - if ((current_age < 0) || (current_age > s->txn_conf->cache_max_stale_age + get_max_age(cached_response))) { + if ((current_age < 0) || (current_age > max_age + max_stale_age)) { TxnDbg(dbg_ctl_http_trans, "document age is too large %" PRId64, (int64_t)current_age); return false; } diff --git a/src/records/RecordsConfig.cc b/src/records/RecordsConfig.cc index f011c0ccd74..1eec9cce822 100644 --- a/src/records/RecordsConfig.cc +++ b/src/records/RecordsConfig.cc @@ -633,8 +633,8 @@ static constexpr RecordElement RecordsConfig[] = // # // # 0 - default. disable cache and goto origin // # 1 - return error if cache miss - // # 2 - serve stale until proxy.config.http.cache.max_stale_age, then goto origin, if revalidate - // # 3 - return error if cache miss or serve stale until proxy.config.http.cache.max_stale_age, then goto origin, if revalidate + // # 2 - serve stale within the configured stale age limits, then goto origin, if revalidate + // # 3 - return error if cache miss or serve stale within the configured stale age limits, then goto origin, if revalidate // # 4 - return error if cache miss or if revalidate // # 5 - retry cache read (read-while-writer) on write lock failure, goto origin if retries exhausted // # 6 - retry cache read on write lock failure, if retries exhausted serve stale if allowed, otherwise goto origin @@ -660,6 +660,8 @@ static constexpr RecordElement RecordsConfig[] = , {RECT_CONFIG, "proxy.config.http.cache.max_stale_age", RECD_INT, "604800", RECU_DYNAMIC, RR_NULL, RECC_NULL, nullptr, RECA_NULL} , + {RECT_CONFIG, "proxy.config.http.cache.max_stale_age_percent", RECD_INT, "0", RECU_DYNAMIC, RR_NULL, RECC_INT, "[0-100]", RECA_NULL} + , {RECT_CONFIG, "proxy.config.http.cache.range.lookup", RECD_INT, "1", RECU_NULL, RR_NULL, RECC_NULL, nullptr, RECA_NULL} , {RECT_CONFIG, "proxy.config.http.cache.range.write", RECD_INT, "0", RECU_NULL, RR_NULL, RECC_NULL, nullptr, RECA_NULL} diff --git a/src/records/unit_tests/test_ConfigReloadTask.cc b/src/records/unit_tests/test_ConfigReloadTask.cc index 9cc1acdb107..559285ea069 100644 --- a/src/records/unit_tests/test_ConfigReloadTask.cc +++ b/src/records/unit_tests/test_ConfigReloadTask.cc @@ -27,7 +27,7 @@ #include "mgmt/config/ReloadCoordinator.h" // Note: These tests verify the default values and basic logic. -// Full integration testing with records is done via autest. +// Full integration testing with records is done via Uranium tests. TEST_CASE("ConfigReloadProgress default timeout", "[config][reload][timeout]") { diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 8d5694a8b72..d0311b5c89f 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -17,7 +17,7 @@ include(add_atsplugin) -function(ADD_AUTEST_PLUGIN _NAME) +function(ADD_URTEST_PLUGIN _NAME) add_library(${_NAME} MODULE ${ARGN}) if(LINK_PLUGINS AND BUILD_SHARED_LIBS) target_link_libraries(${_NAME} PRIVATE ts::tsapi ts::tsutil) @@ -36,91 +36,59 @@ function(ADD_AUTEST_PLUGIN _NAME) endfunction() add_subdirectory(tools/plugins) -add_subdirectory(gold_tests/chunked_encoding) -add_subdirectory(gold_tests/at_headers/plugins) -add_subdirectory(gold_tests/continuations/plugins) -add_subdirectory(gold_tests/jsonrpc/plugins) -add_subdirectory(gold_tests/pluginTest/crash_test) -add_subdirectory(gold_tests/pluginTest/polite_hook_wait) -add_subdirectory(gold_tests/pluginTest/tsapi) -add_subdirectory(gold_tests/pluginTest/TSVConnFd) -add_subdirectory(gold_tests/timeout) -add_subdirectory(gold_tests/tls) +add_subdirectory(uranium_tests/chunked_encoding) +add_subdirectory(uranium_tests/at_headers/plugins) +add_subdirectory(uranium_tests/continuations/plugins) +add_subdirectory(uranium_tests/jsonrpc/plugins) +add_subdirectory(uranium_tests/pluginTest/crash_test) +add_subdirectory(uranium_tests/pluginTest/polite_hook_wait) +add_subdirectory(uranium_tests/pluginTest/tsapi) +add_subdirectory(uranium_tests/pluginTest/TSVConnFd) +add_subdirectory(uranium_tests/timeout) +add_subdirectory(uranium_tests/tls) -set(RUNUV ${UV} --project ${CMAKE_CURRENT_BINARY_DIR}) - -set(CMAKE_GOLD_DIR "${CMAKE_CURRENT_SOURCE_DIR}/gold_tests") -set(CMAKE_SKIP_GOLD_DIR "${CMAKE_CURRENT_SOURCE_DIR}/_skipped_uds_gold_tests") set(CURL_UDS_FLAG "") -if(ENABLE_AUTEST_UDS) +if(ENABLE_URTEST_UDS) set(CURL_UDS_FLAG "--curl-uds") endif() configure_file(pyproject.toml pyproject.toml COPYONLY) -configure_file(autest.sh.in autest.sh) -configure_file(autest-parallel.py.in autest-parallel.py) - -set(PYTEST_REPLAY_COMMAND - ${CMAKE_COMMAND} -E env PYTHONPATH=${CMAKE_CURRENT_SOURCE_DIR}:$ENV{PYTHONPATH} ${RUNUV} run pytest -p - ats_testkit.plugin ${CMAKE_CURRENT_SOURCE_DIR}/pytest_tests ${CMAKE_GOLD_DIR} --ats-bin=${CMAKE_INSTALL_PREFIX}/bin - --proxy-verifier-bin ${PROXY_VERIFIER_PATH} --build-root ${CMAKE_BINARY_DIR} --sandbox ${AUTEST_SANDBOX} - ${PYTEST_OPTIONS_LIST} -) +configure_file(urtest.sh.in urtest.sh @ONLY USE_SOURCE_PERMISSIONS) add_custom_target( - pytest-replay + urtest-replay COMMAND ${CMAKE_COMMAND} --build ${CMAKE_BINARY_DIR} --target install - COMMAND ${RUNUV} sync - COMMAND ${PYTEST_REPLAY_COMMAND} + COMMAND ${CMAKE_CURRENT_BINARY_DIR}/urtest.sh --no-run-in-docker -m uranium_replay WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} USES_TERMINAL ) add_custom_target( - pytest-replay-no-install - COMMAND ${RUNUV} sync - COMMAND ${PYTEST_REPLAY_COMMAND} + urtest-replay-no-install + COMMAND ${CMAKE_CURRENT_BINARY_DIR}/urtest.sh --no-run-in-docker -m uranium_replay WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} USES_TERMINAL ) add_custom_target( - autest + urtest COMMAND ${CMAKE_COMMAND} --build ${CMAKE_BINARY_DIR} --target install - COMMAND ${RUNUV} sync - COMMAND ${PYTEST_REPLAY_COMMAND} - COMMAND ${CMAKE_COMMAND} -E env PYTHONPATH=${CMAKE_GOLD_DIR}/remap:$ENV{PYTHONPATH} ${RUNUV} run autest --directory - ${CMAKE_GOLD_DIR} --ats-bin=${CMAKE_INSTALL_PREFIX}/bin --proxy-verifier-bin ${PROXY_VERIFIER_PATH} - --build-root ${CMAKE_BINARY_DIR} --sandbox ${AUTEST_SANDBOX} ${CURL_UDS_FLAG} ${AUTEST_OPTIONS_LIST} + COMMAND ${CMAKE_CURRENT_BINARY_DIR}/urtest.sh --no-run-in-docker WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} USES_TERMINAL ) add_custom_target( - autest_no_install - COMMAND ${RUNUV} sync - COMMAND ${PYTEST_REPLAY_COMMAND} - COMMAND ${CMAKE_COMMAND} -E env PYTHONPATH=${CMAKE_GOLD_DIR}/remap:$ENV{PYTHONPATH} ${RUNUV} run autest --directory - ${CMAKE_GOLD_DIR} --ats-bin=${CMAKE_INSTALL_PREFIX}/bin --proxy-verifier-bin ${PROXY_VERIFIER_PATH} - --build-root ${CMAKE_BINARY_DIR} --sandbox ${AUTEST_SANDBOX} ${CURL_UDS_FLAG} ${AUTEST_OPTIONS_LIST} + urtest-no-install + COMMAND ${CMAKE_CURRENT_BINARY_DIR}/urtest.sh --no-run-in-docker WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} USES_TERMINAL ) -# autest uds specific target that skips tests and runs with curl flag add_custom_target( - autest-uds + urtest-uds COMMAND ${CMAKE_COMMAND} --build ${CMAKE_BINARY_DIR} --target install - COMMAND ${RUNUV} sync - COMMAND ${CMAKE_COMMAND} -E rename ${CMAKE_GOLD_DIR}/h2 ${CMAKE_SKIP_GOLD_DIR}/h2 - COMMAND ${CMAKE_COMMAND} -E rename ${CMAKE_GOLD_DIR}/tls ${CMAKE_SKIP_GOLD_DIR}/tls - COMMAND ${CMAKE_COMMAND} -E rename ${CMAKE_GOLD_DIR}/tls_hooks ${CMAKE_SKIP_GOLD_DIR}/tls_hooks - COMMAND ${CMAKE_COMMAND} -E env PYTHONPATH=${CMAKE_GOLD_DIR}/remap:$ENV{PYTHONPATH} ${RUNUV} run autest --directory - ${CMAKE_GOLD_DIR} --ats-bin=${CMAKE_INSTALL_PREFIX}/bin --proxy-verifier-bin ${PROXY_VERIFIER_PATH} - --build-root ${CMAKE_BINARY_DIR} --sandbox ${AUTEST_SANDBOX} ${CURL_UDS_FLAG} ${AUTEST_OPTIONS_LIST} - COMMAND ${CMAKE_COMMAND} -E rename ${CMAKE_SKIP_GOLD_DIR}/h2 ${CMAKE_GOLD_DIR}/h2 - COMMAND ${CMAKE_COMMAND} -E rename ${CMAKE_SKIP_GOLD_DIR}/tls ${CMAKE_GOLD_DIR}/tls - COMMAND ${CMAKE_COMMAND} -E rename ${CMAKE_SKIP_GOLD_DIR}/tls_hooks ${CMAKE_GOLD_DIR}/tls_hooks + COMMAND ${CMAKE_CURRENT_BINARY_DIR}/urtest.sh --no-run-in-docker --curl-uds WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} USES_TERMINAL ) diff --git a/tests/README.md b/tests/README.md index 875744b2ea8..fc0fdbb9203 100644 --- a/tests/README.md +++ b/tests/README.md @@ -1,12 +1,17 @@ -# Getting Started +# Uranium Tests -This directory contains different tests for Apache Trafficserver. It is recommended that all tests move to this common area under the correct location based on the type of test being added. +This directory contains Apache Traffic Server's Uranium test suite and its +supporting tools. See the +[developer guide](../doc/developer-guide/testing/uranium-tests.en.rst) for the +name's intentionally tongue-in-cheek origin and the complete authoring guide. ## Layout The current layout is: -**gold_tests/** - contains pytest replay tests and bespoke tests that run on AuTest +**uranium_tests/** - contains Uranium tests collected by pytest. Replay YAML +files run natively; bespoke Python definitions use the AuTest compatibility +backend while they are migrated. **tools/** - contains programs used to help with testing. @@ -15,91 +20,106 @@ The current layout is: # Basic setup -To enable end-to-end tests, set the ENABLE_AUTEST CMake variable: +To enable Uranium tests, set the `ENABLE_URTEST` CMake variable: - $ cmake -B build -DENABLE_AUTEST=ON + $ cmake -B build -DENABLE_URTEST=ON -This builds the required plugins and helper tools and creates the end-to-end test targets. +This builds the required plugins and helper tools and creates the Uranium test targets. # Running tests -Run all pytest replays followed by the remaining bespoke AuTests with the -`autest` target: +Pytest owns the complete Uranium test inventory. Run it with the `urtest` target: - $ cmake --build build -t autest + $ cmake --build build -t urtest This builds and installs ATS, sets up the virtual environment via uv, and runs -both suites. To run only replay tests, use: +native replay items plus bespoke compatibility items under one scheduler. To +run only replay tests, use: - $ cmake --build build -t pytest-replay + $ cmake --build build -t urtest-replay Replay tests are Proxy Verifier files named `.test.yaml`. Pytest collects each file directly, so no companion `.test.py` registration is needed. -Configure `PYTEST_OPTIONS` to pass selection or concurrency arguments: +Configure `URTEST_OPTIONS` to pass selection or concurrency arguments: - $ cmake -B build -DENABLE_AUTEST=ON -DPYTEST_OPTIONS="-n 4 -k cache" + $ cmake -B build -DENABLE_URTEST=ON -DURTEST_OPTIONS="-n 4 -k cache" -To run an individual bespoke AuTest, the CMake build generates a helper script -at `/tests/autest.sh`. This script can run individual tests and further -configure AuTest. +## The urtest.sh entry point -To run a single test, you can use the `--filter` flag to name -which test to run. Bespoke AuTest files are suffixed with `.test.py`. Thus, the -`something_descriptive` test is specified in a file named -`something_descriptive.test.py`. -The corresponding `autest.sh` command is: +`tests/urtest.sh` is the portable entry point. On a normal host it starts the +official `ci.trafficserver.apache.org/ats/fedora:44` image, incrementally builds +ATS in `build-urtest-container`, and runs the tests there. In a Fedora 44 +container it detects that it is already in the official environment and runs +directly, which is also the Jenkins CI behavior. - $ ./autest.sh --filter=something_descriptive +The container uses a short `/tmp` sandbox so ATS Unix socket paths remain +portable. When the run finishes, regular files are copied back to +`build-urtest-container/sandbox` for inspection on the host. -# Running tests in parallel +Force either behavior when needed: -For faster test execution, a parallel test runner is available that distributes -tests across multiple workers. This is especially useful on machines with many -CPU cores. + $ ./tests/urtest.sh --run-in-docker + $ ./tests/urtest.sh --no-run-in-docker - $ python3 autest-parallel.py -j 16 --ats-bin /bin --build-root --sandbox /tmp/autest-parallel +Docker mode matches CI's relevant permissions: `--init`, host networking, and +`SYS_PTRACE`. It does not use a privileged container and does not mount the +Docker socket. Set `ATS_URTEST_DOCKER_IMAGE` to test another image or +`ATS_URTEST_DOCKER_ARGS` to append site-specific Docker arguments. -Key options: +The CMake build also generates `/tests/urtest.sh`. This is the fastest +entry point when ATS is already built and installed in the current environment. +Use `--no-run-in-docker` outside the official container when that is +intentional. + +## Running one test + +The `-f` and `--filter` options select either a native `.test.yaml` item or a +bespoke `.test.py` item by basename. They preserve the familiar +basename-filter workflow: + + $ ./build/tests/urtest.sh -f cache-auth + $ ./build/tests/urtest.sh --filter='cache-*' + +Pytest selection remains available for advanced cases: -* `-j N` - Number of parallel workers (default: number of CPU cores) -* `--ats-bin` - Path to the ATS install bin directory -* `--build-root` - Path to the build directory (for test plugins) -* `--sandbox` - Directory for test sandboxes (default: `/tmp/autest-parallel`) -* `-v` - Verbose output with real-time test progress per worker -* `--collect-timings` - Run tests individually to collect per-test timing data -* `--list` - List all tests and exit (useful for checking test discovery) + $ ./build/tests/urtest.sh -k cache_control -The parallel runner uses port offsets to ensure each worker gets a unique port -range, preventing conflicts between concurrent test instances. Tests known to -require serial execution (listed in `serial_tests.txt`) are run sequentially -after the parallel phase completes. +# Running tests in parallel + +Pytest-xdist distributes both kinds of Uranium test item across isolated workers: + + $ ./build/tests/urtest.sh -j 16 + +Key options: -## Timing-based load balancing +* `-j N` - Number of parallel pytest workers +* `-f NAME` - Run one name or glob; multiple names may follow one `-f` +* `--sandbox` - Directory for test sandboxes +* `-v` - Verbose pytest output +* `--list` - List all tests and exit -If a `test-timings.json` file exists (generated by a previous run with -`--collect-timings`), the runner uses the Longest Processing Time (LPT) -algorithm to distribute tests across workers for balanced execution times. -Without timing data, tests are distributed round-robin. +The pytest runtime uses isolated sandboxes and port ranges. Tests listed in +`serial_tests.txt` take an exclusive execution lock, so they cannot overlap any +other Uranium test item. ## Adding serial tests If a test cannot run in parallel (e.g., it uses hardcoded global resources), -add its path relative to `gold_tests/` to `serial_tests.txt`. +add its path relative to `uranium_tests/` to `serial_tests.txt`. -# Advanced setup +# AuTest compatibility definitions -AuTest and the relevant tools can be install manually instead of using the wrapper script. By doing this, it is often easier to debug issues with the testing system, or the tests. There are two ways this can be done. -1. Run the bootstrap script then source the path with a "source ./env-test/bin/activate" command. At this point autest command should run without the wrapper script -2. Make sure you install python 3.6 or better on your system. From there install these python packages ( ie pip install ): - - hyper - - git+https://bitbucket.org/autestsuite/reusable-gold-testing-system.git - - [traffic-replay](https://bitbucket.org/autestsuite/trafficreplay/src/master/) (This will automatically install [MicroDNS](https://bitbucket.org/autestsuite/microdns/src/master/), [MicroServer](https://bitbucket.org/autestsuite/microserver/src/master/), [TrafficReplayLibrary](https://bitbucket.org/autestsuite/trafficreplaylibrary/src/master/), and dnslib as part of the dependencies.) +New tests should use native replay YAML whenever possible. Existing bespoke +`.test.py` definitions are collected and reported by pytest but still execute +through the pinned AuTest dependency. The APIs below document that migration +boundary. See the [AuTest documentation](https://autestsuite.bitbucket.io/) and +the [CurlHeader README](uranium_tests/autest-site/readme.md) when maintaining one +of those definitions. -# Writing tests for AuTest -When writing for the AuTest system please refer to the current [Online Documentation](https://autestsuite.bitbucket.io/) for general use of the system. To use CurlHeader tester for testing output of curl, please refer to [CurlHeader README](gold_tests/autest-site/readme.md) +## Documentation of the AuTest compatibility extension for ATS -## Documentation of AuTest extension for ATS. -Autest allows for the creation of extensions to help specialize and simplify test writing for a given application domain. Minus API addition the extension code will check that python 3.6 or better is used. There is also a new command line argumented added specifically for Trafficserver: +The pinned AuTest backend supports extensions that specialize test writing for +an application domain. The ATS extension adds this command-line argument: --ats-bin < path to bin directory > diff --git a/tests/ats_testkit/plugin.py b/tests/ats_testkit/plugin.py deleted file mode 100644 index ead77737682..00000000000 --- a/tests/ats_testkit/plugin.py +++ /dev/null @@ -1,154 +0,0 @@ -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. -"""Collect ``*.test.yaml`` Proxy Verifier files as pytest items.""" - -from __future__ import annotations - -from collections.abc import Callable, Iterator -from pathlib import Path -from typing import Any -import os - -import pytest - -from .config import ReplayConfigError, ReplaySpec -from .process import ProcessError -from .replay import ReplaySkip, ReplayTest -from .runtime import TestRuntime - - -def pytest_addoption(parser: pytest.Parser) -> None: - """Register ATS end-to-end runtime paths.""" - - group = parser.getgroup("ATS replay tests") - group.addoption("--ats-bin", help="Directory containing installed ATS executables") - group.addoption("--proxy-verifier-bin", help="Directory containing verifier-client and verifier-server") - group.addoption("--build-root", help="ATS build directory containing test plugins") - group.addoption("--sandbox", help="Directory for isolated test process trees") - - -def pytest_configure(config: pytest.Config) -> None: - """Register the replay marker used for selection.""" - - config.addinivalue_line("markers", "ats_replay: ATS system test driven by a Proxy Verifier replay file") - - -def pytest_ignore_collect(collection_path: Path, config: pytest.Config) -> bool | None: - """Leave legacy ``*.test.py`` files exclusively to AuTest.""" - - del config - return True if collection_path.name.endswith(".test.py") else None - - -def pytest_collect_file(file_path: Path, parent: pytest.Collector) -> pytest.File | None: - """Collect replay files whose basename ends in ``.test.yaml`` or ``.test.yml``.""" - - if file_path.name.endswith((".test.yaml", ".test.yml")): - return ReplayFile.from_parent(parent, path=file_path) - return None - - -class ReplayFile(pytest.File): - """Represent one directly collected replay file.""" - - def collect(self) -> Iterator[pytest.Item]: - """Load collection metadata and yield one test item.""" - - try: - spec = ReplaySpec.load(Path(self.path)) - except ReplayConfigError as error: - raise self.CollectError(str(error)) from error - suffix = ".test.yaml" if spec.path.name.endswith(".test.yaml") else ".test.yml" - item = ReplayItem.from_parent(self, name=spec.path.name.removesuffix(suffix), spec=spec) - item.add_marker("ats_replay") - yield item - - -class ReplayItem(pytest.Item): - """Execute one replay file as a pytest test item.""" - - def __init__(self, *, spec: ReplaySpec, **kwargs: Any) -> None: - super().__init__(**kwargs) - self.spec = spec - - def runtest(self) -> None: - """Run this item's replay lifecycle.""" - - runtime = get_runtime(self.config) - try: - ReplayTest(self.spec, runtime, self.nodeid).run() - except ReplaySkip as reason: - pytest.skip(str(reason)) - - def repr_failure(self, excinfo: pytest.ExceptionInfo[BaseException], style: str | None = None) -> str: - """Present system-test failures without pytest's internal collector frames.""" - - if isinstance(excinfo.value, (AssertionError, ProcessError, ReplayConfigError)): - return str(excinfo.value) - return super().repr_failure(excinfo, style=style) - - def reportinfo(self) -> tuple[Path, int, str]: - """Identify the replay file in pytest reports.""" - - return self.spec.path, 0, self.spec.description - - -def get_runtime(config: pytest.Config) -> TestRuntime: - """Build and cache the runtime selected by pytest command-line options.""" - - cache_name = "_ats_test_runtime" - cached = getattr(config, cache_name, None) - if cached is not None: - return cached - - repository_root = Path(__file__).resolve().parents[2] - values = { - "ats_bin": config.getoption("ats_bin") or os.environ.get("ATS_BIN"), - "verifier_bin": config.getoption("proxy_verifier_bin") or os.environ.get("PROXY_VERIFIER_BIN"), - "build_root": config.getoption("build_root") or os.environ.get("ATS_BUILD_ROOT"), - "sandbox": config.getoption("sandbox") or os.environ.get("ATS_TEST_SANDBOX"), - } - missing = [name for name, value in values.items() if not value] - if missing: - raise pytest.UsageError("Replay tests require " + ", ".join(f"--{name.replace('_', '-')}" for name in missing)) - worker = os.environ.get("PYTEST_XDIST_WORKER", "main") - runtime = TestRuntime.create( - repository_root=repository_root, - build_root=Path(values["build_root"]), - ats_bin=Path(values["ats_bin"]), - verifier_bin=Path(values["verifier_bin"]), - sandbox_root=Path(values["sandbox"]) / worker, - ) - setattr(config, cache_name, runtime) - return runtime - - -@pytest.fixture(scope="session") -def ats_test_runtime(pytestconfig: pytest.Config) -> TestRuntime: - """Provide installed ATS paths and capabilities to handwritten tests.""" - - return get_runtime(pytestconfig) - - -@pytest.fixture -def ats_replay(ats_test_runtime: TestRuntime, request: pytest.FixtureRequest) -> Callable[[Path], None]: - """Return a helper that executes a replay file from a handwritten pytest test.""" - - def run(path: Path) -> None: - spec = ReplaySpec.load(path) - ReplayTest(spec, ats_test_runtime, request.node.nodeid).run() - - return run diff --git a/tests/autest-parallel.py.in b/tests/autest-parallel.py.in deleted file mode 100755 index 1f7f1eedea4..00000000000 --- a/tests/autest-parallel.py.in +++ /dev/null @@ -1,1151 +0,0 @@ -#!/usr/bin/env python3 -''' -Parallel autest runner for Apache Traffic Server. - -This script runs autest tests in parallel by spawning multiple autest processes, -each with a different port offset to avoid port conflicts. - -Usage: - ./autest-parallel.py -j 4 --sandbox /tmp/autest-parallel - ./autest-parallel.py -j 8 --filter "cache-*" --sandbox /tmp/sb - ./autest-parallel.py --list # Just list tests without running -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import argparse -import fnmatch -import json -import os -import re -import signal -import subprocess -import sys -import time -from concurrent.futures import ProcessPoolExecutor, as_completed -from dataclasses import dataclass, field -from datetime import datetime -from pathlib import Path -from typing import Dict, List, Optional, Tuple - -# Default serial tests file location -DEFAULT_SERIAL_TESTS_FILE = Path("${CMAKE_CURRENT_SOURCE_DIR}") / "serial_tests.txt" -# Default estimate for unknown tests (seconds) -DEFAULT_TEST_TIME = 15.0 - - -@dataclass -class TestResult: - """Result from running a single autest process.""" - worker_id: int - tests: List[str] - passed: int = 0 - failed: int = 0 - skipped: int = 0 - warnings: int = 0 - exceptions: int = 0 - unknown: int = 0 - duration: float = 0.0 - failed_tests: List[str] = field(default_factory=list) - test_timings: Dict[str, float] = field(default_factory=dict) - output: str = "" - return_code: int = 0 - is_serial: bool = False - - -def discover_tests(test_dir: Path, filter_patterns: Optional[List[str]] = None) -> List[str]: - """ - Discover all .test.py files in the test directory. - - Args: - test_dir: Path to gold_tests directory - filter_patterns: Optional list of glob patterns to filter tests - - Returns: - List of test names (without .test.py extension) - """ - tests = [] - for test_file in test_dir.rglob("*.test.py"): - # Extract test name (filename without .test.py) - test_name = test_file.stem.replace('.test', '') - - # Apply filters if provided - if filter_patterns: - if any(fnmatch.fnmatch(test_name, pattern) for pattern in filter_patterns): - tests.append(test_name) - else: - tests.append(test_name) - - return sorted(tests) - - -def load_serial_tests(serial_file: Path) -> set: - """ - Load list of tests that must run serially from a file. - - The file format is one test name per line, with # for comments. - Test names can be full paths like ``subdir/test_name.test.py``. - The .test.py extension is stripped, and only the basename (stem) is - used for matching against discovered test names. - - Returns: - Set of test base names that must run serially - """ - serial_tests = set() - if not serial_file.exists(): - return serial_tests - - try: - with open(serial_file) as f: - for line in f: - line = line.strip() - # Skip empty lines and comments - if not line or line.startswith('#'): - continue - # Remove .test.py extension if present - if line.endswith('.test.py'): - line = line[:-8] # Remove .test.py - # Extract just the test name from path - test_name = Path(line).stem.replace('.test', '') - serial_tests.add(test_name) - except IOError: - pass # File is optional; missing file means no serial tests - - return serial_tests - - -def load_timings(timing_file: Path) -> Dict[str, float]: - """Load test timing data from JSON file.""" - if timing_file.exists(): - try: - with open(timing_file) as f: - return json.load(f) - except (json.JSONDecodeError, IOError): - pass # Timing data is optional; fall back to equal partitioning - return {} - - -def save_timings(timing_file: Path, timings: Dict[str, float]): - """Save test timing data to JSON file.""" - try: - with open(timing_file, 'w') as f: - json.dump(timings, f, indent=2, sort_keys=True) - except IOError as e: - print(f"Warning: Could not save timings: {e}", file=sys.stderr) - - -def partition_tests(tests: List[str], num_jobs: int) -> List[List[str]]: - """ - Partition tests into roughly equal groups for parallel execution. - Simple round-robin partitioning (used when no timing data available). - - Args: - tests: List of test names - num_jobs: Number of parallel workers - - Returns: - List of test lists, one per worker - """ - if num_jobs <= 0: - num_jobs = 1 - - partitions = [[] for _ in range(min(num_jobs, len(tests)))] - for i, test in enumerate(tests): - partitions[i % len(partitions)].append(test) - - return [p for p in partitions if p] # Remove empty partitions - - -def partition_tests_by_time(tests: List[str], num_jobs: int, timings: Dict[str, float]) -> Tuple[List[List[str]], List[float]]: - """ - Partition tests using LPT (Longest Processing Time first) algorithm. - This balances the load across workers based on expected test duration. - - Args: - tests: List of test names - num_jobs: Number of parallel workers - timings: Dictionary of test name -> expected duration in seconds - - Returns: - Tuple of (partitions, expected_durations) where: - - partitions: List of test lists, one per worker - - expected_durations: Expected total duration for each worker - """ - if num_jobs <= 0: - num_jobs = 1 - - num_workers = min(num_jobs, len(tests)) - - # Get timing for each test, use default for unknown - test_times = [] - unknown_tests = [] - for test in tests: - if test in timings: - test_times.append((test, timings[test])) - else: - unknown_tests.append(test) - - # Sort known tests by time (longest first) for LPT algorithm - test_times.sort(key=lambda x: x[1], reverse=True) - - # Initialize workers - partitions = [[] for _ in range(num_workers)] - worker_loads = [0.0] * num_workers - - # Assign known tests using LPT: assign to worker with least load - for test, duration in test_times: - min_worker = min(range(num_workers), key=lambda w: worker_loads[w]) - partitions[min_worker].append(test) - worker_loads[min_worker] += duration - - # Distribute unknown tests evenly across workers with least load - # Sort unknown tests and distribute them one at a time to balance - for test in unknown_tests: - min_worker = min(range(num_workers), key=lambda w: worker_loads[w]) - partitions[min_worker].append(test) - worker_loads[min_worker] += DEFAULT_TEST_TIME - - return partitions, worker_loads - - -def strip_ansi(text: str) -> str: - """Remove ANSI escape codes from text.""" - ansi_escape = re.compile(r'\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])') - return ansi_escape.sub('', text) - - -def parse_autest_output(output: str) -> dict: - """ - Parse autest output to extract pass/fail counts and per-test timings. - - Args: - output: Raw autest output string - - Returns: - Dictionary with counts for passed, failed, skipped, etc. and test_timings - """ - result = { - 'passed': 0, - 'failed': 0, - 'skipped': 0, - 'warnings': 0, - 'exceptions': 0, - 'unknown': 0, - 'failed_tests': [], - 'test_timings': {} - } - - # Strip ANSI codes for easier parsing - clean_output = strip_ansi(output) - lines = clean_output.split('\n') - - # First pass: find test results and their line positions - test_results = [] # (line_num, test_name, result) - for i, line in enumerate(lines): - line_stripped = line.strip() - - # Match "Running Test: test_name" or "Test: test_name: Passed/Failed" - running_match = re.match(r'Running Test:\s+(\S+)', line_stripped) - result_match = re.match(r'Test:\s+(\S+):\s+(Passed|Failed|Skipped)', line_stripped, re.IGNORECASE) - - if running_match: - test_results.append((i, running_match.group(1), 'start')) - elif result_match: - test_results.append((i, result_match.group(1), result_match.group(2).lower())) - - # Calculate per-test timing based on line positions - # (rough approximation - actual timing would be better from autest if available) - for i, (line_num, test_name, status) in enumerate(test_results): - if status == 'start': - # Find the corresponding end - for j in range(i + 1, len(test_results)): - end_line, end_name, end_status = test_results[j] - if end_name == test_name and end_status != 'start': - # We don't have actual time, but we'll track it from the worker - break - - # Parse the summary section - for line in lines: - line = line.strip() - if 'Passed:' in line: - try: - result['passed'] = int(line.split(':')[-1].strip()) - except ValueError: - pass - elif 'Failed:' in line and 'test' not in line.lower(): - try: - result['failed'] = int(line.split(':')[-1].strip()) - except ValueError: - pass - elif 'Skipped:' in line: - try: - result['skipped'] = int(line.split(':')[-1].strip()) - except ValueError: - pass - elif re.match(r'warnings?:', line, re.IGNORECASE): - try: - result['warnings'] = int(line.split(':')[-1].strip()) - except ValueError: - pass - elif re.match(r'exceptions?:', line, re.IGNORECASE): - try: - result['exceptions'] = int(line.split(':')[-1].strip()) - except ValueError: - pass - elif re.match(r'unknowns?:', line, re.IGNORECASE): - try: - result['unknown'] = int(line.split(':')[-1].strip()) - except ValueError: - pass - - # Extract failed test names - failed_pattern = re.compile(r'Test:\s+(\S+):\s+Failed', re.IGNORECASE) - for match in failed_pattern.finditer(clean_output): - result['failed_tests'].append(match.group(1)) - - return result - - -def extract_failure_output(output: str, failed_tests: List[str]) -> Dict[str, str]: - """ - Extract the detailed autest output section for each failed test. - - Autest emits a block per test starting with a line like - ``Test: : Failed`` and continuing with indented detail lines until - the next top-level ``Test:`` line or the end of meaningful output. - - Args: - output: Raw autest output from a worker - failed_tests: List of test names that failed - - Returns: - Dictionary mapping each failed test name to its detail block - """ - if not failed_tests: - return {} - - clean = strip_ansi(output) - lines = clean.split('\n') - - # Identify line ranges for each top-level "Test: :" block. - test_block_re = re.compile(r'^Test:\s+(\S+):\s+(Passed|Failed|Skipped)', re.IGNORECASE) - block_starts: List[Tuple[int, str]] = [] - for i, line in enumerate(lines): - m = test_block_re.match(line.strip()) - if m: - block_starts.append((i, m.group(1))) - - failed_set = set(failed_tests) - details: Dict[str, str] = {} - - for idx, (start_line, name) in enumerate(block_starts): - if name not in failed_set: - continue - # The block extends until the next top-level Test: line or end of - # output. - if idx + 1 < len(block_starts): - end_line = block_starts[idx + 1][0] - else: - end_line = len(lines) - block = '\n'.join(lines[start_line:end_line]).rstrip() - if block: - details[name] = block - - return details - - -def extract_worker_diagnostics(output: str) -> str: - """ - Extract the most relevant worker diagnostics for exception/setup failures. - - When autest reports an exception without attributing it to a specific - failed test, the best fallback is the worker's captured output. This - helper prefers traceback/error sections but falls back to the full worker - output if no obvious diagnostic block is found. - """ - clean = strip_ansi(output).strip() - if not clean: - return "" - - lines = clean.split('\n') - ranges: List[Tuple[int, int]] = [] - - def is_marker(line: str) -> bool: - lower = line.lower() - return ( - 'traceback (most recent call last):' in lower or 'error:' in lower or 'timeout' in lower or - 'interrupted' in lower - ) - - for i, line in enumerate(lines): - if not is_marker(line): - continue - start = max(0, i - 5) - end = i + 1 - while end < len(lines): - candidate = lines[end].strip() - if not candidate and end > i + 1: - break - end += 1 - ranges.append((start, min(end, len(lines)))) - - if not ranges: - return clean - - merged_ranges: List[Tuple[int, int]] = [] - for start, end in ranges: - if merged_ranges and start <= merged_ranges[-1][1]: - merged_ranges[-1] = (merged_ranges[-1][0], max(merged_ranges[-1][1], end)) - else: - merged_ranges.append((start, end)) - - blocks = [] - for start, end in merged_ranges: - block = '\n'.join(lines[start:end]).rstrip() - if block: - blocks.append(block) - - return '\n\n...\n\n'.join(blocks) if blocks else clean - - -def run_single_test(test: str, script_dir: Path, sandbox: Path, ats_bin: str, build_root: str, extra_args: List[str], - env: dict) -> Tuple[str, float, str, str]: - """ - Run a single test and return its timing. - - Returns: - Tuple of (test_name, duration, status, output) - status is one of: "PASS", "FAIL", "SKIP" - """ - cmd = [ - 'uv', 'run', 'autest', 'run', '--directory', '${CMAKE_GOLD_DIR}', '--ats-bin', ats_bin, '--proxy-verifier-bin', - '${PROXY_VERIFIER_PATH}', '--build-root', build_root, '--sandbox', - str(sandbox / test), '--filters', test - ] - cmd.extend(extra_args) - - start = time.time() - try: - proc = subprocess.run( - cmd, - cwd=script_dir, - capture_output=True, - text=True, - env=env, - timeout=600 # 10 minute timeout per test - ) - duration = time.time() - start - output = proc.stdout + proc.stderr - parsed = parse_autest_output(output) - # Determine status: - # - SKIP: test was skipped (missing dependency, unsupported feature) - # - PASS: test ran and passed - # - FAIL: test failed, had exceptions, or nothing ran at all - if parsed['skipped'] > 0 and parsed['passed'] == 0 and parsed['failed'] == 0: - status = "SKIP" - elif (parsed['failed'] == 0 and parsed['exceptions'] == 0 and proc.returncode == 0 and - (parsed['passed'] > 0 or parsed['skipped'] > 0)): - status = "PASS" - else: - status = "FAIL" - return (test, duration, status, output) - except subprocess.TimeoutExpired: - return (test, 600.0, "FAIL", "TIMEOUT") - except Exception as e: - return (test, time.time() - start, "FAIL", str(e)) - - -def run_worker( - worker_id: int, - tests: List[str], - script_dir: Path, - sandbox_base: Path, - ats_bin: str, - build_root: str, - extra_args: List[str], - port_offset_step: int = 1000, - verbose: bool = False, - collect_timings: bool = False) -> TestResult: - """ - Run autest on a subset of tests with isolated sandbox and port range. - - Args: - worker_id: Worker identifier (0, 1, 2, ...) - tests: List of test names to run - script_dir: Directory containing autest.sh - sandbox_base: Base sandbox directory - ats_bin: Path to ATS bin directory - build_root: Path to the build directory (for test plugins etc.) - extra_args: Additional arguments to pass to autest - port_offset_step: Port offset between workers - verbose: Whether to print verbose output - collect_timings: If True, run tests one at a time to collect accurate timing - - Returns: - TestResult with pass/fail counts and per-test timings - """ - start_time = time.time() - result = TestResult(worker_id=worker_id, tests=tests) - - # Create worker-specific sandbox - sandbox = sandbox_base / f"worker-{worker_id}" - sandbox.mkdir(parents=True, exist_ok=True) - - # Calculate port offset for this worker - port_offset = worker_id * port_offset_step - - # Set up environment with port offset and PYTHONPATH for test extensions. - env = os.environ.copy() - env['AUTEST_PORT_OFFSET'] = str(port_offset) - pythonpath_dirs = [ - '${CMAKE_CURRENT_SOURCE_DIR}/gold_tests/remap', - '${CMAKE_CURRENT_SOURCE_DIR}/gold_tests/lib', - ] - existing = env.get('PYTHONPATH', '') - env['PYTHONPATH'] = ':'.join(pythonpath_dirs + ([existing] if existing else [])) - - if collect_timings: - # Run tests one at a time to collect accurate timing - all_output = [] - total_tests = len(tests) - try: - for idx, test in enumerate(tests, 1): - test_name, duration, status, output = run_single_test( - test, script_dir, sandbox, ats_bin, build_root, extra_args, env) - result.test_timings[test_name] = duration - all_output.append(output) - - if status == "PASS": - result.passed += 1 - elif status == "SKIP": - result.skipped += 1 - else: - result.failed += 1 - result.failed_tests.append(test_name) - - timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") - # Fixed-width format: date time status duration worker progress test_name - print( - f"{timestamp} {status:4s} {duration:6.1f}s Worker:{worker_id:2d} {idx:2d}/{total_tests:2d} {test}", flush=True) - except KeyboardInterrupt: - result.return_code = 130 - - result.output = "\n".join(all_output) - if result.return_code != 130: - result.return_code = 0 if result.failed == 0 else 1 - else: - # Run all tests in batch (faster but no per-test timing) - cmd = [ - 'uv', - 'run', - 'autest', - 'run', - '--directory', - '${CMAKE_GOLD_DIR}', - '--ats-bin', - ats_bin, - '--proxy-verifier-bin', - '${PROXY_VERIFIER_PATH}', - '--build-root', - build_root, - '--sandbox', - str(sandbox), - ] - - # Add test filters - cmd.append('--filters') - cmd.extend(tests) - - # Add any extra arguments - cmd.extend(extra_args) - - timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") - print(f"{timestamp} Worker:{worker_id:2d} Starting batch of {len(tests)} tests (port offset {port_offset})", flush=True) - if verbose: - print( - f" Worker:{worker_id:2d} Tests: {', '.join(tests[:5])}" - f"{'...' if len(tests) > 5 else ''}", - flush=True) - - try: - if verbose: - # Stream output in real-time so the user sees test progress. - # We use Popen + line-by-line read so partial results are visible - # even if the overall run takes a long time. - proc = subprocess.Popen( - cmd, - cwd=script_dir, - stdout=subprocess.PIPE, - stderr=subprocess.STDOUT, - text=True, - env=env, - ) - output_lines = [] - try: - for line in proc.stdout: - output_lines.append(line) - # Print lines that show test progress - clean = strip_ansi(line).strip() - if clean.startswith('Running Test') or 'Passed' in clean or 'Failed' in clean: - if clean.startswith('Running Test'): - ts = datetime.now().strftime("%H:%M:%S") - print(f" [{ts}] Worker:{worker_id:2d} {clean}", flush=True) - # stdout is exhausted, wait for process to finish - try: - proc.wait(timeout=60) - except subprocess.TimeoutExpired: - proc.kill() - proc.wait() - finally: - # Ensure the subprocess is always cleaned up. - if proc.poll() is None: - proc.terminate() - try: - proc.wait(timeout=5) - except subprocess.TimeoutExpired: - proc.kill() - proc.wait() - result.output = ''.join(output_lines) - result.return_code = proc.returncode - else: - proc = subprocess.run( - cmd, - cwd=script_dir, - capture_output=True, - text=True, - env=env, - timeout=3600 # 1 hour timeout per worker - ) - result.output = proc.stdout + proc.stderr - result.return_code = proc.returncode - - # Parse results - parsed = parse_autest_output(result.output) - result.passed = parsed['passed'] - result.failed = parsed['failed'] - result.skipped = parsed['skipped'] - result.warnings = parsed['warnings'] - result.exceptions = parsed['exceptions'] - result.unknown = parsed['unknown'] - result.failed_tests = parsed['failed_tests'] - - # If no tests ran at all (passed + failed + skipped == 0), - # autest likely errored at setup (e.g., missing proxy verifier). - # Count all tests as failed to avoid false positives. - total_ran = result.passed + result.failed + result.skipped - if total_ran == 0 and (hasattr(proc, 'returncode') and proc.returncode != 0): - result.failed = len(tests) - result.failed_tests = list(tests) - - except KeyboardInterrupt: - result.output = "INTERRUPTED by user" - result.return_code = 130 - result.failed = len(tests) - except subprocess.TimeoutExpired: - result.output = "TIMEOUT: Worker exceeded 1 hour timeout" - result.return_code = -1 - result.failed = len(tests) - except Exception as e: - result.output = f"ERROR: {str(e)}" - result.return_code = -1 - result.failed = len(tests) - - result.duration = time.time() - start_time - return result - - -def print_summary(results: List[TestResult], total_duration: float, expected_timings: Optional[Dict[str, float]] = None): - """Print aggregated results from all workers.""" - total_passed = sum(r.passed for r in results) - total_failed = sum(r.failed for r in results) - total_skipped = sum(r.skipped for r in results) - total_warnings = sum(r.warnings for r in results) - total_exceptions = sum(r.exceptions for r in results) - total_unknown = sum(r.unknown for r in results) - # Use actual test count (top-level tests), not sub-test counts from autest - total_tests = sum(len(r.tests) for r in results) - - # Deduplicate failed test names (a test may appear in sub-test output multiple times) - seen = set() - all_failed_tests = [] - for r in results: - for t in r.failed_tests: - if t not in seen: - seen.add(t) - all_failed_tests.append(t) - - # Collect actual timings from results - actual_timings = {} - for r in results: - actual_timings.update(r.test_timings) - - print("\n" + "=" * 70) - print("PARALLEL AUTEST SUMMARY") - print("=" * 70) - print(f"Workers: {len(results)}") - print(f"Total tests: {total_tests}") - print(f"Duration: {total_duration:.1f}s") - print("-" * 70) - print(f" Passed: {total_passed}") - print(f" Failed: {total_failed}") - print(f" Skipped: {total_skipped}") - print(f" Warnings: {total_warnings}") - print(f" Exceptions: {total_exceptions}") - print(f" Unknown: {total_unknown}") - - if all_failed_tests: - print("-" * 70) - print("FAILED TESTS:") - for test in sorted(all_failed_tests): - print(f" - {test}") - - # Print detailed failure output extracted from each worker's autest - # output so CI logs contain actionable diagnostics. - print("-" * 70) - print("FAILED TEST OUTPUT:") - print("-" * 70) - for r in results: - if not r.failed_tests or not r.output: - continue - details = extract_failure_output(r.output, r.failed_tests) - for test_name in sorted(details): - print(f"\n--- {test_name} ---") - print(details[test_name]) - print("-" * 70) - - worker_diagnostics = [ - r for r in results - if r.output and (r.exceptions > 0 or (r.return_code != 0 and not r.failed_tests)) - ] - if worker_diagnostics: - print("-" * 70) - print("WORKER DIAGNOSTICS:") - print("-" * 70) - for r in worker_diagnostics: - label = "Serial" if r.is_serial else f"Worker {r.worker_id}" - print(f"\n--- {label} (return code {r.return_code}) ---") - print(extract_worker_diagnostics(r.output)) - print("-" * 70) - - # Check for timing discrepancies - if expected_timings and actual_timings: - timing_warnings = [] - for test, actual in actual_timings.items(): - if test in expected_timings: - expected = expected_timings[test] - if expected > 0: - ratio = actual / expected - diff = abs(actual - expected) - # Flag if: (>2x ratio AND >10s diff) OR (>30s diff regardless of ratio) - if ((ratio > 2.0 or ratio < 0.5) and diff > 10) or diff > 30: - timing_warnings.append((test, expected, actual, ratio)) - - if timing_warnings: - print("-" * 70) - print("TIMING DISCREPANCIES (expected vs actual):") - for test, expected, actual, ratio in sorted(timing_warnings, key=lambda x: -abs(x[2] - x[1])): - direction = "slower" if actual > expected else "faster" - print(f" {test}: {expected:.1f}s -> {actual:.1f}s ({ratio:.1f}x {direction})") - - print("=" * 70) - - # Per-worker summary - print("\nPer-worker breakdown:") - for r in results: - status = "OK" if r.failed == 0 and r.exceptions == 0 else "FAIL" - if r.is_serial: - worker_label = " Serial: " - else: - worker_label = f" Worker:{r.worker_id:2d}" - print( - f"{worker_label} {r.passed:3d} passed, {r.failed:3d} failed, " - f"{r.skipped:3d} skipped ({r.duration:6.1f}s) [{status}]") - - # Total summary line - print("-" * 70) - print( - f" TOTAL: {total_passed:3d} passed, {total_failed:3d} failed, " - f"{total_skipped:3d} skipped ({total_duration:6.1f}s)") - - -def _sigterm_handler(signum, frame): - """Translate SIGTERM into KeyboardInterrupt so the same cleanup path runs.""" - raise KeyboardInterrupt - - -def main(): - signal.signal(signal.SIGTERM, _sigterm_handler) - - parser = argparse.ArgumentParser( - description='Run autest tests in parallel', - formatter_class=argparse.RawDescriptionHelpFormatter, - epilog=''' -Examples: - # Run all tests with 4 parallel workers - %(prog)s -j 4 --ats-bin /opt/ats/bin --sandbox /tmp/autest - - # Run specific tests - %(prog)s -j 2 --filter "cache-*" --filter "tls-*" --ats-bin /opt/ats/bin --sandbox /tmp/autest - - # List tests without running - %(prog)s --list --ats-bin /opt/ats/bin - - # Collect timing data (runs tests one at a time for accurate timing) - %(prog)s -j 4 --collect-timings --ats-bin /opt/ats/bin --sandbox /tmp/autest - - # Use saved timing data for load-balanced partitioning - %(prog)s -j 16 --timings-file test-timings.json --ats-bin /opt/ats/bin --sandbox /tmp/autest -''') - - parser.add_argument( - '-j', '--jobs', type=int, default=os.cpu_count() or 4, help='Number of parallel workers (default: CPU count)') - parser.add_argument('--ats-bin', default=None, help='Path to ATS bin directory (required unless --list is used)') - parser.add_argument( - '--build-root', - default=None, - help='Path to the build directory (for test plugins, etc.). ' - 'Defaults to the source tree root.') - parser.add_argument('--sandbox', default='/tmp/autest-parallel', help='Base sandbox directory (default: /tmp/autest-parallel)') - parser.add_argument( - '-f', - '--filter', - nargs='+', - action='extend', - dest='filters', - help='Filter tests by name or glob pattern (multiple names can follow a single -f)') - parser.add_argument('--list', action='store_true', help='List tests without running') - parser.add_argument('--port-offset-step', type=int, default=1000, help='Port offset between workers (default: 1000)') - parser.add_argument('-v', '--verbose', action='store_true', help='Verbose output') - parser.add_argument('--test-dir', default='${CMAKE_GOLD_DIR}', help='Path to gold_tests directory (default: ${CMAKE_GOLD_DIR})') - parser.add_argument( - '--collect-timings', action='store_true', help='Run tests one at a time to collect accurate per-test timing data') - parser.add_argument( - '--timings-file', type=Path, default=None, help='Path to timing data JSON file (default: /test-timings.json)') - parser.add_argument( - '--no-timing', action='store_true', help='Disable timing-based load balancing (use round-robin partitioning)') - parser.add_argument( - '--serial-tests-file', - type=Path, - default=DEFAULT_SERIAL_TESTS_FILE, - help=f'Path to file listing tests that must run serially (default: {DEFAULT_SERIAL_TESTS_FILE})') - parser.add_argument('--no-serial', action='store_true', help='Skip serial tests entirely') - args, unknown_args = parser.parse_known_args() - args.extra_args = unknown_args - - # Default timing file to sandbox to avoid writing into the source tree. - if args.timings_file is None: - args.timings_file = Path(args.sandbox) / "test-timings.json" - - # --ats-bin is required unless --list is used - if not args.list and not args.ats_bin: - parser.error("--ats-bin is required when running tests (not needed for --list)") - - # Determine paths - script_dir = Path(__file__).parent.resolve() - test_dir = script_dir / args.test_dir - - # Resolve build root (defaults to source tree root, i.e. parent of tests/) - if args.build_root: - build_root = str(Path(args.build_root).resolve()) - else: - build_root = str(script_dir.parent) - - if not test_dir.exists(): - print(f"Error: Test directory not found: {test_dir}", file=sys.stderr) - sys.exit(1) - - # Load serial tests list - serial_tests = load_serial_tests(args.serial_tests_file) - if serial_tests: - print(f"Loaded {len(serial_tests)} serial tests from {args.serial_tests_file}") - - # Discover tests - all_tests = discover_tests(test_dir, args.filters) - - if not all_tests: - print("No tests found matching the specified filters.", file=sys.stderr) - sys.exit(1) - - # Separate parallel and serial tests - parallel_tests = [t for t in all_tests if t not in serial_tests] - serial_tests_to_run = [t for t in all_tests if t in serial_tests] - - print(f"Found {len(all_tests)} tests ({len(parallel_tests)} parallel, {len(serial_tests_to_run)} serial)") - - if args.list: - print("\nParallel tests:") - for test in parallel_tests: - print(f" {test}") - if serial_tests_to_run: - print("\nSerial tests (will run after parallel tests):") - for test in serial_tests_to_run: - print(f" {test}") - sys.exit(0) - - # Load existing timing data - timings = {} - if not args.no_timing: - timings = load_timings(args.timings_file) - if timings: - known_tests = sum(1 for t in parallel_tests if t in timings) - print(f"Loaded timing data for {known_tests}/{len(parallel_tests)} parallel tests from {args.timings_file}") - - # Partition parallel tests - num_jobs = min(args.jobs, len(parallel_tests)) if parallel_tests else 0 - - if parallel_tests: - if timings and not args.no_timing: - # Use timing-based load balancing (LPT algorithm) - partitions, expected_loads = partition_tests_by_time(parallel_tests, num_jobs, timings) - print(f"Using timing-based load balancing") - if args.verbose: - for i, load in enumerate(expected_loads): - tests_preview = ', '.join(partitions[i][:3]) - suffix = f", ... (+{len(partitions[i])-3} more)" if len(partitions[i]) > 3 else "" - print(f" Worker {i}: {len(partitions[i])} tests, ~{load:.1f}s expected [{tests_preview}{suffix}]") - else: - # Fall back to simple round-robin partitioning - partitions = partition_tests(parallel_tests, num_jobs) - print(f"Using round-robin partitioning") - if args.verbose: - for i in range(len(partitions)): - tests_preview = ', '.join(partitions[i][:3]) - suffix = f", ... (+{len(partitions[i])-3} more)" if len(partitions[i]) > 3 else "" - print(f" Worker {i}: {len(partitions[i])} tests [{tests_preview}{suffix}]") - else: - partitions = [] - - # Compute totals for progress tracking - total_tests = len(parallel_tests) + (len(serial_tests_to_run) if serial_tests_to_run and not args.no_serial else 0) - serial_count = len(serial_tests_to_run) if serial_tests_to_run and not args.no_serial else 0 - - if partitions: - print(f"Running with {len(partitions)} parallel workers") - print( - f"Total: {total_tests} tests ({len(parallel_tests)} parallel across {len(partitions)} workers" - f"{f', {serial_count} serial' if serial_count else ''})") - print(f"Build root: {build_root}") - print(f"Port offset step: {args.port_offset_step}") - print(f"Sandbox: {args.sandbox}") - if args.collect_timings: - print("Collecting per-test timing data (tests run sequentially per worker)") - print() - - # Create sandbox base directory - sandbox_base = Path(args.sandbox) - sandbox_base.mkdir(parents=True, exist_ok=True) - - # Progress tracking state (counts top-level tests, not autest sub-tests) - tests_done = 0 - tests_failed = 0 - tests_skipped = 0 - workers_done = 0 - total_workers = len(partitions) - - def format_eta(elapsed: float, done: int, total: int) -> str: - """Estimate remaining time based on progress so far.""" - if done == 0 or done >= total: - return "--:--" - rate = elapsed / done - remaining = rate * (total - done) - mins, secs = divmod(int(remaining), 60) - if mins >= 60: - hours, mins = divmod(mins, 60) - return f"{hours}h {mins:02d}m" - return f"{mins}m {secs:02d}s" - - def print_progress(phase: str = "Parallel"): - """Print an in-place progress line using \\r.""" - elapsed = time.time() - start_time - elapsed_str = f"{int(elapsed)}s" - eta = format_eta(elapsed, tests_done, total_tests) - pct = (tests_done * 100 // total_tests) if total_tests > 0 else 0 - fail_str = f" | {tests_failed} FAILED" if tests_failed > 0 else "" - skip_str = f" | {tests_skipped} skipped" if tests_skipped > 0 else "" - line = ( - f"\r[{phase}] {tests_done}/{total_tests} tests ({pct}%) " - f"| {workers_done}/{total_workers} workers done" - f"{fail_str}{skip_str}" - f" | {elapsed_str} elapsed | ETA: {eta} ") - print(line, end='', flush=True) - - # Run workers in parallel - start_time = time.time() - results: List[TestResult] = [] - interrupted = False - futures = {} - serial_result = None - serial_start = None - - try: - if partitions: - print_progress() - print() # Newline so worker output starts on a fresh line - with ProcessPoolExecutor(max_workers=len(partitions)) as executor: - for worker_id, worker_tests in enumerate(partitions): - future = executor.submit( - run_worker, - worker_id=worker_id, - tests=worker_tests, - script_dir=script_dir, - sandbox_base=sandbox_base, - ats_bin=args.ats_bin, - build_root=build_root, - extra_args=args.extra_args or [], - port_offset_step=args.port_offset_step, - verbose=args.verbose, - collect_timings=args.collect_timings) - futures[future] = worker_id - - # Collect results as they complete - for future in as_completed(futures): - worker_id = futures[future] - try: - result = future.result() - results.append(result) - workers_done += 1 - # Use actual test count (top-level), not autest sub-test counts - tests_done += len(result.tests) - # Count top-level tests that failed (from failed_tests list) - tests_failed += len(result.failed_tests) - # Skipped is still useful from autest counts for visibility - tests_skipped += result.skipped - - if args.verbose: - # In verbose mode, print detail line then progress - status = "PASS" if result.failed == 0 else "FAIL" - ts = datetime.now().strftime("%H:%M:%S") - parts = [f"{result.passed} passed", f"{result.failed} failed"] - if result.skipped > 0: - parts.append(f"{result.skipped} skipped") - # Clear the progress line, print detail, then re-print progress - print( - f"\r[{ts}] Worker:{worker_id:2d} Done: {', '.join(parts)} " - f"({result.duration:.1f}s) [{status}]" + " " * 20) - - print_progress() - except Exception as e: - print(f"\r[Worker {worker_id}] Error: {e}" + " " * 20, file=sys.stderr) - workers_done += 1 - tests_done += len(partitions[worker_id]) - tests_failed += len(partitions[worker_id]) - results.append( - TestResult( - worker_id=worker_id, tests=partitions[worker_id], failed=len(partitions[worker_id]), output=str(e))) - print_progress() - - # Clear the progress line after parallel phase - print() - - # Run serial tests after parallel tests complete - if serial_tests_to_run and not args.no_serial: - print(f"{'=' * 70}") - print("RUNNING SERIAL TESTS") - print(f"{'=' * 70}") - serial_start = time.time() - - # Use a special worker ID for serial tests (after parallel workers) - serial_worker_id = len(partitions) if partitions else 0 - - # Set up environment without port offset (serial tests run alone) - env = os.environ.copy() - env['AUTEST_PORT_OFFSET'] = '0' - - serial_result = TestResult(worker_id=serial_worker_id, tests=serial_tests_to_run, is_serial=True) - - for idx, test in enumerate(serial_tests_to_run, 1): - test_name, duration, status, output = run_single_test( - test, script_dir, sandbox_base / "serial", args.ats_bin, build_root, args.extra_args or [], env) - serial_result.test_timings[test_name] = duration - - if status == "PASS": - serial_result.passed += 1 - elif status == "SKIP": - serial_result.skipped += 1 - tests_skipped += 1 - else: - serial_result.failed += 1 - serial_result.failed_tests.append(test_name) - tests_failed += 1 - tests_done += 1 - - if args.verbose: - timestamp = datetime.now().strftime("%H:%M:%S") - print( - f"\r[{timestamp}] {status:4s} {duration:6.1f}s Serial {idx:2d}/{len(serial_tests_to_run):2d} {test}" + - " " * 20) - - print_progress(phase="Serial") - - serial_result.duration = time.time() - serial_start - results.append(serial_result) - - # Clear the progress line after serial phase - print() - - except KeyboardInterrupt: - interrupted = True - print("\n\nInterrupted! Collecting completed results...") - # Collect any parallel worker results that completed before the - # interrupt. The ProcessPoolExecutor's shutdown(wait=True) in __exit__ - # will have waited for workers (which also received SIGINT) to finish, - # so completed futures are available here. - seen_workers = {r.worker_id for r in results} - for future in futures: - if future.done() and not future.cancelled(): - try: - r = future.result(timeout=0) - if r.worker_id not in seen_workers: - results.append(r) - seen_workers.add(r.worker_id) - except Exception: - pass - # Capture partial serial results if we were in the serial phase. - if serial_result is not None and serial_result.worker_id not in seen_workers: - serial_result.duration = time.time() - serial_start - results.append(serial_result) - - total_duration = time.time() - start_time - - # Sort results by worker_id for consistent output - results.sort(key=lambda r: r.worker_id) - - # Collect and save timing data if collected - if args.collect_timings: - new_timings = dict(timings) # Start with existing timings - tests_timed = 0 - for r in results: - for test_name, duration in r.test_timings.items(): - new_timings[test_name] = duration - tests_timed += 1 - if tests_timed > 0: - save_timings(args.timings_file, new_timings) - print(f"\nSaved timing data for {tests_timed} tests to {args.timings_file}") - print(f"Total tests in timing database: {len(new_timings)}") - - # Print summary (pass expected timings for discrepancy check) - print_summary(results, total_duration, timings if args.collect_timings else None) - - # Exit with non-zero if any tests failed - if interrupted: - print("\nRun was interrupted by user.") - sys.exit(130) - total_failed = sum(r.failed + r.exceptions for r in results) - sys.exit(1 if total_failed > 0 else 0) - - -if __name__ == '__main__': - main() diff --git a/tests/autest.sh b/tests/autest.sh deleted file mode 100755 index 938fcb6a954..00000000000 --- a/tests/autest.sh +++ /dev/null @@ -1,40 +0,0 @@ -#! /usr/bin/env bash -# vim: sw=4:ts=4:softtabstop=4:ai:et - -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -readonly SCRIPT_DIR - -fail() -{ - echo $1 - exit 1 -} - -cd "$SCRIPT_DIR" - -./prepare_proxy_verifier.sh || fail "Failed to install Proxy Verifier." -export PYTHONPATH=$(pwd):$PYTHONPATH -export PYTHONPATH=$(pwd)/gold_tests/remap:$(pwd)/gold_tests/remap_yaml:$PYTHONPATH -./test-env-check.sh || fail "Failed Python environment checks." -hash nc 2>/dev/null || fail "Netcat is not installed." -# this is for rhel or centos systems -echo "Environment config finished. Running AuTest..." -exec uv run env \ - HTTP_PROXY= HTTPS_PROXY= NO_PROXY= http_proxy= https_proxy= no_proxy= \ - autest -D gold_tests "$@" diff --git a/tests/autest.sh.in b/tests/autest.sh.in deleted file mode 100755 index 67a50fe7ee1..00000000000 --- a/tests/autest.sh.in +++ /dev/null @@ -1,75 +0,0 @@ -#!/bin/bash -# -# convenience script for running autest after building the target -# -# Usage: -# ./autest.sh [autest args] # run tests sequentially -# ./autest.sh -j N [autest args] # run tests in parallel with N workers -# - -export LD_LIBRARY_PATH=${CMAKE_INSTALL_PREFIX}/lib -export PYTHONPATH=${CMAKE_CURRENT_SOURCE_DIR}/gold_tests/remap:${CMAKE_CURRENT_SOURCE_DIR}/gold_tests/remap_yaml:${CMAKE_CURRENT_SOURCE_DIR}/gold_tests/lib:$PYTHONPATH - -# Move test directories that require features not supported by the current curl -# build. This must happen before the parallel runner dispatches because it -# discovers tests by scanning the gold_tests directory. -if [ -n "${CURL_UDS_FLAG}" ]; then - mkdir -p "${CMAKE_SKIP_GOLD_DIR}" - if [ -d "${CMAKE_GOLD_DIR}/h2" ]; then - mv "${CMAKE_GOLD_DIR}/h2" "${CMAKE_SKIP_GOLD_DIR}/h2" - fi - if [ -d "${CMAKE_GOLD_DIR}/tls" ]; then - mv "${CMAKE_GOLD_DIR}/tls" "${CMAKE_SKIP_GOLD_DIR}/tls" - fi - if [ -d "${CMAKE_GOLD_DIR}/tls_hooks" ]; then - mv "${CMAKE_GOLD_DIR}/tls_hooks" "${CMAKE_SKIP_GOLD_DIR}/tls_hooks" - fi -fi - -# Check whether parallel mode was requested. -parallel_mode=false -for arg in "$@"; do - case "$arg" in - -j|-j[0-9]*|--jobs|--jobs=*) - parallel_mode=true - break - ;; - esac -done - -if $parallel_mode; then - python3 "$(dirname "$0")/autest-parallel.py" \ - --ats-bin=${CMAKE_INSTALL_PREFIX}/bin \ - --build-root ${CMAKE_BINARY_DIR} \ - --sandbox ${AUTEST_SANDBOX} \ - ${CURL_UDS_FLAG} ${AUTEST_OPTIONS} "$@" - autest_exit=$? -else - uv run autest \ - --sandbox ${AUTEST_SANDBOX} \ - --directory ${CMAKE_GOLD_DIR} \ - --ats-bin=${CMAKE_INSTALL_PREFIX}/bin \ - --proxy-verifier-bin ${PROXY_VERIFIER_PATH} \ - --build-root ${CMAKE_BINARY_DIR} \ - ${CURL_UDS_FLAG} ${AUTEST_OPTIONS} \ - "$@" - autest_exit=$? -fi - -# Restore tests back to source tree and remove temp dir. -if [ -n "${CURL_UDS_FLAG}" ]; then - if [ -d "${CMAKE_SKIP_GOLD_DIR}/h2" ]; then - mv "${CMAKE_SKIP_GOLD_DIR}/h2" "${CMAKE_GOLD_DIR}/h2" - fi - if [ -d "${CMAKE_SKIP_GOLD_DIR}/tls" ]; then - mv "${CMAKE_SKIP_GOLD_DIR}/tls" "${CMAKE_GOLD_DIR}/tls" - fi - if [ -d "${CMAKE_SKIP_GOLD_DIR}/tls_hooks" ]; then - mv "${CMAKE_SKIP_GOLD_DIR}/tls_hooks" "${CMAKE_GOLD_DIR}/tls_hooks" - fi - if [ -d "${CMAKE_SKIP_GOLD_DIR}" ]; then - rm -rf "${CMAKE_SKIP_GOLD_DIR}" - fi -fi - -exit $autest_exit diff --git a/tests/gold_tests/ats_probe/ats_probe.test.py b/tests/gold_tests/ats_probe/ats_probe.test.py deleted file mode 100644 index be849d56db7..00000000000 --- a/tests/gold_tests/ats_probe/ats_probe.test.py +++ /dev/null @@ -1,112 +0,0 @@ -'''Verify SystemTap ATS probe behavior.''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import os - -Test.Summary = '''Verify SystemTap ATS probes.''' - -# Skipping this test generally because it requires privilege. Thus most CI -# systems will fail this test. Comment out the following line to run in your -# privileged environment. -Test.SkipUnless(Condition(lambda: False, "Test requires privilege", True)) -Test.SkipUnless(Condition.HasProgram("bpftrace", "Need bpftrace to verify the probe.")) - - -class TestATSProbe: - '''Verify SystemTap ATS probes.''' - replay_file: str = 'ats_probe.replay.yaml' - bt_script: str = 'ats_probe.bt' - - def __init__(self): - '''Configure the TestRun.''' - tr = Test.AddTestRun('Verify ATS probes.') - self._configure_dns(tr) - self._configure_server(tr) - self._configure_traffic_server(tr) - self._configure_bpftrace(tr) - self._configure_client(tr) - - def _configure_dns(self, tr: 'TestRun') -> 'Process': - '''Configure the DNS process. - - :param tr: The TestRun to add the DNS process to. - :return: The DNS process. - ''' - dns = tr.MakeDNServer('dns', default='127.0.0.1') - self._dns = dns - return dns - - def _configure_server(self, tr: 'TestRun') -> 'Process': - '''Configure the server process. - - :param tr: The TestRun to add the server process to. - :return: The server process. - ''' - server = tr.AddVerifierServerProcess('server', self.replay_file) - self._server = server - return server - - def _configure_traffic_server(self, tr: 'TestRun') -> 'Process': - '''Configure the Traffic Server process. - - :param tr: The TestRun to add the Traffic Server process to. - :return: The Traffic Server process. - ''' - ts = tr.MakeATSProcess("ts", enable_cache=False) - self._ts = ts - - ts.Disk.records_config.update( - { - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'http', - 'proxy.config.dns.nameservers': f"127.0.0.1:{self._dns.Variables.Port}", - 'proxy.config.dns.resolv_conf': 'NULL' - }) - server_port = self._server.Variables.http_port - ts.Disk.remap_config.AddLine(f'map / http://backend.server.com:{server_port}') - return ts - - def _configure_bpftrace(self, tr: 'TestRun') -> 'Process': - '''Configure the bpftrace process for the ATS probe. - - :param tr: The TestRun to add the bpftrace process to. - :return: The bpftrace process. - ''' - bpftrace = tr.Processes.Process('bpftrace') - self._bpftrace = bpftrace - tr.Setup.Copy(self.bt_script) - tr_script = os.path.join(tr.RunDirectory, self.bt_script) - bpftrace.Command = f'sudo bpftrace {tr_script}' - bpftrace.ReturnCode = 0 - bpftrace.Streams.All += Testers.ContainsExpression( - 'backend.server.com', 'The probe correctly printed the origin servername.') - return bpftrace - - def _configure_client(self, tr: 'TestRun') -> 'Process': - '''Configure the client process. - - :param tr: The TestRun to add the client process to. - :return: The client process. - ''' - client = tr.AddVerifierClientProcess("client", self.replay_file, http_ports=[self._ts.Variables.port]) - self._ts.StartBefore(self._dns) - self._ts.StartBefore(self._server) - self._bpftrace.StartBefore(self._ts) - client.StartBefore(self._bpftrace) - - -TestATSProbe() diff --git a/tests/gold_tests/ats_probe/cache_dir_probe.test.py b/tests/gold_tests/ats_probe/cache_dir_probe.test.py deleted file mode 100644 index 3b7208c25fb..00000000000 --- a/tests/gold_tests/ats_probe/cache_dir_probe.test.py +++ /dev/null @@ -1,122 +0,0 @@ -'''Verify cache directory SystemTap probes fire on insert and remove.''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import os - -Test.Summary = '''Verify cache directory SystemTap probes fire on cache fill and PURGE.''' - -# Skipping this test generally because it requires privilege. Thus most CI systems will skip it. -Test.SkipUnless( - Condition(lambda: os.geteuid() == 0, "Test requires privilege", True), - Condition.HasProgram("bpftrace", "Need bpftrace to verify the probe.")) - - -class CacheDirProbeTest: - '''Verify cache directory SystemTap probes.''' - bt_script: str = 'cache_dir_probe.bt' - _cache_path: str = '/cacheable' - - def __init__(self): - tr = Test.AddTestRun('Cache directory probes should trigger on insert and purge.') - self._configure_origin(tr) - self._configure_traffic_server(tr) - self._configure_bpftrace(tr) - self._configure_client(tr) - - def _configure_origin(self, tr: 'TestRun') -> 'Process': - '''Configure the origin microserver.''' - origin = Test.MakeOriginServer('origin') - self._origin = origin - - cache_request = { - "headers": f"GET {self._cache_path} HTTP/1.1\r\nHost: cache-probe.test\r\n\r\n", - "timestamp": "1469733493.993", - "body": "" - } - cache_response = { - "headers": "HTTP/1.1 200 OK\r\nConnection: close\r\nCache-Control: max-age=120\r\nContent-Length: 5\r\n\r\n", - "timestamp": "1469733493.993", - "body": "hello" - } - origin.addResponse("sessionlog.json", cache_request, cache_response) - origin.addResponse("sessionlog.json", cache_request, cache_response) - - purge_request = { - "headers": f"PURGE {self._cache_path} HTTP/1.1\r\nHost: cache-probe.test\r\n\r\n", - "timestamp": "1469733493.993", - "body": "" - } - purge_response = { - "headers": "HTTP/1.1 200 OK\r\nConnection: close\r\nContent-Length: 0\r\n\r\n", - "timestamp": "1469733493.993", - "body": "" - } - origin.addResponse("sessionlog.json", purge_request, purge_response) - return origin - - def _configure_traffic_server(self, tr: 'TestRun') -> 'Process': - '''Configure the Traffic Server process.''' - ts = tr.MakeATSProcess("ts_cache_dir_probe", enable_cache=True) - self._ts = ts - ts.Disk.records_config.update( - { - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'http|cache', - 'proxy.config.http.cache.required_headers': 0, - # Keep ATS running as the invoking user inside sudo (no privilege drop). - 'proxy.config.admin.user_id': '#-1', - }) - ts.Disk.remap_config.AddLine(f'map / http://127.0.0.1:{self._origin.Variables.Port}') - return ts - - def _configure_bpftrace(self, tr: 'TestRun') -> 'Process': - '''Configure the bpftrace process for the cache directory probes.''' - bpftrace = tr.Processes.Process('bpftrace') - self._bpftrace = bpftrace - - tr.Setup.Copy(self.bt_script) - tr_script = os.path.join(tr.RunDirectory, self.bt_script) - - # fan out output so AuTest stream checks still work - tee_path = os.path.join(tr.RunDirectory, 'bpftrace.out') - bpftrace.Command = f"bpftrace {tr_script}" - bpftrace.ReturnCode = 0 - bpftrace.Streams.All += Testers.ContainsExpression('cache_dir_insert', 'cache_dir_insert probe fired.') - bpftrace.Streams.All += Testers.ContainsExpression('cache_dir_remove', 'cache_dir_remove probe fired.') - - return bpftrace - - def _configure_client(self, tr: 'TestRun') -> 'Process': - '''Configure the client traffic to exercise cache insert and purge.''' - client = tr.Processes.Default - self._client = client - cache_url = f"http://127.0.0.1:{self._ts.Variables.port}{self._cache_path}" - # Ideally we don't need this "sleep 1", but I haven't been able to get it to work with the Ready = When.FileContains(...) approach. - client.Command = ( - f"sleep 1 && curl -sSf -o /dev/null -H 'Host: cache-probe.test' {cache_url} && " - f"curl -sSf -o /dev/null -X PURGE -H 'Host: cache-probe.test' {cache_url} && " - f"curl -sSf -o /dev/null -H 'Host: cache-probe.test' {cache_url}") - client.ReturnCode = 0 - client.Env = self._ts.Env - - self._ts.StartBefore(self._origin) # origin before ts - self._bpftrace.StartBefore(self._ts) # ts before bpftrace - client.StartBefore(self._bpftrace) # bpftrace before client - return client - - -CacheDirProbeTest() diff --git a/tests/gold_tests/autest-site/.editorconfig b/tests/gold_tests/autest-site/.editorconfig deleted file mode 100644 index b3d77db373e..00000000000 --- a/tests/gold_tests/autest-site/.editorconfig +++ /dev/null @@ -1,26 +0,0 @@ -# -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -# Configuration file for EditorConfig -# More information is available under http://EditorConfig.org - -root = false - -# Python -[*.ext] -indent_size = 4 -charset = utf-8 diff --git a/tests/gold_tests/autest-site/ats_replay.test.ext b/tests/gold_tests/autest-site/ats_replay.test.ext deleted file mode 100644 index 205fac37a84..00000000000 --- a/tests/gold_tests/autest-site/ats_replay.test.ext +++ /dev/null @@ -1,435 +0,0 @@ -''' -Implement general-purpose ATS test extensions using proxy verifier replay files. -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -from typing import Optional -import os -import re -import yaml - -_PATH_VARIABLE_RE = re.compile(r'\{([A-Za-z_][A-Za-z0-9_]*)\}') - - -def _resolve_test_artifact_path(obj, path: str) -> str: - '''Resolve a test artifact path for copy_custom_plugin. - - Resolution order after variable expansion: - 1. Absolute paths. - 2. Paths relative to the current test directory. - 3. Paths relative to the current test's build-tree mirror under - Variables.AtsBuildGoldTestsDir. - 4. Paths relative to Variables.AtsBuildGoldTestsDir. - ''' - - # Expand any specified obj.Variables into the path. - resolved_path = _PATH_VARIABLE_RE.sub(lambda match: str(getattr(obj.Variables, match.group(1), match.group(0))), path) - if os.path.isabs(resolved_path): - return resolved_path - - test_relative_path = os.path.join(obj.TestDirectory, resolved_path) - if os.path.exists(test_relative_path): - return test_relative_path - - gold_tests_source_dir = os.path.join(obj.Variables.RepoDir, 'tests', 'gold_tests') - if os.path.commonpath([gold_tests_source_dir, obj.TestDirectory]) == gold_tests_source_dir: - test_relpath = os.path.relpath(obj.TestDirectory, gold_tests_source_dir) - build_relative_path = os.path.join(obj.Variables.AtsBuildGoldTestsDir, test_relpath, resolved_path) - if os.path.exists(build_relative_path): - return build_relative_path - - # Otherwise, resolve relative to Variables.AtsBuildGoldTestsDir. - return os.path.join(obj.Variables.AtsBuildGoldTestsDir, resolved_path) - - -def _format_plugin_config_entry(plugin_entry) -> str: - '''Format a plugin.config entry from a string or structured config.''' - if isinstance(plugin_entry, str): - return plugin_entry - - if isinstance(plugin_entry, dict): - line = plugin_entry['name'] - for arg in plugin_entry.get('args', []): - line += f' {arg}' - return line - - raise TypeError(f'Unsupported plugin_config entry type: {type(plugin_entry)}') - - -def _contains_expression(contains_entry: dict, default_description: str): - '''Create a ContainsExpression tester from a log validation entry.''' - expression = contains_entry['expression'] - description = contains_entry.get('description', default_description) - reflags = re.S | re.M if contains_entry.get('multiline', False) else 0 - - return Testers.ContainsExpression(expression, description, reflags=reflags) - - -def configure_ats(obj: 'TestRun', server: 'Process', ats_config: dict, dns: Optional['Process'] = None): - '''Configure ATS per the configuration in the replay file. - - :param obj: The Test object to configure ATS for. - :param server: The Proxy Verifier Server process to use. - :param ats_config: The ATS configuration from the replay file. - :param dns: The DNS process to use. Optional. - :returns: The ATS process object. - ''' - name = ats_config.get('name', 'ts') - process_config = ats_config.get('process_config', {}) - ts = obj.MakeATSProcess(name, **process_config) - if 'startup_timeout' in ats_config: - ts.StartupTimeout = ats_config['startup_timeout'] - - # Cripts are compiled with clang at TS startup (during remap load), which is - # slow and scales with the number of cripts. - if process_config.get('enable_cripts', False): - ts.StartupTimeout = 60 - - # Configure records_config if specified. - records_config = ats_config.get('records_config', {}) - ts.Disk.records_config.update(records_config) - - # TLS configs - enable_tls = process_config.get('enable_tls', False) - if enable_tls: - # Configure ssl_multicert.yaml if specified. - ssl_multicert_yaml = ats_config.get('ssl_multicert_yaml', []) - - # setup default cert and key if ssl_multicert_yaml is empty - if ssl_multicert_yaml == []: - ts.addDefaultSSLFiles() - - ts.Disk.records_config.update( - { - 'proxy.config.ssl.server.cert.path': ts.Variables.SSLDir, - 'proxy.config.ssl.server.private_key.path': ts.Variables.SSLDir, - }) - - ssl_multicert_yaml = [ - "ssl_multicert:", - " - ssl_cert_name: server.pem", - " ssl_key_name: server.key", - ' dest_ip: "*"', - ] - - for line in ssl_multicert_yaml: - ts.Disk.ssl_multicert_yaml.AddLine(line) - - # Configure sni.yaml if specified. - sni_yaml = ats_config.get('sni_yaml') - if sni_yaml != None: - ts.Disk.sni_yaml.AddLines(yaml.dump(sni_yaml).split('\n')) - - # Configure plugin_config if specified. - plugin_config = ats_config.get('plugin_config', []) - for plugin_line in plugin_config: - ts.Disk.plugin_config.AddLine(_format_plugin_config_entry(plugin_line)) - - # Stage custom plugin artifacts into the ATS sandbox plugin directory. - for plugin_path in ats_config.get('copy_custom_plugin', []): - ts.Setup.Copy(_resolve_test_artifact_path(obj, plugin_path), ts.Env['PROXY_CONFIG_PLUGIN_PLUGIN_DIR']) - - # Configure parent_config if specified. - parent_config = ats_config.get('parent_config', []) - for parent_line in parent_config: - parent_line = parent_line.replace('{SERVER_HTTP_PORT}', str(server.Variables.http_port)) - parent_line = parent_line.replace('{SERVER_HTTPS_PORT}', str(server.Variables.https_port)) - ts.Disk.parent_config.AddLine(parent_line) - - # Configure cache_config if specified. - cache_config = ats_config.get('cache_config', []) - for cache_line in cache_config: - cache_line = cache_line.replace('{SERVER_HTTP_PORT}', str(server.Variables.http_port)) - cache_line = cache_line.replace('{SERVER_HTTPS_PORT}', str(server.Variables.https_port)) - ts.Disk.cache_config.AddLine(cache_line) - - # Configure logging.yaml if specified. - logging_yaml = ats_config.get('logging_yaml') - if logging_yaml != None: - ts.Disk.logging_yaml.AddLines(yaml.dump(logging_yaml).split('\n')) - - # Configure storage.yaml if specified. - storage_yaml = ats_config.get('storage_yaml') - if storage_yaml is not None: - ts.Disk.storage_yaml.AddLines(yaml.dump(storage_yaml).split('\n')) - - remap_config = ats_config.get('remap_config', []) - for remap_entry in remap_config: - if isinstance(remap_entry, str): - remap_entry = remap_entry.replace('{SERVER_HTTP_PORT}', str(server.Variables.http_port)) - remap_entry = remap_entry.replace('{SERVER_HTTPS_PORT}', str(server.Variables.https_port)) - ts.Disk.remap_config.AddLine(remap_entry) - elif isinstance(remap_entry, dict): - from_url = remap_entry['from'] - to_url = remap_entry['to'] - to_url = to_url.replace('{SERVER_HTTP_PORT}', str(server.Variables.http_port)) - to_url = to_url.replace('{SERVER_HTTPS_PORT}', str(server.Variables.https_port)) - plugins = remap_entry.get('plugins', []) - options = remap_entry.get('options', []) - line = f'map {from_url} {to_url}' - for plugin in plugins: - line += f' @plugin={plugin["name"]}' - for arg in plugin["args"]: - line += f' @pparam={arg}' - for option in options: - line += f' {option}' - ts.Disk.remap_config.AddLine(line) - if dns: - ts.Disk.records_config.update( - { - 'proxy.config.dns.nameservers': f'127.0.0.1:{dns.Variables.Port}', - 'proxy.config.dns.resolv_conf': 'NULL', - }) - - for item in ats_config.get('copy_to_config_dir', []): - item_path = os.path.join(obj.TestDirectory, item) - if os.path.isdir(item_path): - src_dir = item_path - dst_dir = os.path.join(ts.Variables.CONFIGDIR, item) - ts.Setup.MakeDir(dst_dir) - ts.Setup.Copy(src_dir, dst_dir) - else: - ts.Setup.CopyAs(item, ts.Variables.CONFIGDIR) - - # Configure log validation if specified. - log_validation = ats_config.get('log_validation', {}) - if log_validation: - # traffic_out validation. - traffic_out = log_validation.get('traffic_out', {}) - for contains_entry in traffic_out.get('contains', []): - expression = contains_entry['expression'] - ts.Disk.traffic_out.Content += _contains_expression(contains_entry, f'Verify traffic_out contains: {expression}') - for excludes_entry in traffic_out.get('excludes', []): - expression = excludes_entry['expression'] - description = excludes_entry.get('description', f'Verify traffic_out excludes: {expression}') - ts.Disk.traffic_out.Content += Testers.ExcludesExpression(expression, description) - # Gold file validation for traffic_out. - if 'gold_file' in traffic_out: - gold_file = traffic_out['gold_file'] - ts.Disk.traffic_out.Content += gold_file - - # diags_log validation. - diags_log = log_validation.get('diags_log', {}) - for contains_entry in diags_log.get('contains', []): - expression = contains_entry['expression'] - ts.Disk.diags_log.Content += _contains_expression(contains_entry, f'Verify diags_log contains: {expression}') - for excludes_entry in diags_log.get('excludes', []): - expression = excludes_entry['expression'] - description = excludes_entry.get('description', f'Verify diags_log excludes: {expression}') - ts.Disk.diags_log.Content += Testers.ExcludesExpression(expression, description) - # Gold file validation for diags_log. - if 'gold_file' in diags_log: - gold_file = diags_log['gold_file'] - ts.Disk.diags_log.Content += gold_file - - # error_log validation. - error_log = log_validation.get('error_log', {}) - for contains_entry in error_log.get('contains', []): - expression = contains_entry['expression'] - ts.Disk.error_log.Content += _contains_expression(contains_entry, f'Verify error_log contains: {expression}') - for excludes_entry in error_log.get('excludes', []): - expression = excludes_entry['expression'] - description = excludes_entry.get('description', f'Verify error_log excludes: {expression}') - ts.Disk.error_log.Content += Testers.ExcludesExpression(expression, description) - # Gold file validation for error_log. - if 'gold_file' in error_log: - gold_file = error_log['gold_file'] - ts.Disk.error_log.Content += gold_file - - # access_log validation. - access_log = log_validation.get('access_log', {}) - if 'gold_file' in access_log: - obj.Disk.File(os.path.join(ts.Variables.LOGDIR, access_log['filename']), exists=True, content=access_log['gold_file']) - - return ts - - -def _requires_persistent_ats(ats_config: dict) -> bool: - '''Determine whether ATS must span multiple test runs. - - Some features, such as metric verification, require additional test runs - using the ATS process after traffic completes. In those cases the ATS - process must be owned by Test rather than a TestRun so it persists across - test runs. - - :param ats_config: The autest.ats configuration from the replay file. - :returns: True if ATS must span multiple test runs. - ''' - - # This will become more complicated over time. For example, it would be nice - # to add transaction log support which would require a longer-lived ATS - # process. - return bool(ats_config.get('metric_checks')) - - -def ATSReplayTest(obj, replay_file: str): - '''Create a TestRun that configures ATS and runs HTTP traffic using the replay file. - - :param obj: The Test object to add the test run to. - :param replay_file: Replay file specifying the test configuration and test traffic. - :returns: The TestRun object. - ''' - - replay_path = replay_file if os.path.isabs(replay_file) else os.path.join(obj.TestDirectory, replay_file) - with open(replay_path, 'r') as f: - replay_config = yaml.safe_load(f) - - # The user must specify the 'autest' node. - if not 'autest' in replay_config: - raise ValueError(f"Replay file {replay_file} does not contain 'autest' section") - autest_config = replay_config['autest'] - - tr = obj.AddTestRun(autest_config['description']) - - # Copy the specified files and directories down. - tr.Setup.Copy(replay_file, tr.RunDirectory) - - for files_to_copy in autest_config.get('files_to_copy', []): - tr.Setup.Copy(files_to_copy) - for dirs_to_copy in autest_config.get('dirs_to_copy', []): - tr.Setup.Copy(dirs_to_copy) - - # DNS configuration. - dns = None - if 'dns' in autest_config: - dns_config = autest_config['dns'] - name = dns_config['name'] - if 'process_config' in dns_config: - process_config = dns_config['process_config'] - dns = tr.MakeDNServer(name, **process_config) - else: - dns = tr.MakeDNServer(name, default='127.0.0.1') - if 'records' in dns_config: - dns.addRecords(dns_config['records']) - - # Proxy Verifier Server configuration. - if not 'server' in autest_config: - raise ValueError(f"Replay file {replay_file} does not contain 'autest.server' section") - server_config = autest_config['server'] - name = server_config['name'] - process_config = server_config.get('process_config', {}) - server = tr.AddVerifierServerProcess(name, replay_file, **process_config) - - # Set expected return code for server if specified. A list of codes is - # wrapped in Any() so that any of the listed values is accepted. - if 'return_code' in server_config: - rc = server_config['return_code'] - server.ReturnCode = Any(*rc) if isinstance(rc, list) else rc - - # Configure server log validation if specified. - server_log_validation = server_config.get('log_validation', {}) - if server_log_validation: - for contains_entry in server_log_validation.get('contains', []): - expression = contains_entry['expression'] - server.Streams.All += _contains_expression(contains_entry, f'Verify server output contains: {expression}') - for excludes_entry in server_log_validation.get('excludes', []): - expression = excludes_entry['expression'] - description = excludes_entry.get('description', f'Verify server output excludes: {expression}') - server.Streams.All += Testers.ExcludesExpression(expression, description) - - # ATS configuration. - if not 'ats' in autest_config: - raise ValueError(f"Replay file {replay_file} does not contain 'autest.ats' section") - ats_config = autest_config['ats'] - process_config = ats_config.get('process_config', {}) - enable_tls = process_config.get('enable_tls', False) - enable_quic = process_config.get('enable_quic', False) - metric_checks = ats_config.get('metric_checks', []) - log_validation = ats_config.get('log_validation', None) - - ats_owner = obj if _requires_persistent_ats(ats_config) else tr - ts = configure_ats(ats_owner, server=server, ats_config=ats_config, dns=dns) - - # Proxy Verifier Client configuration. - if not 'client' in autest_config: - raise ValueError(f"Replay file {replay_file} does not contain 'autest.client' section") - client_config = autest_config['client'] - name = client_config.get('name', 'client') - process_config = client_config.get('process_config', {}).copy() - http_ports = process_config.pop('http_ports', [ts.Variables.port]) - https_ports = process_config.pop('https_ports', [ts.Variables.ssl_port] if enable_tls else None) - http3_ports = process_config.pop('http3_ports', [ts.Variables.ssl_port] if enable_quic else None) - client = tr.AddVerifierClientProcess( - name, replay_file, http_ports=http_ports, https_ports=https_ports, http3_ports=http3_ports, **process_config) - - # Set expected return code if specified. A list of codes is wrapped in - # Any() so that any of the listed values is accepted. - if 'return_code' in client_config: - rc = client_config['return_code'] - client.ReturnCode = Any(*rc) if isinstance(rc, list) else rc - - # Configure client log validation if specified. - client_log_validation = client_config.get('log_validation', {}) - if client_log_validation: - for contains_entry in client_log_validation.get('contains', []): - expression = contains_entry['expression'] - client.Streams.All += _contains_expression(contains_entry, f'Verify client output contains: {expression}') - for excludes_entry in client_log_validation.get('excludes', []): - expression = excludes_entry['expression'] - description = excludes_entry.get('description', f'Verify client output excludes: {expression}') - client.Streams.All += Testers.ExcludesExpression(expression, description) - - if dns: - ts.StartBefore(dns) - ts.StartBefore(server) - client.StartBefore(ts) - - # Metric verification test runs. - if metric_checks: - tr.StillRunningAfter = ts - - wait_tr = obj.AddTestRun('Wait for stats to propagate') - wait_tr.Processes.Default.Command = 'sleep 2' - wait_tr.Processes.Default.ReturnCode = 0 - wait_tr.StillRunningAfter = ts - - for check in metric_checks: - metric_name = check['metric'] - if 'value' in check: - expected_value = check['value'] - check_tr = obj.AddTestRun(f'Verify {metric_name} == {expected_value}') - check_tr.Processes.Default.Command = f'traffic_ctl metric get {metric_name}' - check_tr.Processes.Default.Env = ts.Env - check_tr.Processes.Default.ReturnCode = 0 - check_tr.Processes.Default.Streams.All = Testers.ContainsExpression( - f'^{re.escape(metric_name)}\\s+{expected_value}$', f'{metric_name} should be {expected_value}') - check_tr.StillRunningAfter = ts - elif 'min' in check: - minimum = check['min'] - check_tr = obj.AddTestRun(f'Verify {metric_name} >= {minimum}') - # Fail if the metric value (field 2) is below the minimum, or absent. - # $$ escapes the awk field ref from autest's command templating. - check_tr.Processes.Default.Command = ( - f"traffic_ctl metric get {metric_name} | " - f"awk 'BEGIN {{ rc = 1 }} {{ if ($$2 + 0 >= {minimum}) rc = 0 }} END {{ exit rc }}'") - check_tr.Processes.Default.Env = ts.Env - check_tr.Processes.Default.ReturnCode = 0 - check_tr.StillRunningAfter = ts - else: - raise ValueError(f"metric_checks entry for '{metric_name}' must specify 'value' or 'min'") - - # wait for error log - if log_validation and log_validation.get('error_log'): - wait_for_log = obj.AddTestRun('Wait for logs of ' + ats_config.get('name', 'ts')) - wait_for_log.Processes.Default.Command = ( - os.path.join(obj.Variables.AtsTestToolsDir, 'condwait') + ' 60 1 -f ' + os.path.join(ts.Variables.LOGDIR, 'error.log')) - wait_for_log.Processes.Default.ReturnCode = 0 - - return tr - - -ExtendTest(ATSReplayTest, name="ATSReplayTest") diff --git a/tests/gold_tests/autest-site/cli_tools.test.ext b/tests/gold_tests/autest-site/cli_tools.test.ext deleted file mode 100644 index 757efd99dd7..00000000000 --- a/tests/gold_tests/autest-site/cli_tools.test.ext +++ /dev/null @@ -1,48 +0,0 @@ -''' -Tools to help with TestRun commands -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -# allows the same command to be repeatedly run in parallel. -# -# example usage: -# curlcmd = 'curl -vs -k https:127.0.0.1:{port}'.format(port = ts.Variables.port) -# -# tr = Test.AddTestRun() -# ps = tr.SpawnCommands(cmdstr=cmdstr, count=25) -# tr.Processes.Default.StartBefore(ts) -# ts.StartAfter(*ps) -# tr.StillRunningAfter = ts -# -# Note that by default, the Default Process is created in this function. - - -def spawn_commands(self, cmdstr, count, retcode=0, use_default=True): - ret = [] - - if use_default: - count = int(count) - 1 - for cnt in range(0, count): - ret.append(self.Processes.Process(name="cmdline-{num}".format(num=cnt), cmdstr=cmdstr, returncode=retcode)) - if use_default: - self.Processes.Default.Command = cmdstr - self.Processes.Default.ReturnCode = retcode - self.Processes.Default.StartBefore(*ret) - return ret - - -ExtendTestRun(spawn_commands, name="SpawnCommands") diff --git a/tests/gold_tests/autest-site/conditions.test.ext b/tests/gold_tests/autest-site/conditions.test.ext deleted file mode 100644 index 090075c4ff8..00000000000 --- a/tests/gold_tests/autest-site/conditions.test.ext +++ /dev/null @@ -1,383 +0,0 @@ -''' -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import os -import subprocess -import json -import re -import tempfile -import time - -from ports import get_port_number - -OPENSSL_TLS_FLAGS = { - "1.0": "-tls1", - "1.1": "-tls1_1", - "1.2": "-tls1_2", - "1.3": "-tls1_3", -} - - -def _version_tuple(value, width=3): - parts = [int(part) for part in re.findall(r'\d+', value)[:width]] - parts.extend([0] * (width - len(parts))) - return tuple(parts) - - -def _terminate_process(process): - if process.poll() is not None: - return - try: - process.terminate() - process.wait(timeout=2) - except (OSError, subprocess.TimeoutExpired): - try: - process.kill() - process.wait(timeout=2) - except (OSError, subprocess.TimeoutExpired): - pass - - -def _probe_openssl_server(tls_version, client_probe): - """Run a local OpenSSL server for TLS capability probes. - - This owns the temporary certificate, port allocation, and server process - lifecycle so AuTest conditions can perform a real handshake with the client - being checked. Local setup failures return ``False`` so callers can skip - dependent tests instead of failing the harness. - - :param tls_version: TLS version string to look up in - ``OPENSSL_TLS_FLAGS``. - :param client_probe: Callable that receives the server port and TLS flag - and returns whether the client completed the expected handshake. - :returns: ``True`` if the client probe succeeds against the local server, - otherwise ``False``. - """ - tls_flag = OPENSSL_TLS_FLAGS.get(tls_version) - if tls_flag is None: - return False - - try: - with tempfile.TemporaryDirectory() as tmpdir: - cert_path = os.path.join(tmpdir, "cert.pem") - key_path = os.path.join(tmpdir, "key.pem") - result = subprocess.run( - [ - "openssl", - "req", - "-x509", - "-newkey", - "rsa:2048", - "-nodes", - "-sha256", - "-keyout", - key_path, - "-out", - cert_path, - "-subj", - "/CN=localhost", - "-days", - "1", - ], - stdout=subprocess.DEVNULL, - stderr=subprocess.DEVNULL, - timeout=10, - ) - if result.returncode != 0: - return False - - with get_port_number() as port: - server = subprocess.Popen( - [ - "openssl", - "s_server", - "-quiet", - "-accept", - f"127.0.0.1:{port}", - "-cert", - cert_path, - "-key", - key_path, - tls_flag, - "-cipher", - "DEFAULT@SECLEVEL=0", - "-www", - ], - stdout=subprocess.DEVNULL, - stderr=subprocess.DEVNULL, - ) - try: - time.sleep(0.5) - if server.poll() is not None: - return False - return client_probe(port, tls_flag) - finally: - _terminate_process(server) - except (OSError, subprocess.SubprocessError): - return False - - -def HasOpenSSLVersion(self, version): - output = subprocess.check_output(os.path.join(self.Variables.BINDIR, "traffic_layout") + " info --versions --json", shell=True) - json_data = output.decode('utf-8') - openssl_str = json.loads(json_data)['openssl_str'] - match = re.search(r'\d+(?:\.\d+)+', openssl_str) - if match is None: - raise ValueError("Error determining version of OpenSSL library needed by traffic_server executable") - exe_ver = match.group(0) - return self.Condition( - lambda: _version_tuple(exe_ver) >= _version_tuple(version), - "OpenSSL library version is " + exe_ver + ", must be at least " + version) - - -def HasOpenSSLQuicClient(self): - """Check whether the openssl CLI supports s_client -quic.""" - - def check_openssl_quic_client(): - try: - result = subprocess.run(["openssl", "s_client", "-help"], capture_output=True, text=True, timeout=5) - except (OSError, subprocess.SubprocessError): - return False - - return "-quic" in result.stdout or "-quic" in result.stderr - - return self.Condition(check_openssl_quic_client, "OpenSSL CLI must support s_client -quic") - - -def IsBoringSSL(self): - output = subprocess.check_output(os.path.join(self.Variables.BINDIR, "traffic_layout") + " info --versions --json", shell=True) - json_data = output.decode('utf-8') - openssl_str = json.loads(json_data)['openssl_str'] - return self.Condition( - # OpenSSL 1.1.1 (compatible; BoringSSL) - lambda: "compatible; BoringSSL" in openssl_str, - "SSL library is not BoringSSL") - - -def IsOpenSSL(self): - output = subprocess.check_output(os.path.join(self.Variables.BINDIR, "traffic_layout") + " info --versions --json", shell=True) - json_data = output.decode('utf-8') - openssl_str = json.loads(json_data)['openssl_str'] - return self.Condition( - # OpenSSL 1.1.1k 25 Mar 2021 - lambda: "OpenSSL" in openssl_str and "compatible; BoringSSL" not in openssl_str, - "SSL library is not OpenSSL") - - -def HasLegacyTLSSupport(self): - """Check if the system supports legacy TLS protocols (TLSv1.0 and TLSv1.1). - - Modern OpenSSL 3.x installations often disable these protocols entirely, - even if the openssl binary still accepts the -tls1 flag and lists TLSv1 ciphers. - - On Fedora/RHEL systems, the crypto-policies framework may disable legacy - TLS at runtime even when OpenSSL is compiled with support for it. This - causes 'openssl ciphers -v -tls1' to still list TLSv1 ciphers, but actual - TLS 1.0 connections will fail with "no protocols available". - - We only probe TLSv1.0 (not TLSv1.1 separately) because crypto-policies - always disable both legacy versions together. If TLSv1.0 is unavailable, - TLSv1.1 will be too. - - The check starts a local OpenSSL server and uses an OpenSSL client to - complete a real TLSv1.0 handshake. This avoids external network dependency - while catching environments that accept legacy TLS command-line flags but - reject the protocol during an actual handshake. - """ - - def check_tls1_support(): - - def client_probe(port, tls_flag): - try: - result = subprocess.run( - [ - 'openssl', - 's_client', - tls_flag, - '-connect', - f'127.0.0.1:{port}', - '-cipher', - 'DEFAULT@SECLEVEL=0', - '-brief', - ], - capture_output=True, - text=True, - timeout=5, - input='Q\n') - output = result.stdout + result.stderr - return result.returncode == 0 and 'Protocol version: TLSv1' in output - except subprocess.TimeoutExpired: - return False - except Exception: - return False - - return _probe_openssl_server("1.0", client_probe) - - return self.Condition(check_tls1_support, "System does not support legacy TLS protocols (TLSv1.0/TLSv1.1)") - - -def HasCurlVersion(self, version): - return self.EnsureVersion(["curl", "--version"], min_version=version) - - -def HasProxyVerifierVersion(self, version): - verifier_path = os.path.join(self.Variables.VerifierBinPath, 'verifier-client') - return self.EnsureVersion([verifier_path, "--version"], min_version=version) - - -def HasGoVersion(self, version): - """Check whether the go command is available at the requested version.""" - - def check_go_version(): - try: - output = subprocess.check_output(["go", "version"], stderr=subprocess.STDOUT, text=True) - except (OSError, subprocess.SubprocessError): - return False - - match = re.search(r'\bgo(\d+\.\d+(?:\.\d+)?)\b', output) - if match is None: - return False - - found = _version_tuple(match.group(1)) - required = _version_tuple(version) - return found >= required - - return self.Condition(check_go_version, "Go must be installed and at least version " + version) - - -def HasCurlFeature(self, feature): - - def default(output): - FEATURE_TAG = 'Features:' - tag = feature.lower() - for line in output.splitlines(): - # look for line with starting with the Features - if line.startswith(FEATURE_TAG): - # get a features and lower case then for safety - line = line[len(FEATURE_TAG):].lower() - tokens = line.split() - for t in tokens: - if t == tag: - return True - return False - - return self.CheckOutput(['curl', '--version'], default, "Curl needs to support feature: {feature}".format(feature=feature)) - - -def HasCurlOption(self, option): - - def default(output): - tag = option.lower() - for line in output.splitlines(): - line = line.lower() - tokens = line.split() - for t in tokens: - if t == tag: - return True - return False - - return self.CheckOutput(['curl', '--help', 'all'], default, "Curl needs to support option: {option}".format(option=option)) - - -def HasCurlTLSVersionSupport(self, tls_version): - """Check whether curl can attempt a given TLS version. - - This probes curl directly because OpenSSL capability checks do not always - reflect curl runtime policy behavior on hardened systems. - """ - - def check_curl_tls_support(): - version_map = { - "1.0": ("--tlsv1", "1.0"), - "1.1": ("--tlsv1.1", "1.1"), - "1.2": ("--tlsv1.2", "1.2"), - "1.3": ("--tlsv1.3", "1.3"), - } - if tls_version not in version_map: - return False - - tls_flag, tls_max = version_map[tls_version] - - def client_probe(port, _tls_flag): - try: - result = subprocess.run( - [ - "curl", - "-svk", - "--connect-timeout", - "2", - "--max-time", - "5", - "--ciphers", - "DEFAULT@SECLEVEL=0", - tls_flag, - "--tls-max", - tls_max, - f"https://127.0.0.1:{port}/", - ], - capture_output=True, - text=True, - timeout=7, - ) - output = (result.stdout + result.stderr).lower() - return result.returncode == 0 and f"ssl connection using tlsv{tls_version}" in output - except subprocess.TimeoutExpired: - return False - except Exception: - return False - - return _probe_openssl_server(tls_version, client_probe) - - return self.Condition( - check_curl_tls_support, "Curl does not support TLSv{version} in this environment".format(version=tls_version)) - - -def HasATSFeature(self, feature): - - val = self.Variables.get(feature, None) - - return self.Condition(lambda: val, "ATS feature not enabled: {feature}".format(feature=feature)) - - -# test if a plugin exists in the libexec folder - - -def PluginExists(self, pluginname): - - path = os.path.join(self.Variables.PLUGINDIR, pluginname) - return self.Condition(lambda: os.path.isfile(path), path + " not found.") - - -def CurlUsingUnixDomainSocket(self): - return self.Condition(lambda: self.Variables.get("CurlUds", False), "Curl using UDS. Not relevant for test") - - -ExtendCondition(HasOpenSSLVersion) -ExtendCondition(HasOpenSSLQuicClient) -ExtendCondition(HasProxyVerifierVersion) -ExtendCondition(HasGoVersion) -ExtendCondition(IsBoringSSL) -ExtendCondition(IsOpenSSL) -ExtendCondition(HasLegacyTLSSupport) -ExtendCondition(HasATSFeature) -ExtendCondition(HasCurlVersion) -ExtendCondition(HasCurlFeature) -ExtendCondition(HasCurlOption) -ExtendCondition(HasCurlTLSVersionSupport) -ExtendCondition(PluginExists) -ExtendCondition(CurlUsingUnixDomainSocket) diff --git a/tests/gold_tests/autest-site/config_reload.test.ext b/tests/gold_tests/autest-site/config_reload.test.ext deleted file mode 100644 index 8a9f352223c..00000000000 --- a/tests/gold_tests/autest-site/config_reload.test.ext +++ /dev/null @@ -1,225 +0,0 @@ -''' -AuTest extension for config reload operations. - -Provides Test.AddConfigReload() to replace the legacy pattern of -fire-and-forget `traffic_ctl config reload` followed by log grepping via -When.FileContains("finished loading", N). - -Test.AddConfigReload(ts, ...) - Triggers a reload via `traffic_ctl config reload -m` (monitor mode), - which blocks until the reload reaches a terminal state (default timeout - 30s), then optionally validates the task tree via the - get_reload_config_status JSONRPC endpoint. - -Note: standalone record-triggered reloads (via traffic_ctl config set, with -no explicit config reload) do not create tasks in the reload framework and -cannot be verified with this extension. -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import os -import jsonrpc - -_reload_counter = 0 - -_EXPECT_EXIT_CODES = { - "success": 0, - "fail": 2, - "timeout": 75, -} - - -def _next_token(): - global _reload_counter - _reload_counter += 1 - return f"autest-reload-{_reload_counter}" - - -def _shell_quote(s): - """Single-quote a string for shell, escaping embedded single quotes.""" - return "'" + s.replace("'", "'\\''") + "'" - - -def _task_matches(info, name): - """Check if a task matches the given name by description, filename, or basename.""" - for field in ("description", "filename"): - val = info.get(field) - if not val or val == "": - continue - if val == name or os.path.basename(val) == name: - return True - return False - - -def _collect_task_names(info, names=None): - """Recursively collect human-friendly task identifiers from the task tree.""" - if names is None: - names = set() - if isinstance(info, dict): - for field in ("description", "filename"): - val = info.get(field) - if val and val != "": - basename = os.path.basename(val) - names.add(basename if basename != val else val) - for sub in info.get("sub_tasks", []): - _collect_task_names(sub, names) - return names - - -def _find_task(info, name): - """Find a task by description or filename (supports basename matching).""" - if isinstance(info, dict): - if _task_matches(info, name): - return info - for sub in info.get("sub_tasks", []): - found = _find_task(sub, name) - if found: - return found - return None - - -def _make_task_validator(expect_tasks, expect_absent_tasks): - """Build a CustomJSONRPCResponse callback for task validation.""" - - def validator(resp): - if resp.is_error(): - return (False, f"RPC error: {resp.error_as_str()}") - - result = resp.__dict__.get("result", {}) - tasks_list = result.get("tasks", []) - if not tasks_list: - if expect_tasks: - return (False, "No reload tasks in response") - if expect_absent_tasks: - return (True, "No tasks present, absence confirmed") - return (True, "No tasks expected, none found") - - info = tasks_list[0] - all_names = _collect_task_names(info) - - if expect_tasks: - items = expect_tasks.items() if isinstance(expect_tasks, dict) else [(t, None) for t in expect_tasks] - for task_name, task_status in items: - if task_name not in all_names: - return (False, f"Task '{task_name}' not found in reload. Found: {sorted(all_names)}") - if task_status is not None: - task = _find_task(info, task_name) - actual = task.get("status", "unknown") if task else "unknown" - if actual != task_status: - return (False, f"Task '{task_name}' status is '{actual}', expected '{task_status}'") - - if expect_absent_tasks: - for task_name in expect_absent_tasks: - if task_name in all_names: - return (False, f"Task '{task_name}' should NOT be present but was found in: {sorted(all_names)}") - - return (True, "All task assertions passed") - - return validator - - -def AddConfigReload( - self, - ts, - expect="success", - token=None, - data=None, - force=False, - timeout="30s", - initial_wait=1.0, - refresh_int=0.5, - expect_tasks=None, - expect_absent_tasks=None, - delay_start=None, - description=None, -): - """Trigger a config reload, block until completion, and validate the result. - - Creates one or two TestRuns: - 1. Runs `traffic_ctl config reload -m` to trigger and monitor the reload. - Asserts the exit code based on the `expect` parameter. - 2. (Optional) If expect_tasks or expect_absent_tasks is set, queries the - get_reload_config_status JSONRPC endpoint and validates the task tree - using CustomJSONRPCResponse. - - Args: - ts: ATS process object. - expect: "success" (exit 0), "fail" (exit 2), "timeout" (exit 75), - or "any" (exit 0 or 2). - token: Custom reload token. Auto-generated if None. - data: Inline YAML string or @file path for --data flag. - force: If True, adds --force. - timeout: Timeout duration string for --timeout (e.g. "30s", "1m"). - Set to None to disable. - initial_wait: Seconds before first poll (--initial-wait). - refresh_int: Seconds between polls (--refresh-int). - expect_tasks: List of task names expected in the reload, or a dict - mapping task names to expected statuses (e.g. {"sni.yaml": "fail"}). - expect_absent_tasks: List of task names that must NOT appear in the reload. - delay_start: Seconds to wait before starting the reload TestRun. - Useful when the previous step writes a config file and CI may - run fast enough that the filesystem timestamp hasn't changed. - description: Custom TestRun description. - - Returns: - The reload TestRun object (first test run). - """ - if expect not in _EXPECT_EXIT_CODES and expect != "any": - raise ValueError(f"expect must be one of {list(_EXPECT_EXIT_CODES) + ['any']}, got '{expect}'") - - if token is None: - token = _next_token() - - if description is None: - description = f"Reload config [{token}]" - if expect != "success": - description += f" (expect {expect})" - - tr = self.AddTestRun(description) - if delay_start is not None: - tr.DelayStart = delay_start - tr.Processes.Default.Env = ts.Env - tr.StillRunningAfter = ts - - cmd = f"traffic_ctl config reload -m -t {_shell_quote(token)}" - cmd += f" -w {initial_wait} -r {refresh_int}" - - if timeout is not None: - cmd += f" -T {_shell_quote(timeout)}" - if force: - cmd += " --force" - if data is not None: - cmd += f" --data {_shell_quote(data)}" - - tr.Processes.Default.Command = cmd - - if expect == "any": - tr.Processes.Default.ReturnCode = Any(0, 2) - else: - tr.Processes.Default.ReturnCode = _EXPECT_EXIT_CODES[expect] - - if expect_tasks or expect_absent_tasks: - vtr = self.AddTestRun(f"Verify [{token}] tasks") - vtr.AddJsonRPCClientRequest(ts, jsonrpc.Request.get_reload_config_status(token=token)) - vtr.Processes.Default.Streams.stdout = Testers.CustomJSONRPCResponse( - _make_task_validator(expect_tasks, expect_absent_tasks)) - vtr.StillRunningAfter = ts - - return tr - - -ExtendTest(AddConfigReload, name="AddConfigReload") diff --git a/tests/gold_tests/autest-site/curl.test.ext b/tests/gold_tests/autest-site/curl.test.ext deleted file mode 100644 index f25d05cae36..00000000000 --- a/tests/gold_tests/autest-site/curl.test.ext +++ /dev/null @@ -1,78 +0,0 @@ -''' -Tools to help with TestRun commands -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -# Adds "curl" prefix to command argument, with possible curl flags -# -# example usage: -# cmd = '-vs -k https:127.0.0.1:{port}'.format(port = ts.Variables.port) -# -# tr = Test.AddTestRun() -# ps = tr.MakeCurlCommand(cmd) -# tr.Processes.Default.StartBefore(ts) -# ts.StartAfter(*ps) -# tr.StillRunningAfter = ts -# - - -def spawn_curl_commands(self, cmdstr, count, ts=None, retcode=0, use_default=True): - ret = [] - - if self.Variables.get("CurlUds", False) and ts != None: - cmdstr = f'curl --unix-socket {ts.Variables.uds_path} ' + cmdstr - else: - cmdstr = 'curl ' + cmdstr - if use_default: - count = int(count) - 1 - for cnt in range(0, count): - ret.append(self.Processes.Process(name="cmdline-{num}".format(num=cnt), cmdstr=cmdstr, returncode=retcode)) - if use_default: - self.Processes.Default.Command = cmdstr - self.Processes.Default.ReturnCode = retcode - self.Processes.Default.StartBefore(*ret) - return ret - - -def curl_command(self, cmd, ts=None, p=None): - if p == None: - p = self.Processes.Default - if self.Variables.get("CurlUds", False) and ts != None: - # curl via UDS doesn't add this header when proxy flag enabled, need to explicitly set for test - # unless pipe is found, indicating multiple commands - if "--proxy" in cmd and "|" not in cmd: - cmd += ' -H "Proxy-Connection: keep-alive"' - p.Command = f'curl --unix-socket {ts.Variables.uds_path} ' + cmd - else: - p.Command = 'curl ' + cmd - return p - - -def curl_multiple_commands(self, cmd, ts=None): - p = self.Processes.Default - if self.Variables.get("CurlUds", False) and ts != None: - p.Command = cmd.format(curl=f'curl --unix-socket {ts.Variables.uds_path}', curl_base='curl') - else: - p.Command = cmd.format(curl='curl', curl_base='curl') - return p - - -ExtendTestRun(spawn_curl_commands, name="SpawnCurlCommands") -ExtendTestRun(curl_command, name="MakeCurlCommand") -ExtendTest(curl_command, name="MakeCurlCommand") -ExtendTestRun(curl_multiple_commands, name="MakeCurlCommandMulti") -ExtendTest(curl_multiple_commands, name="MakeCurlCommandMulti") diff --git a/tests/gold_tests/autest-site/curl_header.test.ext b/tests/gold_tests/autest-site/curl_header.test.ext deleted file mode 100644 index 6de95adfb8a..00000000000 --- a/tests/gold_tests/autest-site/curl_header.test.ext +++ /dev/null @@ -1,206 +0,0 @@ -''' -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -from autest.testers import Tester, tester -import autest.common.is_a as is_a -import hosts.output as host - -import re - - -class CurlHeader(Tester): - - def __init__(self, value, test_value=None, kill_on_failure=False, description_group=None, description=None): - - self._stack = host.getCurrentStack(1) - - if not is_a.Dictionary(value): - host.WriteError("CurlHeader Input: Need to provide a dictionary.", stack=host.getCurrentStack(1)) - - ops = ("equal", "equal_re") - gold_dict = value - - headers = tuple(gold_dict.keys()) - if description is None: - description = 'Checking that all {} headers exist and have matching values: {}'.format( - len(headers), ', '.join(headers) if len(headers) <= 10 else ', '.join(headers[0:10]) + ', etc.') - - # sanity check for input dictionary format - for header, target in gold_dict.items(): - if not isinstance(header, str): - host.WriteError( - 'CurlHeader Input: Unsupported type for header {}. Header needs to be a string.'.format(header), - stack=self._stack) - - if target is None or isinstance(target, str): - continue - elif isinstance(target, dict): - for op, pos_val in target.items(): - if op not in ops: - host.WriteError( - 'CurlHeader Input: Unsupported operation \'{}\' for value at header \'{}\'. The available operations are: {}.' - .format(op, header, ', '.join(ops)), - stack=self._stack) - elif pos_val is None or isinstance(pos_val, str): - continue - elif isinstance(pos_val, list): - for str_ in pos_val: - if not isinstance(str_, str) and str_ is not None: - host.WriteError( - 'CurlHeader Input: Value {} has unsupported type \'{}\' for header \'{}\'. Need to provide a string or None.' - .format(str_, str_.__class__.__name__, header), - stack=self._stack) - else: - host.WriteError( - 'CurlHeader Input: Value {} has unsupported type \'{}\' for header \'{}\'. Need to provide a string, a list or None for possible curl values.' - .format(pos_val, pos_val.__class__.__name__, header), - stack=self._stack) - else: - host.WriteError( - 'CurlHeader Input: Value {} has unsupported type \'{}\' for header \'{}\'. Need to provide either a string, a dictionary or None.' - .format(target, target.__class__.__name__, header), - stack=self._stack) - - super(CurlHeader, self).__init__( - value=value, - test_value=test_value, - kill_on_failure=kill_on_failure, - description_group=description_group, - description=description) - - def test(self, eventinfo, **kw): - # Get curl output file to test against - fname = self._GetContent(eventinfo) - # do test - - if fname is None: - pass - try: - with open(fname) as val_file: - val_content = [v.replace('\r\n', '\n').rstrip('\n') for v in val_file.readlines()] - except (OSError, IOError) as e: - self.Result = tester.ResultType.Failed - self.Reason = str(e) - return - - val_dict = {} - gold_dict = self.Value - - for line in val_content: - # Current version of curl uses ': ' to separate header and value - vals = line.split(': ') - # headers need to be lowercased to avoid inconsistence in letter case - if len(vals) == 1: - val_dict[vals[0].lower()] = '' - elif len(vals) == 2: - val_dict[vals[0].lower()] = vals[1] - else: - val_dict[vals[0].lower()] = ': '.join(vals[1:]) - - # generate a gold dictionary with lowercase header - gold_dict = {k.lower(): v for k, v in gold_dict.items()} - - p_flag = 1 - reason = '' - - for header in gold_dict.keys(): - v = val_dict.get(header) - if v is not None: - res = self.match(gold_dict, header, v) - if res is None: - continue - else: - reason += 'In field: {} \n'.format(header) + res - p_flag = 0 - reason += '\n--------------------------------------------------------------------------------\n' - else: - reason += 'Nonexistent field: {}'.format(header) - p_flag = 0 - reason += '\n--------------------------------------------------------------------------------\n' - - if p_flag == 1: - self.Result = tester.ResultType.Passed - self.Reason = "Curl headers and values match" - else: - self.Result = tester.ResultType.Failed - self.Reason = reason - if self.KillOnFailure: - raise KillOnFailureError - host.WriteVerbose( - ["testers.CurlHeader", "testers"], "{0} - ".format(tester.ResultType.to_color_string(self.Result)), self.Reason) - - # Optional operations to do: - # equal: complete string match - # equal_re: regular expression match for the entire string - # Given in gold_dict: - # a string: equal - # a dict: {equal: [e], equal_re: [r]}. Test will pass if either - # the equal or equal_re is satisfied, who will be satisfied - # if any of the patterns in the list matches. - - def match(self, gold_dictionary, header, val_value): - target = gold_dictionary[header] - - if target is None: - return None - elif isinstance(target, str): - # if given a string, check for an exact match - return 'Not an exact match.\nExpected : {}\nActual : {}'.format(target, val_value) \ - if target != val_value else None - - elif isinstance(target, dict): - # if given a dict, check for valid operations indicated by the keys - for op, pos_val in target.items(): - if pos_val is None: - return None - elif isinstance(pos_val, list): - # print('Need to provide a list of possible values.') - # continue - if op == 'equal': - for str_ in pos_val: - if val_value == str_ or str_ is None: - return None - # return 'No matching strings in the list.' - elif op == 'equal_re': - for regex in pos_val: - if regex is None: - return None - elif re.fullmatch(regex, val_value) is not None: - return None - - elif isinstance(pos_val, str): - if op == 'equal': - if val_value == pos_val: - return None - # else 'Not an exact match.\nExpected : {}\nActual : {}'.format(pos_val, val_value) - - elif op == 'equal_re': - if re.fullmatch(pos_val, val_value) is not None: - return None - - ret = '' - ops = {'equal': 'Any of the following strings: ', 'equal_re': 'Any of the following regular expression: '} - - for op, pos_val in target.items(): - ret += ' {}: \'{}\'\n'.format(ops[op], '\', \''.join(pos_val)) if isinstance(pos_val, list) \ - else ' {}: \'{}\'\n'.format(ops[op], pos_val) - - return 'Value \'{}\' matches none of: \n{}'.format(val_value, ret.strip('\n')) - - -AddTester(CurlHeader) diff --git a/tests/gold_tests/autest-site/httpbin.test.ext b/tests/gold_tests/autest-site/httpbin.test.ext deleted file mode 100644 index 245552c9e99..00000000000 --- a/tests/gold_tests/autest-site/httpbin.test.ext +++ /dev/null @@ -1,47 +0,0 @@ -''' -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -from ports import get_port - - -def MakeHttpBinServer(self, name, ip='127.0.0.1', port=None, options={}) -> 'Process': - data_dir = os.path.join(self.RunDirectory, name) - # create Process - p = self.Processes.Process(name) - if port is None: - port = get_port(p, "Port") - - self._RootRunable.SkipUnless( - Condition.HasProgram("go-httpbin", "go-httpbin needs be installed and in PATH for this extension to run")) - - command = f"go-httpbin -host {ip} -port {port} " - for flag, value in options.items(): - command += f"{flag} {value} " - - # create process - p.Command = command - p.Setup.MakeDir(data_dir) - p.Variables.DataDir = data_dir - p.Ready = When.PortOpen(port, ip) - p.ReturnCode = Any(None, 0) - - return p - - -ExtendTest(MakeHttpBinServer, name="MakeHttpBinServer") -ExtendTestRun(MakeHttpBinServer, name="MakeHttpBinServer") diff --git a/tests/gold_tests/autest-site/init.cli.ext b/tests/gold_tests/autest-site/init.cli.ext deleted file mode 100644 index dcae951e74e..00000000000 --- a/tests/gold_tests/autest-site/init.cli.ext +++ /dev/null @@ -1,47 +0,0 @@ -''' -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import sys -import microserver - -if sys.version_info < (3, 6, 0): - host.WriteError("You need python 3.6 or later to run these tests\n", show_stack=False) - -needed_autest_version = "1.10.4" -found_autest_version = AuTestVersion() -if AuTestVersion() < needed_autest_version: - host.WriteError( - f"Tests need AuTest version {needed_autest_version} or better, found version {found_autest_version}\n" - "Please update AuTest:\n rm -rf .venv && uv sync\n", - show_stack=False) - -needed_microserver_version = "1.0.8" -found_microserver_version = microserver.__version__ -if found_microserver_version < needed_microserver_version: - host.WriteError( - f"Tests need a MicroServer version {needed_microserver_version} or better, found version {found_microserver_version}\n" - "Please update MicroServer:\n rm -rf .venv && uv sync\n", - show_stack=False) - -Settings.path_argument(["--ats-bin"], required=True, help="A user provided directory to ATS bin") - -Settings.path_argument(["--build-root"], required=False, help="The location of the build root for out of source builds") - -Settings.path_argument(["--proxy-verifier-bin"], required=False, help="A location for system proxy-verifier binaries to test with.") - -Settings.add_argument(["--curl-uds"], action="store_true", required=False, help="Run all curl commands with --unix-socket flag") diff --git a/tests/gold_tests/autest-site/ip.test.ext b/tests/gold_tests/autest-site/ip.test.ext deleted file mode 100755 index ca38ee91e2d..00000000000 --- a/tests/gold_tests/autest-site/ip.test.ext +++ /dev/null @@ -1,39 +0,0 @@ -''' -Extend Autest with IP-related utilities. -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -from ports import get_port - -# this forms is for the global process define - -# New member function for Test object, that reserves TCP port(s) from the list of available TCP ports, for -# use in the test. Each argument is a string, containing the name of a variable to add to Test.Variables . -# For each argument, a port will be reserved, and its number will be assigned to the new variable for the -# argument. -# - - -def get_tcp_port(obj, *newVariables): - for v in newVariables: - if not isinstance(v, str): - raise TypeError("all function arguments must be strings") - get_port(obj, v) - - -#AddTestEntityMember(get_tcp_port, name="GetTcpPort") -ExtendTest(get_tcp_port, name="GetTcpPort") diff --git a/tests/gold_tests/autest-site/jsonrpc_client.test.ext b/tests/gold_tests/autest-site/jsonrpc_client.test.ext deleted file mode 100644 index 45628d8005d..00000000000 --- a/tests/gold_tests/autest-site/jsonrpc_client.test.ext +++ /dev/null @@ -1,293 +0,0 @@ -''' -JSONRPC client test extension. -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import os -import tempfile -import jsonrpc -import json -import sys -import typing -from jsonschema import validate -from jsonschema.exceptions import ValidationError -from autest.testers import Tester, tester -import hosts.output as host -from autest.exceptions.killonfailure import KillOnFailureError - - -class SchemaValidator: - ''' - Class that provides some handy schema validation function. It's in a class so some Testers can use this functionality without - exporting the class or method. - ''' - - def validate_request_schema(self, event, file_name, is_request=True, schema_file_name=None, field_schema_file_name=None): - ''' - Perform Schema validation on the JSONRPC params and also to the particular params field. - - file_name: - main json request file, schema check will be applied to the content of this file. - schema_file_name: - main doc schema file, this should only contain the wider JSONRPC 2.0 schema(not including params or result) - is_request: - This lets the function know if it should apply the 'field_schema_file_name' to the 'params' or the 'result' field. - field_schema_file_name: - param or result field schema file. - - ''' - - with open(file_name, 'r') as f: - r = f.read() - rdata = json.loads(r) - - if schema_file_name: - with open(schema_file_name, 'r') as f: - s = f.read() - sdata = json.loads(s) - try: - # validate may throw if invalid schema. - if schema_file_name: - validate(instance=rdata, schema=sdata) - - if field_schema_file_name: - fieldName = 'params' if is_request else 'result' - jsonField = rdata[fieldName] if fieldName in rdata else None - - if jsonField: - with open(field_schema_file_name, 'r') as f: - p = f.read() - psdata = json.loads(p) - validate(instance=jsonField, schema=psdata) - else: - return (False, f"There is no {fieldName} field to validate", "Error found.") - except ValidationError as ve: - event.object.Stop() - return (False, "Check JSONRPC 2.0 schema validation", str(ve)) - - return (True, "Check JSONRPC 2.0 schema validation", "All good") - - -def AddJsonRPCClientRequest(obj, ts, request='', file=None, schema_file_name=None, params_field_schema_file_name=None): - ''' - Function to add a JSONRPC request into a process. This function will internally generate a call to traffic_ctl. - As traffic_ctl can send request by reading from a file, internally this function will create a temporary json file - and will be passed as parameter to traffic_ctl, taking only the output as response (-z). - - Args: - ts: - traffic_server object, this is needed in order to traffic_ctl find the right socket. - - file: The file name used to read the request. - - request: - request should be created by the Request api(jsonrpc.py). ie: - - tr = Test.AddTestRun("Test JSONRPC foo_bar()") - tr.AddJsonRPCClientRequest(ts, Request.foo_bar(fqdn=["yahoo.com", "aol.com", "vz.com"])) - - schema_file_name: - Used to validate the request against a schema file. if empty no request schema - validation will be performed. - - params_field_schema_file_name: - Schema file to validate the params field in the jsonrpc message. - - Validating the response: - - Either by the regular validation mechanism already provided by the Testing framework or by using CustomJSONRPCResponse Tester - which will let you read the response as a dict and play with it. See CustomJSONRPCResponse for more details. - - Errors: - If there is an error in the schema validation, either the params or the whole json message, the test will not run, an exception - will be thrown with the specific error. - - ''' - - fileName = '' - process = obj.Processes.Default - if file is None: - reqFile = tempfile.NamedTemporaryFile(delete=False, dir=process.RunDirectory, suffix=f"_{obj.Name}.json") - fileName = reqFile.name - with open(fileName, "w") as req: - req.write(str(request)) - else: - fileName = file - - command = f"{ts.Variables.BINDIR}/traffic_ctl rpc file {fileName} " - if ts: - command += f" --run-root {ts.Disk.runroot_yaml.Name}" - - command += ' --format json' # we only want the output. - - process.Command = command - process.ReturnCode = 0 - - if schema_file_name != "": - process.SetupEvent.Connect( - Testers.Lambda( - lambda ev: SchemaValidator().validate_request_schema( - ev, fileName, True, schema_file_name, params_field_schema_file_name))) - return process - - -def AddJsonRPCShowRegisterHandlerRequest(obj, ts): - ''' - Handy function to request all the registered endpoints in the RPC engine. A good way to validate that your new RPC handler - is available through the RPC by calling this function and validating the response. ie: - - tr = Test.AddTestRun("Test registered API - using AddJsonRPCShowRegisterHandlerRequest") - tr.AddJsonRPCShowRegisterHandlerRequest(ts) - - tr.Processes.Default.Streams.stdout = All( - Testers.IncludesExpression('foo_bar', 'Should be listed'), - ) - ''' - return AddJsonRPCClientRequest(obj, ts, jsonrpc.Request.show_registered_handlers()) - - -# Testers -class CustomJSONRPCResponse(Tester): - ''' - Custom tester that provides the user the ability to be called with the response from the RPC. The registered function will be - called with the jsonrpc.Response(jsonrpc.py). - - Args: - func: - The function that will be called to perform a custom validation of the jsonrpc - message. - - Example: - - tr = Test.AddTestRun("Test update_host_status") - Params = [ - {'name': 'yahoo', 'status': 'up'} - ] - - tr.AddJsonRPCClientRequest(ts, Request.update_host_status(hosts=Params)) - - - def check_no_error_on_response(resp: Response): - # we only check if it's an error. - if resp.is_error(): - return (False, resp.error_as_str()) - return (True, "All good") - - tr.Processes.Default.Streams.stdout = Testers.CustomJSONRPCResponse(check_no_error_on_response) - - ''' - - def __init__( - self, - func: typing.Any, - test_value=None, - kill_on_failure: bool = False, - description_group: typing.Optional[str] = None, - description: typing.Optional[str] = None): - if description is None: - description = "Validating JSONRPC 2.0 response" - - super(CustomJSONRPCResponse, self).__init__( - value=func, - test_value=test_value, - kill_on_failure=kill_on_failure, - description_group=description_group, - description=description) - - def test(self, eventinfo, **kw): - - response_text = {} - with open(self._GetContent(eventinfo), "r") as resp: - response_text = resp.read() - - (testPassed, reason) = self.Value(jsonrpc.Response(text=response_text)) - - if testPassed: - self.Result = tester.ResultType.Passed - self.Reason = f"Returned value: {reason}" - host.WriteVerbose( - ["testers.CustomJSONRPCResponse", "testers"], f"tester.ResultType.to_color_string(self.Result) - ", self.Reason) - else: - self.Result = tester.ResultType.Failed - self.Reason = f"Returned value: {reason}" - if self.KillOnFailure: - raise KillOnFailureError - - -# Testers - - -class JSONRPCResponseSchemaValidator(Tester, SchemaValidator): - ''' - Tester for response schema validation. - This class can perform a JSONRPC 2.0 schema validation and also the 'result' field validation if provided. - - schema_file_name: - Main JSONRPC 2.0 schema validation file. - - result_field_schema_file_name: - result field schema validation, this is optional, if not provided the main schema will just check that the result matches - the JSONRPC 2.0 specs. - ''' - - def __init__( - self, - schema_file_name, - result_field_schema_file_name=None, - value=None, - test_value=None, - kill_on_failure: bool = False, - description_group: typing.Optional[str] = None, - description: typing.Optional[str] = None): - if description is None: - description = "Validating JSONRPC 2.0 response schema" - self._schema_file_name = schema_file_name - self._result_field_schema_file_name = result_field_schema_file_name - - super(JSONRPCResponseSchemaValidator, self).__init__( - value=value, - test_value=test_value, - kill_on_failure=kill_on_failure, - description_group=description_group, - description=description) - - def test(self, eventinfo, **kw): - response_text = {} - with open(self._GetContent(eventinfo), "r") as resp: - response_text = resp.read() - - (testPassed, reason, cmm) = self.validate_request_schema( - eventinfo, self._GetContent(eventinfo), False, self._schema_file_name, self._result_field_schema_file_name) - - if testPassed: - self.Result = tester.ResultType.Passed - self.Reason = f"Returned value: {reason}" - host.WriteVerbose( - ["testers.JSONRPCResponseSchemaValidator", "testers"], f"tester.ResultType.to_color_string(self.Result) - ", - self.Reason) - else: - self.Result = tester.ResultType.Failed - self.Reason = f"Returned value: {reason}" - if self.KillOnFailure: - raise KillOnFailureError - - -# Export -AddTester(CustomJSONRPCResponse) -AddTester(JSONRPCResponseSchemaValidator) -ExtendTestRun(AddJsonRPCShowRegisterHandlerRequest, name="AddJsonRPCShowRegisterHandlerRequest") -ExtendTestRun(AddJsonRPCClientRequest, name="AddJsonRPCClientRequest") diff --git a/tests/gold_tests/autest-site/microDNS.test.ext b/tests/gold_tests/autest-site/microDNS.test.ext deleted file mode 100644 index 5f1a0ec4f07..00000000000 --- a/tests/gold_tests/autest-site/microDNS.test.ext +++ /dev/null @@ -1,123 +0,0 @@ -''' -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import json -import os - -import trlib.ipconstants as IPConstants -from ports import get_port - - -# AddRecord registers a list of ip address against hostname -def AddRecord(hostname, list_ip_addr): - - record = dict() - record[hostname] = list_ip_addr - return record - - -# dict in format {'domain': [IPs]} -# json file in the same mappings/otherwise format that uDNS takes - - -def addRecords(self, records=None, jsonFile=None): - jsondata = None - - # at this point the default file and fields should have been created - if os.path.exists(self.Variables.zone_file): - with open(self.Variables.zone_file, 'r') as f: - jsondata = json.load(f) - else: - raise FileNotFoundError("default zone file doesn't exist, but it should.") - - if records: - for domain in records: - # Let's do this for the test writer's convenience. - normalized_domain = domain - if normalized_domain[-1] != '.': - normalized_domain += '.' - record = AddRecord(normalized_domain, records[domain]) - - jsondata["mappings"].append(record) - - if jsonFile: - jsonFile = os.path.join(self.TestDirectory, jsonFile) - self.Setup.Copy(jsonFile, self.Variables.DataDir) - - with open(jsonFile, 'r') as f: - entries = json.load(f) - - if entries: - # not copying over entries['otherwise']. dont see the need to - for record in entries["mappings"]: - jsondata["mappings"].append(record) - - with open(self.Variables.zone_file, 'w+') as f: - f.write(json.dumps(jsondata)) - - -def MakeDNServer(obj, name, filename="dns_file.json", port=False, ip='INADDR_LOOPBACK', rr=False, default=None, options={}): - data_dir = os.path.join(obj.RunDirectory, name) - filepath = os.path.join(data_dir, filename) - obj.Variables.zone_file = filepath - - # provided file doesn't exist, so we create it and fill it in with the 'otherwise' values - if not os.path.exists(os.path.dirname(filepath)): - os.makedirs(os.path.dirname(filepath)) - - jsondata = {'mappings': []} - - if default: - if isinstance(default, str): - # MicroDNS expects 'otherwise' to be a sequence. - default = [default] - jsondata['otherwise'] = default - - with open(filepath, 'w') as f: - f.write(json.dumps(jsondata)) - - # create Process - p = obj.Processes.Process(name) - if (port == False): - port = get_port(p, "Port") - command = "microdns {0} {1} {2}".format(ip, port, filepath) - - if rr: - command += " --rr" - - # create process - p.Command = command - p.Setup.MakeDir(data_dir) - p.Variables.DataDir = data_dir - p.ReturnCode = 0 - - if IPConstants.isIPv6(ip): - p.Ready = When.PortOpenv6(port) - else: - p.Ready = When.PortOpenv4(port) - - AddMethodToInstance(p, AddRecord) - AddMethodToInstance(p, addRecords) - - return p - - -ExtendTestRun(MakeDNServer, name="MakeDNServer") -ExtendTestRun(MakeDNServer, name="MakeDNS") -ExtendTest(MakeDNServer, name="MakeDNServer") -ExtendTest(MakeDNServer, name="MakeDNS") diff --git a/tests/gold_tests/autest-site/microserver.test.ext b/tests/gold_tests/autest-site/microserver.test.ext deleted file mode 100644 index 70c734f42db..00000000000 --- a/tests/gold_tests/autest-site/microserver.test.ext +++ /dev/null @@ -1,266 +0,0 @@ -''' -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import json -import os -import socket -import ssl - -from autest.api import AddWhenFunction -import hosts.output as host -from ports import get_port - -import trlib.ipconstants as IPConstants -from trlib import Transaction, Request, Response, Session - -DEFAULT_LOOKUP_KEY = '{PATH}' - - -def addMethod(self, testName, request_header, functionName): - return - - -# creates the full request or response block using headers and message data - - -def httpObject(self, header, data): - r = dict() - r["timestamp"] = "" - r["headers"] = header - r["body"] = data - return r - - -def getHeaderFieldVal(request_header, field): - requestline = request_header["headers"].split("\r\n")[0] - requestline = requestline.split(" ")[1] - field = field + ':' - valField = request_header["headers"].split(field, 1) - val = "" - if len(valField) > 1: - field_v = valField[1].split("\r\n", 1) - if len(field_v) > 0: - val = field_v[0].strip() - return val - - -# addResponse adds customized response with respect to request_header. request_header and response_header are both dictionaries -def addResponse(self, filename, request_header, response_header): - client_request = Request.fromRequestLine( - request_header["headers"], "" if "body" not in request_header else request_header["body"], - None if "options" not in request_header else request_header["options"]) - - server_response = Response.fromRequestLine( - response_header["headers"], "" if "body" not in response_header else response_header["body"], - None if "options" not in response_header else response_header["options"]) - - # timestamp field is left None because that needs to be revised for better implementation - txn = Transaction(client_request, None, server_response, None, None, None) - - absFilepath = os.path.join(self.Variables.DataDir, filename) - addTransactionToSession(txn, absFilepath) - return - - -# adds transaction in json format to the specified file -def addTransactionToSession(txn, JFile): - jsondata = None - if not os.path.exists(os.path.dirname(JFile)): - os.makedirs(os.path.dirname(JFile)) - if os.path.exists(JFile): - jf = open(JFile, 'r') - jsondata = json.load(jf) - - # hard coding only 1 session per file - # since for the purpose of testing, we don't need multiple sessions in a file - if jsondata is None: - jsondata = {} - jsondata["sessions"] = [] - - jsondata["sessions"].append(Session(JFile.split("/")[-1], None, None, [txn]).toJSON()) - jsondata["meta"] = {} - jsondata["meta"]["version"] = "1.0" - else: - # hardcoding 0 because for testing we only have 1 session - jsondata["sessions"][0]["transactions"].append(txn.toJSON()) - - with open(JFile, 'w+') as jf: - jf.write(json.dumps(jsondata)) - - -def addSessionFromFiles(self, session_dir): - self.Setup.Copy(session_dir, self.Variables.DataDir) - - -# make headers with the key and values provided -def makeHeader(self, requestString, **kwargs): - headerStr = requestString + '\r\n' - for k, v in kwargs.items(): - headerStr += k + ': ' + v + '\r\n' - headerStr = headerStr + '\r\n' - return headerStr - - -def uServerUpAndRunning(serverHost, port, isSsl, isIPv6, request, clientcert='', clientkey=''): - if isIPv6: - plain_sock = socket.socket(socket.AF_INET6) - else: - plain_sock = socket.socket(socket.AF_INET) - - if isSsl: - context = ssl.create_default_context(ssl.Purpose.SERVER_AUTH) - context.check_hostname = False - context.verify_mode = ssl.CERT_NONE - if clientcert != '' or clientkey != '': - context.load_cert_chain(certfile=clientcert, keyfile=clientkey) - sock = context.wrap_socket(plain_sock) - else: - sock = plain_sock - - try: - sock.connect((serverHost, port)) - except ConnectionRefusedError: - host.WriteDebug(['uServerUpAndRunning', 'when'], "Connection refused: {0}:{1}".format(serverHost, port)) - return False - except ssl.SSLError as e: - host.WriteDebug(['uServerUpAndRunning', 'when'], "SSL connection error: {0}:{1}:{2}".format(serverHost, port, e)) - return False - - sock.sendall(request.encode()) - decoded_output = '' - sock.settimeout(10.0) # 10 second timeout for recv - try: - while True: - host.WriteDebug("??") - output = sock.recv(4096) # suggested bufsize from docs.python.org - host.WriteDebug("!!") - if len(output) <= 0: - break - else: - decoded_output += output.decode('utf-8', errors='replace') - except socket.timeout: - host.WriteDebug(['uServerUpAndRunning', 'when'], "Socket timeout waiting for response") - sock.close() - return False - sock.close() - sock = None - - expected_response = "HTTP/1.1 200 OK\r\nConnection: close\r\nContent-Length: 4\r\n\r\nimok" - if decoded_output == expected_response: - return True - - host.WriteError('\n'.join(['Got invalid response from microserver:', '----', decoded_output, '----'])) - - -AddWhenFunction(uServerUpAndRunning) - - -def MakeOriginServer( - obj, - name, - port=None, - s_port=None, - ip='INADDR_LOOPBACK', - delay=None, - ssl=False, - lookup_key=DEFAULT_LOOKUP_KEY, - clientcert='', - clientkey='', - both=False, - options={}): - data_dir = os.path.join(obj.RunDirectory, name) - p = obj.Processes.Process(name) - - ipaddr = IPConstants.getIP(ip) - - command = "microserver --data-dir {0} --ip_address {1} --lookupkey '{2}'".format(data_dir, ipaddr, lookup_key) - - if delay: - command += " --delay {0}".format(delay) - - if both or ssl: - if not s_port: - s_port = get_port(p, "SSL_Port") - - command += " --both" if both else " --ssl" - key = clientkey if clientkey else os.path.join(obj.Variables["AtsTestToolsDir"], "microserver", "ssl", "server.pem") - cert = clientcert if clientcert else os.path.join(obj.Variables["AtsTestToolsDir"], "microserver", "ssl", "server.crt") - command += " --key {0}".format(key) - command += " --cert {0}".format(cert) - command += " --s_port {0}".format(s_port) - - # this might break if user specifies both both and ssl - if not ssl: # in both or HTTP only mode - if not port: - port = get_port(p, "Port") - - command += " --port {0}".format(port) - - for flag, value in options.items(): - command += " {} {}".format(flag, value if value else '') - - p.Command = command - p.Setup.MakeDir(data_dir) - p.Variables.DataDir = data_dir - p.Variables.lookup_key = lookup_key - AddMethodToInstance(p, addResponse) - AddMethodToInstance(p, addTransactionToSession) - AddMethodToInstance(p, addSessionFromFiles) - - custom_lookup_header = '' - keys = lookup_key.split("}") - - for key in keys: - if key not in ['{PATH', '{URL', '{HOST', '{%Host']: - k = key.replace("{%", "") - - if len(k) > 0: - custom_lookup_header += '{0}: healthcheck\r\n'.format(k) - - healthcheck_request = { - "headers": "GET /ruok HTTP/1.1\r\nHost: {0}\r\n{1}\r\n".format(ipaddr, custom_lookup_header), - "timestamp": "1469733493.993", - "body": "" - } - - # Set up health check. - addResponse( - p, "healthcheck.json", healthcheck_request, { - "headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", - "timestamp": "1469733493.993", - "body": "imok", - "options": { - "skipHooks": None - } - }) - - p.Ready = When.uServerUpAndRunning( - ipaddr, - s_port if ssl else port, - ssl, - IPConstants.isIPv6(ip), - healthcheck_request["headers"], - clientcert=clientcert, - clientkey=clientkey) - p.ReturnCode = Any(None, 0) - - return p - - -ExtendTest(MakeOriginServer, name="MakeOriginServer") -ExtendTest(MakeOriginServer, name="MakeOrigin") diff --git a/tests/gold_tests/autest-site/ordered_set_queue.py b/tests/gold_tests/autest-site/ordered_set_queue.py deleted file mode 100644 index c6182617145..00000000000 --- a/tests/gold_tests/autest-site/ordered_set_queue.py +++ /dev/null @@ -1,110 +0,0 @@ -''' -Implement an OrderedSetQueue -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import collections -try: - collectionsAbc = collections.abc -except AttributeError: - collectionsAbc = collections - -try: - import queue as Queue -except ImportError: - import Queue - -# -# This is borrowed from the following (MIT licensed) recipe: -# https://code.activestate.com/recipes/576694/ -# - - -class OrderedSet(collectionsAbc.MutableSet): - - def __init__(self, iterable=None): - self.end = end = [] - end += [None, end, end] # sentinel node for doubly linked list - self.map = {} # key --> [key, prev, next] - if iterable is not None: - self |= iterable - - def __len__(self): - return len(self.map) - - def __contains__(self, key): - return key in self.map - - def add(self, key): - if key not in self.map: - end = self.end - curr = end[1] - curr[2] = end[1] = self.map[key] = [key, curr, end] - - def discard(self, key): - if key in self.map: - key, prev, next = self.map.pop(key) - prev[2] = next - next[1] = prev - - def __iter__(self): - end = self.end - curr = end[2] - while curr is not end: - yield curr[0] - curr = curr[2] - - def __reversed__(self): - end = self.end - curr = end[1] - while curr is not end: - yield curr[0] - curr = curr[1] - - # We set last=False so that FIFO is the default behavior. - def pop(self, last=False): - if not self: - raise KeyError('set is empty') - key = self.end[1][0] if last else self.end[2][0] - self.discard(key) - return key - - def __repr__(self): - if not self: - return '%s()' % (self.__class__.__name__,) - return '%s(%r)' % (self.__class__.__name__, list(self)) - - def __eq__(self, other): - if isinstance(other, OrderedSet): - return len(self) == len(other) and list(self) == list(other) - return set(self) == set(other) - - -class OrderedSetQueue(Queue.Queue): - """ - Make use of the OrderedSet to make a FIFO Queue.Queue that only has unique - elements in it. - """ - - def _init(self, maxsize): - self.queue = OrderedSet() - - def _put(self, item): - self.queue.add(item) - - def _get(self): - return self.queue.pop() diff --git a/tests/gold_tests/autest-site/ports.py b/tests/gold_tests/autest-site/ports.py deleted file mode 100644 index 3674601aab2..00000000000 --- a/tests/gold_tests/autest-site/ports.py +++ /dev/null @@ -1,313 +0,0 @@ -''' -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -from contextlib import contextmanager -from typing import Set -import socket -import subprocess -import os -import platform -import psutil - -import hosts.output as host - -from ordered_set_queue import OrderedSetQueue - -g_ports = None # ports we can use - - -class PortQueueSelectionError(Exception): - """ - An exception for when there are problems selecting a port from the port - queue. - """ - pass - - -def PortOpen(port: int, address: str = None, bound_ports: Set[int] = None) -> bool: - """ - Detect whether the port is open, that is a socket is currently using that port. - - Open ports are currently in use by an open socket and are therefore not available - for a server to listen on them. - - Args: - port: The port to check. - address: The address to check. Defaults to localhost. - bound_ports: A set of ports that are currently bound. If a port is in - this set, it is considered open. - - Returns: - True if a socket is currently bound to the port or accepts a TCP - connection, False otherwise. - """ - ret = False - if address is None: - address = "localhost" - - if port in bound_ports: - host.WriteDebug('PortOpen', f"{port} is open because it is in the bound sockets set.") - return True - - address = (address, port) - - try: - # Try to connect on that port. If we can connect on it, then someone is - # listening on that port and therefore the port is open. - s = socket.create_connection(address, timeout=.5) - s.close() - ret = True - host.WriteDebug( - 'PortOpen', f"Connection to port {port} succeeded, the port is open, " - "and a future connection cannot use it") - except OSError: - host.WriteDebug( - 'PortOpen', f"socket error for port {port}, port is closed, " - "and therefore a future connection can use it") - except socket.timeout: - host.WriteDebug( - 'PortOpen', f"Timeout error for port {port}, port is closed, " - "and therefore a future connection can use it") - - return ret - - -def _get_available_port(queue): - """ - Get the next available port from the port queue and return it. - - Since there can be a delay between when the queue is populated and when the - port is requested, this checks the next port in the queue to see whether it - is still not open. If it isn't, it is dropped from the queue and the next - one is inspected. This process continues until either an available port is - found, or, if the queue is exhausted, PortQueueSelectionError is raised. - - Returns: - An available (i.e., non-open) port. - - Throws: - PortQueueSelectionError if the port queue is exhausted. - """ - - if queue.qsize() == 0: - host.WriteWarning("Port queue is empty.") - raise PortQueueSelectionError("Could not get a valid port because the queue is empty") - - bound_ports = _get_bound_ports() - port = queue.get() - while PortOpen(port, bound_ports=bound_ports): - host.WriteDebug('_get_available_port', f"Port was closed but now is used: {port}") - if queue.qsize() == 0: - host.WriteWarning("Port queue is empty.") - raise PortQueueSelectionError("Could not get a valid port because the queue is empty") - port = queue.get() - return port - - -def _is_bound(conn) -> bool: - """Return whether an internet socket connection occupies its local port.""" - return bool( - conn.family in (socket.AF_INET, socket.AF_INET6) and conn.laddr and - (conn.status == psutil.CONN_LISTEN or conn.type == socket.SOCK_DGRAM)) - - -def _get_bound_ports() -> Set[int]: - """Use psutil to get the set of ports that are currently bound. - - TCP sockets report a listening status, but UDP sockets have no comparable - status. Any UDP socket with a local address is bound and therefore makes - its port unavailable to AuTest processes. - - :return: The set of ports that are currently bound. - """ - ports: Set[int] = set() - try: - connections = psutil.net_connections(kind='all') - for conn in connections: - if _is_bound(conn): - ports.add(conn.laddr.port) - except psutil.AccessDenied: - # Mac OS X doesn't allow net_connections() to be called without root. - for proc in psutil.process_iter(['pid', 'name']): - try: - connections = proc.connections(kind='all') - except (psutil.AccessDenied, psutil.NoSuchProcess): - continue - for conn in connections: - if _is_bound(conn): - ports.add(conn.laddr.port) - return ports - - -def _setup_port_queue(amount=1000): - """ - Build up the set of ports that the OS in theory will not use. - - The AUTEST_PORT_OFFSET environment variable can be used to offset the - starting port range. This is useful when running multiple autest processes - in parallel to avoid port conflicts. Each parallel worker should use a - different offset (e.g., 0, 1000, 2000, etc.). - """ - global g_ports - if g_ports is None: - host.WriteDebug('_setup_port_queue', "Populating the port queue.") - g_ports = OrderedSetQueue() - else: - # The queue has already been populated. - host.WriteDebug('_setup_port_queue', f"Queue was previously populated. Queue size: {g_ports.qsize()}") - return - - # Get port offset for parallel execution support - try: - port_offset = int(os.environ.get('AUTEST_PORT_OFFSET', 0)) - except ValueError: - host.WriteWarning("AUTEST_PORT_OFFSET is not a valid integer, defaulting to 0") - port_offset = 0 - # Clamp to a safe range to avoid exceeding the valid port space - port_offset = max(0, min(port_offset, 60000)) - if port_offset > 0: - host.WriteVerbose('_setup_port_queue', f"Using port offset: {port_offset}") - - try: - # Use sysctl to find the range of ports that the OS publishes it uses. - # some docker setups don't have sbin setup correctly - new_env = os.environ.copy() - new_env['PATH'] = "/sbin:/usr/sbin:" + new_env['PATH'] - if 'Darwin' == platform.system(): - dmin = subprocess.check_output(["sysctl", "net.inet.ip.portrange.first"], env=new_env).decode().split(":")[1].split()[0] - dmax = subprocess.check_output(["sysctl", "net.inet.ip.portrange.last"], env=new_env).decode().split(":")[1].split()[0] - else: - dmin, dmax = subprocess.check_output(["sysctl", "net.ipv4.ip_local_port_range"], - env=new_env).decode().split("=")[1].split() - dmin = int(dmin) - dmax = int(dmax) - except Exception: - host.WriteWarning("Unable to call sysctrl!\n Tests may fail because of bad port selection!") - return - - rmin = dmin - 2000 - rmax = 65536 - dmax - - bound_ports = _get_bound_ports() - if rmax > amount: - # Fill in ports, starting above the upper OS-usable port range. - # Add port_offset to support parallel test execution. - port = dmax + 1 + port_offset - while port < 65536 and g_ports.qsize() < amount: - if PortOpen(port, bound_ports=bound_ports): - host.WriteDebug('_setup_port_queue', f"Rejecting an already bound port: {port}") - else: - host.WriteDebug('_setup_port_queue', f"Adding a possible port to connect to: {port}") - g_ports.put(port) - port += 1 - if rmin > amount and g_ports.qsize() < amount: - # Fill in more ports, starting at 2001, well above well known ports, - # and going up until the minimum port range used by the OS. - # Add port_offset to support parallel test execution (same as high range). - port = 2001 + port_offset - while port < dmin and g_ports.qsize() < amount: - if PortOpen(port, bound_ports=bound_ports): - host.WriteDebug('_setup_port_queue', f"Rejecting an already bound port: {port}") - else: - host.WriteDebug('_setup_port_queue', f"Adding a possible port to connect to: {port}") - g_ports.put(port) - port += 1 - - -def _get_port_by_bind(): - """ - Create a socket, bind with REUSEADDR, and return the bound port. - - Because SO_REUSEADDR is applied as an option to the socket, the future - server should be able to use this port. This method is considered - sub-optimal in comparison to the OS-unused port range method used above - because another process might bind to this port in the meantime. - - Returns: - A port value that can be used for a connection. - """ - sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - sock.bind(('', 0)) # bind to all interfaces on an ephemeral port - port = sock.getsockname()[1] - return port - - -def _reserve_port(): - """ - Get a port from the global port queue. - - Returns: - A tuple containing the port value and whether it should be recycled - into the queue when the caller is done with it. - """ - _setup_port_queue() - if g_ports.qsize() > 0: - try: - port = _get_available_port(g_ports) - host.WriteVerbose("_reserve_port", f"Using port from port queue: {port}") - return port, True - except PortQueueSelectionError: - port = _get_port_by_bind() - host.WriteVerbose("_reserve_port", f"Queue was drained. Using port from a bound socket: {port}") - return port, False - - # Since the queue could not be populated, use a port via bind. - port = _get_port_by_bind() - host.WriteVerbose("_reserve_port", f"Queue is empty. Using port from a bound socket: {port}") - return port, False - - -@contextmanager -def get_port_number(): - """ - Reserve a port number from the same allocator used by get_port(). - - This is useful for helper code that needs a temporary listening port but - does not have an AuTest object with Setup hooks for recycling it. Queue - ports are recycled when the context exits. - - :returns: A context manager yielding the reserved port value. - """ - port, recycle_port = _reserve_port() - try: - yield port - finally: - if recycle_port: - g_ports.put(port) - - -def get_port(obj, name): - ''' - Get a port and set it to the specified variable on the object. - - Args: - obj: The object upon which to set the port variable. - name: The name of the variable that receives the port value. - - Returns: - The port value. - ''' - port, recycle_port = _reserve_port() - if recycle_port: - obj.Setup.Lambda( - func_cleanup=lambda: g_ports.put(port), description=f"recycling port: {port}, queue size: {g_ports.qsize()}") - - # Assign to the named variable. - obj.Variables[name] = port - return port diff --git a/tests/gold_tests/autest-site/readme.md b/tests/gold_tests/autest-site/readme.md deleted file mode 100644 index 72c89c7b419..00000000000 --- a/tests/gold_tests/autest-site/readme.md +++ /dev/null @@ -1,31 +0,0 @@ - -# CurlHeader Tester - -This tester tests for the output of curl. User should provide a reference python dictionary whose key-value pair should be (header, op_value) for each line of the expected curl output. For header field, user should provide a string that matches case-insensitively the expected header. For op_value field, which performs comparison operations against the actual value corresponding to that header, use can provide one of the following inputs: - * **A string**: If provided a string, it will be compared with the actual value for an exact match. - * **A dictionary**: If provided a dictionary, the key-value pair should be (op, value) or (op, [values]), where 'op' indicates the operation to perform with the actual value and the expected value. Following are the available operations: - * **equal**: This operation tries to find an exact match. In case of given a single string, it checks for exact match between it and the actual value. In case of given a list of strings, it checks for exact match with any string of the list. - * **equal_re**: This operation tries to find a regular expression match . In case of give a single python regular expression string, it checks if the actual value matches the expression. In case of given a list of regular expressions, it checks if the actual value can match any one of the expressions. - * **NoneType**: If provided a None, the tester will ignore the value for this header - -To use the CurlHeader tester when writing tests, which is not one of the default testers, user should invoke the tester in the following way: - 1. tr = Test.AddTestRun() - 2. tr.Processes.Default.Streams.stdout = Testers.CurlHeader(reference_dict) - - Examples: - * To check for header 'X-Cache' that have value 'miss' and header 'cache-control' that have value starting with 'max', provide the following dictionary to tester: - - { - 'X-Cache' : 'miss', - 'cache-control' : {'equal_re' : 'max.*'} - } - - * To check for header 'Age' that can have any value and header 'etag' that have value either matching 'myetag' or end with 'p', provide the following dictionary to tester: - - { - 'Age' : None, - 'etag' : { - 'equal' : 'myetag', - 'equal_re' : '.*p' - } - } diff --git a/tests/gold_tests/autest-site/setup.cli.ext b/tests/gold_tests/autest-site/setup.cli.ext deleted file mode 100644 index f7460ffa00c..00000000000 --- a/tests/gold_tests/autest-site/setup.cli.ext +++ /dev/null @@ -1,126 +0,0 @@ -''' -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import json -import os -from os.path import dirname -import pprint -import subprocess -import shutil - -PROXY_VERIFIER_VERSION_FILENAME = 'proxy-verifier-version.txt' - -test_root = dirname(dirname(AutestSitePath)) -repo_root = dirname(test_root) - -if Arguments.ats_bin is not None: - # Add environment variables - ENV['ATS_BIN'] = Arguments.ats_bin - -if Arguments.build_root is not None: - ENV['BUILD_ROOT'] = Arguments.build_root -else: - # Assume the build root is the same directory tree as the test location. - ENV['BUILD_ROOT'] = repo_root - -host.WriteVerbose(['ats'], "Test build root: {}:".format(ENV['BUILD_ROOT'])) - -proxy_verifer_version_file = os.path.join(test_root, PROXY_VERIFIER_VERSION_FILENAME) -proxy_verifer_version = open(proxy_verifer_version_file, "rt").read().strip() - -if Arguments.proxy_verifier_bin is not None: - ENV['VERIFIER_BIN'] = Arguments.proxy_verifier_bin - host.WriteVerbose(['ats'], "Expecting Proxy Verifier to be in user-supplied bin path: ", ENV['VERIFIER_BIN']) -else: - # No Verifier bin path was specified. First see if a Proxy Verifier was - # unpacked as a part of preparing for this test. - unpack_bin = os.path.join(test_root, 'proxy-verifier', 'unpack', proxy_verifer_version, 'bin') - if os.path.exists(os.path.join(unpack_bin, 'verifier-client')): - ENV['VERIFIER_BIN'] = unpack_bin - host.WriteVerbose(['ats'], "Using locally unpacked Proxy Verifier: ", ENV['VERIFIER_BIN']) - else: - # Finally check the PATH. - path_search = shutil.which('verifier-client') - if path_search is not None: - ENV['VERIFIER_BIN'] = dirname(path_search) - host.WriteVerbose(['ats'], "Using Proxy Verifier found in PATH: ", ENV['VERIFIER_BIN']) - else: - prepare_proxy_verifier_path = os.path.join(test_root, "prepare_proxy_verifier.sh") - host.WriteError("Could not find Proxy Verifier binaries. " - "Try running: ", prepare_proxy_verifier_path) - -required_pv_version = Version(proxy_verifer_version[1:]) -verifier_client = os.path.join(ENV['VERIFIER_BIN'], 'verifier-client') -pv_version_out = subprocess.check_output([verifier_client, "--version"]) -pv_version = Version(pv_version_out.decode("utf-8").split()[1]) -if pv_version < required_pv_version: - host.WriteError( - f"Proxy Verifier at {verifier_client} is too old. " - f"Version required: {required_pv_version}, version found: {pv_version}") -else: - host.WriteVerbose(['ats'], f"Proxy Verifier at {verifier_client} has version: {pv_version}") - -if ENV['ATS_BIN'] is not None: - # Add variables for Tests - traffic_layout = os.path.join(ENV['ATS_BIN'], "traffic_layout") - if not os.path.isdir(ENV['ATS_BIN']): - host.WriteError("--ats-bin requires a directory", show_stack=False) - # setting up data from traffic_layout - # this is getting layout structure - if not os.path.isfile(traffic_layout): - hint = '' - if os.path.isfile(os.path.join(ENV['ATS_BIN'], 'bin', 'traffic_layout')): - hint = "\nDid you mean '--ats-bin {}'?".\ - format(os.path.join(ENV['ATS_BIN'], 'bin')) - host.WriteError("traffic_layout is not found. Aborting tests - Bad build or install.{}".format(hint), show_stack=False) - try: - out = subprocess.check_output([traffic_layout, "--json"]) - except subprocess.CalledProcessError: - host.WriteError("traffic_layout is broken. Aborting tests - The build of traffic server is bad.", show_stack=False) - out = json.loads(out.decode("utf-8")) - for k, v in out.items(): - out[k] = v[:-1] if v.endswith('/') else v - Variables.update(out) - host.WriteVerbose(['ats'], "Traffic server layout Data:\n", pprint.pformat(out)) - # if the above worked this should as well - # this gets feature data - out = subprocess.check_output([traffic_layout, "--features", "--json"]) - out = json.loads(out.decode("utf-8")) - Variables.update(out) - host.WriteVerbose(['ats'], "Traffic server feature data:\n", pprint.pformat(out)) - - # update version number - out = subprocess.check_output([traffic_layout, "--version"]) - out = Version(out.decode("utf-8").split("-")[2].strip()) - Variables.trafficserver_version = out - host.WriteVerbose(['ats'], "Traffic server version:", out) - -Variables.AtsExampleDir = os.path.join(AutestSitePath, '..', '..', '..', 'example') -Variables.AtsToolsDir = os.path.join(AutestSitePath, '..', '..', '..', 'tools') -Variables.AtsTestToolsDir = os.path.join(AutestSitePath, '..', '..', 'tools') -Variables.VerifierBinPath = ENV['VERIFIER_BIN'] -Variables.BuildRoot = ENV['BUILD_ROOT'] -Variables.RepoDir = repo_root -Variables.AtsTestPluginsDir = os.path.join(Variables.BuildRoot, 'tests', 'tools', 'plugins', '.libs') -Variables.AtsBuildGoldTestsDir = os.path.join(Variables.BuildRoot, 'tests', 'gold_tests') -Variables.CurlUds = Arguments.curl_uds - -# modify delay times as we always have to kill Trafficserver -# no need to wait -Variables.Autest.StopProcessLongDelaySeconds = 0 -Variables.Autest.KillDelaySecond = 30 diff --git a/tests/gold_tests/autest-site/traffic_replay.test.ext b/tests/gold_tests/autest-site/traffic_replay.test.ext deleted file mode 100644 index c276fb2e445..00000000000 --- a/tests/gold_tests/autest-site/traffic_replay.test.ext +++ /dev/null @@ -1,99 +0,0 @@ -''' -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -# default 'mixed' for connection type since it doesn't hurt - - -def Replay(obj, name, replay_dir, key=None, cert=None, conn_type='mixed', options={}): - # ATS setup - one line because we leave records and remap config to user - ts = obj.MakeATSProcess("ts", select_ports=False) # select ports can be disabled once we add ssl port selection in extension - - # TEMP - ts.Variables.ssl_port = 4443 - - ts.addSSLfile(os.path.join(obj.Variables["AtsTestToolsDir"], "microserver", "ssl", "server.pem")) - ts.addSSLfile(os.path.join(obj.Variables["AtsTestToolsDir"], "microserver", "ssl", "server.crt")) - - ts.Disk.ssl_multicert_yaml.AddLines( - """ - ssl_multicert: - - ssl_cert_name: server.pem - ssl_key_name: server.pem - dest_ip: "*" - """.split("\n")) - - # MicroServer setup - NOTE: expand to multiple microserver in future? - server = obj.MakeOriginServer("server", both=True, lookup_key='{%uuid}') - server.addSessionFromFiles(replay_dir) - - # MicroDNS setup - dns = obj.MakeDNServer("dns", default=['127.0.0.1']) - - # Traffic Replay setup - data_dir = os.path.join(obj.RunDirectory, name) - - # NOTE: we are forcing mixed connection types for now for the sake of simplicity - - # if conn_type != 'nossl': - # if not key: - # host.WriteError("Must provide SSL key to traffic-replay.") - - # if not cert: - # host.WriteError("Must provide SSL key to traffic-replay.") - - # if not ts.Variables.ssl_port: - # host.WriteError("Must set traffic server with an ssl port") - - # NOTE: does this need change? - hostIP = '127.0.0.1' - - if not key: - key = os.path.join(obj.Variables["AtsTestToolsDir"], "microserver", "ssl", "server.pem") - - if not cert: - cert = os.path.join(obj.Variables["AtsTestToolsDir"], "microserver", "ssl", "server.crt") - - command = 'traffic-replay --log_dir {0} --type {1} --verify --host {2} --port {3} --s_port {4} '.format( - data_dir, conn_type, hostIP, ts.Variables.port, ts.Variables.ssl_port) - - if key: - command += "-k {0} ".format(key) - - if cert: - command += "--ca_cert {0} ".format(cert) - - if options: - for flag, value in options.items(): - command += "{} {} ".format(flag, value if value else '') - - tr = obj.AddTestRun(name) - tr.Command = command - # tr.Command = "echo Hi" - tr.Setup.MakeDir(data_dir) - tr.Setup.Copy(replay_dir, data_dir) - tr.Processes.Default.StartBefore(server) - tr.Processes.Default.StartBefore(ts) - tr.Processes.Default.StartBefore(dns) - tr.ReturnCode = Any(None, 0) - tr.Processes.Default.Streams.All = Testers.ExcludesExpression("FAIL", "No fails allowed.") - - # return all the stuff in case user wants to do extra optimization - return (ts, server, dns, tr) - - -AddTestRunSet(Replay) diff --git a/tests/gold_tests/autest-site/trafficserver.test.ext b/tests/gold_tests/autest-site/trafficserver.test.ext deleted file mode 100755 index fa31324a14c..00000000000 --- a/tests/gold_tests/autest-site/trafficserver.test.ext +++ /dev/null @@ -1,728 +0,0 @@ -''' -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -from __future__ import print_function -import os -import yaml - -from ports import get_port - - -def make_id(s): - return s.replace(".", "_").replace('-', '_') - - -def get_typename(s): - file_ext = os.path.splitext(s)[1] - typename = "ats:config" - - # We want the file to be handled as yaml file so we get the benefits of it. - if file_ext == ".yaml" or file_ext == ".yml": - typename = "ats:config:yaml" - - return typename - - -# A mapping from log type to the log name. 'stdout' and 'stderr' are handled -# specially and are used to indicate the stdout and stderr streams, -# respectively. -default_log_data = {'diags': 'diags.log', 'error': 'error.log', 'manager': 'manager.log'} - - -def MakeATSProcess( - obj, - name, - command='traffic_server', - select_ports=True, - enable_tls=False, - enable_cache=True, - enable_quic=False, - enable_uds=True, - enable_cripts=False, - block_for_debug=False, - log_data=default_log_data, - use_traffic_out=True, - dump_runroot=True, - enable_proxy_protocol=False, - enable_proxy_protocol_cp_src=False, - disable_log_checks=False): - """Create a traffic server process. - - :param block_for_debug: if True, causes traffic_server to run with the - --block option enabled, and effectively disables timeouts that could be - triggered by running traffic_server under a debugger. In the debugger, - `set cmd_block = 0`, set any desired break points, then `c` to continue - to let the test proceed. - """ - ##################################### - # common locations - - # directory we will setup for the ATS to run under - ts_dir = os.path.join(obj.RunDirectory, name) - # common bin directory - bin_dir = 'bin' - - # manually set up all directory for the test - - # configuration directory - config_dir = os.path.join(ts_dir, 'config') - - # directory contains the html response templates - template_dir = os.path.join(config_dir, 'body_factory') - - # contains plugins - plugin_dir = os.path.join(ts_dir, 'plugin') - - # the log directory - log_dir = os.path.join(ts_dir, 'log') - - # runtime dir - runtime_dir = os.path.join(ts_dir, 'runtime') - - #ssl, storage & cache - ssl_dir = os.path.join(ts_dir, 'ssl') - storage_dir = os.path.join(ts_dir, 'storage') - cache_dir = os.path.join(ts_dir, 'cache') - - ts_args = '' - if use_traffic_out: - # Bind stdout/err to traffic.out. This allows tests to wait upon - # content from the traffic.out file, something that cannot be done on - # process stdout/stderr in AuTest. - traffic_out = os.path.join(log_dir, 'traffic.out') - ts_args += f' --bind_stderr {traffic_out}' - ts_args += f' --bind_stdout {traffic_out}' - - if block_for_debug: - ts_args += ' --block' - - # Have the full command including arguments in the cmdline so tool scripts - # can eventually use some keyword from the path to trace a particular TS instance - process_cmd = f"{os.path.join(ts_dir, bin_dir)}/{command} {ts_args}" - # create process - p = obj.Processes.Process(name, process_cmd) - #p_debug = obj.Processes.Process("port-debug", "ss --listen --tcp --process") - #p_debug.Env['PATH'] = "/usr/sbin" + os.pathsep + p.ComposeEnv()['PATH'] - # p.StartBefore(p_debug) - # we want to have a few directories more fixed - # this helps with debugging as location are common - # we do this by overriding locations from the "layout" - # used as part of build. This means loctaion such as - # PROXY_CONFIG_BIN_PATH with always be $root/bin - # not something else such as bin64 - ##### - - # set root for this test - p.Env['TS_ROOT'] = ts_dir - p.Setup.MakeDir(ts_dir) - - # set bin location - - p.Env['PROXY_CONFIG_BIN_PATH'] = bin_dir - bin_path = os.path.join(ts_dir, bin_dir) - p.Env['PATH'] = bin_path + os.pathsep + p.ComposeEnv()['PATH'] - p.Setup.Copy(p.Variables.BINDIR, bin_path, CopyLogic.SoftFiles) - - # setup config directory - AddMethodToInstance(p, chownForATSProcess) - - # copy all basic config files we need to get this to work - cfg_dir = os.path.join(AUTEST_SITE_PATH, "min_cfg") - - p.Setup.MakeDir(config_dir) - p.chownForATSProcess(config_dir) - - for f in os.listdir(cfg_dir): - p.Setup.CopyAs(os.path.join(cfg_dir, f), config_dir) - - ######################################################### - # setup config directory - p.Env['PROXY_CONFIG_CONFIG_DIR'] = config_dir - p.Variables.CONFIGDIR = config_dir - ######################################################### - # setup read-only data directory in config. Needed for response body - # responses - - p.Env['PROXY_CONFIG_BODY_FACTORY_TEMPLATE_SETS_DIR'] = template_dir - p.Variables.BODY_FACTORY_TEMPLATE_DIR = template_dir - p.Setup.Copy(os.path.join(p.Variables.SYSCONFDIR, 'body_factory'), template_dir) - - ######################################################### - # setup cache directory - p.Setup.MakeDir(cache_dir) - p.Env['PROXY_CONFIG_CACHE_DIR'] = cache_dir - p.Variables.CACHEDIR = cache_dir - - ######################################################### - # setup read-only data directory for plugins - - p.Env['PROXY_CONFIG_PLUGIN_PLUGIN_DIR'] = plugin_dir - p.Setup.Copy(p.Variables.PLUGINDIR, plugin_dir, CopyLogic.SoftFiles) - - ######################################################### - # create subdirectories that need to exist (but are empty) - # log directory has to be created with correct permissions - p.Setup.MakeDir(log_dir) # log directory has to be created - p.chownForATSProcess(log_dir) - - # set env so traffic server uses correct locations - p.Env['PROXY_CONFIG_LOG_LOGFILE_DIR'] = log_dir - p.Variables.LOGDIR = log_dir - - # this is needed for cache and communication sockets - # Below was to make shorter paths but the code in - # set runtime directory(local state dir) - p.Env['PROXY_CONFIG_LOCAL_STATE_DIR'] = runtime_dir - p.Variables.RUNTIMEDIR = runtime_dir - p.Variables.LOCALSTATEDIR = runtime_dir - - p.Setup.MakeDir(runtime_dir) - p.chownForATSProcess(runtime_dir) - - ########################################################## - # create subdirectories that need to exist (but are empty) - # ssl directory has to be created for keeping certs and keys - p.Setup.MakeDir(ssl_dir) - p.chownForATSProcess(ssl_dir) - - # set env so traffic server uses correct locations - p.Env['PROXY_CONFIG_SSL_DIR'] = ssl_dir - p.Variables.SSLDir = ssl_dir - AddMethodToInstance(p, addSSLfile) - - # Add default cert folder & register handy method. - AddMethodToInstance(p, addDefaultSSLFiles) - AddMethodToInstance(p, addSSLFileFromDefaultTestFolder) - ######################################################## - # cache.db directory - p.Setup.MakeDir(storage_dir) - p.chownForATSProcess(storage_dir) - - # set env so traffic server uses correct locations - p.Env['PROXY_CONFIG_STORAGE_DIR'] = storage_dir - p.Variables.STORAGEDIR = storage_dir - ######################################################### - # define the basic file for a given test run - # traffic.out ?? # cannot find it at the moment... - # squid.log - fname = "squid.log" - tmpname = os.path.join(log_dir, fname) - p.Disk.File(tmpname, id=make_id(fname)) - # error.log - fname = log_data['error'] - if fname == 'stdout': - p.Disk.error_log = p.Streams.stdout - elif fname == 'stderr': - p.Disk.error_log = p.Streams.stderr - else: - tmpname = os.path.join(log_dir, fname) - p.Disk.File(tmpname, id='error_log') - # diags.log - fname = log_data['diags'] - if fname == 'stdout': - p.Disk.diags_log = p.Streams.stdout - elif fname == 'stderr': - p.Disk.diags_log = p.Streams.stderr - else: - tmpname = os.path.join(log_dir, fname) - p.Disk.File(tmpname, id='diags_log') - # add this test back once we have network namespaces working again - if not disable_log_checks: - p.Disk.diags_log.Content = Testers.ExcludesExpression("ERROR:", f"Diags log file {fname} should not contain errors") - p.Disk.diags_log.Content += Testers.ExcludesExpression("FATAL:", f"Diags log file {fname} should not contain errors") - p.Disk.diags_log.Content += Testers.ExcludesExpression( - "Unrecognized configuration value", - f"Diags log file {fname} should not contain a warning about an unrecognized configuration") - - # traffic.out - fname = "traffic.out" - tmpname = os.path.join(log_dir, fname) - p.Disk.File(tmpname, id='traffic_out') - - # config files - def MakeConfigFile(self, fname): - tmpname = os.path.join(config_dir, fname) - return self.File(tmpname, id=make_id(fname), typename=get_typename(fname)) - - AddMethodToInstance(p.Disk, MakeConfigFile) - - # "Core" config files are pre-defined as variables. - fname = "records.yaml" - tmpname = os.path.join(config_dir, fname) - # Note: make_id -> We keep the records_config to be used in autests just for backward compatibility - p.Disk.File(tmpname, id=make_id("records.config"), typename="ats:config:records") - - fname = "cache.config" - tmpname = os.path.join(config_dir, fname) - p.Disk.File(tmpname, id=make_id(fname), typename="ats:config") - - fname = "hosting.config" - tmpname = os.path.join(config_dir, fname) - p.Disk.File(tmpname, id=make_id(fname), typename="ats:config") - - fname = "ip_allow.yaml" - tmpname = os.path.join(config_dir, fname) - p.Disk.File(tmpname, id=make_id(fname), typename="ats:config") - AddMethodToInstance(p, addPrivateConnectAllowYaml) - - fname = "logging.yaml" - tmpname = os.path.join(config_dir, fname) - p.Disk.File(tmpname, id=make_id(fname), typename="ats:config") - - fname = "parent.config" - tmpname = os.path.join(config_dir, fname) - p.Disk.File(tmpname, id=make_id(fname), typename="ats:config") - - fname = "plugin.config" - tmpname = os.path.join(config_dir, fname) - p.Disk.File(tmpname, id=make_id(fname), typename="ats:config") - - fname = "remap.config" - tmpname = os.path.join(config_dir, fname) - p.Disk.File(tmpname, id=make_id(fname), typename="ats:config") - - fname = "remap.yaml" - tmpname = os.path.join(config_dir, fname) - p.Disk.File(tmpname, id=make_id(fname), typename="ats:config") - - fname = "socks.config" - tmpname = os.path.join(config_dir, fname) - p.Disk.File(tmpname, id=make_id(fname), typename="ats:config") - - fname = "splitdns.config" - tmpname = os.path.join(config_dir, fname) - p.Disk.File(tmpname, id=make_id(fname), typename="ats:config") - - fname = "ssl_multicert.yaml" - tmpname = os.path.join(config_dir, fname) - p.Disk.File(tmpname, id=make_id(fname), typename="ats:config") - - fname = "sni.yaml" - tmpname = os.path.join(config_dir, fname) - p.Disk.File(tmpname, id=make_id(fname), typename="ats:config") - - fname = "storage.yaml" - tmpname = os.path.join(config_dir, fname) - p.Disk.File(tmpname, id=make_id(fname), typename="ats:config") - - # The big motivation in exposing this file is that we need to tell the traffic_ctl - # where to find the socket to interact with the TS. traffic_ctl cannot rely only - # in the build layout for unit test. - fname = "runroot.yaml" - tmpname = os.path.join(config_dir, fname) - p.Disk.File(tmpname, id=make_id(fname), typename="ats:config") - - # Fill in the runroot file and set the config var, this will be used traffic_server - # and traffic_ctl - if dump_runroot: - p.Disk.runroot_yaml.AddLines( - [ - f'runtimedir: {runtime_dir}', - f'cachedir: {cache_dir}', - f'localstatedir: {runtime_dir}', - f'bindir: {bin_dir}', - f'prefix: {ts_dir}', - f'logdir: {log_dir}', - f'sysconfdir: {config_dir}', - ]) - # Add more if needed - p.Env['TS_RUNROOT'] = os.path.join(config_dir, "runroot.yaml") - - ########################################################## - # set up default ports - # get some ports TODO make it so we can hold on to the socket - if select_ports: - - # some system have a bug in which ipv4 and ipv6 share port space - # Make two different ports to avoid this - get_port(p, "port") - get_port(p, "portv6") - - if enable_tls: - get_port(p, "ssl_port") - get_port(p, "ssl_portv6") - - if enable_proxy_protocol: - get_port(p, "proxy_protocol_port") - get_port(p, "proxy_protocol_portv6") - - if enable_tls: - get_port(p, "proxy_protocol_ssl_port") - get_port(p, "proxy_protocol_ssl_portv6") - else: - p.Variables.port = 8080 - p.Variables.portv6 = 8080 - - if enable_tls: - p.Variables.ssl_port = 4443 - p.Variables.ssl_portv6 = 4444 - - # unix domain socket path - uds_path = os.path.join(runtime_dir, 'uds.socket') - p.Variables.uds_path = uds_path - - get_port(p, "manager_port") - get_port(p, "admin_port") - - if enable_tls or enable_quic: - fname = "tls_session_keys.txt" - tmpname = os.path.join(log_dir, fname) - p.Disk.File(tmpname, id='tls_session_keys') - p.Disk.records_config.update({ - 'proxy.config.ssl.keylog_file': tmpname, - }) - - # For config reload tests, no need to wait the default 3 seconds to detect the need for a reload. - p.Disk.records_config.update({'proxy.config.config_update_interval_ms': 20}) - - if enable_cache: - # In records.yaml, the cache is enabled by default so there's nothing - # we have to do here to functionally enable it. However, the tests that - # rely upon the cache will not function correctly if ATS starts - # processing traffic before the cache is ready. Thus we set the - # wait_for_cache configuration. - p.Disk.records_config.update( - { - # Do not accept connections from clients until cache subsystem is - # operational. - 'proxy.config.http.wait_for_cache': 1, - }) - else: - # The user wants the cache to be disabled. - p.Disk.records_config.update({'proxy.config.http.cache.http': 0}) - - if enable_quic: - p.Disk.records_config.update({ - 'proxy.config.udp.threads': 1, - }) - - if enable_cripts: - cripts_compiler = os.path.join(bin_path, 'cripts_compiler.sh') - p.Setup.Copy(os.path.join(p.Variables.RepoDir, 'tools', 'cripts', 'compiler.sh'), cripts_compiler) - p.Variables.cripts_compiler = cripts_compiler - p.Disk.records_config.update({ - 'proxy.config.plugin.compiler_path': cripts_compiler, - }) - # ATS_ROOT is used by compiler.sh to find the ATS install directory. - p.Env['ATS_ROOT'] = p.Variables.PREFIX - # Cripts are compiled at startup, which can exceed the default 10s readiness window - # under CI load and flake the "fully initialized" wait. Give cripts startup more room. - p.StartupTimeout = 60 - - # The following message was added so that tests and users can know when - # Traffic Server is ready to both receive and optimize traffic. - p.Ready = When.FileContains(p.Disk.diags_log.AbsPath, "NOTE: Traffic Server is fully initialized") - - if select_ports: - # default config - port_str = "{port} {v6_port}:ipv6".format(port=p.Variables.port, v6_port=p.Variables.portv6) - if enable_tls: - port_str += " {ssl_port}:ssl {ssl_portv6}:ssl:ipv6".format( - ssl_port=p.Variables.ssl_port, ssl_portv6=p.Variables.ssl_portv6) - if enable_quic: - port_str += " {ssl_port}:quic {ssl_portv6}:quic:ipv6".format( - ssl_port=p.Variables.ssl_port, ssl_portv6=p.Variables.ssl_portv6) - if enable_proxy_protocol: - ppcfg = "pp" - if enable_proxy_protocol_cp_src: - ppcfg += ":pp-clnt" - port_str += f" {p.Variables.proxy_protocol_port}:{ppcfg} {p.Variables.proxy_protocol_portv6}:{ppcfg}:ipv6" - if enable_tls: - port_str += f" {p.Variables.proxy_protocol_ssl_port}:{ppcfg}:ssl {p.Variables.proxy_protocol_ssl_portv6}:{ppcfg}:ssl:ipv6" - if enable_uds: - port_str += f" {uds_path}:{ppcfg}" - elif enable_uds: - port_str += f" {uds_path}" - #p.Env['PROXY_CONFIG_HTTP_SERVER_PORTS'] = port_str - p.Disk.records_config.update({ - 'proxy.config.http.server_ports': port_str, - }) - - p.Env['PROXY_CONFIG_PROCESS_MANAGER_MGMT_PORT'] = str(p.Variables.manager_port) - p.Env['PROXY_CONFIG_ADMIN_SYNTHETIC_PORT'] = str(p.Variables.admin_port) - p.Env['PROXY_CONFIG_ADMIN_AUTOCONF_PORT'] = str(p.Variables.admin_port) # support pre ATS 6.x - - p.ReturnCode = 0 - - if block_for_debug: - p.Env['PROXY_BLOCK'] = '1' - # Cause traffic_server to wait effectviely indefinitely (10 hours) for the debugger to attach. - p.StartupTimeout = 10 * 60 * 60 - # Hitting breakpoints may cause long delays in traffic_server transaction processing, so make - # the timeout for test processes (that may interact with traffic_server) effectively forever - # (10 hours). - obj.Variables.Autest.Process.TimeOut = 10 * 60 * 60 - - return p - - -################################## -# added to ats process object to help deal with config files - - -class Config(File): - ''' - Class to represent a config file - ''' - - def __init__(self, runable, name, exists=None, size=None, content_tester=None, execute=False, runtime=True, content=None): - super(Config, self).__init__(runable, name, exists=None, size=None, content_tester=None, execute=False, runtime=True) - - self.content = content - self._added = False - - def AddLines(self, lines): - for line in lines: - self.AddLine(line) - - def _do_write(self, name): - ''' - Write contents to disk - ''' - host.WriteVerbosef('ats-config-file', "Writing out file {0}", self.Name) - if self.content is not None: - with open(name, 'w') as f: - f.write(self.content) - return (True, "Appended file {0}".format(self.Name), "Success") - - def AddLine(self, line): - if not self._added: - self.WriteCustomOn(self._do_write) - self._added = True - if self.content is None: - self.content = "" - if not line.endswith('\n'): - line += '\n' - self.content += line - - -class YAMLFile(File): - ''' - Class representing a YAML config file. - ''' - - def __init__( - self, - runable, - name, - exists=None, - size=None, - content_tester=None, - execute=False, - runtime=True, - content=None, - root_tag=None): - - super(YAMLFile, self).__init__(runable, name, exists=None, size=None, content_tester=None, execute=False, runtime=True) - self.__root_tag = root_tag - # We support multiple docs in the same file, every time append_to_document - # is called, a new item will be created in the following list. At the end - # all of them will be written in the same stream. - self.__content = [{}] - - self.WriteCustomOn(self._do_write) - - def _do_write(self, name): - host.WriteVerbosef('ats-config-file', "Writing out file {0}", name) - - def float_representer(dumper, value): - return dumper.represent_scalar(u'tag:yaml.org,2002:float', str(value), style="'") - - def int_representer(dumper, value): - return dumper.represent_scalar(u'tag:yaml.org,2002:int', str(value), style="'") - - if len(self.__content) > 0: - with open(name, 'w') as f: - yaml.add_representer(float, float_representer) - yaml.add_representer(int, int_representer) - docs = [] - for content in self.__content: - updated_content = {} - - if self.__root_tag: - updated_content[self.__root_tag] = content - else: - updated_content = content - - docs.append(updated_content) - - yaml.dump_all(docs, f) - - return (True, "Writing config file {0}".format(os.path.split(self.Name)[-1]), "Success") - - def __transform_content(self, content): - if isinstance(content, str): # from new style - return yaml.load(content, Loader=yaml.Loader) - else: - return content # Already an object. - - def __update(self, content: dict, out: dict): - for key, value in content.items(): - if key in out: - if not isinstance(out[key], dict): - # Simple: just set the value. - out[key] = value - continue - - # Recurse: update the nested dictionary. - self.__update(value, out[key]) - else: - out[key] = value - - def update(self, content): - self.__update(self.__transform_content(content), self.__content[0]) - - def append_to_document(self, content): - # We want to append a new doc in the same YAML stream. - new_doc = {} - self.__update(self.__transform_content(content), new_doc) - self.__content.append(new_doc) - - -class RecordsYAML(YAMLFile): - - def __init__(self, runable, name, exists=None, size=None, content_tester=None, execute=False, runtime=True): - super(RecordsYAML, self).__init__(runable, name, exists, size, content_tester, execute, runtime, root_tag="records") - - def _make_obj_from_legacy_record(self, config, var, value): - ''' - Walk the record down and build up the YAML structure. - ''' - key = '' - index = var.find('.') - if index < 0: # last part - config[var] = value - else: - key = var[:index] - if key not in config: - config[key] = {} - - self._make_obj_from_legacy_record(config[key], var[index + 1:], value) - - def __legacy_update(self, obj, new_doc=False): - config = {} - for name, value in obj.items(): - if name.startswith("proxy.config."): - name = name[len("proxy.config."):] - elif name.startswith("local.config."): - name = name[len("local.config."):] - # Maybe an non standard variable name. - - # unwrap the record and make it a YAML field. - self._make_obj_from_legacy_record(config, name, value) - - if new_doc: - super(RecordsYAML, self).append_to_document(config) - else: - super(RecordsYAML, self).update(config) - - def update(self, data): - if isinstance(data, dict): - ''' - If the data is already parsed or (more likely) is a legacy objects - we still support it, so update from the passed object. - ''' - self.__legacy_update(data) - else: - ''' - We also support plain YAML(str) to be passed on. Base class will load from the yaml parser. - ''' - super(RecordsYAML, self).update(data) - - def append_to_document(self, data): - if isinstance(data, dict): - ''' - If the data is already parsed or (more likely) is a legacy objects - we still support it, so update from the passed object. - ''' - self.__legacy_update(data, new_doc=True) - else: - ''' - We also support plain YAML(str) to be passed on. Base class will load from the yaml parser. - ''' - super(RecordsYAML, self).append_to_document(data) - - -########################################################################## - - -def chownForATSProcess(self, path): - self.Setup.Chown(path, "nobody", "nobody", ignore=True) - # covers ubuntu's unprivileged group - self.Setup.Chown(path, "nobody", "nogroup", ignore=True) - - -def addSSLfile(self, filename): - self.Setup.CopyAs(filename, self.Variables.SSLDir) - - -def addDefaultSSLFiles(self): - addSSLfile(self, os.path.join(self.Variables.AtsTestToolsDir, "ssl", "server.pem")) - addSSLfile(self, os.path.join(self.Variables.AtsTestToolsDir, "ssl", "server.key")) - - -def addSSLFileFromDefaultTestFolder(self, filename): - addSSLfile(self, os.path.join(self.Variables.AtsTestToolsDir, "ssl", filename)) - - -CONNECT_DENY_RULE_MARKER = """ - apply: out - ip_addrs: - - 0.0.0.0/8 -""" - - -def _addPrivateConnectAllowRule(content, methods="CONNECT"): - rule = "\n".join([ - " - apply: out", - " ip_addrs: 127.0.0.1", - " action: allow", - f" methods: {methods}", - ]) + "\n" - - if rule in content: - return content - - marker_index = content.find(CONNECT_DENY_RULE_MARKER) - if marker_index >= 0: - return content[:marker_index] + rule + content[marker_index:] - - raise RuntimeError("Could not find default outbound CONNECT deny rule in ip_allow.yaml") - - -def addPrivateConnectAllowYaml(self, methods="CONNECT"): - """Allow tests to intentionally CONNECT to a loopback endpoint.""" - - def update_ip_allow_yaml(name): - with open(name) as f: - content = f.read() - with open(name, "w") as f: - f.write(_addPrivateConnectAllowRule(content, methods)) - return (True, "Updated file {0}".format(self.Disk.ip_allow_yaml.Name), "Success") - - if self.Disk.ip_allow_yaml.content is not None: - self.Disk.ip_allow_yaml.content = _addPrivateConnectAllowRule(self.Disk.ip_allow_yaml.content, methods) - else: - self.Disk.ip_allow_yaml.WriteCustomOn(update_ip_allow_yaml) - - -RegisterFileType(Config, "ats:config") -RegisterFileType(YAMLFile, "ats:config:yaml") -RegisterFileType(RecordsYAML, "ats:config:records") -ExtendTestRun(chownForATSProcess, name="ChownForATSProcess") -ExtendTest(MakeATSProcess, name="MakeATSProcess") -ExtendTestRun(MakeATSProcess, name="MakeATSProcess") diff --git a/tests/gold_tests/autest-site/trafficserver_plugins.test.ext b/tests/gold_tests/autest-site/trafficserver_plugins.test.ext deleted file mode 100644 index 243f3d89014..00000000000 --- a/tests/gold_tests/autest-site/trafficserver_plugins.test.ext +++ /dev/null @@ -1,109 +0,0 @@ -''' -Builds, installs, and enables an ATS plugin in the sandbox environment -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import os - -import hosts.output as host - - -def prepare_plugin_helper(so_name, tsproc, plugin_args="", copy_plugin=True): - """ - Installs and enables an ATS plugin in the sandbox environment. - - Args: - so_name (str): The path or filename of a built .so file. - - tsproc (Process): The Traffic Server process whose plugin.config should - be configured to use the provided plugin. - - plugin_args (str): The arguments to provide the plugin in the - plugin.config. - - copy_plugin (bool): Whether to copy the plugin to the sandbox's plugin - directory. - """ - - filename, extension = os.path.splitext(so_name) - if extension != ".so": - raise ValueError('so_name argument must have a ".so" extension. ' - 'Received: {}'.format(so_name)) - - plugin_dir = tsproc.Env['PROXY_CONFIG_PLUGIN_PLUGIN_DIR'] - if copy_plugin: - host.WriteVerbose("prepare_plugin", "Copying down {} into {}.".format(so_name, plugin_dir)) - tsproc.Setup.Copy(so_name, plugin_dir) - else: - host.WriteVerbose("prepare_plugin", "Skipping copying {} into {} due to configuration.".format(so_name, plugin_dir)) - - # Add an entry to plugin.config. - basename = os.path.basename(so_name) - config_line = "{0} {1}".format(basename, plugin_args) - host.WriteVerbose("prepare_plugin", 'Adding line to plugin.config: "{}"'.format(config_line)) - tsproc.Disk.plugin_config.AddLine(config_line) - - -def prepare_test_plugin(self, so_path, tsproc, plugin_args=""): - """ - Installs and enables an ATS plugin in the sandbox environment. - - Args: - so_path (str): The path to a built .so file. - - tsproc (Process): The Traffic Server process whose plugin.config should - be configured to use the provided plugin. - - plugin_args (str): The arguments to provide the plugin in the - plugin.config. - """ - if not os.path.exists(so_path): - raise ValueError('PrepareTestPlugin: file does not exist: "{}"'.format(so_path)) - - prepare_plugin_helper(so_path, tsproc, plugin_args, copy_plugin=True) - - -def prepare_installed_plugin(self, so_name, tsproc, plugin_args=""): - """ - Configures an already-installed ATS plugin in the sandbox environment. - - Args: - so_name (str): The name of a plugin to configure. - - tsproc (Process): The Traffic Server process whose plugin.config should - be configured to use the provided plugin. - - plugin_args (str): The arguments to provide the plugin in the - plugin.config. - """ - if os.path.dirname(so_name): - raise ValueError('PrepareInstalledPlugin expects a filename not a path: ' - '"{}"'.format(so_name)) - prepare_plugin_helper(so_name, tsproc, plugin_args, copy_plugin=False) - - -""" -PrepareTestPlugin should be used for the test-specific plugins that need to -be copied down into the sandbox directory. -""" -ExtendTest(prepare_test_plugin, name="PrepareTestPlugin") -""" -PrepareInstalledPlugin should be used for the plugins installed via Automake -make install. They are already sym linked into the test directory via the -MakeATSProcess extension. -""" -ExtendTest(prepare_installed_plugin, name="PrepareInstalledPlugin") diff --git a/tests/gold_tests/autest-site/txn_box.test.ext b/tests/gold_tests/autest-site/txn_box.test.ext deleted file mode 100755 index 007b31254ce..00000000000 --- a/tests/gold_tests/autest-site/txn_box.test.ext +++ /dev/null @@ -1,250 +0,0 @@ -''' -Implement txn_box extensions for Traffic Server. -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import os.path -import types -from urllib.parse import urlparse - - -def append_if(name, suffix=""): - """ - Append the optional suffix to name if it exists and return the result. - """ - return name if not suffix else f"{name}-{suffix}" - - -def TxnBoxUrlRewrite(tester, rules, ports): - if rules: - for line in rules: - pparams = None - target = line[0] - if len(line) == 1: - replacement = target - elif len(line) == 2: - if isinstance(line[1], str): - replacement = line[1] - else: - replacement = target - pparams = line[1] - elif len(line) == 3: - replacement = line[1] - pparams = line[2] - else: - raise ValueError("remap entries must be either (from, to) or (from, to, [pparams])") - - # Parse the replacement URL and change just the port. - url = urlparse(replacement) - port = ports[url.scheme] - url = url._replace(netloc='{}:{}'.format(url.hostname, port)) - line = 'map {} {} '.format(target, url.geturl()) - if pparams: - line += '@plugin=txn_box.so ' - for pparam in pparams: - line += '@pparam={} '.format(pparam) - tester.Disk.remap_config.AddLine(line) - - -def TxnBoxRxUrlRewrite(tester, rules, ports): - if rules: - for rule in rules: - pparams = None - target = rule[0] - if len(rule) == 2: - if isinstance(rule[1], str): - replacement = rule[1] - else: - pparams = rule[1] - elif len(rule) == 3: - replacement = rule[1] - pparams = rule[2] - else: - raise ValueError("remap entries must be either (from, to) or (from, to, [pparams])") - - url = urlparse(replacement) - port = ports[url.scheme] - url = url._replace(netloc='{}:{}'.format(url.hostname, port)) - line = 'regex_map {} {} '.format(target, url.geturl()) - if pparams: - line += '@plugin=txn_box.so ' - for pparam in pparams: - line += '@pparam={} '.format(pparam) - tester.Disk.remap_config.AddLine(line) - - -def TxnBoxTest( - self, - replay_path, - config_path=None, - config_key="meta.txn_box", - remap=None, - regex_map=None, - command='traffic_server', - enable_tls=False, - suffix="", - verifier_server_args="--verbose info"): - """ - Create a base test object specialized for TxnBox testing. - - Args: - replay_path: Replay file for the PV server. - config_path: (str) Path to the global configuration file for txn_box.so. If omitted only - remap configuration is done. If it's 'Auto' then the replay path is used. - config_key: (str) The --key parameter to pass into txn_box.so in the - plugin.config file. - remap: [(from, to, [pparams])] Allows the specification of a - set of remap config lines. If pparams is provided, then - @plugin=txn_box.so is set and the set of pparams are placed in. - if to is missing, it is set equal to from yielding an identity remap. - """ - - ts_name = append_if("ts", suffix) - ts = self.MakeATSProcess(ts_name, command=command, enable_tls=enable_tls) - self.Variables.TXNBOX_TS = ts - self.Variables.TXNBOX_REPLAY_PATH = replay_path - self.Variables.TXNBOX_DEPENDENCIES = False - - # Put the txn_box.so into the sandbox. - # plugin_dir = ts.Env['PROXY_CONFIG_PLUGIN_PLUGIN_DIR'] - # from os.path import dirname - # git_root = dirname(dirname(dirname(ts.TestRoot))) - # txn_box_lib = os.path.join(git_root, "lib", "txn_box.so") - # ts.Setup.Copy(txn_box_lib, plugin_dir, CopyLogic.SoftFiles) - self.PrepareInstalledPlugin('txn_box.so', ts) - - # Configure txn_box in Traffic Server if there's a config. Otherwise assume it's remap only. - if config_path == 'Auto': - config_path = replay_path - - if config_path: - txn_box_command = 'txn_box.so' - - if config_key: - txn_box_command += ' --key {}'.format(config_key) - - ts.Setup.Copy(config_path, ts.Variables.CONFIGDIR) - txn_box_command += ' {}'.format(os.path.basename(config_path)) - - ts.Disk.plugin_config.AddLine(txn_box_command) - - # Configure Traffic Server to use the DNS process. - dns_name = append_if("dns", suffix) - self.Variables.TXNBOX_DNS = self.MakeDNServer(dns_name, ip='127.0.0.1', default='127.0.0.1') - - server_name = append_if("pv-server", suffix) - self.Variables.TXNBOX_SERVER = self.MakeVerifierServerProcess(server_name, replay_path, other_args=verifier_server_args) - - ts.Disk.records_config.update( - { - 'proxy.config.dns.nameservers': '127.0.0.1:{0}'.format(self.Variables.TXNBOX_DNS.Variables.Port), - 'proxy.config.dns.resolv_conf': 'NULL', - 'proxy.config.ssl.client.verify.server.policy': 'DISABLED' - # The following is needed for ATS 9 and later. - # 'proxy.config.plugin.dynamic_reload': 0 - }) - - TxnBoxUrlRewrite( - ts, remap, { - "http": self.Variables.TXNBOX_SERVER.Variables.http_port, - "https": self.Variables.TXNBOX_SERVER.Variables.https_port - }) - TxnBoxRxUrlRewrite( - ts, regex_map, { - "http": self.Variables.TXNBOX_SERVER.Variables.http_port, - "https": self.Variables.TXNBOX_SERVER.Variables.https_port - }) - - if remap == None and regex_map == None: - ts.Disk.remap_config.AddLine('map / http://127.0.0.1:{0}'.format(self.Variables.TXNBOX_SERVER.Variables.http_port)) - - # Make this accessible to TxnBox via the TS process environment. - ts.Env['PV_HTTP_PORT'] = "{}".format(self.Variables.TXNBOX_SERVER.Variables.http_port) - ts.Env['PV_HTTPS_PORT'] = "{}".format(self.Variables.TXNBOX_SERVER.Variables.https_port) - return ts - - -def TxnBoxRun(self, text, replay_path=None, ssl_cert=None, suffix="", verifier_client_args=""): - """ - Set up a standard test run for TxnBox - - Args: - text: (str) Description for test run. - Keywords - replay_path: (str) Path to the replay file. Defaults to the path used for TxnBoxTest. - """ - - if replay_path == None: - replay_path = self.Variables.TXNBOX_REPLAY_PATH - - run = self.AddTestRun(text) - run.Variables.TEST = self - ts = run.Variables.TS = self.Variables.TXNBOX_TS - - vc_args = {'other_args': "--verbose diag {}".format(verifier_client_args), 'http_ports': [ts.Variables.port]} - - if ssl_cert != None: - vc_args['ssl_cert'] = ssl_cert - - try: - vc_args['https_ports'] = [ts.Variables.ssl_port] - except: - pass # not an error if it's missing, just don't pass it. - - vc_name = append_if("pv-client", suffix) - pv_client = run.AddVerifierClientProcess(vc_name, replay_path, **vc_args) - run.Variables.CLIENT = pv_client - - pv_server = self.Variables.TXNBOX_SERVER - run.Variables.SERVER = pv_server - - if not self.Variables.TXNBOX_DEPENDENCIES: - self.Variables.TXNBOX_DEPENDENCIES = True - run.Processes.Default.StartBefore(pv_server) - run.Processes.Default.StartBefore(self.Variables.TXNBOX_DNS) - run.Processes.Default.StartBefore(ts) - - return run - - -def TxnBoxTestAndRun( - self, - name, - replay_path, - config_path=None, - config_key="meta.txn_box", - remap=None, - verifier_client_args="--verbose diag", - verifier_server_args="--verbose info", - command="traffic_server", - enable_tls=False, - suffix=""): - self.TxnBoxTest( - replay_path, - config_path, - config_key, - remap, - command=command, - enable_tls=enable_tls, - suffix=suffix, - verifier_server_args=verifier_server_args) - return self.TxnBoxRun(name, verifier_client_args=verifier_client_args, suffix=suffix) - - -ExtendTest(TxnBoxTest, name="TxnBoxTest") -ExtendTest(TxnBoxRun, name="TxnBoxRun") -ExtendTest(TxnBoxTestAndRun, name="TxnBoxTestAndRun") diff --git a/tests/gold_tests/autest-site/verifier_client.test.ext b/tests/gold_tests/autest-site/verifier_client.test.ext deleted file mode 100755 index 60ca3942272..00000000000 --- a/tests/gold_tests/autest-site/verifier_client.test.ext +++ /dev/null @@ -1,230 +0,0 @@ -''' -Implement the Proxy Verifier client extensions. -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import os -from verifier_common import create_address_argument, substitute_context_in_replay_file - - -def _configure_client( - obj, - process, - name, - replay_path, - http_ports=None, - https_ports=None, - http3_ports=None, - keys=None, - ssl_cert='', - ca_cert='', - verbose=True, - other_args='', - run_parallel=False, - context=None, - poll_timeout=None): - """ - Configure the process for running the verifier-client. - - Args: - obj: An object that has a RunDirectory attribute. - - process: (Process) The test process to configure for verifier-client. - - See AddVerifierClientProcess for a description of all other arguments. - """ - - # Create the directory we will setup for the replay binaries to run under. - client_dir = os.path.join(obj.RunDirectory, name) - - # Create a link of the binary to the rundir and set up the PATH variable. - # This will be the verifier-client that gets run. - bin_dir = 'bin' - process.Env['CLIENT_BIN_PATH'] = bin_dir - bin_path = os.path.join(client_dir, bin_dir) - process.Env['PATH'] = bin_path + os.pathsep + process.ComposeEnv()['PATH'] - process.Setup.Copy(process.Variables.VerifierBinPath, bin_path, CopyLogic.SoftFiles) - - if http_ports is None and https_ports is None and http3_ports is None: - raise ValueError("At least one of the following [http_ports, https_ports, http3_ports] must be supplied.") - - # Configure the verifier-client command line arguments. - command = "verifier-client run " - if replay_path: - if context: - # replace the passed replay file with the new one generated using the passed - # context - replay_path = substitute_context_in_replay_file(process, replay_path, context) - # Create a copy of the replay directory in the run directory. - run_replay_path = os.path.join(client_dir, os.path.basename(replay_path)) - process.Setup.Copy(replay_path, run_replay_path, CopyLogic.SoftFiles) - command += f" {run_replay_path} " - - if not http_ports: - http_ports = [8080] - if http_ports: - command += "--connect-http " - command += create_address_argument(http_ports) - command += " " - - if not https_ports: - https_ports = [4443] - if https_ports: - command += "--connect-https " - command += create_address_argument(https_ports) - command += " " - - if not http3_ports: - http3_ports = [4443] - if http3_ports: - command += "--connect-http3 " - command += create_address_argument(http3_ports) - command += " " - - qlog_basename = "qlog_directory" - qlog_dir = os.path.join(client_dir, qlog_basename) - command += f"--qlog-dir {qlog_dir} " - obj.Variables['qlog_dir'] = qlog_dir - - if https_ports or http3_ports: - if ssl_cert == '': - ssl_cert = os.path.join(obj.Variables["AtsTestToolsDir"], "proxy-verifier", "ssl", "client.pem") - - if not os.path.isfile(ssl_cert): - raise ValueError(f"Tried to use '{ssl_cert}' for --client-cert, but it is not " - "a valid file.") - if ssl_cert: - run_ssl_cert = os.path.join(client_dir, os.path.basename(ssl_cert)) - process.Setup.Copy(ssl_cert, run_ssl_cert, CopyLogic.SoftFiles) - command += f' --client-cert "{run_ssl_cert}" ' - - tls_secrets_log_filename = "tls_secrets.txt" - tls_secrets_log_path = os.path.join(client_dir, tls_secrets_log_filename) - command += f"--tls-secrets-log-file {tls_secrets_log_path} " - obj.Variables['tls_secrets_log_path'] = tls_secrets_log_path - - if ca_cert == '': - ca_cert = os.path.join(obj.Variables["AtsTestToolsDir"], "proxy-verifier", "ssl", "ca.pem") - - if not os.path.isfile(ca_cert): - raise ValueError(f"Tried to use '{ca_cert}' for --ca-certs, but it is not " - "a valid file.") - if ca_cert: - run_ca_cert = os.path.join(client_dir, os.path.basename(ca_cert)) - process.Setup.Copy(ca_cert, run_ca_cert, CopyLogic.SoftFiles) - command += f' --ca-certs "{run_ca_cert}" ' - - if verbose: - command += ' --verbose diag ' - - if other_args: - command += f" {other_args}" - - if keys is not None: - command += f" --keys {keys}" - - if poll_timeout is not None: - command += f" --poll-timeout {poll_timeout}" - - # Generally we prefer the deterministic behavior of a single thread where - # each connection is processed sequentially. - if not run_parallel and 'thread-limit' not in command: - command += " --thread-limit 1" - - process.Command = command - process.ReturnCode = 0 - - process.Streams.stdout = Testers.ExcludesExpression( - "Violation|Invalid status", "There should be no Proxy Verifier violation errors.") - - -def AddVerifierClientProcess( - run, - name, - replay_path, - http_ports=None, - https_ports=None, - http3_ports=None, - keys=None, - ssl_cert='', - ca_cert='', - verbose=True, - other_args='', - run_parallel=False, - context=None, - poll_timeout=None): - """ - Set the Default process of the test run to a verifier-client Process. - - Args: - run: (TestRun) The test run to which the client process is added. - - name: (str) The name to apply to this particular verifier-client instance. - Individual verifier-client processes must have unique names to - distinguish between them. - - replay_path: (path) The file or directory containing the traffic replay - specification. - - http_ports: (list of ints) The set of HTTP ports to connect on. - - https_ports: (list of ints) The set of HTTPS ports to connect on. - - http3_ports: (list of ints) The set of HTTP3 ports to connect on. - - ssl_cert: (path) The location of the cert for HTTPS encryption. If this - is not provided and stock ssl_cert will be used. - - ca_cert: (path) The location of the CA for HTTPS encryption. If this - is not provided a stock CA will be used. - - verbose: (bool) Whether to enable debug level logging. - - other_args: (str) Any other arbitrary options to pass to verifier-client. - - run_parallel: (bool) Whether to run the verifier-client in parallel. - - context: Any dictionary-like object with keys that match the placeholders - in the replay file. - Template strings support $-based substitutions in the replay file. - You can refer to https://docs.python.org/3/library/string.html#template-strings - for more information how to add template strings to the replay file. - - poll_timeout: (int) The poll timeout in milliseconds passed to - verifier-client via --poll-timeout. Defaults to None which leaves the - verifier-client default (3000 ms) in place. - Returns: - The newly constructed verifier-client for the test run, which is also the - Default Process of the test run. - Throws: - ValueError: - If context substitution is expected to be done but a directory is passed as a - replay_path. - OSError in case of any issues related to I/O error, ie: File Not found for the replay - file when a context substitution is expected. - KeyError if placeholders are missing from the mapping between context and the replay file. - """ - - p = run.Processes.Default - _configure_client( - run, p, name, replay_path, http_ports, https_ports, http3_ports, keys, ssl_cert, ca_cert, verbose, other_args, run_parallel, - context, poll_timeout) - return p - - -########################################################################## -ExtendTestRun(AddVerifierClientProcess, name="AddVerifierClientProcess") diff --git a/tests/gold_tests/autest-site/verifier_server.test.ext b/tests/gold_tests/autest-site/verifier_server.test.ext deleted file mode 100755 index 24cf762f6fa..00000000000 --- a/tests/gold_tests/autest-site/verifier_server.test.ext +++ /dev/null @@ -1,255 +0,0 @@ -''' -Implement the Proxy Verifier test server extension. -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import os - -from ports import get_port -from verifier_common import create_address_argument, substitute_context_in_replay_file - - -def _configure_server( - obj, - process, - name, - replay_path, - http_ports=None, - https_ports=None, - http3_ports=None, - ssl_cert='', - ca_cert='', - verbose=True, - other_args='', - context=None): - """ - Configure the provided process to run a verifier-server command. - - Args - obj: An object that has a RunDirectory attribute. - - process: (Process) The test process to configure for verifier-server. - - For all other arguments, see MakeVerifierServerProcess. - """ - # Create the directory we will setup for the replay binaries to run under. - server_dir = os.path.join(obj.RunDirectory, name) - - # Create a link of the binary to the rundir and set up the PATH variable. - # This will be the verifier-server that gets run. - bin_dir = 'bin' - process.Env['SERVER_BIN_PATH'] = bin_dir - bin_path = os.path.join(server_dir, bin_dir) - process.Env['PATH'] = bin_path + os.pathsep + process.ComposeEnv()['PATH'] - process.Setup.Copy(process.Variables.VerifierBinPath, bin_path, CopyLogic.SoftFiles) - - # Configure the verifier-server command line arguments. - command = "verifier-server run " - - if http_ports is None: - get_port(process, "http_port") - http_ports = [process.Variables.http_port] - else: - process.Variables['http_port'] = http_ports[0] - - if len(http_ports) > 0: - command += "--listen-http " - command += create_address_argument(http_ports) - command += " " - - if https_ports is None: - get_port(process, "https_port") - https_ports = [process.Variables.https_port] - else: - process.Variables['https_port'] = https_ports[0] - - if len(https_ports) > 0: - command += '--listen-https ' - command += create_address_argument(https_ports) - command += " " - - if http3_ports is None: - get_port(process, "http3_port") - http3_ports = [process.Variables.http3_port] - else: - process.Variables['http3_port'] = http3_ports[0] - - # Add in the following when Proxy Verifier server-side HTTP/3 is supported. - # if len(http3_ports) > 0: - # command += '--listen-http3 ' - # command += create_address_argument(http3_ports) - # command += " " - # - # qlog_basename = "qlog_directory" - # qlog_dir = os.path.join(server_dir, qlog_basename) - # command += f"--qlog-dir {qlog_dir} " - # obj.Variables['qlog_dir'] = qlog_dir - - if https_ports or http3_ports: - if ssl_cert == '': - ssl_cert = os.path.join(obj.Variables["AtsTestToolsDir"], "proxy-verifier", "ssl", "server.pem") - - if not os.path.isfile(ssl_cert): - raise ValueError("Tried to use '{}' for --server-cert, but it is not " - "a valid file.".format(ssl_cert)) - if ssl_cert: - run_ssl_cert = os.path.join(server_dir, os.path.basename(ssl_cert)) - process.Setup.Copy(ssl_cert, run_ssl_cert, CopyLogic.SoftFiles) - command += ' --server-cert "{}" '.format(run_ssl_cert) - - tls_secrets_log_filename = "tls_secrets.txt" - tls_secrets_log_path = os.path.join(server_dir, tls_secrets_log_filename) - command += f"--tls-secrets-log-file {tls_secrets_log_path} " - obj.Variables['tls_secrets_log_path'] = tls_secrets_log_path - - if ca_cert == '': - ca_cert = os.path.join(obj.Variables["AtsTestToolsDir"], "proxy-verifier", "ssl", "ca.pem") - - if not os.path.isfile(ca_cert): - raise ValueError("Tried to use '{}' for --ca-certs, but it is not " - "a valid file.".format(ca_cert)) - if ca_cert: - run_ca_cert = os.path.join(server_dir, os.path.basename(ca_cert)) - process.Setup.Copy(ca_cert, run_ca_cert, CopyLogic.SoftFiles) - command += ' --ca-certs "{}" '.format(run_ca_cert) - - if replay_path: - if context: - # replace the passed replay file with the new one generated using the passed - # context - replay_path = substitute_context_in_replay_file(process, replay_path, context) - - run_replay_path = os.path.join(server_dir, os.path.basename(replay_path)) - # Create a copy of the replay directory in the run directory. - process.Setup.Copy(replay_path, run_replay_path, CopyLogic.SoftFiles) - command += "{} ".format(run_replay_path) - - if verbose: - command += ' --verbose diag ' - - if other_args: - command += "{} ".format(other_args) - process.Command = command - - process.Ready = When.PortOpenv4(process.Variables.http_port) - process.ReturnCode = 0 - - process.Streams.stdout = Testers.ExcludesExpression("Violation", "There should be no Proxy Verifier violation errors.") - - -def MakeVerifierServerProcess( - test, - name, - replay_path, - http_ports=None, - https_ports=None, - http3_ports=None, - ssl_cert='', - ca_cert='', - verbose=True, - other_args='', - context=None): - """ - Create a verifier-server process for the Test. - - Args: - test: (Test) The Test object being updated with the server process. - - name: (str) The name to apply to this particular verifier-server instance. - Individual verifier-server processes must have unique names to - distinguish between them. - - replay_path: (path) The file or directory containing the traffic replay - specification. - - http_ports: (list of ints) The set of HTTP ports to listen on. If not - passed in, a single open port will be selected. - - https_ports: (list of ints) The set of HTTPS ports to listen on. If not - passed in, a single open port will be selected. - - http3_ports: (list of ints) The set of HTTP3 ports to listen on. If not - passed in, a single open port will be selected. - - ssl_cert: (path) The location of the cert for HTTPS encryption. If this - is not provided and stock ssl_cert will be used. - - ca_cert: (path) The location of the CA for HTTPS encryption. If this - is not provided a stock CA will be used. - - verbose: (bool) Whether to enable debug level logging. - - other_args: (str) Any other arbitrary options to pass to verifier-server. - - context: Any dictionary-like object with keys that match the placeholders - in the replay file. - Template strings support $-based substitutions in the - replay file. - You can refer to https://docs.python.org/3/library/string.html#template-strings - for more information how to add template strings to the replay file. - Raises: - ValueError if https_ports is non-empty and a valid ssl_cert could not - be derived. - If context substitution is expected to be done but a directory is passed as a - replay_path. - OSError in case of any issues related to I/O error, ie: File Not found for the replay - file when a context substitution is expected. - KeyError if placeholders are missing from the mapping between context and the replay file. - """ - server = test.Processes.Process(name) - _configure_server( - test, server, name, replay_path, http_ports, https_ports, http3_ports, ssl_cert, ca_cert, verbose, other_args, context) - return server - - -def AddVerifierServerProcess( - run, - name, - replay_path, - http_ports=None, - https_ports=None, - http3_ports=None, - ssl_cert='', - ca_cert='', - verbose=True, - other_args='', - context=None): - """ - Create a verifier-server process and configure it for the given TestRun. - - Args: - run: (TestRun) The test run to which the server process is added. - - For all other arguments, see MakeVerifierServerProcess. - - Returns: - The newly created server process. - """ - - server = run.Processes.Process(name) - _configure_server( - run, server, name, replay_path, http_ports, https_ports, http3_ports, ssl_cert, ca_cert, verbose, other_args, context) - - client = run.Processes.Default - client.StartBefore(server) - - return server - - -########################################################################## -ExtendTest(MakeVerifierServerProcess, name="MakeVerifierServerProcess") -ExtendTestRun(AddVerifierServerProcess, name="AddVerifierServerProcess") diff --git a/tests/gold_tests/autest-site/when.test.ext b/tests/gold_tests/autest-site/when.test.ext deleted file mode 100644 index 8975d27cb18..00000000000 --- a/tests/gold_tests/autest-site/when.test.ext +++ /dev/null @@ -1,101 +0,0 @@ -''' -When extensions. -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -from autest.api import AddWhenFunction -import hosts.output as host -import os -import re - - -def FileContains(haystack, needle, desired_count=1): - """ - Return whether the file haystack contains the string needle. - - Args: - haystack (str): The path to the file to be inspected. - needle (str): The content to look for in haystack. This can be a - regular expression which will be used in Python's re.search - function. - desired_count (int): How many times the caller desires to see needle in - haystack before considering the When condition fulfilled. - - Returns: - True if the haystack exists as a file and contains needle, False - otherwise. - """ - - if desired_count < 0: - raise ValueError("Cannot pass a negative desired_count value to FileContains.") - if desired_count == 0: - raise ValueError("Cannot pass a desired_count of 0 to FileContains.") - - if not os.path.exists(haystack): - host.WriteDebug( - ['FileContains', 'when'], "Testing for file content '{0}' in file '{1}': file does not exist".format(needle, haystack)) - return False - - needle_regex = re.compile(needle) - with open(haystack) as f: - needle_seen_count = 0 - line_count = 0 - for line in f: - line_count += 1 - if needle_regex.search(line): - host.WriteDebug( - ['FileContains', 'when'], "Found '{0}' in file '{1}' in line: '{2}', line number: {3}".format( - needle, haystack, line.rstrip(), line_count)) - needle_seen_count += 1 - - if needle_seen_count >= desired_count: - host.WriteDebug( - ['FileContains', 'when'], "Testing for file content '{0}' in file '{1}', " - "successfully found it the desired {2} times".format(needle, haystack, needle_seen_count)) - return True - - host.WriteDebug( - ['FileContains', 'when'], "Testing for file content '{0}' in file '{1}', only seen {2} " - "out of the desired {3} times".format(needle, haystack, needle_seen_count, desired_count)) - - return False - - -def AddAwaitFileContainsTestRun(test, name, file_path, needle, desired_count=1) -> 'TestRun': - '''Add a test run to await upon the presence of a string in a file. - - :param test: The Test object to which the TestRun is added. - :param name: The name of the TestRun. - :param file_path: The path to the file to be inspected. - :param needle: The content to look for in the file. This can be a regular - expression which will be used in Python's re.search function. - :param desired_count: How many times the caller desires to see needle in - the file before considering the TestRun successful. The default is 1. - :return: The TestRun object that was added to the Test. - ''' - tr = test.AddTestRun(name) - p = tr.Processes.Default - p.Command = f'echo waiting for file content in {file_path}' - await_process = tr.Processes.Process('await', 'sleep 60') - await_process.Ready = When.FileContains(file_path, needle, desired_count) - await_process.StartupTimeout = 30 - p.StartBefore(await_process) - return tr - - -AddWhenFunction(FileContains) -ExtendTest(AddAwaitFileContainsTestRun, name="AddAwaitFileContainsTestRun") diff --git a/tests/gold_tests/bad_http_fmt/bad_http_fmt.test.py b/tests/gold_tests/bad_http_fmt/bad_http_fmt.test.py deleted file mode 100644 index 869894ea153..00000000000 --- a/tests/gold_tests/bad_http_fmt/bad_http_fmt.test.py +++ /dev/null @@ -1,186 +0,0 @@ -''' -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -Test.Summary = ''' -Test requests with bad HTTP formats -''' - -ArbitraryTimestamp = '12345678' - -random_path = "/sdfsdf" - -random_method = "xyzxyz" - -ts = Test.MakeATSProcess("ts") - -Test.GetTcpPort("upstream_port") - -method_server = Test.Processes.Process( - "method-server", "bash -c '" + Test.TestDirectory + f"/method-server.sh {Test.Variables.upstream_port} outserver'") - -server = Test.MakeOriginServer("server", ssl=False) -request_header = { - "headers": "GET {}/0 HTTP/1.1\r\nX-Req-Id: 0\r\nHost: example.com\r\n\r\n".format(random_path), - "timestamp": "1469733493.993", - "body": "" -} -response_header = { - "headers": "HTTP/1.1 200 OK\r\nX-Resp-Id: 1\r\nConnection: close\r\n\r\n", - "timestamp": "1469733493.993", - "body": "" -} -server.addResponse("sessionlog.json", request_header, response_header) - -request_header = { - "headers": "GET {}/1 HTTP/1.1\r\nX-Req-Id: 1\r\nHost: example.com\r\n\r\n".format(random_path), - "timestamp": "1469733493.993", - "body": "" -} -response_header = { - "headers": "HTTP/1.1 200 OK\r\nX-Resp-Id: 2\r\nConnection: close\r\n\r\n", - "timestamp": "1469733493.993", - "body": "" -} -server.addResponse("sessionlog.json", request_header, response_header) - -request_header = { - "headers": "GET /example/1 HTTP/1.1\r\nX-Req-Id: 6\r\nHost: example.com\r\n\r\n", - "timestamp": "1469733493.993", - "body": "" -} -response_header = { - "headers": "HTTP/1.1 200 OK\r\nX-Resp-Id: 3\r\nConnection: close\r\n\r\n", - "timestamp": "1469733493.993", - "body": "" -} -server.addResponse("sessionlog.json", request_header, response_header) - -request_header = { - "headers": "GET /example/2 HTTP/1.1\r\nX-Req-Id: 7\r\nHost: example.com\r\n\r\n", - "timestamp": "1469733493.993", - "body": "" -} -response_header = { - "headers": "HTTP/1.1 200 OK\r\nX-Resp-Id: 4\r\nConnection: close\r\n\r\n", - "timestamp": "1469733493.993", - "body": "" -} -server.addResponse("sessionlog.json", request_header, response_header) - -ts.Disk.records_config.update( - { - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'http|dns', - 'proxy.config.url_remap.remap_required': 0, - 'proxy.config.http.strict_uri_parsing': 1, - 'proxy.config.http.cache.http': 0, - }) - -# Allow random_method for 128.0.0.1, do not allow it for ::1. -# -ts.Disk.ip_allow_yaml.AddLines( - [ - 'ip_allow:', - ' - apply: in', - ' ip_addrs: 127.0.0.1', - ' action: allow', - ' methods:', - ' - GET', - f' - {random_method}', - ]) -ts.Disk.ip_allow_yaml.AddLines([ - ' - apply: in', - ' ip_addrs: ::1', - ' action: allow', - ' methods:', - ' - GET', -]) - -ts.Disk.remap_config.AddLine('map /add-method http://127.0.0.1:{0}/'.format(Test.Variables.upstream_port)) -ts.Disk.remap_config.AddLine('map / http://127.0.0.1:{0}/'.format(server.Variables.Port)) - -# Even if the request from the client is HTTP version 1.0, ATS's request to server will be HTTP version 1.1. -# -tr = Test.AddTestRun("success-1.0") -tr.Processes.Default.StartBefore(ts) -tr.Processes.Default.StartBefore(server) -tr.Processes.Default.Command = ( - f"printf 'GET {random_path}/0HTTP/1.0\r\n" + "Host: example.com\r\n" + "Connection: close\r\n" + "X-Req-Id: 1\r\n\r\n'" + - f" | nc localhost {ts.Variables.port} >> client.log" + " ; echo '======' >> client.log") -tr.Processes.Default.ReturnCode = 0 - -tr = Test.AddTestRun("success-1.1") -tr.Processes.Default.Command = ( - f"printf 'GET {random_path}/1HTTP/1.1\r\n" + "Host: example.com\r\n" + "Connection: close\r\n" + "X-Req-Id: 2\r\n\r\n'" + - f" | nc localhost {ts.Variables.port} >> client.log" + " ; echo '======' >> client.log") -tr.Processes.Default.ReturnCode = 0 - -tr = Test.AddTestRun("invalid-url-line") -tr.Processes.Default.Command = ( - f"printf 'GET {random_path}> client.log" + " ; echo '======' >> client.log") -tr.Processes.Default.ReturnCode = 0 - -tr = Test.AddTestRun("bad-http-version") -tr.Processes.Default.Command = ( - f"printf 'GET {random_path} HTTP/1.2\r\n" + "Host: example.com\r\n" + "Connection: close\r\n" + "X-Req-Id: 3\r\n\r\n'" + - f" | nc localhost {ts.Variables.port} >> client.log" + " ; echo '======' >> client.log") -tr.Processes.Default.ReturnCode = 0 - -tr = Test.AddTestRun("bad-request-maybe-http-version") -tr.Processes.Default.Command = ( - f"printf 'GET {random_path} HTTP/0.9\r\n" + "Host: example.com\r\n" + "Connection: close\r\n" + "X-Req-Id: 4\r\n\r\n'" + - f" | nc localhost {ts.Variables.port} >> client.log" + " ; echo '======' >> client.log") -tr.Processes.Default.ReturnCode = 0 - -tr = Test.AddTestRun("same-as-last-case?") -tr.Processes.Default.Command = ( - fr"printf 'GET {random_path} HTTP/0.9\r\n" + "Host: example.com\r\n" + "Connection: close\r\n" + "X-Req-Id: 5\r\n\r\n'" + - fr"| nc localhost {ts.Variables.port} >> client.log" + " ; echo '======' >> client.log") -tr.Processes.Default.ReturnCode = 0 - -tr = Test.AddTestRun("allowed-random-method") -tr.Processes.Default.StartBefore(method_server) -tr.Processes.Default.Command = ( - fr"printf '{random_method} /add-method HTTP/1.1\r\n" + "Host: example.com\r\n" + "Connection: close\r\n" + - "X-Req-Id: 6\r\n\r\n'" + f" | nc localhost {ts.Variables.port} >> client.log" + " ; echo '======' >> client.log") -tr.Processes.Default.ReturnCode = 0 - -tr = Test.AddTestRun("valid-ipv6") -tr.Processes.Default.Command = ( - f"printf 'GET /example/2 HTTP/1.1\r\n" + "Host: example.com\r\n" + "Connection: close\r\n" + "X-Req-Id: 7\r\n\r\n'" + - f" | nc ::1 {ts.Variables.portv6} >> client.log" + " ; echo '======' >> client.log") -tr.Processes.Default.ReturnCode = 0 - -tr = Test.AddTestRun("unallowed-method-v6") -tr.Processes.Default.Command = ( - f"printf '{random_method} /example/1 HTTP/1.1\r\n" + "Host: example.com\r\n" + "Connection: close\r\n\r\n'" + - f" | nc ::1 {ts.Variables.portv6} >> client.log" + " ; echo '======' >> client.log") -tr.Processes.Default.ReturnCode = 0 - -tr = Test.AddTestRun() -tr.Processes.Default.Command = "grep -e '^===' -e '^HTTP/' -e 'X-Resp-Id:' -e '' client.log" -tr.Processes.Default.Streams.stdout = 'client.gold' -tr.Processes.Default.ReturnCode = 0 - -server.Streams.All += Testers.ContainsExpression("Serving GET /sdfsdf/0... Finished", "Served 1") -server.Streams.All += Testers.ContainsExpression("Serving GET /sdfsdf/1... Finished", "Served 2") -server.Streams.All += Testers.ContainsExpression("Serving GET /example/2... Finished", "Served 3") - -outserver = Test.Disk.File("outserver") -outserver.Content = "server.gold" diff --git a/tests/gold_tests/basic/config.test.py b/tests/gold_tests/basic/config.test.py deleted file mode 100644 index c78769465c7..00000000000 --- a/tests/gold_tests/basic/config.test.py +++ /dev/null @@ -1,30 +0,0 @@ -''' -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -Test.Summary = "Test start up of Traffic server with configuration modification of starting port" - -ts = Test.MakeATSProcess("ts") -ts.Disk.records_config.update({ - 'proxy.config.http.server_ports': str(ts.Variables.port) + f" {ts.Variables.uds_path}", -}) -ts.Ready = When.PortOpen(ts.Variables.port) -t = Test.AddTestRun("Test traffic server started properly") -t.Processes.Default.StartBefore(ts) -t.MakeCurlCommand("127.0.0.1:{port}".format(port=ts.Variables.port), ts=ts) -t.ReturnCode = 0 -t.StillRunningAfter = ts diff --git a/tests/gold_tests/basic/copy_config.test.py b/tests/gold_tests/basic/copy_config.test.py deleted file mode 100644 index 38961f8a851..00000000000 --- a/tests/gold_tests/basic/copy_config.test.py +++ /dev/null @@ -1,48 +0,0 @@ -''' -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -Test.Summary = "Test start up of Traffic server with configuration modification of starting port of different servers at the same time" - -# set up some ATS processes with dynamic port selection -ts1 = Test.MakeATSProcess("ts1") -ts1.Disk.records_config.update({ - 'proxy.config.http.server_ports': str(ts1.Variables.port) + f" {ts1.Variables.uds_path}", -}) -ts1.Ready = When.PortOpen(ts1.Variables.port) - -ts2 = Test.MakeATSProcess("ts2", enable_uds=False) -ts2.Disk.records_config.update({ - 'proxy.config.http.server_ports': str(ts2.Variables.port), -}) -ts2.Ready = When.PortOpen(ts2.Variables.port) - -# setup a testrun -t = Test.AddTestRun("Talk to ts1") -t.Processes.Default.StartBefore(ts1) -t.Processes.Default.StartBefore(ts2) -t.MakeCurlCommand("127.0.0.1:{port}".format(port=ts1.Variables.port), ts=ts1) -t.ReturnCode = 0 -t.StillRunningAfter = ts1 -t.StillRunningAfter += ts2 - -# setup a testrun -t = Test.AddTestRun("Talk to ts2") -t.MakeCurlCommandMulti("{{curl_base}} 127.0.0.1:{port}".format(port=ts2.Variables.port), ts=ts2) -t.ReturnCode = 0 -t.StillRunningAfter = ts1 -t.StillRunningAfter += ts2 diff --git a/tests/gold_tests/basic/copy_config2.test.py b/tests/gold_tests/basic/copy_config2.test.py deleted file mode 100644 index 68ecdbba682..00000000000 --- a/tests/gold_tests/basic/copy_config2.test.py +++ /dev/null @@ -1,43 +0,0 @@ -''' -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -Test.Summary = "Test start up of Traffic server with generated ports of more than one servers at the same time" - -# set up some ATS processes -ts1 = Test.MakeATSProcess("ts1") -ts2 = Test.MakeATSProcess("ts2") - -# setup a testrun -t = Test.AddTestRun("Talk to ts1") -t.StillRunningAfter = ts1 -t.StillRunningAfter += ts2 -p = t.Processes.Default -t.MakeCurlCommand("127.0.0.1:{0}".format(ts1.Variables.port), ts=ts1) -p.ReturnCode = 0 -p.StartBefore(Test.Processes.ts1) -p.StartBefore(Test.Processes.ts2) - -# setup a testrun -t = Test.AddTestRun("Talk to ts2") -t.StillRunningBefore = ts1 -t.StillRunningBefore += ts2 -t.StillRunningAfter = ts1 -t.StillRunningAfter += ts2 -p = t.Processes.Default -t.MakeCurlCommand("127.0.0.1:{0}".format(ts2.Variables.port), ts=ts2) -p.ReturnCode = 0 diff --git a/tests/gold_tests/basic/deny0.test.py b/tests/gold_tests/basic/deny0.test.py deleted file mode 100644 index 84d3aca7429..00000000000 --- a/tests/gold_tests/basic/deny0.test.py +++ /dev/null @@ -1,113 +0,0 @@ -''' -Test that Trafficserver rejects requests for host 0 -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import os -import sys - -Test.Summary = ''' -Test that Trafficserver rejects requests for host 0 -''' - -Test.ContinueOnFail = True - -HOST1 = 'redirect.test' - -redirect_serv = Test.MakeOriginServer("redirect_serv", ip='0.0.0.0') - -dns = Test.MakeDNServer("dns") -dns.addRecords(records={HOST1: ['127.0.0.1']}) - -ts = Test.MakeATSProcess("ts", enable_cache=False) -ts.Disk.records_config.update( - { - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'http|dns|redirect', - 'proxy.config.http.number_of_redirections': 1, - 'proxy.config.dns.nameservers': '127.0.0.1:{0}'.format(dns.Variables.Port), - 'proxy.config.dns.resolv_conf': 'NULL', - 'proxy.config.url_remap.remap_required': 0 # need this so the domain gets a chance to be evaluated through DNS - }) - -Test.Setup.Copy(os.path.join(Test.Variables.AtsTestToolsDir, 'tcp_client.py')) - -data_dirname = 'generated_test_data' -data_path = os.path.join(Test.TestDirectory, data_dirname) -os.makedirs(data_path, exist_ok=True) -gold_filepath = os.path.join(data_path, 'deny0.gold') -with open(os.path.join(data_path, 'deny0.gold'), 'w') as f: - f.write('HTTP/1.1 400 Bad Destination Address\r\n') - -isFirstTest = True - - -def buildMetaTest(testName, requestString): - tr = Test.AddTestRun(testName) - global isFirstTest - if isFirstTest: - isFirstTest = False - tr.Processes.Default.StartBefore(ts) - tr.Processes.Default.StartBefore(redirect_serv, ready=When.PortOpen(redirect_serv.Variables.Port)) - tr.Processes.Default.StartBefore(dns) - - requestCommandPath = os.path.join(data_path, tr.Name) - with open(requestCommandPath, 'w') as f: - f.write(requestString) - tr.Processes.Default.Command = \ - (f"{sys.executable} tcp_client.py 127.0.0.1 {ts.Variables.port} " - f"{requestCommandPath} | head -1") - tr.ReturnCode = 0 - tr.Processes.Default.Streams.stdout = gold_filepath - tr.StillRunningAfter = ts - tr.StillRunningAfter = redirect_serv - tr.StillRunningAfter = dns - - -buildMetaTest( - 'RejectInterfaceAnyIpv4', 'GET / HTTP/1.1\r\nHost: 0:{port}\r\nConnection: close\r\n\r\n'.format(port=ts.Variables.port)) - -buildMetaTest( - 'RejectInterfaceAnyIpv6', 'GET / HTTP/1.1\r\nHost: [::]:{port}\r\nConnection: close\r\n\r\n'.format(port=ts.Variables.portv6)) - -# Sets up redirect to IPv4 ANY address -redirect_request_header = {"headers": "GET /redirect-0 HTTP/1.1\r\nHost: *\r\n\r\n", "timestamp": "5678", "body": ""} -redirect_response_header = { - "headers": "HTTP/1.1 302 Found\r\nLocation: http://0:{0}/\r\nConnection: close\r\n\r\n".format(ts.Variables.port), - "timestamp": "5678", - "body": "" -} -redirect_serv.addResponse("sessionfile.log", redirect_request_header, redirect_response_header) - -buildMetaTest( - 'RejectRedirectToInterfaceAnyIpv4', - 'GET /redirect-0 HTTP/1.1\r\nHost: {host}:{port}\r\n\r\n'.format(host=HOST1, port=redirect_serv.Variables.Port)) - -# Sets up redirect to IPv6 ANY address -redirect_request_header = {"headers": "GET /redirect-0v6 HTTP/1.1\r\nHost: *\r\n\r\n", "timestamp": "5678", "body": ""} -redirect_response_header = { - "headers": "HTTP/1.1 302 Found\r\nLocation: http://[::]:{0}/\r\nConnection: close\r\n\r\n".format(ts.Variables.port), - "timestamp": "5678", - "body": "" -} -redirect_serv.addResponse("sessionfile.log", redirect_request_header, redirect_response_header) - -buildMetaTest( - 'RejectRedirectToInterfaceAnyIpv6', - 'GET /redirect-0v6 HTTP/1.1\r\nHost: {host}:{port}\r\n\r\n'.format(host=HOST1, port=redirect_serv.Variables.Port)) - -Test.Setup.Copy(data_path) diff --git a/tests/gold_tests/bigobj/bigobj.test.py b/tests/gold_tests/bigobj/bigobj.test.py deleted file mode 100644 index d9430750721..00000000000 --- a/tests/gold_tests/bigobj/bigobj.test.py +++ /dev/null @@ -1,149 +0,0 @@ -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import os - -Test.Summary = ''' -Test PUSHing an object into the cache and the GETting it with a few variations on the client connection protocol. -''' - -# NOTE: You can also use this to test client-side communication when GET-ing very large (multi-GB) objects -# by increasing the value of the obj_kilobytes variable below. (But do not increase it on any shared branch -# that we do CI runs on.) - -Test.SkipUnless(Condition.HasCurlFeature('http2')) - -ts = Test.MakeATSProcess("ts1", enable_tls=True) -ts.addDefaultSSLFiles() - -ts.Disk.records_config.update( - { - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'http|dns|cache', - 'proxy.config.http.cache.required_headers': 0, # No required headers for caching - 'proxy.config.http.push_method_enabled': 1, - 'proxy.config.proxy_name': 'Poxy_Proxy', # This will be the server name. - 'proxy.config.ssl.server.cert.path': ts.Variables.SSLDir, - 'proxy.config.ssl.server.private_key.path': ts.Variables.SSLDir, - 'proxy.config.url_remap.remap_required': 0 - }) - -ts.Disk.ssl_multicert_yaml.AddLines( - """ -ssl_multicert: - - dest_ip: "*" - ssl_cert_name: server.pem - ssl_key_name: server.key -""".split("\n")) - -ts.Disk.remap_config.AddLine(f'map https://localhost:{ts.Variables.ssl_port} http://localhost:{ts.Variables.port}') -ts.Disk.remap_config.AddLine(f'map https://localhost:{ts.Variables.ssl_portv6} http://localhost:{ts.Variables.port}') - -# Size of object to get. (NOTE: If you increase this significantly you may also have to increase cache -# capacity in tests/gold_tests/autest-size/min_cfg/storage.yaml. Also, for very large objects, if -# proxy.config.diags.debug.enabled is 1, the PUSH request will timeout and fail.) -# -obj_kilobytes = 10 * 1024 -obj_bytes = obj_kilobytes * 10 -header = "HTTP/1.1 200 OK\r\nContent-length: {}\r\n\r\n".format(obj_bytes) - - -def create_pushfile(): - f = open(Test.RunDirectory + "/objfile", "w") - f.write(header) - f.write("x" * obj_bytes) - f.close() - return True - - -ipv4flag = "" -if not Condition.CurlUsingUnixDomainSocket(): - ipv4flag = "--ipv4" - -tr = Test.AddTestRun("PUSH an object to the cache") -# Delay on readiness of TS IPv4 ssl port -tr.Processes.Default.StartBefore(ts, ready=lambda: create_pushfile()) -# Put object with URL http://localhost/bigobj in cache using PUSH request. -tr.MakeCurlCommand( - "-v -H 'Content-Type: application/octet-stream' --data-binary @{}/objfile -X PUSH http://localhost:{}/bigobj -H 'Content-Length:{}'" - .format(Test.RunDirectory, ts.Variables.port, - len(header) + obj_bytes), - ts=ts) -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Streams.All = Testers.ContainsExpression("HTTP/1.1 201 Created", "The PUSH request should have succeeded") - -tr = Test.AddTestRun("GET bigobj: cleartext, HTTP/1.1, IPv4") -tr.MakeCurlCommand(f'--verbose {ipv4flag} --http1.1 http://localhost:{ts.Variables.port}/bigobj', ts=ts) -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Streams.All = Testers.ContainsExpression("HTTP/1.1 200 OK", "Should fetch pushed object") -tr.Processes.Default.Streams.All = Testers.ContainsExpression("Content-length: 102400", "Content size should be accurate") - -if not Condition.CurlUsingUnixDomainSocket(): - tr = Test.AddTestRun("GET bigobj: TLS, HTTP/1.1, IPv4") - tr.MakeCurlCommand(f'--verbose --ipv4 --http1.1 --insecure https://localhost:{ts.Variables.ssl_port}/bigobj', ts=ts) - tr.Processes.Default.ReturnCode = 0 - tr.Processes.Default.Streams.All = Testers.ContainsExpression("HTTP/1.1 200 OK", "Should fetch pushed object") - tr.Processes.Default.Streams.All = Testers.ContainsExpression("Content-length: 102400", "Content size should be accurate") - - tr = Test.AddTestRun("GET bigobj: TLS, HTTP/2, IPv4") - tr.MakeCurlCommand(f'--verbose --ipv4 --http2 --insecure https://localhost:{ts.Variables.ssl_port}/bigobj', ts=ts) - tr.Processes.Default.ReturnCode = 0 - tr.Processes.Default.Streams.All = Testers.ContainsExpression("HTTP/2 200", "Should fetch pushed object") - tr.Processes.Default.Streams.All = Testers.ContainsExpression("content-length: 102400", "Content size should be accurate") - - tr = Test.AddTestRun("GET bigobj: TLS, HTTP/2, IPv6") - tr.MakeCurlCommand(f'--verbose --ipv6 --http2 --insecure https://localhost:{ts.Variables.ssl_portv6}/bigobj', ts=ts) - tr.Processes.Default.ReturnCode = 0 - tr.Processes.Default.Streams.All = Testers.ContainsExpression("HTTP/2 200", "Should fetch pushed object") - tr.Processes.Default.Streams.All = Testers.ContainsExpression("content-length: 102400", "Content size should be accurate") - -# Verify that PUSH requests are rejected when push_method_enabled is 0 (the -# default configuration). -ts = Test.MakeATSProcess("ts2", enable_tls=True) -ts.addDefaultSSLFiles() - -ts.Disk.records_config.update( - { - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'http|dns|cache', - 'proxy.config.http.cache.required_headers': 0, # No required headers for caching - 'proxy.config.proxy_name': 'Poxy_Proxy', # This will be the server name. - 'proxy.config.ssl.server.cert.path': ts.Variables.SSLDir, - 'proxy.config.ssl.server.private_key.path': ts.Variables.SSLDir, - 'proxy.config.url_remap.remap_required': 0 - }) - -ts.Disk.ssl_multicert_yaml.AddLines( - """ -ssl_multicert: - - dest_ip: "*" - ssl_cert_name: server.pem - ssl_key_name: server.key -""".split("\n")) - -ts.Disk.remap_config.AddLine(f'map https://localhost:{ts.Variables.ssl_port} http://localhost:{ts.Variables.port}') -ts.Disk.remap_config.AddLine(f'map https://localhost:{ts.Variables.ssl_portv6} http://localhost:{ts.Variables.port}') - -tr = Test.AddTestRun("PUSH request is rejected when push_method_enabled is 0") -tr.Processes.Default.StartBefore(ts) -tr.MakeCurlCommand( - "-v -H 'Content-Type: application/octet-stream' --data-binary @{}/objfile -X PUSH http://localhost:{}/bigobj -H 'Content-Length:{}'" - .format(Test.RunDirectory, ts.Variables.port, - len(header) + obj_bytes), - ts=ts) -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Streams.All = Testers.ContainsExpression( - "403 Access Denied", "The PUSH request should have received a 403 response.") diff --git a/tests/gold_tests/body_factory/http204_response.test.py b/tests/gold_tests/body_factory/http204_response.test.py deleted file mode 100644 index 7258b78895b..00000000000 --- a/tests/gold_tests/body_factory/http204_response.test.py +++ /dev/null @@ -1,86 +0,0 @@ -''' -Tests that 204 responses conform to rfc2616, unless custom templates override. -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import os -import sys - -Test.Summary = ''' -Tests that 204 responses conform to rfc2616, unless custom templates override. -''' - -ts = Test.MakeATSProcess("ts") -server = Test.MakeOriginServer("server") - -DEFAULT_204_HOST = 'www.default204.test' -CUSTOM_TEMPLATE_204_HOST = 'www.customtemplate204.test' - -ts.Disk.records_config.update({ - # enable domain specific body factory - 'proxy.config.body_factory.enable_customizations': 3, -}) - -# Create a template body for a 204. -body_factory_dir = ts.Variables.BODY_FACTORY_TEMPLATE_DIR -ts.Disk.File(os.path.join(body_factory_dir, 'default', CUSTOM_TEMPLATE_204_HOST + '_default')).\ - WriteOn( - """ - -Spec-breaking 204! - - - -

This is body content for a 204.

-
- - -Description: According to rfc7231 I should not have been sent to you! - -
- -""") - -regex_remap_conf_file = "maps.reg" - -ts.Disk.remap_config.AddLine( - 'map http://{0} http://127.0.0.1:{1} @plugin=regex_remap.so @pparam={2} @pparam=no-query-string @pparam=host'.format( - DEFAULT_204_HOST, server.Variables.Port, regex_remap_conf_file)) -ts.Disk.remap_config.AddLine( - 'map http://{0} http://127.0.0.1:{1} @plugin=regex_remap.so @pparam={2} @pparam=no-query-string @pparam=host @plugin=conf_remap.so @pparam=proxy.config.body_factory.template_base={0}' - .format(CUSTOM_TEMPLATE_204_HOST, server.Variables.Port, regex_remap_conf_file)) -ts.Disk.MakeConfigFile(regex_remap_conf_file).AddLine('//.*/ http://127.0.0.1:{0} @status=204'.format(server.Variables.Port)) - -Test.Setup.Copy(os.path.join(os.pardir, os.pardir, 'tools', 'tcp_client.py')) -Test.Setup.Copy('data') - -defaultTr = Test.AddTestRun("Test domain {0}".format(DEFAULT_204_HOST)) -defaultTr.Processes.Default.StartBefore(Test.Processes.ts) -defaultTr.StillRunningAfter = ts - -defaultTr.Processes.Default.Command = f"{sys.executable} tcp_client.py 127.0.0.1 {ts.Variables.port} data/{DEFAULT_204_HOST}_get.txt" -defaultTr.Processes.Default.TimeOut = 5 # seconds -defaultTr.Processes.Default.ReturnCode = 0 -defaultTr.Processes.Default.Streams.stdout = "gold/http-204.gold" - -customTemplateTr = Test.AddTestRun(f"Test domain {CUSTOM_TEMPLATE_204_HOST}") -customTemplateTr.StillRunningBefore = ts -customTemplateTr.StillRunningAfter = ts -customTemplateTr.Processes.Default.Command = f"{sys.executable} tcp_client.py 127.0.0.1 {ts.Variables.port} data/{CUSTOM_TEMPLATE_204_HOST}_get.txt" -customTemplateTr.Processes.Default.TimeOut = 5 # seconds -customTemplateTr.Processes.Default.ReturnCode = 0 -customTemplateTr.Processes.Default.Streams.stdout = "gold/http-204-custom.gold" diff --git a/tests/gold_tests/body_factory/http204_response_plugin.test.py b/tests/gold_tests/body_factory/http204_response_plugin.test.py deleted file mode 100644 index 72ba642c908..00000000000 --- a/tests/gold_tests/body_factory/http204_response_plugin.test.py +++ /dev/null @@ -1,51 +0,0 @@ -''' -Tests that plugins may break HTTP by sending 204 response bodies -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import os -import sys - -Test.Summary = ''' -Tests that plugins may break HTTP by sending 204 response bodies -''' - -ts = Test.MakeATSProcess("ts") -server = Test.MakeOriginServer("server") - -CUSTOM_PLUGIN_204_HOST = 'www.customplugin204.test' - -regex_remap_conf_file = "maps.reg" - -ts.Disk.remap_config.AddLine( - f'map http://{CUSTOM_PLUGIN_204_HOST} http://127.0.0.1:{server.Variables.Port} @plugin=regex_remap.so @pparam={regex_remap_conf_file} @pparam=no-query-string @pparam=host' -) -ts.Disk.MakeConfigFile(regex_remap_conf_file).AddLine('//.*/ http://donotcare.test @status=204') - -Test.PrepareTestPlugin(os.path.join(Test.Variables.AtsTestPluginsDir, 'custom204plugin.so'), ts) - -Test.Setup.Copy(os.path.join(os.pardir, os.pardir, 'tools', 'tcp_client.py')) -Test.Setup.Copy('data') - -tr = Test.AddTestRun("Test domain {0}".format(CUSTOM_PLUGIN_204_HOST)) -tr.Processes.Default.StartBefore(Test.Processes.ts) -tr.StillRunningAfter = ts - -tr.Processes.Default.Command = f"{sys.executable} tcp_client.py 127.0.0.1 {ts.Variables.port} data/{CUSTOM_PLUGIN_204_HOST}_get.txt" -tr.Processes.Default.TimeOut = 5 # seconds -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Streams.stdout = "gold/http-204-custom-plugin.gold" diff --git a/tests/gold_tests/body_factory/http304_response.test.py b/tests/gold_tests/body_factory/http304_response.test.py deleted file mode 100644 index b892d1ac2e8..00000000000 --- a/tests/gold_tests/body_factory/http304_response.test.py +++ /dev/null @@ -1,50 +0,0 @@ -''' -Tests 304 responses -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import os -import sys - -Test.Summary = ''' -Tests 304 responses -''' - -ts = Test.MakeATSProcess("ts") -server = Test.MakeOriginServer("server") - -DEFAULT_304_HOST = 'www.default304.test' - -regex_remap_conf_file = "maps.reg" - -ts.Disk.remap_config.AddLine( - 'map http://{0} http://127.0.0.1:{1} @plugin=regex_remap.so @pparam={2} @pparam=no-query-string @pparam=host'.format( - DEFAULT_304_HOST, server.Variables.Port, regex_remap_conf_file)) -ts.Disk.MakeConfigFile(regex_remap_conf_file).AddLine('//.*/ http://127.0.0.1:{0} @status=304'.format(server.Variables.Port)) - -Test.Setup.Copy(os.path.join(os.pardir, os.pardir, 'tools', 'tcp_client.py')) -Test.Setup.Copy('data') - -tr = Test.AddTestRun(f"Test domain {DEFAULT_304_HOST}") -tr.Processes.Default.StartBefore(Test.Processes.ts) -tr.StillRunningAfter = ts - -cmd_tpl = f"{sys.executable} tcp_client.py 127.0.0.1 {ts.Variables.port} data/{DEFAULT_304_HOST}_get.txt" -tr.Processes.Default.Command = cmd_tpl -tr.Processes.Default.TimeOut = 5 # seconds -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Streams.stdout = "gold/http-304.gold" diff --git a/tests/gold_tests/body_factory/http_head_no_origin.test.py b/tests/gold_tests/body_factory/http_head_no_origin.test.py deleted file mode 100644 index b3c47abc004..00000000000 --- a/tests/gold_tests/body_factory/http_head_no_origin.test.py +++ /dev/null @@ -1,42 +0,0 @@ -''' -Tests that HEAD requests return proper responses when origin fails -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import os -import sys - -Test.Summary = ''' -Tests that HEAD requests return proper responses when origin fails -''' - -ts = Test.MakeATSProcess("ts") -server = Test.MakeOriginServer("server") - -HOST = 'www.example.test' - -Test.Setup.Copy(os.path.join(os.pardir, os.pardir, 'tools', 'tcp_client.py')) -Test.Setup.Copy('data') - -tr = Test.AddTestRun("Test domain {0}".format(HOST)) -tr.Processes.Default.StartBefore(Test.Processes.ts) -tr.StillRunningAfter = ts - -tr.Processes.Default.Command = f"{sys.executable} tcp_client.py 127.0.0.1 {ts.Variables.port} data/{HOST}_head.txt" -tr.Processes.Default.TimeOut = 5 # seconds -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Streams.stdout = "gold/http-head-no-origin.gold" diff --git a/tests/gold_tests/body_factory/http_with_origin.test.py b/tests/gold_tests/body_factory/http_with_origin.test.py deleted file mode 100644 index 0b24fb5c588..00000000000 --- a/tests/gold_tests/body_factory/http_with_origin.test.py +++ /dev/null @@ -1,103 +0,0 @@ -''' -Tests that HEAD requests return proper responses -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import os -import sys - -Test.Summary = ''' -Tests that HEAD requests return proper responses -''' - -ts = Test.MakeATSProcess("ts") - -HOST = 'www.example.test' - -server = Test.MakeOriginServer("server") - -ts.Disk.remap_config.AddLine('map http://{0} http://127.0.0.1:{1}'.format(HOST, server.Variables.Port)) - -server.addResponse( - "sessionfile.log", { - "headers": "HEAD /head200 HTTP/1.1\r\nHost: {0}\r\n\r\n".format(HOST), - "timestamp": "1469733493.993", - "body": "" - }, { - "headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", - "timestamp": "1469733493.993", - "body": "This body should not be returned for a HEAD request." - }) - -server.addResponse( - "sessionfile.log", { - "headers": "GET /get200 HTTP/1.1\r\nHost: {0}\r\n\r\n".format(HOST), - "timestamp": "1469733493.993", - "body": "" - }, { - "headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", - "timestamp": "1469733493.993", - "body": "This body should be returned for a GET request." - }) - -server.addResponse( - "sessionfile.log", { - "headers": "GET /get304 HTTP/1.1\r\nHost: {0}\r\n\r\n".format(HOST), - "timestamp": "1469733493.993", - "body": "" - }, { - "headers": "HTTP/1.1 304 Not Modified\r\nConnection: close\r\n\r\n", - "timestamp": "1469733493.993", - "body": "" - }) - -Test.Setup.Copy(os.path.join(os.pardir, os.pardir, 'tools', 'tcp_client.py')) -Test.Setup.Copy('data') - -trhead200 = Test.AddTestRun("Test domain {0}".format(HOST)) -trhead200.Processes.Default.StartBefore(Test.Processes.ts) -trhead200.Processes.Default.StartBefore(server, ready=When.PortOpen(server.Variables.Port)) -trhead200.StillRunningAfter = ts -trhead200.StillRunningAfter = server - -trhead200.Processes.Default.Command = f"{sys.executable} tcp_client.py 127.0.0.1 {ts.Variables.port} data/{HOST}_head_200.txt" -trhead200.Processes.Default.TimeOut = 5 # seconds -trhead200.Processes.Default.ReturnCode = 0 -trhead200.Processes.Default.Streams.stdout = "gold/http-head-200.gold" - -trget200 = Test.AddTestRun("Test domain {0}".format(HOST)) -trget200.StillRunningBefore = ts -trget200.StillRunningBefore = server -trget200.StillRunningAfter = ts -trget200.StillRunningAfter = server - -trget200.Processes.Default.Command = f"{sys.executable} tcp_client.py 127.0.0.1 {ts.Variables.port} data/{HOST}_get_200.txt" -trget200.Processes.Default.TimeOut = 5 # seconds -trget200.Processes.Default.ReturnCode = 0 -trget200.Processes.Default.Streams.stdout = "gold/http-get-200.gold" - -trget304 = Test.AddTestRun("Test domain {0}".format(HOST)) -trget304.StillRunningBefore = ts -trget304.StillRunningBefore = server -trget304.StillRunningAfter = ts -trget304.StillRunningAfter = server - -cmd_tpl = f"{sys.executable} tcp_client.py 127.0.0.1 {ts.Variables.port} data/{HOST}_get_304.txt" -trget304.Processes.Default.Command = cmd_tpl -trget304.Processes.Default.TimeOut = 5 # seconds -trget304.Processes.Default.ReturnCode = 0 -trget304.Processes.Default.Streams.stdout = "gold/http-get-304.gold" diff --git a/tests/gold_tests/cache/background_fill.test.py b/tests/gold_tests/cache/background_fill.test.py deleted file mode 100644 index c9f5024f520..00000000000 --- a/tests/gold_tests/cache/background_fill.test.py +++ /dev/null @@ -1,189 +0,0 @@ -''' -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -from enum import Enum - -Test.Summary = 'Exercise Background Fill' -Test.SkipUnless(Condition.HasCurlFeature('http2'), Condition.HasProxyVerifierVersion('2.8.0')) -Test.ContinueOnFail = True - - -class BackgroundFillTest: - """ - https://docs.trafficserver.apache.org/en/latest/admin-guide/files/records.yaml.en.html#proxy-config-http-background-fill-completed-threshold - """ - - class State(Enum): - """ - State of process - """ - INIT = 0 - RUNNING = 1 - - def __init__(self): - self.state = self.State.INIT - self.ts = {} - self.__setupOriginServer() - self.__setupTS(['for_httpbin', 'for_pv']) - - def __setupOriginServer(self): - self.httpbin = Test.MakeHttpBinServer("httpbin") - self.pv_server = Test.MakeVerifierServerProcess("server0", "replay/bg_fill.yaml") - - def __setupTS(self, ts_names=['default']): - for name in ts_names: - self.ts[name] = Test.MakeATSProcess(name, select_ports=True, enable_tls=True, enable_cache=True) - - self.ts[name].addDefaultSSLFiles() - self.ts[name].Disk.ssl_multicert_yaml.AddLines( - """ -ssl_multicert: - - dest_ip: "*" - ssl_cert_name: server.pem - ssl_key_name: server.key -""".split("\n")) - - self.ts[name].Disk.records_config.update( - { - "proxy.config.http.server_ports": - f"{self.ts[name].Variables.port} {self.ts[name].Variables.ssl_port}:ssl {self.ts[name].Variables.uds_path}", - "proxy.config.http.background_fill_active_timeout": "0", - "proxy.config.http.background_fill_completed_threshold": "0.0", - "proxy.config.http.cache.required_headers": 0, # Force cache - "proxy.config.http.insert_response_via_str": 2, - 'proxy.config.http.server_session_sharing.pool': 'thread', - 'proxy.config.http.server_session_sharing.match': 'ip,sni,cert', - 'proxy.config.exec_thread.autoconfig.enabled': 0, - 'proxy.config.exec_thread.limit': 1, - 'proxy.config.ssl.server.cert.path': f"{self.ts[name].Variables.SSLDir}", - 'proxy.config.ssl.server.private_key.path': f"{self.ts[name].Variables.SSLDir}", - 'proxy.config.ssl.client.alpn_protocols': 'h2,http/1.1', - 'proxy.config.ssl.client.verify.server.policy': 'PERMISSIVE', - "proxy.config.diags.debug.enabled": 3, - "proxy.config.diags.debug.tags": "http", - }) - - self.ts[name].Disk.plugin_config.AddLine('xdebug.so --enable=x-cache') - - if name == 'for_httpbin' or name == 'default': - self.ts[name].Disk.remap_config.AddLines([ - f"map / http://127.0.0.1:{self.httpbin.Variables.Port}", - ]) - else: - self.ts[name].Disk.remap_config.AddLines([ - f'map / https://127.0.0.1:{self.pv_server.Variables.https_port}', - ]) - - def __checkProcessBefore(self, tr): - if self.state == self.State.RUNNING: - tr.StillRunningBefore = self.httpbin - tr.StillRunningBefore = self.pv_server - tr.StillRunningBefore = self.ts['for_httpbin'] - tr.StillRunningBefore = self.ts['for_pv'] - else: - tr.Processes.Default.StartBefore(self.httpbin) - tr.Processes.Default.StartBefore(self.pv_server) - tr.Processes.Default.StartBefore(self.ts['for_httpbin']) - tr.Processes.Default.StartBefore(self.ts['for_pv']) - self.state = self.State.RUNNING - - def __checkProcessAfter(self, tr): - assert (self.state == self.State.RUNNING) - tr.StillRunningAfter = self.httpbin - tr.StillRunningAfter = self.pv_server - tr.StillRunningAfter = self.ts['for_httpbin'] - tr.StillRunningAfter = self.ts['for_pv'] - - def __testCase0(self): - """ - HTTP/1.1 over TCP - """ - tr = Test.AddTestRun() - self.__checkProcessBefore(tr) - tr.MakeCurlCommandMulti( - f""" -{{curl}} -X PURGE --http1.1 -vs http://127.0.0.1:{self.ts['for_httpbin'].Variables.port}/drip?duration=4; -timeout 1 {{curl}} --http1.1 -vs http://127.0.0.1:{self.ts['for_httpbin'].Variables.port}/drip?duration=4; -sleep 5; -{{curl}} --http1.1 -vs http://127.0.0.1:{self.ts['for_httpbin'].Variables.port}/drip?duration=4 -H "x-debug: x-cache" -""", - ts=self.ts['for_httpbin']) - tr.Processes.Default.ReturnCode = 0 - tr.Processes.Default.Streams.stderr = "gold/background_fill_0_stderr_H.gold" - self.__checkProcessAfter(tr) - - def __testCase1(self): - """ - HTTP/1.1 over TLS - """ - tr = Test.AddTestRun() - self.__checkProcessBefore(tr) - tr.MakeCurlCommandMulti( - f""" -{{curl}} -X PURGE --http1.1 -vsk https://127.0.0.1:{self.ts['for_httpbin'].Variables.ssl_port}/drip?duration=4; -timeout 1 {{curl}} --http1.1 -vsk https://127.0.0.1:{self.ts['for_httpbin'].Variables.ssl_port}/drip?duration=4; -sleep 5; -{{curl}} --http1.1 -vsk https://127.0.0.1:{self.ts['for_httpbin'].Variables.ssl_port}/drip?duration=4 -H "x-debug: x-cache" -""", - ts=self.ts['for_httpbin']) - tr.Processes.Default.ReturnCode = 0 - tr.Processes.Default.Streams.stderr = "gold/background_fill_1_stderr_H.gold" - self.__checkProcessAfter(tr) - - def __testCase2(self): - """ - HTTP/2 over TLS - """ - tr = Test.AddTestRun() - self.__checkProcessBefore(tr) - tr.MakeCurlCommandMulti( - f""" -{{curl}} -X PURGE --http2 -vsk https://127.0.0.1:{self.ts['for_httpbin'].Variables.ssl_port}/drip?duration=4; -timeout 1 {{curl}} --http2 -vsk https://127.0.0.1:{self.ts['for_httpbin'].Variables.ssl_port}/drip?duration=4; -sleep 5; -{{curl}} --http2 -vsk https://127.0.0.1:{self.ts['for_httpbin'].Variables.ssl_port}/drip?duration=4 -H "x-debug: x-cache" -""", - ts=self.ts['for_httpbin']) - tr.Processes.Default.ReturnCode = 0 - tr.Processes.Default.Streams.stderr = "gold/background_fill_2_stderr_H.gold" - self.__checkProcessAfter(tr) - - def __testCase3(self): - """ - HTTP/2 over TLS using ProxyVerifier - """ - tr = Test.AddTestRun() - self.__checkProcessBefore(tr) - tr.AddVerifierClientProcess( - "pv_client", - "replay/bg_fill.yaml", - http_ports=[self.ts['for_pv'].Variables.port], - https_ports=[self.ts['for_pv'].Variables.ssl_port]) - tr.Processes.Default.ReturnCode = 0 - tr.Processes.Default.Streams.stdout = "gold/background_fill_3_stdout.gold" - self.__checkProcessAfter(tr) - - def run(self): - self.__testCase0() - if not Condition.CurlUsingUnixDomainSocket(): - self.__testCase1() - self.__testCase2() - self.__testCase3() - - -BackgroundFillTest().run() diff --git a/tests/gold_tests/cache/cache-generation-clear.test.py b/tests/gold_tests/cache/cache-generation-clear.test.py deleted file mode 100644 index 8095f2c5194..00000000000 --- a/tests/gold_tests/cache/cache-generation-clear.test.py +++ /dev/null @@ -1,101 +0,0 @@ -''' -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import uuid - -Test.Summary = ''' -Test that incrementing the cache generation acts like a cache clear -''' -Test.ContinueOnFail = True -# Define default ATS -ts = Test.MakeATSProcess("ts") - -# setup some config file for this server -ts.Disk.records_config.update( - { - 'proxy.config.body_factory.enable_customizations': 3, # enable domain specific body factory - 'proxy.config.http.cache.generation': -1, # Start with cache turned off - }) - -ts.Disk.plugin_config.AddLine('xdebug.so --enable=x-cache,x-cache-key,via,x-cache-generation') -ts.Disk.remap_config.AddLines( - [ - 'map /default/ http://127.0.0.1/ @plugin=generator.so', - # line 2 - 'map /generation1/ http://127.0.0.1/' + ' @plugin=conf_remap.so @pparam=proxy.config.http.cache.generation=1' + - ' @plugin=generator.so', - # line 3 - 'map /generation2/ http://127.0.0.1/' + ' @plugin=conf_remap.so @pparam=proxy.config.http.cache.generation=2' + - ' @plugin=generator.so', - ]) - -objectid = uuid.uuid4() -# first test is a miss for default -tr = Test.AddTestRun() -tr.MakeCurlCommand( - '"http://127.0.0.1:{0}/default/cache/10/{1}" -H "x-debug: x-cache,x-cache-key,via,x-cache-generation" --verbose'.format( - ts.Variables.port, objectid), - ts=ts) -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.StartBefore(Test.Processes.ts) -tr.Processes.Default.Streams.All = "gold/miss_default-1.gold" - -# Second touch is a HIT for default. -tr = Test.AddTestRun() -tr.MakeCurlCommand( - '"http://127.0.0.1:{0}/default/cache/10/{1}" -H "x-debug: x-cache,x-cache-key,via,x-cache-generation" --verbose'.format( - ts.Variables.port, objectid), - ts=ts) -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Streams.All = "gold/hit_default-1.gold" - -# Clear the cache through traffic_ctl. The command advances the initial generation from -1 to 0. -tr = Test.AddTestRun() -tr.Processes.Default.Command = 'traffic_ctl cache clear' -tr.Processes.Default.ForceUseShell = False -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Env = ts.Env # set the environment for traffic_control to run in - -# new generation should first be a miss. -tr = Test.AddTestRun() -tr.DelayStart = 15 # delay start of test run to allow previous command to take effect -# create a new traffic_ctrl call and the environment -tr.MakeCurlCommand( - '"http://127.0.0.1:{0}/default/cache/10/{1}" -H "x-debug: x-cache,x-cache-key,via,x-cache-generation" --verbose'.format( - ts.Variables.port, objectid), - ts=ts) -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Streams.All = "gold/miss_default0.gold" - -# The new generation should now hit. -tr = Test.AddTestRun() -tr.MakeCurlCommand( - '"http://127.0.0.1:{0}/default/cache/10/{1}" -H "x-debug: x-cache,x-cache-key,via,x-cache-generation" --verbose'.format( - ts.Variables.port, objectid), - ts=ts) -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Streams.All = "gold/hit_default0.gold" - -# should still hit. -tr = Test.AddTestRun() -tr.MakeCurlCommand( - '"http://127.0.0.1:{0}/default/cache/10/{1}" -H "x-debug: x-cache,x-cache-key,via,x-cache-generation" --verbose'.format( - ts.Variables.port, objectid), - ts=ts) -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Streams.All = "gold/hit_default0.gold" diff --git a/tests/gold_tests/cache/cache-generation-disjoint.test.py b/tests/gold_tests/cache/cache-generation-disjoint.test.py deleted file mode 100644 index d2adce9ca24..00000000000 --- a/tests/gold_tests/cache/cache-generation-disjoint.test.py +++ /dev/null @@ -1,101 +0,0 @@ -''' -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import uuid - -Test.Summary = ''' -Test that the same URL path in different cache generations creates disjoint objects -''' - -Test.ContinueOnFail = True -# Define default ATS -ts = Test.MakeATSProcess("ts") - -# setup some config file for this server -ts.Disk.records_config.update( - { - 'proxy.config.body_factory.enable_customizations': 3, # enable domain specific body factory - 'proxy.config.http.cache.generation': -1, # Start with cache turned off - }) -ts.Disk.plugin_config.AddLine('xdebug.so --enable=x-cache,x-cache-key,via,x-cache-generation') -ts.Disk.remap_config.AddLines( - [ - 'map /default/ http://127.0.0.1/ @plugin=generator.so', - # line 2 - 'map /generation1/ http://127.0.0.1/' + ' @plugin=conf_remap.so @pparam=proxy.config.http.cache.generation=1' + - ' @plugin=generator.so', - # line 3 - 'map /generation2/ http://127.0.0.1/' + ' @plugin=conf_remap.so @pparam=proxy.config.http.cache.generation=2' + - ' @plugin=generator.so', - ]) - -objectid = uuid.uuid4() -# first test is a miss for default -tr = Test.AddTestRun() -tr.MakeCurlCommand( - '"http://127.0.0.1:{0}/default/cache/10/{1}" -H "x-debug: x-cache,x-cache-key,via,x-cache-generation" --verbose'.format( - ts.Variables.port, objectid), - ts=ts) -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.StartBefore(Test.Processes.ts) -tr.Processes.Default.Streams.All = "gold/miss_default-1.gold" - -# Same URL in generation 1 is a MISS. -tr = Test.AddTestRun() -tr.MakeCurlCommand( - '"http://127.0.0.1:{0}/generation1/cache/10/{1}" -H "x-debug: x-cache,x-cache-key,via,x-cache-generation" --verbose'.format( - ts.Variables.port, objectid), - ts=ts) -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Streams.All = "gold/miss_gen1.gold" - -# Same URL in generation 2 is still a MISS. -tr = Test.AddTestRun() -tr.MakeCurlCommand( - '"http://127.0.0.1:{0}/generation2/cache/10/{1}" -H "x-debug: x-cache,x-cache-key,via,x-cache-generation" --verbose'.format( - ts.Variables.port, objectid), - ts=ts) -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Streams.All = "gold/miss_gen2.gold" - -# Second touch is a HIT for default. -tr = Test.AddTestRun() -tr.MakeCurlCommand( - '"http://127.0.0.1:{0}/default/cache/10/{1}" -H "x-debug: x-cache,x-cache-key,via,x-cache-generation" --verbose'.format( - ts.Variables.port, objectid), - ts=ts) -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Streams.All = "gold/hit_default-1.gold" - -# Second touch is a HIT for generation1. -tr = Test.AddTestRun() -tr.MakeCurlCommand( - '"http://127.0.0.1:{0}/generation1/cache/10/{1}" -H "x-debug: x-cache,x-cache-key,via,x-cache-generation" --verbose'.format( - ts.Variables.port, objectid), - ts=ts) -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Streams.All = "gold/hit_gen1.gold" - -# Second touch is a HIT for generation2. -tr = Test.AddTestRun() -tr.MakeCurlCommand( - '"http://127.0.0.1:{0}/generation2/cache/10/{1}" -H "x-debug: x-cache,x-cache-key,via,x-cache-generation" --verbose'.format( - ts.Variables.port, objectid), - ts=ts) -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Streams.All = "gold/hit_gen2.gold" diff --git a/tests/gold_tests/cache/cache-write-lock-contention.test.py b/tests/gold_tests/cache/cache-write-lock-contention.test.py deleted file mode 100644 index 7e8ebc9ad1c..00000000000 --- a/tests/gold_tests/cache/cache-write-lock-contention.test.py +++ /dev/null @@ -1,175 +0,0 @@ -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. -''' -Test cache write lock contention behavior with parallel requests for actions 5 and 6. - -This test issues multiple parallel curl requests to trigger actual cache write lock -contention. It is skipped by default because the behavior is timing-sensitive and -not suitable for CI environments. - -To run this test manually: - export RUN_CACHE_CONTENTION_TEST=1 - cd build/tests - ./autest.sh --sandbox /tmp/sbcursor --clean=none -f cache-write-lock-contention -''' - -import os - -Test.Summary = ''' -Test cache write lock contention with parallel requests (actions 5 and 6). - -This test is SKIPPED by default. Set RUN_CACHE_CONTENTION_TEST=1 to run it. -''' - -Test.ContinueOnFail = True - -Test.SkipUnless( - Condition( - lambda: os.environ.get('RUN_CACHE_CONTENTION_TEST', '').lower() in ('1', 'true', 'yes'), - "Set RUN_CACHE_CONTENTION_TEST=1 to run this timing-sensitive test", True)) - - -def make_parallel_curl(url, count, stagger_delay=None): - """ - Generate a parallel curl command string. - - Args: - url: The URL to request - count: Number of parallel requests - stagger_delay: If set, add this delay between requests (e.g., "0.1") - If None, all requests fire simultaneously - """ - parts = ['('] - for i in range(1, count + 1): - if i > 1 and stagger_delay: - parts.append(f'sleep {stagger_delay} && ') - parts.append( - f'{{curl}} -s -o /dev/null -w "req{i}: %{{{{http_code}}}}\\n" "{url}" ' - f'-H "Host: example.com" -H "X-Request: req{i}" & ') - parts.append('wait)') - return ''.join(parts) - - -class ContentionTest: - """ - Base class for cache write lock contention tests. - - Tests parallel requests to trigger cache write lock contention and verify - that the configured fail_action behaves correctly. - """ - - def __init__(self, name, action, description, stale_scenario=False): - self.name = name - self.action = action - self.description = description - self.stale_scenario = stale_scenario - self._setup() - - def _setup(self): - origin_delay = 5 if self.stale_scenario else 3 - self.server = Test.MakeOriginServer(f"server_{self.name}", delay=origin_delay) - - max_age = 1 if self.stale_scenario else 300 - self.server.addResponse( - "sessionlog.json", { - "headers": f"GET /test-{self.name} HTTP/1.1\r\nHost: example.com\r\n\r\n", - "timestamp": "1469733493.993", - "body": "" - }, { - "headers": - f"HTTP/1.1 200 OK\r\n" - f"Content-Length: 100\r\n" - f"Cache-Control: max-age={max_age}\r\n" - f"X-Origin: {self.name}\r\n" - f"Connection: close\r\n\r\n", - "timestamp": "1469733493.993", - "body": "X" * 100 - }) - - self.ts = Test.MakeATSProcess(f"ts_{self.name}", enable_cache=True) - - read_retries = 2 if self.stale_scenario else 10 - retry_time = 100 if self.stale_scenario else 500 - - self.ts.Disk.records_config.update( - { - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'http|cache|http_cache|http_trans', - 'proxy.config.http.cache.open_write_fail_action': self.action, - 'proxy.config.http.cache.max_open_write_retries': 1, - 'proxy.config.http.cache.max_open_write_retry_timeout': 0, - 'proxy.config.http.cache.max_open_read_retries': read_retries, - 'proxy.config.http.cache.open_read_retry_time': retry_time, - 'proxy.config.cache.enable_read_while_writer': 1, - 'proxy.config.http.cache.max_stale_age': 300, - }) - self.ts.Disk.remap_config.AddLine(f'map http://example.com/ http://127.0.0.1:{self.server.Variables.Port}/') - - def run(self): - url_path = f"/test-{self.name}" - url = f"http://127.0.0.1:{self.ts.Variables.port}{url_path}" - - if self.stale_scenario: - tr = Test.AddTestRun(f"{self.description} - Prime cache") - tr.Processes.Default.StartBefore(self.server) - tr.Processes.Default.StartBefore(self.ts) - tr.MakeCurlCommand(f'-s -D - "{url}" -H "Host: example.com"', ts=self.ts) - tr.Processes.Default.ReturnCode = 0 - tr.StillRunningAfter = self.ts - tr.StillRunningAfter = self.server - - tr = Test.AddTestRun(f"{self.description} - Wait for stale") - tr.Processes.Default.Command = "sleep 3" - tr.Processes.Default.ReturnCode = 0 - tr.StillRunningAfter = self.ts - tr.StillRunningAfter = self.server - - tr = Test.AddTestRun(self.description) - if not self.stale_scenario: - tr.Processes.Default.StartBefore(self.server) - tr.Processes.Default.StartBefore(self.ts) - - count = 8 if self.stale_scenario else 5 - stagger = None if self.stale_scenario else "0.1" - tr.MakeCurlCommandMulti(make_parallel_curl(url, count, stagger), ts=self.ts) - tr.Processes.Default.ReturnCode = 0 - tr.StillRunningAfter = self.ts - tr.StillRunningAfter = self.server - - tr = Test.AddTestRun(f"{self.description} - Verify cache") - tr.MakeCurlCommand(f'-s -D - "{url}" -H "Host: example.com"', ts=self.ts) - tr.Processes.Default.ReturnCode = 0 - tr.Processes.Default.Streams.stdout = Testers.ContainsExpression("200 OK", "Cache serves 200") - tr.StillRunningAfter = self.ts - tr.StillRunningAfter = self.server - - self.ts.Disk.traffic_out.Content = Testers.ExcludesExpression("FATAL|ink_release_assert|ink_abort", "No crashes") - - if self.stale_scenario and self.action == 6: - self.ts.Disk.traffic_out.Content += Testers.ContainsExpression( - "serving stale \\(action 6\\)|object stale, serving stale", "Stale fallback triggered") - else: - self.ts.Disk.traffic_out.Content += Testers.ContainsExpression( - "cache open read failure.*retrying|read while write", "Cache contention occurred") - - -# Action 5: READ_RETRY - retries cache reads, goes to origin if exhausted -ContentionTest("a5_fresh", 5, "Action 5 - fresh contention").run() -ContentionTest("a5_stale", 5, "Action 5 - stale revalidation", stale_scenario=True).run() - -# Action 6: READ_RETRY_STALE_ON_REVALIDATE - like action 5, but serves stale if available -ContentionTest("a6_fresh", 6, "Action 6 - fresh contention").run() -ContentionTest("a6_stale", 6, "Action 6 - stale fallback", stale_scenario=True).run() diff --git a/tests/gold_tests/cache/cache_config_reload.test.py b/tests/gold_tests/cache/cache_config_reload.test.py deleted file mode 100644 index 52ca11aad66..00000000000 --- a/tests/gold_tests/cache/cache_config_reload.test.py +++ /dev/null @@ -1,64 +0,0 @@ -''' -Test cache.config and hosting.config reload via ConfigRegistry. - -Verifies that: -1. cache.config reload works after file touch -2. hosting.config reload works after file touch (requires cache to be initialized) -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import os - -Test.Summary = ''' -Test cache.config and hosting.config reload via ConfigRegistry. -''' - -Test.ContinueOnFail = True - -# Create ATS with cache enabled (needed for hosting.config registration in open_done) -ts = Test.MakeATSProcess("ts", enable_cache=True) -ts.Disk.records_config.update({ - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'rpc|config', -}) - -# Set up initial cache.config with a caching rule -ts.Disk.cache_config.AddLine('dest_domain=example.com ttl-in-cache=30d') - -config_dir = ts.Variables.CONFIGDIR - -# --- Test 1: Touch cache.config and reload --- - -tr = Test.AddTestRun("Touch cache.config to trigger change detection") -tr.Processes.Default.StartBefore(ts) -tr.Processes.Default.Command = f"touch {os.path.join(config_dir, 'cache.config')} && sleep 2" -tr.Processes.Default.ReturnCode = 0 -tr.StillRunningAfter = ts - -tr = Test.AddConfigReload( - ts, expect="any", expect_tasks=["cache.config"], token="reload_cache_test", description="Reload after cache.config touch") - -# --- Test 2: Touch hosting.config and reload --- - -tr = Test.AddTestRun("Touch hosting.config to trigger change detection") -tr.DelayStart = 3 -tr.Processes.Default.Command = f"touch {os.path.join(config_dir, 'hosting.config')} && sleep 2" -tr.Processes.Default.ReturnCode = 0 -tr.StillRunningAfter = ts - -tr = Test.AddConfigReload( - ts, expect="any", expect_tasks=["hosting.config"], token="reload_hosting_test", description="Reload after hosting.config touch") diff --git a/tests/gold_tests/cache/cache_corrupt_recursive_uaf.test.py b/tests/gold_tests/cache/cache_corrupt_recursive_uaf.test.py deleted file mode 100644 index 2dc20fec9bb..00000000000 --- a/tests/gold_tests/cache/cache_corrupt_recursive_uaf.test.py +++ /dev/null @@ -1,127 +0,0 @@ -''' -Recursive cache-read tracking must not use-after-free on failure. - -This autest sets ATS_TEST_FORCE_CORRUPT_DOC=1 in the ATS environment, which -makes openReadStartEarliest treat every doc magic as corrupt. Reading a -multi-fragment object then drives the recursive earliest-read path: each level -re-enters openReadStartEarliest via do_read_call, and when an inner level falls -into free_CacheVC the outer frame -- on the *unfixed* code -- decrements the -recursion counter through the now-freed CacheVC member. A sanitizer build -catches that as a use-after-free. Note the depth limit ("Too many recursive -calls") is NOT required to trigger the bug: the dangling decrement happens at -whatever level the inner call frees `this`. - -With the fix in place the counter lives in a thread_local that outlives any -freed CacheVC, so the recursive run completes cleanly and the proxy stays up. - -NOTE ON COVERAGE: the use-after-free is only *caught* when ATS is built with a -memory sanitizer (e.g. the ASan preset the security CI uses). On a non-sanitized -build the dangling decrement is typically a silent no-op, so this test would -pass even on the unfixed sources. What the test verifies unconditionally is that -the forced-corrupt earliest-read path actually runs ("Doc magic does not match") -and that the proxy never crashes (no FATAL). Run it under a sanitizer build to -get the regression-catching guarantee. -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -Test.Summary = 'Recursive cache read must not use-after-free when every doc magic is corrupt' - -# The corrupt-doc env hook is compiled in only when TS_HAS_TESTS is enabled; -# without it the hook is a constexpr false and this test would silently exercise -# a normal cache hit instead of the recursive corrupt-doc path. -Test.SkipUnless(Condition.HasATSFeature('TS_HAS_TESTS')) - -Test.ContinueOnFail = True - -server = Test.MakeOriginServer("server") - -# The corrupt-doc hook lives in openReadStartEarliest, which is only reached when -# the cached object spans more than one fragment (a single-fragment object is -# served entirely from openReadStartHead and never enters the recursive earliest -# read path). Use a multi-fragment body together with a small target_fragment_size -# below so the second read actually drives openReadStartEarliest. 64 KiB over an -# 8 KiB fragment size is several fragments while keeping the test lightweight. -body = "x" * (64 * 1024) - -server.addResponse( - "sessionlog.json", { - "headers": "GET /obj HTTP/1.1\r\nHost: *\r\n\r\n", - "timestamp": "1", - "body": "" - }, { - "headers": "HTTP/1.1 200 OK\r\nContent-Length: {0}\r\nCache-Control: max-age=3600\r\n\r\n".format(len(body)), - "timestamp": "1", - "body": body - }) - -ts = Test.MakeATSProcess("ts", enable_cache=True) -# Force the corrupt-doc path inside openReadStartEarliest for every cache read. -ts.Env['ATS_TEST_FORCE_CORRUPT_DOC'] = '1' - -ts.Disk.records_config.update( - { - 'proxy.config.http.wait_for_cache': 1, - # Force the object to be written as several small fragments so the read - # path enters openReadStartEarliest (sizeof(Doc) < value <= cap). Debug - # logging is intentionally left off: the corruption/recursion diagnostics - # asserted below are Warning/Error level and always reach diags.log, and - # cache_read debug over many fragments would bloat the run's disk use. - 'proxy.config.cache.target_fragment_size': 8192, - }) - -ts.Disk.remap_config.AddLine('map http://example.com/ http://127.0.0.1:{0}/'.format(server.Variables.Port)) - -# Assert the forced-corrupt earliest-read path actually ran. The depth-limit -# diagnostic ("Too many recursive calls") is deliberately NOT required: the -# recursion frees `this` at whatever level the inner read fails, which is the -# use-after-free locus, and it does not need to reach MAX_READ_RECURSION_DEPTH. -# So the reliable, sanitizer-independent signal that the vulnerable path was -# exercised is the corruption warning itself. The real regression guard is a -# sanitizer build catching the UAF on the unfixed sources (see module docstring). -# Assigning Content with `=` replaces the framework's default ERROR:/FATAL: -# guards, so re-add a FATAL: guard to keep asserting the proxy never crashed. -ts.Disk.diags_log.Content = Testers.ContainsExpression("Doc magic does not match", "the forced-corrupt earliest-read path must run") -ts.Disk.diags_log.Content += Testers.ExcludesExpression("FATAL:", "ATS must not crash on the recursive corrupt-doc path") - -# First request: cache miss, populate cache from origin. -tr1 = Test.AddTestRun() -tr1.MakeCurlCommandMulti( - '{curl} -sS -i -x 127.0.0.1:TSPORT http://example.com/obj'.replace('TSPORT', str(ts.Variables.port)), ts=ts) -tr1.Processes.Default.StartBefore(ts) -tr1.Processes.Default.StartBefore(server) -tr1.Processes.Default.ReturnCode = 0 -# The populate read also walks the forced-corrupt earliest path; assert ATS and -# origin survive it (a UAF crash here would otherwise look like a later failure). -tr1.StillRunningAfter = ts -tr1.StillRunningAfter = server - -# Second request: cache hit; openReadStartEarliest treats the doc as corrupt -# (env var) and drives the recursive earliest-read path. With the fix ATS -# survives; the unfixed code would be caught by a sanitizer as UAF on the -# recursion counter. -tr2 = Test.AddTestRun() -tr2.MakeCurlCommandMulti( - '{curl} -sS -i -x 127.0.0.1:TSPORT http://example.com/obj'.replace('TSPORT', str(ts.Variables.port)), ts=ts) -tr2.Processes.Default.ReturnCode = 0 -# Distinguishing assertion: ATS must remain up and respond. Body content -# doesn't matter; what matters is the proxy didn't crash. -tr2.Processes.Default.Streams.stdout = Testers.ContainsExpression("HTTP/1.1", "ATS must respond, not crash") -# Belt-and-suspenders: the proxy (and origin) must still be alive after serving -# the recursive corrupt-doc read -- catches a crash that lands right after the -# response is written. -tr2.StillRunningAfter = ts -tr2.StillRunningAfter = server diff --git a/tests/gold_tests/cache/cache_shm_bad_disk_dropped.test.py b/tests/gold_tests/cache/cache_shm_bad_disk_dropped.test.py deleted file mode 100644 index 168fb5593af..00000000000 --- a/tests/gold_tests/cache/cache_shm_bad_disk_dropped.test.py +++ /dev/null @@ -1,232 +0,0 @@ -''' -Verify per-stripe partial attach when a disk is dropped from storage.yaml -(the "bad disk" case). A storage change no longer cold-starts every stripe: -the stripes on healthy, unchanged disks fast-attach their prior shm segments -while the segment left behind by the removed disk is reclaimed. - -ts1 caches an object across two disks and clean-shuts-down (marking the shm -clean). ts2 starts against the *same* shm prefix but with the second disk -removed from storage.yaml -- simulating a bad disk dropped by the operator. -ts2 must: - - keep the existing control segment (partial attach, not a full recreate), - - fast-attach the surviving disk's stripe by its stable identity, - - reclaim the orphaned stripe segment of the removed disk, - - and still serve traffic. -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import os -import sys -import uuid - -Test.Summary = ''' -Dropping a disk from storage.yaml fast-attaches the surviving stripes from -shm and reclaims the orphaned stripe segment of the removed disk. -''' -Test.ContinueOnFail = True - - -class CacheShmBadDiskDroppedTest: - """ - A stripe's shm identity is its hash_text -- the disk seed (path or - hash_base_string) plus that disk's own dir_skip:blocks, read from the - disk's persisted header. None of those depend on the other disks, so when - one disk is removed from storage.yaml the surviving disks compute the - same hash_text as before and re-attach their prior shm segments. The - removed disk's stripe is no longer present, so its control entry is never - claimed and finalize_attach() reclaims the orphaned segment. - - ts1 starts cold across disk_a + disk_b, populates the cache, and clean-shuts - down. ts2 starts against disk_a only (disk_b "fails"/dropped) sharing the - shm prefix, and asserts ts2: - - enters partial-attach mode (storage signature changed) keeping the - control segment rather than recreating it, - - fast-attaches the surviving disk_a stripe from shm, - - reclaims exactly the orphaned disk_b stripe segment, - - reports neither an unclean shutdown nor a schema/ABI mismatch, - - and serves a request (200). - """ - - TS_PID_SCRIPT = 'ts_process_handler.py' - - SHARED_DISK_SIZE_BYTES = 256 * 1024 * 1024 # 256 MiB per disk - - def __init__(self): - self._setup_shared_state() - # ts1 sees both disks; ts2 sees only disk_a (disk_b dropped). - self.ts1 = self._configure_ts('shmbd_ts1', [self._storage_path_a, self._storage_path_b]) - self.ts2 = self._configure_ts('shmbd_ts2', [self._storage_path_a]) - self._add_diags_log_assertions() - self._url_path = f'/cache/40/{uuid.uuid4()}' - - def _setup_shared_state(self): - Test.Setup.Copy(os.path.join(Test.TestDirectory, '..', 'logging', self.TS_PID_SCRIPT)) - - shared_storage_dir = os.path.join(Test.RunDirectory, 'shared-storage') - os.makedirs(shared_storage_dir, exist_ok=True) - # Absolute paths keep the spans independent of MakeATSProcess's - # per-instance STORAGEDIR so disk_a has identical geometry for ts1 and - # ts2 (hence identical stripe identity -> fast attach). - self._storage_path_a = os.path.join(shared_storage_dir, 'disk_a.img') - self._storage_path_b = os.path.join(shared_storage_dir, 'disk_b.img') - for path in (self._storage_path_a, self._storage_path_b): - with open(path, 'ab') as f: - f.truncate(self.SHARED_DISK_SIZE_BYTES) - - # macOS PSHMNAMLEN is 31 chars incl. '/'; 'bd' = bad-disk-dropped variant. - self._shm_prefix = f'/cshmbd-{os.getpid() % 100000}-' - - def _configure_ts(self, name, storage_paths): - ts = Test.MakeATSProcess(name) - storage_lines = ['cache:', ' spans:'] - for i, storage_path in enumerate(storage_paths): - storage_lines += [ - f' - name: disk.{i}', - f' path: {storage_path}', - f' size: {self.SHARED_DISK_SIZE_BYTES}', - ] - storage_lines += [' volumes:', ' - id: 1', ' scheme: http', ' size: 100%'] - ts.Disk.storage_yaml.AddLines(storage_lines) - ts.Disk.records_config.update( - { - 'proxy.config.cache.shm.enabled': 1, - 'proxy.config.cache.shm.name_prefix': self._shm_prefix, - 'proxy.config.cache.shm.use_hugepages': 0, - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'cache_shm|cache_init', - 'proxy.config.diags.output.diag': 'L', - 'proxy.config.http.wait_for_cache': 1, - }) - ts.Disk.plugin_config.AddLine('xdebug.so --enable=x-cache,via') - ts.Disk.remap_config.AddLine('map / http://127.0.0.1/ @plugin=generator.so') - return ts - - def _add_diags_log_assertions(self): - # ts1 cold start across both disks, clean shutdown. - self.ts1.Disk.diags_log.Content += Testers.ContainsExpression( - r'cache shm: creating fresh control segment', 'ts1 should create a fresh shm control segment on first start') - self.ts1.Disk.diags_log.Content += Testers.ContainsExpression( - r'created stripe \S+ \(\d+ bytes\) for key=', 'ts1 should create the shm-backed stripe segments') - self.ts1.Disk.diags_log.Content += Testers.ContainsExpression( - r'cache shm: marking clean shutdown', 'ts1 should mark the shm clean before exit') - - # ts2 warm start with disk_b dropped: partial attach -- the surviving - # disk_a stripe attaches, the orphaned disk_b segment is reclaimed. - self.ts2.Disk.diags_log.Content += Testers.ContainsExpression( - r'attaching up to \d+ stripes \(fast restart, partial -- storage changed\)', - 'ts2 must enter partial-attach mode after the disk was dropped') - self.ts2.Disk.diags_log.Content += Testers.ContainsExpression( - r'attached stripe \S+ \(\d+ bytes\) for key=', 'ts2 must fast-attach the surviving disk_a stripe from shm') - self.ts2.Disk.diags_log.Content += Testers.ContainsExpression( - r'cache shm: reclaiming orphaned stripe segment', 'ts2 must reclaim the dropped disk_b stripe segment') - self.ts2.Disk.diags_log.Content += Testers.ContainsExpression( - r'reclaimed \d+ orphaned stripe segment\(s\) after attach', 'ts2 must report the reclaim summary') - self.ts2.Disk.diags_log.Content += Testers.ExcludesExpression( - r'cache shm: creating fresh control segment', 'ts2 must keep the control segment across the disk drop') - self.ts2.Disk.diags_log.Content += Testers.ExcludesExpression( - r'cache shm: previous run did not shutdown cleanly', - 'the partial attach must be due to the disk drop, not an unclean shutdown') - self.ts2.Disk.diags_log.Content += Testers.ExcludesExpression( - r'cache shm: (schema|ABI) mismatch', 'the partial attach must be due to the disk drop, not schema/ABI') - - def _populate_cache(self): - tr = Test.AddTestRun('Populate cache via ts1 (disk_a + disk_b)') - tr.Processes.Default.StartBefore(self.ts1) - tr.MakeCurlCommand( - f'-s -o /dev/null -w "%{{http_code}}\\n" ' - f'-H "x-debug: x-cache,via" ' - f'http://127.0.0.1:{self.ts1.Variables.port}{self._url_path}') - tr.Processes.Default.ReturnCode = 0 - tr.Processes.Default.Streams.stdout = Testers.ContainsExpression('200', 'ts1 first GET should return 200') - tr.StillRunningAfter = self.ts1 - - def _clean_shutdown_ts1(self): - tr = Test.AddTestRun('Drain and clean-shutdown ts1') - tr.Processes.Default.Env = self.ts1.Env - tr.Processes.Default.Command = ( - f'traffic_ctl server drain && sleep 1 && ' - f'{sys.executable} ./{self.TS_PID_SCRIPT} shmbd_ts1 --signal TERM && sleep 3') - tr.Processes.Default.ReturnCode = 0 - - def _dump_shm_state(self): - # Between ts1's clean shutdown and ts2's start the control segment is - # marked clean and records both stripes (nothing reclaimed yet). Capture - # it with `traffic_ctl cache shm status` and compare against a gold file. - # The gold masks the run-specific names, the ABI/storage hashes, and the - # page-rounded sizes with the `` wildcard, so what is asserted literally - # is the meaningful state: valid magic/schema, clean_shutdown=1, - # stripe_count=2, both stripe segments present, and a retained owner_pid - # that reads as stale (a clean shutdown keeps the pid so a platform - # without flock cannot attach into the shutdown window). - tr = Test.AddTestRun('Dump shm control state after ts1 clean shutdown') - # Use ts1's Env: it has been started, so the per-instance bin dir is on - # PATH (ts2's Env only gains it once ts2 starts, which is the next step). - # `cache shm status` reads the segment directly and needs no live server. - tr.Processes.Default.Env = self.ts1.Env - tr.Processes.Default.Command = f'traffic_ctl cache shm status --prefix {self._shm_prefix}' - tr.Processes.Default.ReturnCode = 0 - tr.Processes.Default.Streams.stdout = 'gold/cache_shm_state_after_shutdown.gold' - # The gold's stripe rows end at `present`. A stripe marked untrusted at shutdown appends to that line, so say so - # explicitly -- otherwise a stray mark (e.g. an AIO write still in flight) surfaces as an opaque gold diff. - tr.Processes.Default.Streams.stdout += Testers.ExcludesExpression( - 'untrusted', 'this cache was shut down cleanly, so no stripe should be marked untrusted') - - def _verify_survivor_attach_and_reclaim(self): - tr = Test.AddTestRun('Start ts2 (disk_b dropped); verify survivor fast-attach + orphan reclaim') - tr.Processes.Default.StartBefore(self.ts2) - tr.MakeCurlCommand( - f'-s -o /dev/null -w "%{{http_code}}\\n" ' - f'-H "x-debug: x-cache,via" ' - f'http://127.0.0.1:{self.ts2.Variables.port}{self._url_path}') - tr.Processes.Default.ReturnCode = 0 - tr.Processes.Default.Streams.stdout = Testers.ContainsExpression( - '200', 'ts2 should serve correctly after the partial attach') - tr.StillRunningAfter = self.ts2 - - def _clean_shutdown_ts2(self): - # Stop ts2 before clearing the shm: `cache shm clear` refuses to unlink a - # segment a live traffic_server still owns, so the owner must be gone - # (its retained owner_pid then reads as dead) before cleanup runs. - tr = Test.AddTestRun('Drain and clean-shutdown ts2') - tr.Processes.Default.Env = self.ts2.Env - tr.Processes.Default.Command = ( - f'traffic_ctl server drain && sleep 1 && ' - f'{sys.executable} ./{self.TS_PID_SCRIPT} shmbd_ts2 --signal TERM && sleep 3') - tr.Processes.Default.ReturnCode = 0 - - def _cleanup_shm(self): - # A clean shutdown deliberately keeps the control + live stripe segments - # for the next fast restart, so they outlive the test. Unlink them by - # prefix to avoid leaking POSIX shm across repeated local runs (macOS has - # no /dev/shm to clear out of band). - tr = Test.AddTestRun('Unlink the test shm segments') - tr.Processes.Default.Env = self.ts2.Env - tr.Processes.Default.Command = f'traffic_ctl cache shm clear --prefix {self._shm_prefix}' - tr.Processes.Default.ReturnCode = 0 - tr.Processes.Default.Streams.stderr = Testers.ExcludesExpression( - 'Invalid argument', 'clear must skip tombstoned slots, not fail on them') - - def run(self): - self._populate_cache() - self._clean_shutdown_ts1() - self._dump_shm_state() - self._verify_survivor_attach_and_reclaim() - self._clean_shutdown_ts2() - self._cleanup_shm() - - -CacheShmBadDiskDroppedTest().run() diff --git a/tests/gold_tests/cache/cache_shm_concurrent_attach.test.py b/tests/gold_tests/cache/cache_shm_concurrent_attach.test.py deleted file mode 100644 index cfbe544de95..00000000000 --- a/tests/gold_tests/cache/cache_shm_concurrent_attach.test.py +++ /dev/null @@ -1,185 +0,0 @@ -''' -Verify the concurrent-attach guard: a second traffic_server must never map the -shm directory read-write underneath a live owner. ts1 cold-starts and becomes -the owner of the control segment (it sets owner_pid and, on Linux, holds an -exclusive flock for its lifetime). While ts1 is still running, ts2 starts -against the *same* shm prefix; it must refuse shm for this run, disable it, and -come up on its own disk cache without touching ts1's segment. ts1 keeps serving -throughout. - -The two instances use *separate* on-disk cache files so the test isolates the -shm concurrent-attach guard from any contention over a shared cache file. -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import os -import sys -import uuid - -Test.Summary = ''' -A second traffic_server refuses to attach the shm directory while a live owner -holds it, disabling shm for its run instead of attaching concurrently. -''' -Test.ContinueOnFail = True - - -class CacheShmConcurrentAttachTest: - """ - The concurrent-attach guard (P0). A live owner is still mapping the Dir - read-write; a second writer would corrupt it, and clean_shutdown is no - protection against a concurrent *live* run. The guard fires from either of - two mechanisms, so this test asserts on the shared tail of both messages: - - Linux: ts1 holds an exclusive flock on the control segment for its - lifetime; ts2's lock attempt returns HeldByOther ("... is locked by a - live owner ..."). - - macOS (flock unsupported): the owner_pid liveness backstop fires - instead ("... claims a live owner ..."). - Both end in "disabling shm this run to avoid concurrent attach" and set the - run to shm-disabled, which is what this test pins -- so it runs on every - platform. - - ts2 must: - - log the concurrent-attach refusal, - - NOT create or attach a control segment (it bails before either), - - still serve a request (200) from its own disk cache. - ts1 must keep running and serving the whole time. - """ - - TS_PID_SCRIPT = 'ts_process_handler.py' - - SHARED_DISK_SIZE_BYTES = 256 * 1024 * 1024 # 256 MiB - - def __init__(self): - self._setup_shared_state() - # Same shm prefix, different storage files: the collision under test is - # purely on the shm control segment, not the on-disk cache. - self.ts1 = self._configure_ts('shmc_ts1', self._storage_path_a) - self.ts2 = self._configure_ts('shmc_ts2', self._storage_path_b) - self._add_diags_log_assertions() - self._url_path = f'/cache/40/{uuid.uuid4()}' - - def _setup_shared_state(self): - Test.Setup.Copy(os.path.join(Test.TestDirectory, '..', 'logging', self.TS_PID_SCRIPT)) - - shared_storage_dir = os.path.join(Test.RunDirectory, 'shared-storage') - os.makedirs(shared_storage_dir, exist_ok=True) - self._storage_path_a = os.path.join(shared_storage_dir, 'disk_a.img') - self._storage_path_b = os.path.join(shared_storage_dir, 'disk_b.img') - for path in (self._storage_path_a, self._storage_path_b): - with open(path, 'ab') as f: - f.truncate(self.SHARED_DISK_SIZE_BYTES) - - # macOS PSHMNAMLEN is 31 chars incl. '/'; 'c' = concurrent-attach variant. - self._shm_prefix = f'/cshmc-{os.getpid() % 100000}-' - - def _configure_ts(self, name, storage_path): - ts = Test.MakeATSProcess(name) - ts.Disk.storage_yaml.AddLines( - [ - 'cache:', - ' spans:', - ' - name: disk.0', - f' path: {storage_path}', - f' size: {self.SHARED_DISK_SIZE_BYTES}', - ' volumes:', - ' - id: 1', - ' scheme: http', - ' size: 100%', - ]) - ts.Disk.records_config.update( - { - 'proxy.config.cache.shm.enabled': 1, - 'proxy.config.cache.shm.name_prefix': self._shm_prefix, - 'proxy.config.cache.shm.use_hugepages': 0, - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'cache_shm', - 'proxy.config.diags.output.diag': 'L', - 'proxy.config.http.wait_for_cache': 1, - }) - ts.Disk.plugin_config.AddLine('xdebug.so --enable=x-cache,via') - ts.Disk.remap_config.AddLine('map / http://127.0.0.1/ @plugin=generator.so') - return ts - - def _add_diags_log_assertions(self): - # ts1 is the owner: it creates the fresh control segment. - self.ts1.Disk.diags_log.Content += Testers.ContainsExpression( - r'cache shm: creating fresh control segment', 'ts1 should create and own the shm control segment') - - # ts2 starts while ts1 owns the segment: it must refuse and disable shm. - # The message head differs by platform (flock vs owner_pid backstop); the - # tail is common, so anchor on it. - self.ts2.Disk.diags_log.Content += Testers.ContainsExpression( - r'disabling shm this run to avoid concurrent attach', 'ts2 must refuse to attach while ts1 owns the segment') - self.ts2.Disk.diags_log.Content += Testers.ExcludesExpression( - r'cache shm: creating fresh control segment', 'ts2 must not create a control segment when it refuses shm') - self.ts2.Disk.diags_log.Content += Testers.ExcludesExpression( - r'cache shm: attaching up to \d+ stripes \(fast restart', "ts2 must not attach ts1's live control segment") - - def _start_owner(self): - tr = Test.AddTestRun('Cold-start ts1 (becomes the shm owner)') - tr.Processes.Default.StartBefore(self.ts1) - tr.MakeCurlCommand( - f'-s -o /dev/null -w "%{{http_code}}\\n" ' - f'-H "x-debug: x-cache,via" ' - f'http://127.0.0.1:{self.ts1.Variables.port}{self._url_path}') - tr.Processes.Default.ReturnCode = 0 - tr.Processes.Default.Streams.stdout = Testers.ContainsExpression('200', 'ts1 GET should return 200') - tr.StillRunningAfter = self.ts1 - - def _start_second_refused(self): - # ts1 is still running (kept alive by StillRunningAfter above), so ts2's - # start hits the concurrent-attach guard. - tr = Test.AddTestRun('Start ts2 while ts1 is live; ts2 must refuse shm and serve from disk') - tr.Processes.Default.StartBefore(self.ts2) - tr.MakeCurlCommand( - f'-s -o /dev/null -w "%{{http_code}}\\n" ' - f'-H "x-debug: x-cache,via" ' - f'http://127.0.0.1:{self.ts2.Variables.port}{self._url_path}') - tr.Processes.Default.ReturnCode = 0 - tr.Processes.Default.Streams.stdout = Testers.ContainsExpression( - '200', 'ts2 should serve from its own disk cache with shm disabled') - tr.StillRunningAfter = self.ts1 - tr.StillRunningAfter = self.ts2 - - def _clean_shutdown(self, ts, name): - tr = Test.AddTestRun(f'Drain and clean-shutdown {name}') - tr.Processes.Default.Env = ts.Env - tr.Processes.Default.Command = ( - f'traffic_ctl server drain && sleep 1 && ' - f'{sys.executable} ./{self.TS_PID_SCRIPT} {name} --signal TERM && sleep 3') - tr.Processes.Default.ReturnCode = 0 - - def _cleanup_shm(self): - # ts1 (the owner) is stopped before this so clean_shutdown clears - # owner_pid; otherwise `cache shm clear` refuses a live owner. - tr = Test.AddTestRun('Unlink the test shm segments') - tr.Processes.Default.Env = self.ts1.Env - tr.Processes.Default.Command = f'traffic_ctl cache shm clear --prefix {self._shm_prefix}' - tr.Processes.Default.ReturnCode = 0 - tr.Processes.Default.Streams.stderr = Testers.ExcludesExpression( - 'Invalid argument', 'clear must skip tombstoned slots, not fail on them') - - def run(self): - self._start_owner() - self._start_second_refused() - # Stop the non-owner first, then the owner, then clear: its retained owner_pid reads as dead once it exits. - self._clean_shutdown(self.ts2, 'shmc_ts2') - self._clean_shutdown(self.ts1, 'shmc_ts1') - self._cleanup_shm() - - -CacheShmConcurrentAttachTest().run() diff --git a/tests/gold_tests/cache/cache_shm_control_size_mismatch.test.py b/tests/gold_tests/cache/cache_shm_control_size_mismatch.test.py deleted file mode 100644 index 9da1ba298f7..00000000000 --- a/tests/gold_tests/cache/cache_shm_control_size_mismatch.test.py +++ /dev/null @@ -1,202 +0,0 @@ -''' -Verify a control segment whose size is not this build's sizeof(CacheShmControl) is -dropped and recreated rather than wedging shm off. Such a segment is what an upgrade -that changes the control layout (a MAX_STRIPES bump, a longer shm_name, a new -StripeEntry field) leaves behind: the size-checked attach cannot map it, and without -the frozen-header drop path the O_EXCL create would then fail with EEXIST on every -restart until an operator ran `traffic_ctl cache shm clear`. - -ts1 cold-starts, caches an object, and clean-shuts-down. The control segment file -under /dev/shm is then grown past any size this build could have written, so ts2 sees -a foreign-size segment: it must drop it, create a fresh one, and rebuild from disk. -ts3 then starts against what ts2 left behind and must fast-attach normally, which is -what proves the drop actually healed rather than deferring the wedge by one restart. - -Linux-only: it resizes the /dev/shm segment file, which exists only on Linux (macOS -POSIX shm segments are not path-addressable). -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import os -import platform -import sys -import uuid - -Test.Summary = ''' -A control segment written with a different sizeof(CacheShmControl) is dropped and -recreated, never left to wedge the create path. -''' -Test.ContinueOnFail = True - -# The poke drives the gate by editing /dev/shm directly, which is a Linux facility; -# macOS POSIX shm is not exposed as a file. There is no Condition for the platform, -# so gate with a lambda (ports.py branches on platform the same way). -Test.SkipUnless(Condition(lambda: platform.system() == 'Linux', "shm byte-poke gates need Linux /dev/shm")) - - -class CacheShmControlSizeMismatchTest: - """ - The control-segment size gate. Every field the attach path needs to identify a - segment (magic, schema_version, abi_hash) and to guard dropping it (owner_pid, - clean_shutdown) lives in a frozen prefix, so a segment of any other size is still - readable that far: it is guarded, dropped, and recreated in one start. - - Sequence: ts1 creates a clean segment; the segment file is grown past this build's - size; ts2 must report the size mismatch, drop, recreate, and serve; ts3 must then - fast-attach the segment ts2 created. - """ - - TS_PID_SCRIPT = 'ts_process_handler.py' - POKE_SCRIPT = 'shm_poke.py' - - SHARED_DISK_SIZE_BYTES = 256 * 1024 * 1024 # 256 MiB - - # Writing one byte at this offset grows the (sparse) segment file well past - # sizeof(CacheShmControl) rounded up to a page, which is what the attach path - # accepts. MAX_CONTROL_SEGMENT_BYTES caps the struct at 32 KiB, so 1 MiB is - # beyond reach of any layout this build could compile. The header itself is left - # intact, so the size is the only thing that makes the segment foreign. - GROW_OFFSET = 1024 * 1024 - GROW_BYTE_HEX = '00' - - def __init__(self): - self._setup_shared_state() - self.ts1 = self._configure_ts('shmz_ts1') - self.ts2 = self._configure_ts('shmz_ts2') - self.ts3 = self._configure_ts('shmz_ts3') - self._add_diags_log_assertions() - self._url_path = f'/cache/40/{uuid.uuid4()}' - - def _setup_shared_state(self): - Test.Setup.Copy(os.path.join(Test.TestDirectory, '..', 'logging', self.TS_PID_SCRIPT)) - Test.Setup.Copy(os.path.join(Test.TestDirectory, self.POKE_SCRIPT)) - - shared_storage_dir = os.path.join(Test.RunDirectory, 'shared-storage') - os.makedirs(shared_storage_dir, exist_ok=True) - self._shared_storage_path = os.path.join(shared_storage_dir, 'disk.img') - with open(self._shared_storage_path, 'ab') as f: - f.truncate(self.SHARED_DISK_SIZE_BYTES) - - # macOS PSHMNAMLEN is 31 chars incl. '/'; 'z' = size-mismatch variant. - # (This test is Linux-only, but keep the prefix short for consistency.) - self._shm_prefix = f'/cshmz-{os.getpid() % 100000}-' - self._control_file = '/dev/shm/' + self._shm_prefix.lstrip('/') + 'control' - - def _configure_ts(self, name): - ts = Test.MakeATSProcess(name) - ts.Disk.storage_yaml.AddLines( - [ - 'cache:', - ' spans:', - ' - name: disk.0', - f' path: {self._shared_storage_path}', - f' size: {self.SHARED_DISK_SIZE_BYTES}', - ' volumes:', - ' - id: 1', - ' scheme: http', - ' size: 100%', - ]) - ts.Disk.records_config.update( - { - 'proxy.config.cache.shm.enabled': 1, - 'proxy.config.cache.shm.name_prefix': self._shm_prefix, - 'proxy.config.cache.shm.use_hugepages': 0, - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'cache_shm', - 'proxy.config.diags.output.diag': 'L', - 'proxy.config.http.wait_for_cache': 1, - }) - ts.Disk.plugin_config.AddLine('xdebug.so --enable=x-cache,via') - ts.Disk.remap_config.AddLine('map / http://127.0.0.1/ @plugin=generator.so') - return ts - - def _add_diags_log_assertions(self): - # ts1 cold start, clean shutdown -- a valid, clean segment to resize. - self.ts1.Disk.diags_log.Content += Testers.ContainsExpression( - r'cache shm: creating fresh control segment', 'ts1 should create a fresh shm control segment on first start') - self.ts1.Disk.diags_log.Content += Testers.ContainsExpression( - r'cache shm: marking clean shutdown', 'ts1 should mark the shm clean before exit') - - # ts2 start against the resized segment: report, drop, recreate, rebuild. - self.ts2.Disk.diags_log.Content += Testers.ContainsExpression( - r"cache shm: control segment \S+ is \d+ bytes, not this build's \d+; dropping it", - 'ts2 must report the control segment size mismatch') - self.ts2.Disk.diags_log.Content += Testers.ContainsExpression( - r'cache shm: creating fresh control segment', 'ts2 must recreate the control segment after the drop') - self.ts2.Disk.diags_log.Content += Testers.ExcludesExpression( - r'cache shm: failed to create control segment', - 'the drop must leave the name free, so the O_EXCL create cannot fail with EEXIST') - self.ts2.Disk.diags_log.Content += Testers.ExcludesExpression( - r'\(fast restart, recovery skipped\)', 'ts2 must rebuild from disk, never fast-attach a foreign-size segment') - - # ts3 fast-attaches what ts2 created: the drop healed, it did not just defer. - self.ts3.Disk.diags_log.Content += Testers.ContainsExpression( - r'cache shm: attaching up to \d+ stripes \(fast restart', 'ts3 should attach the segment ts2 created') - self.ts3.Disk.diags_log.Content += Testers.ContainsExpression( - r"attaching cached directory from shm for '.+' \(fast restart", 'ts3 should reuse the per-stripe directory from shm') - self.ts3.Disk.diags_log.Content += Testers.ExcludesExpression( - r'cache shm: control segment \S+ is \d+ bytes', 'ts3 should see a segment of the expected size') - self.ts3.Disk.diags_log.Content += Testers.ExcludesExpression( - r'cache shm: creating fresh control segment', 'ts3 should not have to create another control segment') - - def _get(self, ts, description): - tr = Test.AddTestRun(description) - tr.Processes.Default.StartBefore(ts) - tr.MakeCurlCommand( - f'-s -o /dev/null -w "%{{http_code}}\\n" ' - f'-H "x-debug: x-cache,via" ' - f'http://127.0.0.1:{ts.Variables.port}{self._url_path}') - tr.Processes.Default.ReturnCode = 0 - tr.Processes.Default.Streams.stdout = Testers.ContainsExpression('200', f'{description}: should return 200') - tr.StillRunningAfter = ts - - def _clean_shutdown(self, ts, name): - tr = Test.AddTestRun(f'Drain and clean-shutdown {name}') - tr.Processes.Default.Env = ts.Env - tr.Processes.Default.Command = ( - f'traffic_ctl server drain && sleep 1 && ' - f'{sys.executable} ./{self.TS_PID_SCRIPT} {name} --signal TERM && sleep 3') - tr.Processes.Default.ReturnCode = 0 - - def _grow_control_segment(self): - # ts1 is dead; the segment is just a file now. Extend it without touching the - # header, so only its size makes it foreign to this build. - tr = Test.AddTestRun('Grow the shm control segment past this build\'s size') - tr.Processes.Default.Command = ( - f'{sys.executable} ./{self.POKE_SCRIPT} {self._control_file} {self.GROW_OFFSET} {self.GROW_BYTE_HEX}') - tr.Processes.Default.ReturnCode = 0 - - def _cleanup_shm(self): - tr = Test.AddTestRun('Unlink the test shm segments') - tr.Processes.Default.Env = self.ts3.Env - tr.Processes.Default.Command = f'traffic_ctl cache shm clear --prefix {self._shm_prefix}' - tr.Processes.Default.ReturnCode = 0 - tr.Processes.Default.Streams.stderr = Testers.ExcludesExpression( - 'Invalid argument', 'clear must skip tombstoned slots, not fail on them') - - def run(self): - self._get(self.ts1, 'Cold-start ts1 and cache an object') - self._clean_shutdown(self.ts1, 'shmz_ts1') - self._grow_control_segment() - self._get(self.ts2, 'Start ts2; the foreign-size segment is dropped and recreated') - self._clean_shutdown(self.ts2, 'shmz_ts2') - self._get(self.ts3, 'Start ts3; the recreated segment fast-attaches') - self._clean_shutdown(self.ts3, 'shmz_ts3') - self._cleanup_shm() - - -CacheShmControlSizeMismatchTest().run() diff --git a/tests/gold_tests/cache/cache_shm_dir_invalid.test.py b/tests/gold_tests/cache/cache_shm_dir_invalid.test.py deleted file mode 100644 index 108745ba37b..00000000000 --- a/tests/gold_tests/cache/cache_shm_dir_invalid.test.py +++ /dev/null @@ -1,237 +0,0 @@ -''' -Verify the per-stripe shm directory trust gates: an in-shm directory whose header -fields are out of range is rejected and rebuilt from disk, never fast-attached. -These branches (Stripe::_shm_directory_is_valid) are what stand between a stale or -torn shm directory and out-of-bounds disk I/O, so each is driven directly. - -ts1 cold-starts, caches an object, and clean-shuts-down, which leaves the control -segment marked clean and the stripe segment holding a valid directory. The stripe -segment file under /dev/shm is then tampered with between runs: - - * ts2 sees write_pos pushed past the end of the stripe. - * ts3 sees freelist[0] pushed past the segment's entry count. - -Each instance must attach the shm segments, reject the directory, fall back to the -disk read + recover_data(), and still serve the object out of cache. - -Linux-only: it pokes raw bytes in the /dev/shm segment files, which exist only on -Linux (macOS POSIX shm segments are not path-addressable). -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import os -import platform -import sys - -Test.Summary = ''' -An in-shm stripe directory with out-of-range header fields is rejected and rebuilt -from disk, never fast-attached. -''' -Test.ContinueOnFail = True - -# The byte-poke drives the gate by editing /dev/shm directly, which is a Linux -# facility; macOS POSIX shm is not exposed as a file. There is no Condition for -# the platform, so gate with a lambda (ports.py branches on platform the same way). -Test.SkipUnless(Condition(lambda: platform.system() == 'Linux', "shm byte-poke gates need Linux /dev/shm")) - - -class CacheShmDirInvalidTest: - """ - The per-stripe directory gates. On a fast restart the in-shm directory is used - verbatim -- the disk read and recover_data() are both skipped -- so every header - field the cache later trusts is range-checked first. A rejection must be safe and - silent to clients: the stripe falls back to the disk read, recovers, and serves - the same object. - - Sequence, per tampered field: - - poke the field in the /dev/shm stripe segment left by a clean shutdown, - - start the next ts, which must log "shm directory invalid ... falling back to - disk read" and must NOT log the fast-attach line, - - replay the cache-hit transaction, which must be served from cache. - """ - - TS_PID_SCRIPT = 'ts_process_handler.py' - POKE_SCRIPT = 'shm_poke.py' - - REPLAY_FILE = 'replay/cache-shm-dir-invalid.replay.yaml' - - SHARED_DISK_SIZE_BYTES = 256 * 1024 * 1024 # 256 MiB - - # StripeHeaderFooter layout (P_CacheDir.h) at the head of the stripe segment: - # magic @0, version @4, create_time @8, write_pos @16, last_write_pos @24, - # agg_pos @32, ... sector_size @64, unused @68, freelist[0] @72. - WRITE_POS_OFFSET = 16 - FREELIST_0_OFFSET = 72 - - # Little-endian off_t 0x0000FFFFFFFFFFFF: far beyond skip + len for a 256 MiB - # stripe, so the write_pos range check rejects it. Leaving the top two bytes - # zero keeps the value positive, which exercises the upper-bound branch rather - # than the negative-offset one. - BOGUS_WRITE_POS_LE_HEX = 'ffffffffffff0000' - # Little-endian uint16 65535. The largest value a Dir next/prev field can hold, - # and past the entry count of any segment with fewer than 16384 buckets -- which - # a 256 MiB stripe is. - BOGUS_FREELIST_LE_HEX = 'ffff' - - def __init__(self): - self._setup_shared_state() - # A single verifier-server is the origin for every ts, started before ts1 - # and kept running for the whole test. - self.server = Test.MakeVerifierServerProcess('shmd-origin', self.REPLAY_FILE) - self.ts1 = self._configure_ts('shmd_ts1') - self.ts2 = self._configure_ts('shmd_ts2') - self.ts3 = self._configure_ts('shmd_ts3') - self._add_diags_log_assertions() - - def _setup_shared_state(self): - Test.Setup.Copy(os.path.join(Test.TestDirectory, '..', 'logging', self.TS_PID_SCRIPT)) - Test.Setup.Copy(os.path.join(Test.TestDirectory, self.POKE_SCRIPT)) - - shared_storage_dir = os.path.join(Test.RunDirectory, 'shared-storage') - os.makedirs(shared_storage_dir, exist_ok=True) - self._shared_storage_path = os.path.join(shared_storage_dir, 'disk.img') - with open(self._shared_storage_path, 'ab') as f: - f.truncate(self.SHARED_DISK_SIZE_BYTES) - - # macOS PSHMNAMLEN is 31 chars incl. '/'; 'd' = dir-invalid variant. - # (This test is Linux-only, but keep the prefix short for consistency.) - self._shm_prefix = f'/cshmd-{os.getpid() % 100000}-' - # A single span with a single volume yields exactly one stripe, so its - # segment is index 0. On Linux each segment is a file under /dev/shm by the - # same name (sans the leading '/'). - self._stripe_file = '/dev/shm/' + self._shm_prefix.lstrip('/') + 's0' - - def _configure_ts(self, name): - ts = Test.MakeATSProcess(name) - # An absolute span path keeps the span independent of MakeATSProcess's - # per-instance STORAGEDIR, so every ts shares the same on-disk cache and - # therefore the same stripe geometry (hence the same shm identity). - ts.Disk.storage_yaml.AddLines( - [ - 'cache:', - ' spans:', - ' - name: disk.0', - f' path: {self._shared_storage_path}', - f' size: {self.SHARED_DISK_SIZE_BYTES}', - ' volumes:', - ' - id: 1', - ' scheme: http', - ' size: 100%', - ]) - ts.Disk.records_config.update( - { - 'proxy.config.cache.shm.enabled': 1, - 'proxy.config.cache.shm.name_prefix': self._shm_prefix, - 'proxy.config.cache.shm.use_hugepages': 0, - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'cache_shm', - 'proxy.config.diags.output.diag': 'L', - 'proxy.config.http.wait_for_cache': 1, - }) - ts.Disk.plugin_config.AddLine('xdebug.so --enable=x-cache,via') - ts.Disk.remap_config.AddLine(f'map / http://127.0.0.1:{self.server.Variables.http_port}/') - return ts - - def _add_reject_assertions(self, ts, label): - # The shm segments themselves are still attached -- the control segment is - # untouched and clean, so this is specifically the per-stripe directory gate. - ts.Disk.diags_log.Content += Testers.ContainsExpression( - r'cache shm: attaching up to \d+ stripes \(fast restart', f'{label} should attach the existing control segment') - ts.Disk.diags_log.Content += Testers.ContainsExpression( - r'cache shm: attached stripe \S+ \(\d+ bytes\) for key=', f'{label} should attach the existing stripe segment') - ts.Disk.diags_log.Content += Testers.ContainsExpression( - r"shm directory invalid for '.+'; falling back to disk read", f'{label} must reject the tampered shm directory') - ts.Disk.diags_log.Content += Testers.ExcludesExpression( - r'attaching cached directory from shm for', f'{label} must not fast-attach the tampered directory') - # The rejection is per-stripe: the control segment stays valid, so none of - # the whole-segment drop reasons should appear. - ts.Disk.diags_log.Content += Testers.ExcludesExpression( - r'cache shm: (schema|ABI) mismatch', f'{label} should reject the directory, not the control segment') - ts.Disk.diags_log.Content += Testers.ExcludesExpression( - r'cache shm: previous run did not shutdown cleanly', f'{label} should see the shm marked clean') - - def _add_diags_log_assertions(self): - # ts1 cold start, clean shutdown -- a valid, clean stripe segment to tamper with. - self.ts1.Disk.diags_log.Content += Testers.ContainsExpression( - r'cache shm: creating fresh control segment', 'ts1 should create a fresh shm control segment on first start') - self.ts1.Disk.diags_log.Content += Testers.ContainsExpression( - r'cache shm: created stripe \S+ \(\d+ bytes\) for key=', 'ts1 should create the shm-backed stripe segment') - self.ts1.Disk.diags_log.Content += Testers.ContainsExpression( - r'cache shm: marking clean shutdown', 'ts1 should mark the shm clean before exit') - self.ts1.Disk.diags_log.Content += Testers.ExcludesExpression( - r'shm directory invalid for', 'ts1 has no shm directory to reject on cold start') - - self._add_reject_assertions(self.ts2, 'ts2 (write_pos)') - self._add_reject_assertions(self.ts3, 'ts3 (freelist[0])') - - def _fill(self): - tr = Test.AddTestRun('Cold-start ts1 and cache an object') - tr.AddVerifierClientProcess( - 'shmd-fill-client', self.REPLAY_FILE, http_ports=[self.ts1.Variables.port], keys='fill', other_args='--thread-limit 1') - tr.Processes.Default.StartBefore(self.server) - tr.Processes.Default.StartBefore(self.ts1) - tr.StillRunningAfter = self.server - tr.StillRunningAfter = self.ts1 - - def _clean_shutdown(self, ts, name): - tr = Test.AddTestRun(f'Drain and clean-shutdown {name}') - tr.Processes.Default.Env = ts.Env - tr.Processes.Default.Command = ( - f'traffic_ctl server drain && sleep 1 && ' - f'{sys.executable} ./{self.TS_PID_SCRIPT} {name} --signal TERM && sleep 3') - tr.Processes.Default.ReturnCode = 0 - tr.StillRunningAfter = self.server - - def _poke(self, description, offset, hex_bytes): - # The previous ts is dead; the stripe segment is just a file now. - tr = Test.AddTestRun(description) - tr.Processes.Default.Command = (f'{sys.executable} ./{self.POKE_SCRIPT} {self._stripe_file} {offset} {hex_bytes}') - tr.Processes.Default.ReturnCode = 0 - tr.StillRunningAfter = self.server - - def _verify_reject(self, ts, key, description): - tr = Test.AddTestRun(description) - tr.AddVerifierClientProcess( - f'shmd-{key}-client', self.REPLAY_FILE, http_ports=[ts.Variables.port], keys=key, other_args='--thread-limit 1') - tr.Processes.Default.StartBefore(ts) - tr.StillRunningAfter = self.server - tr.StillRunningAfter = ts - - def _cleanup_shm(self): - tr = Test.AddTestRun('Unlink the test shm segments') - tr.Processes.Default.Env = self.ts3.Env - tr.Processes.Default.Command = f'traffic_ctl cache shm clear --prefix {self._shm_prefix}' - tr.Processes.Default.ReturnCode = 0 - tr.Processes.Default.Streams.stderr = Testers.ExcludesExpression( - 'Invalid argument', 'clear must skip tombstoned slots, not fail on them') - - def run(self): - self._fill() - self._clean_shutdown(self.ts1, 'shmd_ts1') - - self._poke('Tamper write_pos in the in-shm stripe header', self.WRITE_POS_OFFSET, self.BOGUS_WRITE_POS_LE_HEX) - self._verify_reject(self.ts2, 'hit_write_pos', 'Start ts2; an out-of-range write_pos is rejected and rebuilt from disk') - self._clean_shutdown(self.ts2, 'shmd_ts2') - - self._poke('Tamper freelist[0] in the in-shm stripe header', self.FREELIST_0_OFFSET, self.BOGUS_FREELIST_LE_HEX) - self._verify_reject(self.ts3, 'hit_freelist', 'Start ts3; an out-of-range freelist head is rejected and rebuilt from disk') - self._clean_shutdown(self.ts3, 'shmd_ts3') - - self._cleanup_shm() - - -CacheShmDirInvalidTest().run() diff --git a/tests/gold_tests/cache/cache_shm_fast_restart.test.py b/tests/gold_tests/cache/cache_shm_fast_restart.test.py deleted file mode 100644 index b4269fb9f8a..00000000000 --- a/tests/gold_tests/cache/cache_shm_fast_restart.test.py +++ /dev/null @@ -1,247 +0,0 @@ -''' -Verify the cache directory survives a clean shutdown via shared memory and is -attached on the next start (fast restart). Two ATS instances share an on-disk -cache file and a POSIX shm name prefix; ts1 populates the cache and is shut -down via traffic_ctl drain + SIGTERM, then ts2 starts and serves the same URL -out of cache without re-fetching from the origin. - -Traffic is driven with Proxy Verifier: a single verifier-server acts as the -origin and the verifier-client replays cache-shm-fast-restart.replay.yaml -- -the "fill" transaction against ts1 and the "hit" transaction against ts2. -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import os -import sys - -Test.Summary = ''' -Cache directory survives clean shutdown via POSIX shared memory. -''' -Test.ContinueOnFail = True - - -class CacheShmFastRestartTest: - """ - Cover the cache shm fast-restart scenario end-to-end. - - Sequence: - 1. ts1 cold-start: creates a fresh shm control segment and per-stripe - segments; populates the cache via the "fill" transaction (cache miss, - fetched from the verifier-server origin). - 2. ts1 is drained and SIGTERM'd. The shutdown hook flushes the directory - and marks the shm clean. - 3. ts2 starts against the same on-disk file and shm prefix: attaches the - existing control segment, attaches per-stripe segments, and reuses the - cached directory without re-reading it from disk. - 4. ts2 serves the same URL out of cache via the "hit" transaction - (X-Cache: hit-fresh). The transaction's origin response is a 502 - sentinel, so any forward to the origin would fail the run. - - Each step is verified both at the response level (proxy-verifier) and via - diags-log assertions on the cache_shm / cache_dir_init code paths. - """ - - # Helper script for sending signals to a traffic_server process by command-line - # identifier match. Reused from gold_tests/logging. - TS_PID_SCRIPT = 'ts_process_handler.py' - - # The replay file driving both the populate ("fill") and verify ("hit") - # transactions. They share a cache key and differ only by uuid. - REPLAY_FILE = 'replay/cache-shm-fast-restart.replay.yaml' - - # Stripe size for the shared cache. Must be large enough that the directory - # contains real entries; small enough that the disk.img is cheap to create. - SHARED_DISK_SIZE_BYTES = 256 * 1024 * 1024 # 256 MiB - - def __init__(self): - self._setup_shared_state() - # A single verifier-server is the origin for both ts1 and ts2. It is - # started before ts1 and kept running across the whole test. - self.server = Test.MakeVerifierServerProcess('shm-origin', self.REPLAY_FILE) - self.ts1 = self._configure_ts('shm_ts1') - self.ts2 = self._configure_ts('shm_ts2') - self._add_diags_log_assertions() - - def _setup_shared_state(self): - Test.Setup.Copy(os.path.join(Test.TestDirectory, '..', 'logging', self.TS_PID_SCRIPT)) - - # Shared storage file used by both ts1 and ts2. The absolute path makes - # storage.yaml independent of MakeATSProcess's per-instance STORAGEDIR. - # ATS opens regular-file spans with O_RDONLY first to stat them -- it - # does not auto-create the backing file -- so pre-create disk.img at the - # configured size before either ts starts. - shared_storage_dir = os.path.join(Test.RunDirectory, 'shared-storage') - os.makedirs(shared_storage_dir, exist_ok=True) - self._shared_storage_path = os.path.join(shared_storage_dir, 'disk.img') - with open(self._shared_storage_path, 'ab') as f: - f.truncate(self.SHARED_DISK_SIZE_BYTES) - - # POSIX shm names: macOS PSHMNAMLEN limit is 31 chars including '/'. - # Keep the prefix short and unique per test run so concurrent autest - # runs do not collide. - self._shm_prefix = f'/cshm-{os.getpid() % 100000}-' - - def _configure_ts(self, name): - ts = Test.MakeATSProcess(name) - # Master configures cache storage via storage.yaml. An absolute span path - # keeps the span independent of MakeATSProcess's per-instance STORAGEDIR so - # ts1 and ts2 share the same on-disk cache, which yields identical stripe - # geometry (hence identical shm identity). - ts.Disk.storage_yaml.AddLines( - [ - 'cache:', - ' spans:', - ' - name: disk.0', - f' path: {self._shared_storage_path}', - f' size: {self.SHARED_DISK_SIZE_BYTES}', - ' volumes:', - ' - id: 1', - ' scheme: http', - ' size: 100%', - ]) - ts.Disk.records_config.update( - { - 'proxy.config.cache.shm.enabled': 1, - 'proxy.config.cache.shm.name_prefix': self._shm_prefix, - 'proxy.config.cache.shm.use_hugepages': 0, - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'cache_shm', - # The per-stripe 'created/attached stripe' lines are Dbg() calls; - # route debug output to diags.log (default is stderr) so the - # ContainsExpression assertions below can match them. - 'proxy.config.diags.output.diag': 'L', - 'proxy.config.http.wait_for_cache': 1, - }) - ts.Disk.plugin_config.AddLine('xdebug.so --enable=x-cache,via') - ts.Disk.remap_config.AddLine(f'map / http://127.0.0.1:{self.server.Variables.http_port}/') - return ts - - def _add_diags_log_assertions(self): - # These assertions match the *stable core* of each cache-shm log line and - # deliberately stop before the trailing parenthetical qualifier. The shm - # code appends optional context to several of these messages as it evolves - # -- "attaching N stripes" became "attaching up to N stripes", - # "(fast restart)" became "(fast restart, recovery skipped)" on the stripe - # path and "(fast restart, partial -- storage changed)" on the control - # path. Anchoring on the invariant prefix (not the closing paren) keeps the - # test from breaking every time such a qualifier is added. Likewise, the - # excludes name only log strings that actually exist in the source: an - # exclude on a non-existent string can never fire and gives false comfort. - - # ts1 (cold start): creates fresh shm, marks it clean on shutdown, and must - # NOT report any "drop" reason since there is nothing to drop. - self.ts1.Disk.diags_log.Content += Testers.ContainsExpression( - r'cache shm: creating fresh control segment', 'ts1 should create a fresh shm control segment on first start') - self.ts1.Disk.diags_log.Content += Testers.ContainsExpression( - r'cache shm: created stripe \S+ \(\d+ bytes\) for key=', 'ts1 should create at least one shm-backed stripe segment') - self.ts1.Disk.diags_log.Content += Testers.ContainsExpression( - r'cache shm: marking clean shutdown', 'ts1 should mark the shm clean before exit') - self.ts1.Disk.diags_log.Content += Testers.ExcludesExpression( - r'cache shm: (schema|ABI) mismatch', 'ts1 should not detect any shm mismatch on cold start') - self.ts1.Disk.diags_log.Content += Testers.ExcludesExpression( - r'cache shm: previous run did not shutdown cleanly', 'ts1 should not see a dirty shm on cold start') - self.ts1.Disk.diags_log.Content += Testers.ExcludesExpression( - r'cache shm: stripe \S+ size mismatch', 'ts1 should not see a stripe size mismatch on cold start') - - # ts2 (warm start): attaches the existing control segment, fast-attaches the - # per-stripe segment, reuses the cached directory, and must NOT fall back to - # the disk-rebuild path. - self.ts2.Disk.diags_log.Content += Testers.ContainsExpression( - r'cache shm: attaching up to \d+ stripes \(fast restart', 'ts2 should attach the existing shm (fast restart)') - self.ts2.Disk.diags_log.Content += Testers.ContainsExpression( - r'cache shm: attached stripe \S+ \(\d+ bytes\) for key=', 'ts2 should attach at least one shm-backed stripe segment') - self.ts2.Disk.diags_log.Content += Testers.ContainsExpression( - r"attaching cached directory from shm for '.+' \(fast restart", 'ts2 should reuse the per-stripe directory from shm') - self.ts2.Disk.diags_log.Content += Testers.ExcludesExpression( - r'cache shm: creating fresh control segment', 'ts2 should not create a fresh control segment') - self.ts2.Disk.diags_log.Content += Testers.ExcludesExpression( - r'cache shm: (schema|ABI) mismatch', 'ts2 should not detect any shm mismatch on warm start') - self.ts2.Disk.diags_log.Content += Testers.ExcludesExpression( - r'cache shm: previous run did not shutdown cleanly', 'ts2 should see the shm marked clean') - self.ts2.Disk.diags_log.Content += Testers.ExcludesExpression( - r'shm directory invalid for', 'ts2 should not fall back from shm to disk read') - self.ts2.Disk.diags_log.Content += Testers.ExcludesExpression( - r'cache shm: stripe \S+ size mismatch', 'ts2 should fast-attach without a stripe size-mismatch recreate') - - def _start_ts1(self): - # Cold start ts1 against the verifier-server origin and replay the - # "fill" transaction: a cache miss that ATS fetches and stores. - tr = Test.AddTestRun('Start ts1, then cache contents (fill)') - tr.AddVerifierClientProcess( - 'shm-fill-client', self.REPLAY_FILE, http_ports=[self.ts1.Variables.port], keys='fill', other_args='--thread-limit 1') - tr.Processes.Default.StartBefore(self.server) - tr.Processes.Default.StartBefore(self.ts1) - tr.StillRunningAfter = self.server - tr.StillRunningAfter = self.ts1 - - def _clean_shutdown_ts1(self): - # Drain + SIGTERM ts1. SIGTERM goes through AutoStopCont which invokes - # TS_LIFECYCLE_SHUTDOWN_HOOK -> sync_cache_dir_on_shutdown -> - # CacheShm::mark_clean_shutdown. - tr = Test.AddTestRun('Drain and clean-shutdown ts1') - tr.Processes.Default.Env = self.ts1.Env - tr.Processes.Default.Command = ( - f'traffic_ctl server drain && sleep 1 && ' - f'{sys.executable} ./{self.TS_PID_SCRIPT} shm_ts1 --signal TERM && sleep 3') - tr.Processes.Default.ReturnCode = 0 - tr.StillRunningAfter = self.server - - def _start_ts2(self): - # ts2 attaches the CacheDir from the shm created by ts1. Replay the - # "hit" transaction: ATS must serve it from cache (X-Cache: hit-fresh) - # without contacting the origin -- the replay's 502 sentinel response - # would otherwise surface as a proxy-response mismatch. - tr = Test.AddTestRun('Start ts2; verify shm fast-attach and cache HIT') - tr.AddVerifierClientProcess( - 'shm-hit-client', self.REPLAY_FILE, http_ports=[self.ts2.Variables.port], keys='hit', other_args='--thread-limit 1') - tr.Processes.Default.StartBefore(self.ts2) - tr.StillRunningAfter = self.server - tr.StillRunningAfter = self.ts2 - - def _clean_shutdown_ts2(self): - # Stop ts2 before clearing the shm: `cache shm clear` refuses to unlink a - # segment a live traffic_server still owns, so the owner must be gone - # (its retained owner_pid then reads as dead) before cleanup runs. - tr = Test.AddTestRun('Drain and clean-shutdown ts2') - tr.Processes.Default.Env = self.ts2.Env - tr.Processes.Default.Command = ( - f'traffic_ctl server drain && sleep 1 && ' - f'{sys.executable} ./{self.TS_PID_SCRIPT} shm_ts2 --signal TERM && sleep 3') - tr.Processes.Default.ReturnCode = 0 - tr.StillRunningAfter = self.server - - def _cleanup_shm(self): - # A clean shutdown deliberately keeps the control + live stripe segments - # for the next fast restart, so they outlive the test. Unlink them by - # prefix to avoid leaking POSIX shm across repeated local runs (macOS has - # no /dev/shm to clear out of band). - tr = Test.AddTestRun('Unlink the test shm segments') - tr.Processes.Default.Env = self.ts2.Env - tr.Processes.Default.Command = f'traffic_ctl cache shm clear --prefix {self._shm_prefix}' - tr.Processes.Default.ReturnCode = 0 - tr.Processes.Default.Streams.stderr = Testers.ExcludesExpression( - 'Invalid argument', 'clear must skip tombstoned slots, not fail on them') - - def run(self): - self._start_ts1() - self._clean_shutdown_ts1() - self._start_ts2() - self._clean_shutdown_ts2() - self._cleanup_shm() - - -CacheShmFastRestartTest().run() diff --git a/tests/gold_tests/cache/cache_shm_purge_on_disable.test.py b/tests/gold_tests/cache/cache_shm_purge_on_disable.test.py deleted file mode 100644 index 51fd57cdedf..00000000000 --- a/tests/gold_tests/cache/cache_shm_purge_on_disable.test.py +++ /dev/null @@ -1,232 +0,0 @@ -''' -Purge-stale-on-start: when shm is disabled but a prior run left segments behind, -proxy.config.cache.shm.purge_stale_on_start=1 removes them at startup. - -This guards two hazards of running with the feature disabled after it had been -enabled (see records.yaml docs): (a) the leftover segments keep consuming tmpfs -the disabled instance never reads, and (b) a later re-enabled run would otherwise -fast-attach a directory that went stale while ATS ran disabled (writing only to -disk). - -Three scenarios, each on its own shm prefix + on-disk storage so they do not -interact: - - - PURGE (positive): a seed instance runs shm-enabled and clean-shuts-down, - leaving a clean control + stripe segment. A second instance runs disabled - with purge_stale_on_start=1 and must remove them. Confirmed three ways: the - seed's "clean" segment exists before (traffic_ctl cache shm status, exit 0), - the disabled instance logs the purge Note, and the segment is gone after - (status exits 2, "not found"). - - - KEEP (negative): same seed, but the disabled instance has - purge_stale_on_start=0. It must NOT log the purge and the segment must remain. - - - NOOP (no leftover): a disabled instance with purge_stale_on_start=1 against a - never-used prefix must do nothing quietly -- no purge Note, no "cannot open" - warning. - -The segments are inspected with traffic_ctl (POSIX shm is not path-addressable on -macOS, so /dev/shm cannot be listed directly). -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import os -import re -import sys - -Test.Summary = ''' -shm.purge_stale_on_start removes leftover shm segments at startup when shm is -disabled, only when set, and only when a control segment exists. -''' -Test.ContinueOnFail = True - - -class CacheShmPurgeOnDisableTest: - - TS_PID_SCRIPT = 'ts_process_handler.py' - DISK_SIZE_BYTES = 256 * 1024 * 1024 # 256 MiB; matches the other shm gold tests. - - # CtrlCommand sets this exit code when a shm control segment is absent/invalid - # (src/traffic_ctl/TrafficCtlStatus.h). - CTRL_EX_ERROR = 2 - - PURGE_NOTE = r"cache shm: purged stale segments while disabled \(removed [1-9]" - - def __init__(self): - Test.Setup.Copy(os.path.join(Test.TestDirectory, '..', 'logging', self.TS_PID_SCRIPT)) - - pid = os.getpid() % 100000 - # Each control name is "control"; keep well under macOS PSHMNAMLEN (31). - self._prefix_purge = f'/cshmp-{pid}-' # positive: must be purged - self._prefix_keep = f'/cshmk-{pid}-' # negative: must remain - self._prefix_noop = f'/cshmz-{pid}-' # no leftover: nothing to do - - # Seed (shm enabled) instances that create the leftover segments. - self.seed_purge = self._make_ts('cshm_seed_p', self._prefix_purge, 'disk_p.img', enabled=True, purge=False) - self.seed_keep = self._make_ts('cshm_seed_k', self._prefix_keep, 'disk_k.img', enabled=True, purge=False) - - # Disabled instances under test. - self.run_purge = self._make_ts('cshm_run_p', self._prefix_purge, 'disk_p.img', enabled=False, purge=True) - self.run_keep = self._make_ts('cshm_run_k', self._prefix_keep, 'disk_k.img', enabled=False, purge=False) - self.run_noop = self._make_ts('cshm_run_z', self._prefix_noop, 'disk_z.img', enabled=False, purge=True) - - self._add_diags_assertions() - - def _make_ts(self, name, prefix, disk_name, enabled, purge): - disk_path = self._ensure_disk(disk_name) - ts = Test.MakeATSProcess(name) - ts.Disk.storage_yaml.AddLines( - [ - 'cache:', - ' spans:', - ' - name: disk.0', - f' path: {disk_path}', - f' size: {self.DISK_SIZE_BYTES}', - ' volumes:', - ' - id: 1', - ' scheme: http', - ' size: 100%', - ]) - ts.Disk.remap_config.AddLine('map / http://127.0.0.1:8080/') # never exercised; keeps remap.config non-empty - ts.Disk.records_config.update( - { - 'proxy.config.cache.shm.enabled': 1 if enabled else 0, - 'proxy.config.cache.shm.name_prefix': prefix, - 'proxy.config.cache.shm.use_hugepages': 0, - 'proxy.config.cache.shm.purge_stale_on_start': 1 if purge else 0, - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'cache_shm', - 'proxy.config.diags.output.diag': 'L', - 'proxy.config.http.wait_for_cache': 1, - }) - return ts - - def _ensure_disk(self, disk_name): - storage_dir = os.path.join(Test.RunDirectory, 'storage') - os.makedirs(storage_dir, exist_ok=True) - path = os.path.join(storage_dir, disk_name) - if not os.path.exists(path): - with open(path, 'ab') as f: - f.truncate(self.DISK_SIZE_BYTES) - return path - - def _add_diags_assertions(self): - # Seeds create a fresh control segment and mark it clean on the way out -- - # that is the "fast-attachable but now stale" state the purge must clean up. - for seed in (self.seed_purge, self.seed_keep): - seed.Disk.diags_log.Content += Testers.ContainsExpression( - r'cache shm: creating fresh control segment', 'seed should create a fresh shm control segment') - seed.Disk.diags_log.Content += Testers.ContainsExpression( - r'cache shm: marking clean shutdown', 'seed should mark the shm clean before exit') - - # Positive: the disabled+purge instance logs the purge of at least one segment. - self.run_purge.Disk.diags_log.Content += Testers.ContainsExpression( - self.PURGE_NOTE, 'disabled instance with purge_stale_on_start=1 should purge the leftover segments') - - # Negative: purge_stale_on_start=0 must never purge. - self.run_keep.Disk.diags_log.Content += Testers.ExcludesExpression( - r'cache shm: purged stale segments', 'purge_stale_on_start=0 must not purge') - - # No-op: nothing exists for this prefix, so neither a purge nor an error. - self.run_noop.Disk.diags_log.Content += Testers.ExcludesExpression( - r'cache shm: purged stale segments', 'no leftover means nothing is purged') - self.run_noop.Disk.diags_log.Content += Testers.ExcludesExpression( - r'cache shm: cannot open control segment', 'a missing control segment is a quiet no-op, not a warning') - - def _shm_status(self, description, ts, prefix, expect_present): - """Run `traffic_ctl cache shm status` and assert the control segment is (not) there.""" - control_name = prefix + 'control' - tr = Test.AddTestRun(description) - tr.Processes.Default.Env = ts.Env - tr.Processes.Default.Command = f'traffic_ctl cache shm status --prefix {prefix}' - if expect_present: - tr.Processes.Default.ReturnCode = 0 - tr.Processes.Default.Streams.stdout = Testers.ContainsExpression( - r'Control segment:\s+' + re.escape(control_name), 'control segment should be present') - else: - tr.Processes.Default.ReturnCode = self.CTRL_EX_ERROR - tr.Processes.Default.Streams.stderr = Testers.ContainsExpression( - r"control segment '" + re.escape(control_name) + r"' not found", 'control segment should be gone') - return tr - - def _start_seed(self, description, seed, prefix): - # Starting the seed (shm enabled) creates the control + stripe segments; the - # status probe also confirms they exist while the seed is the live owner. - tr = self._shm_status(description, seed, prefix, expect_present=True) - tr.Processes.Default.StartBefore(seed) - tr.StillRunningAfter = seed - return tr - - def _clean_shutdown(self, description, seed, name): - tr = Test.AddTestRun(description) - tr.Processes.Default.Env = seed.Env - tr.Processes.Default.Command = ( - f'traffic_ctl server drain && sleep 1 && ' - f'{sys.executable} ./{self.TS_PID_SCRIPT} {name} --signal TERM && sleep 3') - tr.Processes.Default.ReturnCode = 0 - - def _start_disabled(self, description, ts, prefix, expect_present): - # Start the disabled instance under test; its purge (or no-op) runs during - # cache init, so by the time it is ready the status below reflects the result. - tr = self._shm_status(description, ts, prefix, expect_present=expect_present) - tr.Processes.Default.StartBefore(ts) - tr.StillRunningAfter = ts - return tr - - def _cleanup(self): - tr = Test.AddTestRun('Unlink any remaining test shm segments') - tr.Processes.Default.Env = self.run_keep.Env - tr.Processes.Default.Command = ( - f'traffic_ctl cache shm clear --prefix {self._prefix_purge} ; ' - f'traffic_ctl cache shm clear --prefix {self._prefix_keep} ; ' - f'traffic_ctl cache shm clear --prefix {self._prefix_noop}') - tr.Processes.Default.ReturnCode = 0 - - def run(self): - # PURGE (positive) - self._start_seed('PURGE: start shm-enabled seed; control segment is created', self.seed_purge, self._prefix_purge) - self._clean_shutdown('PURGE: clean-shutdown seed (leaves a clean segment)', self.seed_purge, 'cshm_seed_p') - # Probe with a Env whose bin/ autest has already populated (seed_purge was - # started above); run_purge has not started yet, so its bin/ does not exist. - self._shm_status( - 'PURGE: precondition -- clean leftover segment is present', self.seed_purge, self._prefix_purge, - expect_present=True).Processes.Default.Streams.stdout += Testers.ContainsExpression( - r'clean_shutdown:\s+1 \(clean\)', 'leftover segment should be marked clean (the stale-but-attachable case)') - self._start_disabled( - 'PURGE: start disabled+purge=1; leftover segments are removed', - self.run_purge, - self._prefix_purge, - expect_present=False) - - # KEEP (negative) - self._start_seed('KEEP: start shm-enabled seed; control segment is created', self.seed_keep, self._prefix_keep) - self._clean_shutdown('KEEP: clean-shutdown seed (leaves a clean segment)', self.seed_keep, 'cshm_seed_k') - self._start_disabled( - 'KEEP: start disabled+purge=0; leftover segments remain', self.run_keep, self._prefix_keep, expect_present=True) - - # NOOP (no leftover) - tr = Test.AddTestRun('NOOP: start disabled+purge=1 against an unused prefix; nothing to do') - tr.Processes.Default.Env = self.run_noop.Env - tr.Processes.Default.Command = f'traffic_ctl cache shm status --prefix {self._prefix_noop}' - tr.Processes.Default.ReturnCode = self.CTRL_EX_ERROR # never existed - tr.Processes.Default.StartBefore(self.run_noop) - tr.StillRunningAfter = self.run_noop - - self._cleanup() - - -CacheShmPurgeOnDisableTest().run() diff --git a/tests/gold_tests/cache/cache_shm_schema_mismatch.test.py b/tests/gold_tests/cache/cache_shm_schema_mismatch.test.py deleted file mode 100644 index fac47ebde57..00000000000 --- a/tests/gold_tests/cache/cache_shm_schema_mismatch.test.py +++ /dev/null @@ -1,207 +0,0 @@ -''' -Verify the shm schema-version trust gate: a control segment whose schema_version -does not match the running build is dropped, never fast-attached. ts1 cold-starts, -caches an object, and clean-shuts-down (marking the segment clean). The segment -file under /dev/shm is then tampered -- schema_version is overwritten with a -bogus value -- before ts2 starts against the same shm prefix. ts2 must detect the -mismatch, drop the segment, recreate it fresh, and rebuild the directory from disk. - -Linux-only: it pokes raw bytes in the /dev/shm segment file, which exists only on -Linux (macOS POSIX shm segments are not path-addressable). -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import os -import platform -import sys -import uuid - -Test.Summary = ''' -A control segment with a mismatched schema_version is dropped and rebuilt from -disk, never fast-attached. -''' -Test.ContinueOnFail = True - -# The byte-poke drives the gate by editing /dev/shm directly, which is a Linux -# facility; macOS POSIX shm is not exposed as a file. There is no Condition for -# the platform, so gate with a lambda (ports.py branches on platform the same way). -Test.SkipUnless(Condition(lambda: platform.system() == 'Linux', "shm byte-poke gates need Linux /dev/shm")) - - -class CacheShmSchemaMismatchTest: - """ - The schema-version gate. The control header records the build's - CACHE_SHM_SCHEMA_VERSION; on attach, a segment whose recorded version differs - is dropped ("schema mismatch ( vs ), dropping") rather than trusted - -- the on-disk struct layout it describes may no longer match this build. The - ABI-hash gate (abi_hash @16) works identically; this test exercises the - schema field (@8) as the representative case. - - Sequence: ts1 creates a clean segment, then schema_version is poked to a bogus - value, then ts2 starts and must: - - log the schema mismatch and drop, - - recreate a fresh control segment, - - NOT fast-attach, - - and still serve a request (200). - """ - - TS_PID_SCRIPT = 'ts_process_handler.py' - POKE_SCRIPT = 'shm_poke.py' - - SHARED_DISK_SIZE_BYTES = 256 * 1024 * 1024 # 256 MiB - - # CacheShmControl layout (CacheShmLayout.h): magic[8] @0, schema_version @8. - SCHEMA_VERSION_OFFSET = 8 - # Little-endian uint32 = 9; the build's CACHE_SHM_SCHEMA_VERSION is small, so - # any value it never uses works. 9 is comfortably out of range. - BOGUS_SCHEMA_LE_HEX = '09000000' - - def __init__(self): - self._setup_shared_state() - self.ts1 = self._configure_ts('shmx_ts1') - self.ts2 = self._configure_ts('shmx_ts2') - self._add_diags_log_assertions() - self._url_path = f'/cache/40/{uuid.uuid4()}' - - def _setup_shared_state(self): - Test.Setup.Copy(os.path.join(Test.TestDirectory, '..', 'logging', self.TS_PID_SCRIPT)) - Test.Setup.Copy(os.path.join(Test.TestDirectory, self.POKE_SCRIPT)) - - shared_storage_dir = os.path.join(Test.RunDirectory, 'shared-storage') - os.makedirs(shared_storage_dir, exist_ok=True) - self._shared_storage_path = os.path.join(shared_storage_dir, 'disk.img') - with open(self._shared_storage_path, 'ab') as f: - f.truncate(self.SHARED_DISK_SIZE_BYTES) - - # macOS PSHMNAMLEN is 31 chars incl. '/'; 'x' = schema-mismatch variant. - # (This test is Linux-only, but keep the prefix short for consistency.) - self._shm_prefix = f'/cshmx-{os.getpid() % 100000}-' - # The control segment is name_prefix + "control"; on Linux it is a file - # under /dev/shm by the same name (sans the leading '/'). - self._control_file = '/dev/shm/' + self._shm_prefix.lstrip('/') + 'control' - - def _configure_ts(self, name): - ts = Test.MakeATSProcess(name) - ts.Disk.storage_yaml.AddLines( - [ - 'cache:', - ' spans:', - ' - name: disk.0', - f' path: {self._shared_storage_path}', - f' size: {self.SHARED_DISK_SIZE_BYTES}', - ' volumes:', - ' - id: 1', - ' scheme: http', - ' size: 100%', - ]) - ts.Disk.records_config.update( - { - 'proxy.config.cache.shm.enabled': 1, - 'proxy.config.cache.shm.name_prefix': self._shm_prefix, - 'proxy.config.cache.shm.use_hugepages': 0, - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'cache_shm', - 'proxy.config.diags.output.diag': 'L', - 'proxy.config.http.wait_for_cache': 1, - }) - ts.Disk.plugin_config.AddLine('xdebug.so --enable=x-cache,via') - ts.Disk.remap_config.AddLine('map / http://127.0.0.1/ @plugin=generator.so') - return ts - - def _add_diags_log_assertions(self): - # ts1 cold start, clean shutdown -- a valid, clean segment to tamper with. - self.ts1.Disk.diags_log.Content += Testers.ContainsExpression( - r'cache shm: creating fresh control segment', 'ts1 should create a fresh shm control segment on first start') - self.ts1.Disk.diags_log.Content += Testers.ContainsExpression( - r'cache shm: marking clean shutdown', 'ts1 should mark the shm clean before exit') - - # ts2 start against the poked segment: detect, drop, recreate, rebuild. - self.ts2.Disk.diags_log.Content += Testers.ContainsExpression( - r'cache shm: schema mismatch \(\d+ vs \d+\), dropping', 'ts2 must detect the schema mismatch and drop the segment') - self.ts2.Disk.diags_log.Content += Testers.ContainsExpression( - r'cache shm: creating fresh control segment', 'ts2 must recreate the control segment after the drop') - self.ts2.Disk.diags_log.Content += Testers.ExcludesExpression( - r'\(fast restart, recovery skipped\)', 'ts2 must rebuild from disk, never fast-attach the mismatched segment') - self.ts2.Disk.diags_log.Content += Testers.ExcludesExpression( - r'cache shm: previous run did not shutdown cleanly', - 'the drop must be due to the schema mismatch, not an unclean shutdown') - - def _populate_cache(self): - tr = Test.AddTestRun('Cold-start ts1 and cache an object') - tr.Processes.Default.StartBefore(self.ts1) - tr.MakeCurlCommand( - f'-s -o /dev/null -w "%{{http_code}}\\n" ' - f'-H "x-debug: x-cache,via" ' - f'http://127.0.0.1:{self.ts1.Variables.port}{self._url_path}') - tr.Processes.Default.ReturnCode = 0 - tr.Processes.Default.Streams.stdout = Testers.ContainsExpression('200', 'ts1 first GET should return 200') - tr.StillRunningAfter = self.ts1 - - def _clean_shutdown_ts1(self): - tr = Test.AddTestRun('Drain and clean-shutdown ts1') - tr.Processes.Default.Env = self.ts1.Env - tr.Processes.Default.Command = ( - f'traffic_ctl server drain && sleep 1 && ' - f'{sys.executable} ./{self.TS_PID_SCRIPT} shmx_ts1 --signal TERM && sleep 3') - tr.Processes.Default.ReturnCode = 0 - - def _poke_schema_version(self): - # ts1 is dead; the segment is just a file now. Overwrite schema_version. - tr = Test.AddTestRun('Tamper schema_version in the shm control segment') - tr.Processes.Default.Command = ( - f'{sys.executable} ./{self.POKE_SCRIPT} {self._control_file} ' - f'{self.SCHEMA_VERSION_OFFSET} {self.BOGUS_SCHEMA_LE_HEX}') - tr.Processes.Default.ReturnCode = 0 - - def _verify_mismatch_drop(self): - tr = Test.AddTestRun('Start ts2; verify the schema mismatch is dropped and rebuilt from disk') - tr.Processes.Default.StartBefore(self.ts2) - tr.MakeCurlCommand( - f'-s -o /dev/null -w "%{{http_code}}\\n" ' - f'-H "x-debug: x-cache,via" ' - f'http://127.0.0.1:{self.ts2.Variables.port}{self._url_path}') - tr.Processes.Default.ReturnCode = 0 - tr.Processes.Default.Streams.stdout = Testers.ContainsExpression( - '200', 'ts2 should serve correctly after dropping the mismatched segment') - tr.StillRunningAfter = self.ts2 - - def _clean_shutdown_ts2(self): - tr = Test.AddTestRun('Drain and clean-shutdown ts2') - tr.Processes.Default.Env = self.ts2.Env - tr.Processes.Default.Command = ( - f'traffic_ctl server drain && sleep 1 && ' - f'{sys.executable} ./{self.TS_PID_SCRIPT} shmx_ts2 --signal TERM && sleep 3') - tr.Processes.Default.ReturnCode = 0 - - def _cleanup_shm(self): - tr = Test.AddTestRun('Unlink the test shm segments') - tr.Processes.Default.Env = self.ts2.Env - tr.Processes.Default.Command = f'traffic_ctl cache shm clear --prefix {self._shm_prefix}' - tr.Processes.Default.ReturnCode = 0 - tr.Processes.Default.Streams.stderr = Testers.ExcludesExpression( - 'Invalid argument', 'clear must skip tombstoned slots, not fail on them') - - def run(self): - self._populate_cache() - self._clean_shutdown_ts1() - self._poke_schema_version() - self._verify_mismatch_drop() - self._clean_shutdown_ts2() - self._cleanup_shm() - - -CacheShmSchemaMismatchTest().run() diff --git a/tests/gold_tests/cache/cache_shm_storage_mismatch.test.py b/tests/gold_tests/cache/cache_shm_storage_mismatch.test.py deleted file mode 100644 index ff037fb29ff..00000000000 --- a/tests/gold_tests/cache/cache_shm_storage_mismatch.test.py +++ /dev/null @@ -1,210 +0,0 @@ -''' -Verify that a changed storage layout never fast-attaches a stale directory. -A storage.yaml change no longer drops the whole shm control segment; instead -each stripe is matched to its prior segment by its own identity. ts1 caches an -object against one storage file and clean-shuts-down (marking the shm clean); -ts2 starts against a *different* storage file but the *same* shm name prefix. -ts2 finds ts1's control segment, keeps it (partial attach), but because its -stripe identity no longer matches any recorded entry it creates a fresh stripe -segment and reclaims ts1's orphaned one -- it must never fast-attach a segment -that describes a different on-disk layout. -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import os -import sys -import uuid - -Test.Summary = ''' -A changed storage layout never fast-attaches a stale directory: the control -segment is kept (partial attach), the relocated stripe creates a fresh segment, -and the orphaned prior segment is reclaimed. -''' -Test.ContinueOnFail = True - - -class CacheShmStorageMismatchTest: - """ - The storage signature is a fingerprint of every span's path and geometry, - stored in the shm control header. It is no longer a hard gate: a storage - change keeps the control segment and lets each stripe attach by its own - identity (its hash_text, which includes the disk path). This test points - ts1 and ts2 at different storage files (a repath) while sharing one shm - prefix, and asserts ts2: - - keeps the existing control segment (does NOT recreate it), - - enters partial-attach mode because the storage signature changed, - - never fast-attaches any stripe segment (its identity differs, so the - stale directory built for storage A is never reused), - - creates a fresh stripe segment for its own (storage B) layout, - - reclaims ts1's now-orphaned stripe segment, - - and still serves a request (200). - Because the storage change does not gate the clean-shutdown check, ts2 must - NOT report the prior run as unclean: the only reason for the recreate is the - storage change. - """ - - TS_PID_SCRIPT = 'ts_process_handler.py' - - SHARED_DISK_SIZE_BYTES = 256 * 1024 * 1024 # 256 MiB - - def __init__(self): - self._setup_shared_state() - # ts1 and ts2 share the shm prefix but use different storage files. - self.ts1 = self._configure_ts('shms_ts1', self._storage_path_a) - self.ts2 = self._configure_ts('shms_ts2', self._storage_path_b) - self._add_diags_log_assertions() - self._url_path = f'/cache/40/{uuid.uuid4()}' - - def _setup_shared_state(self): - Test.Setup.Copy(os.path.join(Test.TestDirectory, '..', 'logging', self.TS_PID_SCRIPT)) - - shared_storage_dir = os.path.join(Test.RunDirectory, 'shared-storage') - os.makedirs(shared_storage_dir, exist_ok=True) - # Two distinct storage files -> distinct span paths -> distinct - # storage signatures, which is exactly the "repath" case under test. - self._storage_path_a = os.path.join(shared_storage_dir, 'disk_a.img') - self._storage_path_b = os.path.join(shared_storage_dir, 'disk_b.img') - for path in (self._storage_path_a, self._storage_path_b): - with open(path, 'ab') as f: - f.truncate(self.SHARED_DISK_SIZE_BYTES) - - # macOS PSHMNAMLEN is 31 chars incl. '/'; 's' = storage-mismatch variant. - self._shm_prefix = f'/cshms-{os.getpid() % 100000}-' - - def _configure_ts(self, name, storage_path): - ts = Test.MakeATSProcess(name) - ts.Disk.storage_yaml.AddLines( - [ - 'cache:', - ' spans:', - ' - name: disk.0', - f' path: {storage_path}', - f' size: {self.SHARED_DISK_SIZE_BYTES}', - ' volumes:', - ' - id: 1', - ' scheme: http', - ' size: 100%', - ]) - ts.Disk.records_config.update( - { - 'proxy.config.cache.shm.enabled': 1, - 'proxy.config.cache.shm.name_prefix': self._shm_prefix, - 'proxy.config.cache.shm.use_hugepages': 0, - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'cache_shm', - 'proxy.config.diags.output.diag': 'L', - 'proxy.config.http.wait_for_cache': 1, - }) - ts.Disk.plugin_config.AddLine('xdebug.so --enable=x-cache,via') - ts.Disk.remap_config.AddLine('map / http://127.0.0.1/ @plugin=generator.so') - return ts - - def _add_diags_log_assertions(self): - # ts1 cold start against storage A, clean shutdown. - self.ts1.Disk.diags_log.Content += Testers.ContainsExpression( - r'cache shm: creating fresh control segment', 'ts1 should create a fresh shm control segment on first start') - self.ts1.Disk.diags_log.Content += Testers.ContainsExpression( - r'created stripe \S+ \(\d+ bytes\) for key=', 'ts1 should create at least one shm-backed stripe segment') - self.ts1.Disk.diags_log.Content += Testers.ContainsExpression( - r'cache shm: marking clean shutdown', 'ts1 should mark the shm clean before exit') - - # ts2 start against storage B: the storage signature differs, so the - # control segment is kept (partial attach) but the relocated stripe - # creates a fresh segment rather than fast-attaching the stale one. - self.ts2.Disk.diags_log.Content += Testers.ContainsExpression( - r'attaching up to \d+ stripes \(fast restart, partial -- storage changed\)', - 'ts2 must enter partial-attach mode after the storage change') - self.ts2.Disk.diags_log.Content += Testers.ContainsExpression( - r'created stripe \S+ \(\d+ bytes\) for key=', 'ts2 must create a fresh stripe segment for its own layout') - self.ts2.Disk.diags_log.Content += Testers.ContainsExpression( - r'cache shm: reclaiming orphaned stripe segment', "ts2 must reclaim ts1's orphaned stripe segment") - self.ts2.Disk.diags_log.Content += Testers.ContainsExpression( - r'reclaimed \d+ orphaned stripe segment\(s\) after attach', 'ts2 must report the reclaim summary') - self.ts2.Disk.diags_log.Content += Testers.ExcludesExpression( - r'attached stripe \S+ \(\d+ bytes\) for key=', - 'ts2 must never fast-attach a stripe segment built for a different layout') - self.ts2.Disk.diags_log.Content += Testers.ExcludesExpression( - r'cache shm: creating fresh control segment', 'ts2 must keep the control segment across the storage change') - self.ts2.Disk.diags_log.Content += Testers.ExcludesExpression( - r'cache shm: (schema|ABI) mismatch', 'the recreate must be due to the storage change, not schema/ABI') - self.ts2.Disk.diags_log.Content += Testers.ExcludesExpression( - r'cache shm: previous run did not shutdown cleanly', - 'the recreate must be due to the storage change, not an unclean shutdown') - - def _populate_cache(self): - tr = Test.AddTestRun('Populate cache via ts1 (storage A)') - tr.Processes.Default.StartBefore(self.ts1) - tr.MakeCurlCommand( - f'-s -o /dev/null -w "%{{http_code}}\\n" ' - f'-H "x-debug: x-cache,via" ' - f'http://127.0.0.1:{self.ts1.Variables.port}{self._url_path}') - tr.Processes.Default.ReturnCode = 0 - tr.Processes.Default.Streams.stdout = Testers.ContainsExpression('200', 'ts1 first GET should return 200') - tr.StillRunningAfter = self.ts1 - - def _clean_shutdown_ts1(self): - tr = Test.AddTestRun('Drain and clean-shutdown ts1') - tr.Processes.Default.Env = self.ts1.Env - tr.Processes.Default.Command = ( - f'traffic_ctl server drain && sleep 1 && ' - f'{sys.executable} ./{self.TS_PID_SCRIPT} shms_ts1 --signal TERM && sleep 3') - tr.Processes.Default.ReturnCode = 0 - - def _verify_partial_attach_and_reclaim(self): - tr = Test.AddTestRun('Start ts2 (storage B); verify partial attach: fresh stripe + orphan reclaim') - tr.Processes.Default.StartBefore(self.ts2) - tr.MakeCurlCommand( - f'-s -o /dev/null -w "%{{http_code}}\\n" ' - f'-H "x-debug: x-cache,via" ' - f'http://127.0.0.1:{self.ts2.Variables.port}{self._url_path}') - tr.Processes.Default.ReturnCode = 0 - tr.Processes.Default.Streams.stdout = Testers.ContainsExpression( - '200', 'ts2 should serve correctly after the partial attach') - tr.StillRunningAfter = self.ts2 - - def _clean_shutdown_ts2(self): - # Stop ts2 before clearing the shm: `cache shm clear` refuses to unlink a - # segment a live traffic_server still owns, so the owner must be gone - # (its retained owner_pid then reads as dead) before cleanup runs. - tr = Test.AddTestRun('Drain and clean-shutdown ts2') - tr.Processes.Default.Env = self.ts2.Env - tr.Processes.Default.Command = ( - f'traffic_ctl server drain && sleep 1 && ' - f'{sys.executable} ./{self.TS_PID_SCRIPT} shms_ts2 --signal TERM && sleep 3') - tr.Processes.Default.ReturnCode = 0 - - def _cleanup_shm(self): - # A clean shutdown deliberately keeps the control + live stripe segments - # for the next fast restart, so they outlive the test. Unlink them by - # prefix to avoid leaking POSIX shm across repeated local runs (macOS has - # no /dev/shm to clear out of band). - tr = Test.AddTestRun('Unlink the test shm segments') - tr.Processes.Default.Env = self.ts2.Env - tr.Processes.Default.Command = f'traffic_ctl cache shm clear --prefix {self._shm_prefix}' - tr.Processes.Default.ReturnCode = 0 - tr.Processes.Default.Streams.stderr = Testers.ExcludesExpression( - 'Invalid argument', 'clear must skip tombstoned slots, not fail on them') - - def run(self): - self._populate_cache() - self._clean_shutdown_ts1() - self._verify_partial_attach_and_reclaim() - self._clean_shutdown_ts2() - self._cleanup_shm() - - -CacheShmStorageMismatchTest().run() diff --git a/tests/gold_tests/cache/cache_shm_unclean_shutdown.test.py b/tests/gold_tests/cache/cache_shm_unclean_shutdown.test.py deleted file mode 100644 index 4711636c70b..00000000000 --- a/tests/gold_tests/cache/cache_shm_unclean_shutdown.test.py +++ /dev/null @@ -1,185 +0,0 @@ -''' -Verify the shm fast-restart path refuses to trust a directory left by a crash. -A clean shutdown marks the control segment clean; a crash (SIGKILL) does not, -so clean_shutdown stays 0. ts1 cold-starts, caches an object, and is *killed* -(no drain, no SIGTERM) so the shutdown hook never runs. ts2 starts against the -same on-disk cache and shm prefix: it must find the dirty segment, drop the -whole thing, rebuild the directory from disk, and never take the fast-attach -"recovery skipped" path. -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import os -import sys -import uuid - -Test.Summary = ''' -An unclean shutdown (SIGKILL) leaves the shm control segment dirty, so the next -start drops it and rebuilds the directory from disk instead of fast-attaching. -''' -Test.ContinueOnFail = True - - -class CacheShmUncleanShutdownTest: - """ - The crash-safety gate. clean_shutdown is set to 1 only by the shutdown hook - (CacheShm::mark_clean_shutdown); a SIGKILL bypasses it, leaving the segment - with clean_shutdown == 0. On the next start the control segment is found but - rejected -- a crash may have left dir entries pointing at content that never - reached disk, so no stripe can safely skip recovery. ts2 must: - - log "previous run did not shutdown cleanly, dropping", - - recreate a fresh control segment, - - NOT take the stripe fast-attach "recovery skipped" path, - - and still serve a request (200). - - This gate is cross-platform: clean_shutdown lives in the control segment, so - it does not depend on the Linux-only flock path. - """ - - TS_PID_SCRIPT = 'ts_process_handler.py' - - SHARED_DISK_SIZE_BYTES = 256 * 1024 * 1024 # 256 MiB - - def __init__(self): - self._setup_shared_state() - # ts1 and ts2 share the same on-disk cache file and shm prefix so ts2 - # would fast-attach ts1's directory -- were it not left dirty by the kill. - self.ts1 = self._configure_ts('shmu_ts1') - # ts1 is SIGKILLed mid-test, so it exits on signal 9 (returncode -9, or - # 137 where the runner reports 128+signal). Declare that expected exit so - # the managed-process check does not flag the deliberate kill. ts1 still - # starts normally, so leave Ready at its default (port-open) condition. - self.ts1.ReturnCode = Any(-9, 137) - self.ts2 = self._configure_ts('shmu_ts2') - self._add_diags_log_assertions() - self._url_path = f'/cache/40/{uuid.uuid4()}' - - def _setup_shared_state(self): - Test.Setup.Copy(os.path.join(Test.TestDirectory, '..', 'logging', self.TS_PID_SCRIPT)) - - shared_storage_dir = os.path.join(Test.RunDirectory, 'shared-storage') - os.makedirs(shared_storage_dir, exist_ok=True) - self._shared_storage_path = os.path.join(shared_storage_dir, 'disk.img') - with open(self._shared_storage_path, 'ab') as f: - f.truncate(self.SHARED_DISK_SIZE_BYTES) - - # macOS PSHMNAMLEN is 31 chars incl. '/'; 'u' = unclean-shutdown variant. - self._shm_prefix = f'/cshmu-{os.getpid() % 100000}-' - - def _configure_ts(self, name): - ts = Test.MakeATSProcess(name) - ts.Disk.storage_yaml.AddLines( - [ - 'cache:', - ' spans:', - ' - name: disk.0', - f' path: {self._shared_storage_path}', - f' size: {self.SHARED_DISK_SIZE_BYTES}', - ' volumes:', - ' - id: 1', - ' scheme: http', - ' size: 100%', - ]) - ts.Disk.records_config.update( - { - 'proxy.config.cache.shm.enabled': 1, - 'proxy.config.cache.shm.name_prefix': self._shm_prefix, - 'proxy.config.cache.shm.use_hugepages': 0, - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'cache_shm', - 'proxy.config.diags.output.diag': 'L', - 'proxy.config.http.wait_for_cache': 1, - }) - ts.Disk.plugin_config.AddLine('xdebug.so --enable=x-cache,via') - ts.Disk.remap_config.AddLine('map / http://127.0.0.1/ @plugin=generator.so') - return ts - - def _add_diags_log_assertions(self): - # ts1 cold start: creates a fresh segment but is killed before it can mark - # the shutdown clean. - self.ts1.Disk.diags_log.Content += Testers.ContainsExpression( - r'cache shm: creating fresh control segment', 'ts1 should create a fresh shm control segment on first start') - self.ts1.Disk.diags_log.Content += Testers.ExcludesExpression( - r'cache shm: marking clean shutdown', 'ts1 is SIGKILLed, so it must never mark the shm clean') - - # ts2 start: finds the dirty segment, drops it, recreates, and rebuilds - # from disk -- it must NOT fast-attach. - self.ts2.Disk.diags_log.Content += Testers.ContainsExpression( - r'cache shm: previous run did not shutdown cleanly, dropping', 'ts2 must reject the dirty segment left by the crash') - self.ts2.Disk.diags_log.Content += Testers.ContainsExpression( - r'cache shm: creating fresh control segment', 'ts2 must recreate the control segment after dropping the dirty one') - self.ts2.Disk.diags_log.Content += Testers.ExcludesExpression( - r'\(fast restart, recovery skipped\)', 'ts2 must rebuild from disk, never take the fast-attach path') - self.ts2.Disk.diags_log.Content += Testers.ExcludesExpression( - r'cache shm: attaching up to \d+ stripes \(fast restart', 'ts2 must not attach the dirty control segment') - - def _populate_cache(self): - tr = Test.AddTestRun('Cold-start ts1 and cache an object') - tr.Processes.Default.StartBefore(self.ts1) - tr.MakeCurlCommand( - f'-s -o /dev/null -w "%{{http_code}}\\n" ' - f'-H "x-debug: x-cache,via" ' - f'http://127.0.0.1:{self.ts1.Variables.port}{self._url_path}') - tr.Processes.Default.ReturnCode = 0 - tr.Processes.Default.Streams.stdout = Testers.ContainsExpression('200', 'ts1 first GET should return 200') - tr.StillRunningAfter = self.ts1 - - def _kill_ts1(self): - # SIGKILL -- no drain, no SIGTERM -- so the shutdown hook never runs and - # the control segment is left with clean_shutdown == 0. - tr = Test.AddTestRun('SIGKILL ts1 (unclean shutdown)') - tr.Processes.Default.Env = self.ts1.Env - tr.Processes.Default.Command = (f'{sys.executable} ./{self.TS_PID_SCRIPT} shmu_ts1 --signal KILL && sleep 3') - tr.Processes.Default.ReturnCode = 0 - - def _verify_dirty_drop(self): - tr = Test.AddTestRun('Start ts2; verify the dirty segment is dropped and rebuilt from disk') - tr.Processes.Default.StartBefore(self.ts2) - tr.MakeCurlCommand( - f'-s -o /dev/null -w "%{{http_code}}\\n" ' - f'-H "x-debug: x-cache,via" ' - f'http://127.0.0.1:{self.ts2.Variables.port}{self._url_path}') - tr.Processes.Default.ReturnCode = 0 - tr.Processes.Default.Streams.stdout = Testers.ContainsExpression( - '200', 'ts2 should serve correctly after dropping the dirty segment') - tr.StillRunningAfter = self.ts2 - - def _clean_shutdown_ts2(self): - tr = Test.AddTestRun('Drain and clean-shutdown ts2') - tr.Processes.Default.Env = self.ts2.Env - tr.Processes.Default.Command = ( - f'traffic_ctl server drain && sleep 1 && ' - f'{sys.executable} ./{self.TS_PID_SCRIPT} shmu_ts2 --signal TERM && sleep 3') - tr.Processes.Default.ReturnCode = 0 - - def _cleanup_shm(self): - tr = Test.AddTestRun('Unlink the test shm segments') - tr.Processes.Default.Env = self.ts2.Env - tr.Processes.Default.Command = f'traffic_ctl cache shm clear --prefix {self._shm_prefix}' - tr.Processes.Default.ReturnCode = 0 - tr.Processes.Default.Streams.stderr = Testers.ExcludesExpression( - 'Invalid argument', 'clear must skip tombstoned slots, not fail on them') - - def run(self): - self._populate_cache() - self._kill_ts1() - self._verify_dirty_drop() - self._clean_shutdown_ts2() - self._cleanup_shm() - - -CacheShmUncleanShutdownTest().run() diff --git a/tests/gold_tests/cache/disjoint-wait-for-cache.test.py b/tests/gold_tests/cache/disjoint-wait-for-cache.test.py deleted file mode 100644 index c9377645d0a..00000000000 --- a/tests/gold_tests/cache/disjoint-wait-for-cache.test.py +++ /dev/null @@ -1,104 +0,0 @@ -''' -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import uuid - -Test.Summary = ''' -Same as cache-generation-disjoint, but uses proxy.config.http.wait_for_cache which should delay -the server from accepting connection till the cache is loaded -''' - -Test.ContinueOnFail = True -# Define default ATS -ts = Test.MakeATSProcess("ts") - -# Setup some config file for this server. Note that setting wait_for_cache to 3 -# will intentionally override the value set in MakeATSProcess. -ts.Disk.records_config.update( - { - 'proxy.config.body_factory.enable_customizations': 3, # enable domain specific body factory - 'proxy.config.http.cache.generation': -1, # Start with cache turned off - 'proxy.config.http.wait_for_cache': 3, - }) -ts.Disk.plugin_config.AddLine('xdebug.so --enable=x-cache,x-cache-key,via,x-cache-generation') -ts.Disk.remap_config.AddLines( - [ - 'map /default/ http://127.0.0.1/ @plugin=generator.so', - # line 2 - 'map /generation1/ http://127.0.0.1/' + ' @plugin=conf_remap.so @pparam=proxy.config.http.cache.generation=1' + - ' @plugin=generator.so', - # line 3 - 'map /generation2/ http://127.0.0.1/' + ' @plugin=conf_remap.so @pparam=proxy.config.http.cache.generation=2' + - ' @plugin=generator.so', - ]) - -objectid = uuid.uuid4() -# first test is a miss for default -tr = Test.AddTestRun() -tr.MakeCurlCommand( - '"http://127.0.0.1:{0}/default/cache/10/{1}" -H "x-debug: x-cache,x-cache-key,via,x-cache-generation" --verbose'.format( - ts.Variables.port, objectid), - ts=ts) -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.StartBefore(Test.Processes.ts) -tr.Processes.Default.Streams.All = "gold/miss_default-1.gold" - -# Same URL in generation 1 is a MISS. -tr = Test.AddTestRun() -tr.MakeCurlCommand( - '"http://127.0.0.1:{0}/generation1/cache/10/{1}" -H "x-debug: x-cache,x-cache-key,via,x-cache-generation" --verbose -o /dev/null' - .format(ts.Variables.port, objectid), - ts=ts) -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Streams.All = "gold/miss_gen1.gold" - -# Same URL in generation 2 is still a MISS. -tr = Test.AddTestRun() -tr.MakeCurlCommand( - '"http://127.0.0.1:{0}/generation2/cache/10/{1}" -H "x-debug: x-cache,x-cache-key,via,x-cache-generation" --verbose -o /dev/null' - .format(ts.Variables.port, objectid), - ts=ts) -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Streams.All = "gold/miss_gen2.gold" - -# Second touch is a HIT for default. -tr = Test.AddTestRun() -tr.MakeCurlCommand( - '"http://127.0.0.1:{0}/default/cache/10/{1}" -H "x-debug: x-cache,x-cache-key,via,x-cache-generation" --verbose -o /dev/null' - .format(ts.Variables.port, objectid), - ts=ts) -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Streams.All = "gold/hit_default-1.gold" - -# Second touch is a HIT for generation1. -tr = Test.AddTestRun() -tr.MakeCurlCommand( - '"http://127.0.0.1:{0}/generation1/cache/10/{1}" -H "x-debug: x-cache,x-cache-key,via,x-cache-generation" --verbose -o /dev/null' - .format(ts.Variables.port, objectid), - ts=ts) -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Streams.All = "gold/hit_gen1.gold" - -# Second touch is a HIT for generation2. -tr = Test.AddTestRun() -tr.MakeCurlCommand( - '"http://127.0.0.1:{0}/generation2/cache/10/{1}" -H "x-debug: x-cache,x-cache-key,via,x-cache-generation" --verbose -o /dev/null' - .format(ts.Variables.port, objectid), - ts=ts) -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Streams.All = "gold/hit_gen2.gold" diff --git a/tests/gold_tests/cache/host_down_range_recursion.test.py b/tests/gold_tests/cache/host_down_range_recursion.test.py deleted file mode 100644 index db3711fdaf1..00000000000 --- a/tests/gold_tests/cache/host_down_range_recursion.test.py +++ /dev/null @@ -1,81 +0,0 @@ -''' -Verify that a Range request to a DOWN host backed by a cache HIT does not -trigger unbounded recursion. -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -from jsonrpc import Request - -Test.Summary = ''' -Verify that a Range request the cache cannot satisfy against a host marked -DOWN does not trigger unbounded recursion in HttpTransact. -''' - - -class HostDownRangeRecursionTest: - prime_replay = "replay/host_down_range_recursion_prime.replay.yaml" - range_replay = "replay/host_down_range_recursion_range.replay.yaml" - - def __init__(self): - # Same server process serves both replay files. Only the prime phase - # actually reaches the server; the range phase is expected to be - # short-circuited by the DOWN host check. - self._server = Test.MakeVerifierServerProcess("server", self.prime_replay) - self._configure_ts() - - def _configure_ts(self): - self._ts = Test.MakeATSProcess("ts", enable_cache=True) - - self._ts.Disk.records_config.update( - { - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'http|host_statuses', - 'proxy.config.http.cache.range.write': 1, - 'proxy.config.http.insert_response_via_str': 3, - }) - - self._ts.Disk.remap_config.AddLine(f'map http://backend.example.com/ http://127.0.0.1:{self._server.Variables.http_port}/') - - def run(self): - # Phase 1: prime the cache with the full object. - prime = Test.AddTestRun("Prime cache with full object") - prime.AddVerifierClientProcess("prime-client", self.prime_replay, http_ports=[self._ts.Variables.port]) - prime.Processes.Default.StartBefore(self._server) - prime.Processes.Default.StartBefore(self._ts) - prime.StillRunningAfter = self._server - prime.StillRunningAfter = self._ts - - # Phase 2: mark the origin host DOWN via JSON-RPC. - mark_down = Test.AddTestRun("Mark host DOWN") - mark_down.AddJsonRPCClientRequest( - self._ts, Request.admin_host_set_status(operation='down', host=['127.0.0.1'], reason='manual', time='0')) - mark_down.StillRunningAfter = self._server - mark_down.StillRunningAfter = self._ts - - # Phase 3: send an out-of-order multi-range request. The cache cannot - # satisfy the range, so ATS must fall back to the origin; with the host - # marked DOWN it should return 502 Bad Gateway. This is a regression - # test for a stack-overflow crash where this scenario instead drove an - # unbounded recursion between the cache-fallback and DNS-lookup paths. - range_run = Test.AddTestRun("Out-of-order Range against DOWN host") - range_run.AddVerifierClientProcess("range-client", self.range_replay, http_ports=[self._ts.Variables.port]) - range_run.Processes.Default.TimeOut = 10 - range_run.StillRunningAfter = self._server - range_run.StillRunningAfter = self._ts - - -HostDownRangeRecursionTest().run() diff --git a/tests/gold_tests/cache/negative-caching.test.py b/tests/gold_tests/cache/negative-caching.test.py deleted file mode 100644 index a949751f0be..00000000000 --- a/tests/gold_tests/cache/negative-caching.test.py +++ /dev/null @@ -1,125 +0,0 @@ -''' -Test negative caching. -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -Test.Summary = ''' -Test negative caching. -''' - -# -# Verify correct proxy.config.http.negative_caching_lifetime behavior. -# These tests require multiple test runs with shared ATS processes, so use class-based approach. -# -ts = Test.MakeATSProcess("ts-lifetime") -ts.Disk.records_config.update( - { - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'http', - 'proxy.config.http.insert_age_in_response': 0, - 'proxy.config.http.negative_caching_enabled': 1, - 'proxy.config.http.negative_caching_lifetime': 2 - }) -# This should all behave the same as the default enabled case above. -tr = Test.AddTestRun("Add a 404 response to the cache") -replay_file = "replay/negative-caching-default.replay.yaml" -server = tr.AddVerifierServerProcess("server-lifetime-no-cc", replay_file) -# Use the same port across the two servers so that the remap config will work -# across both. -server_port = server.Variables.http_port -tr.AddVerifierClientProcess("client-lifetime-no-cc", replay_file, http_ports=[ts.Variables.port]) -ts.Disk.remap_config.AddLine('map / http://127.0.0.1:{0}'.format(server_port)) -tr.Processes.Default.StartBefore(ts) -tr.StillRunningAfter = ts - -# Wait enough time that the item should be aged out of the cache. -tr = Test.AddTestRun("Wait for cached object to be stale.") -tr.Processes.Default.Command = "sleep 4" -tr.StillRunningAfter = ts - -# Verify the item is retrieved from the server instead of the cache. -replay_file = "replay/negative-caching-timeout.replay.yaml" -tr = Test.AddTestRun("Make sure object is stale") -tr.AddVerifierServerProcess("server-timeout", replay_file, http_ports=[server_port]) -tr.AddVerifierClientProcess("client-timeout", replay_file, http_ports=[ts.Variables.port]) -tr.StillRunningAfter = ts - -# -# Verify that the server's Cache-Control overrides the -# proxy.config.http.negative_caching_lifetime. -# -ts = Test.MakeATSProcess("ts-lifetime-2") -ts.Disk.records_config.update( - { - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'http', - 'proxy.config.http.insert_age_in_response': 0, - 'proxy.config.http.negative_caching_enabled': 1, - 'proxy.config.http.negative_caching_lifetime': 2 - }) -tr = Test.AddTestRun("Add a 404 response with explicit max-age=300 to the cache") -replay_file = "replay/negative-caching-300-second-timeout.replay.yaml" -server = tr.AddVerifierServerProcess("server-lifetime-cc", replay_file) -# Use the same port across the two servers so that the remap config will work -# across both. -server_port = server.Variables.http_port -tr.AddVerifierClientProcess("client-lifetime-cc", replay_file, http_ports=[ts.Variables.port]) -ts.Disk.remap_config.AddLine('map / http://127.0.0.1:{0}'.format(server_port)) -tr.Processes.Default.StartBefore(ts) -tr.StillRunningAfter = ts - -# Wait enough time that the item should be aged out of the cache if -# proxy.config.http.negative_caching_lifetime is incorrectly used. -tr = Test.AddTestRun("Wait for cached object to be stale if lifetime is incorrectly used.") -tr.Processes.Default.Command = "sleep 4" -tr.StillRunningAfter = ts - -# Verify the item is retrieved from the cache instead of going to the origin. -replay_file = "replay/negative-caching-no-timeout.replay.yaml" -tr = Test.AddTestRun("Make sure object is fresh") -tr.AddVerifierServerProcess("server-no-timeout", replay_file, http_ports=[server_port]) -tr.AddVerifierClientProcess("client-no-timeout", replay_file, http_ports=[ts.Variables.port]) -tr.StillRunningAfter = ts - -# -# Verify that negative_caching_lifetime is respected even when cache.config -# has ttl-in-cache configured. -# -replay_file = "replay/negative-caching-ttl-in-cache.replay.yaml" -tr = Test.AddTestRun("Verify negative_caching_lifetime and ttl-in-cache interaction.") -dns = tr.MakeDNServer("dns", default="127.0.0.1") -server = tr.AddVerifierServerProcess("server-ttl-in-cache", replay_file) -server_port = server.Variables.http_port -ts = tr.MakeATSProcess("ts-ttl-in-cache") -ts.Disk.records_config.update( - { - 'proxy.config.dns.nameservers': f"127.0.0.1:{dns.Variables.Port}", - 'proxy.config.dns.resolv_conf': 'NULL', - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'http', - 'proxy.config.http.insert_age_in_response': 0, - 'proxy.config.http.negative_caching_enabled': 1, - 'proxy.config.http.negative_caching_lifetime': 2 - }) -ts.Disk.remap_config.AddLine(f'map / http://backend.example.com:{server_port}') -# Configure cache.config with a long ttl-in-cache that should NOT override -# negative_caching_lifetime for negative responses. -ts.Disk.cache_config.AddLine('dest_domain=backend.example.com ttl-in-cache=30d') -p = tr.AddVerifierClientProcess("client-ttl-in-cache", replay_file, http_ports=[ts.Variables.port]) -p.StartBefore(dns) -p.StartBefore(server) -p.StartBefore(ts) diff --git a/tests/gold_tests/cache/proxy_serve_stale_dns_fail.test.py b/tests/gold_tests/cache/proxy_serve_stale_dns_fail.test.py deleted file mode 100644 index 3e6624d1d88..00000000000 --- a/tests/gold_tests/cache/proxy_serve_stale_dns_fail.test.py +++ /dev/null @@ -1,81 +0,0 @@ -''' -Test proxy serving stale content when DNS lookup fails -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -Test.ContinueOnFail = True -# Set up hierarchical caching processes -ts_child = Test.MakeATSProcess("ts_child") -ts_parent = Test.MakeATSProcess("ts_parent", enable_uds=False) -nameserver = Test.MakeDNServer("dns") -server_name = "http://unknown.domain.com/" - -Test.testName = "STALE" - -# Config child proxy to route to parent proxy -ts_child.Disk.records_config.update( - { - 'proxy.config.http.push_method_enabled': 1, - 'proxy.config.url_remap.pristine_host_hdr': 1, - 'proxy.config.http.cache.max_stale_age': 10, - 'proxy.config.http.parent_proxy.self_detect': 0, - 'proxy.config.dns.nameservers': f"127.0.0.1:{nameserver.Variables.Port}", - }) -ts_child.Disk.parent_config.AddLine( - f'dest_domain=. parent=localhost:{ts_parent.Variables.port} round_robin=consistent_hash go_direct=false') -ts_child.Disk.remap_config.AddLine(f'map http://localhost:{ts_child.Variables.port} {server_name}') - -# Configure parent proxy -ts_parent.Disk.records_config.update( - { - 'proxy.config.http.push_method_enabled': 1, - 'proxy.config.url_remap.pristine_host_hdr': 1, - 'proxy.config.http.cache.max_stale_age': 10, - 'proxy.config.dns.nameservers': f"127.0.0.1:{nameserver.Variables.Port}", - }) -ts_parent.Disk.remap_config.AddLine(f'map http://localhost:{ts_parent.Variables.port} {server_name}') -ts_parent.Disk.remap_config.AddLine(f'map {server_name} {server_name}') - -# Configure nameserver -nameserver.addRecords(records={"localhost": ["127.0.0.1"]}) - -# Object to push to proxies -stale_5 = "HTTP/1.1 200 OK\nServer: ATS/10.0.0\nAccept-Ranges: bytes\nContent-Length: 6\nCache-Control: public, max-age=5\n\nCACHED" -stale_10 = "HTTP/1.1 200 OK\nServer: ATS/10.0.0\nAccept-Ranges: bytes\nContent-Length: 6\nCache-Control: public, max-age=10\n\nCACHED" - -# Testing scenarios -child_curl_request = ( - # Test child serving stale with failed DNS OS lookup - f'{{curl}} -X PUSH -d "{stale_5}" "http://localhost:{ts_child.Variables.port}";' - f'{{curl}} -X PUSH -d "{stale_10}" "http://localhost:{ts_parent.Variables.port}";' - f'sleep 7; {{curl}} -s -v http://localhost:{ts_child.Variables.port};' - f'sleep 17; {{curl}} -s -v http://localhost:{ts_child.Variables.port};' - # Test parent serving stale with failed DNS OS lookup - f'{{curl_base}} -X PUSH -d "{stale_5}" "http://localhost:{ts_parent.Variables.port}";' - f'sleep 7; {{curl_base}} -s -v http://localhost:{ts_parent.Variables.port};' - f'sleep 17; {{curl_base}} -s -v http://localhost:{ts_parent.Variables.port};') - -# Test case for when parent server is down but child proxy can serve cache object -tr = Test.AddTestRun() -tr.MakeCurlCommandMulti(child_curl_request, ts=ts_child) -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.StartBefore(ts_child) -tr.Processes.Default.StartBefore(ts_parent) -tr.Processes.Default.StartBefore(nameserver) -tr.Processes.Default.Streams.stderr = "gold/serve_stale_dns_fail.gold" -tr.StillRunningAfter = ts_child -tr.StillRunningAfter = ts_parent diff --git a/tests/gold_tests/cache/storage-metrics.test.py b/tests/gold_tests/cache/storage-metrics.test.py deleted file mode 100644 index 006edcbc3e9..00000000000 --- a/tests/gold_tests/cache/storage-metrics.test.py +++ /dev/null @@ -1,129 +0,0 @@ -''' -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import os - -Test.Summary = 'Test loading storage metrics' -Test.ContinueOnFail = True - -# CAVEAT: Below test cases doesn't have multiple span cases that requires RAW devices. -test_cases = [ - { - "case": 0, - "description": "default config", - "storage": ''' -cache: - spans: - - name: disk.0 - path: storage - size: 256M -''' - }, - { - "case": 1, - "description": "four equally divided volumes", - "storage": - ''' -cache: - spans: - - name: disk.0 - path: storage - size: 1G - volumes: - - id: 1 - size: 25% - - id: 2 - size: 25% - - id: 3 - size: 25% - - id: 4 - size: 25% -''' - }, - { - "case": 2, - "description": "exclusive span", - "storage": - ''' -cache: - spans: - - name: disk.0 - path: storage - size: 256M - volumes: - - id: 1 - spans: - - use: disk.0 - size: 100% -''', - "hosting": ''' -hostname=* volume=1 -''' - }, - { - "case": 3, - "description": "two equally divided volumes without size option", - "storage": - ''' -cache: - spans: - - name: disk.0 - path: storage - size: 512M - volumes: - - id: 1 - - id: 2 -''' - }, -] - - -class StorageMetricsTest: - """ - Test loading storage.yaml - - 1. Spawn TS process with configs in test_cases - 2. Get 'proxy.process.cache.*' metrics - 3. Check with 'gold/storage_N_stdout.gold' file - """ - - def run(self): - for config in test_cases: - i = config["case"] - ts = Test.MakeATSProcess(f"ts_{i}") - ts.Disk.storage_yaml.AddLine(config["storage"]) - - if "hosting" in config: - ts.Disk.hosting_config.AddLine(config["hosting"]) - - ts.Disk.records_config.update({ - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'cache', - }) - - tr = Test.AddTestRun() - - tr.Processes.Default.StartBefore(ts) - tr.Processes.Default.Env = ts.Env - tr.Processes.Default.Command = 'traffic_ctl --debug metric match proxy.process.cache.' - tr.Processes.Default.ReturnCode = 0 - tr.Processes.Default.Streams.stdout = f"gold/storage_metrics_{i}_stdout.gold" - tr.StillRunningAfter = ts - - -StorageMetricsTest().run() diff --git a/tests/gold_tests/chunked_encoding/bad_chunked_encoding.test.py b/tests/gold_tests/chunked_encoding/bad_chunked_encoding.test.py deleted file mode 100644 index 46638f315aa..00000000000 --- a/tests/gold_tests/chunked_encoding/bad_chunked_encoding.test.py +++ /dev/null @@ -1,223 +0,0 @@ -''' -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import os - -Test.Summary = ''' -Test unsupported values for chunked_encoding -''' - -Test.ContinueOnFail = True - -# Define default ATS -ts = Test.MakeATSProcess("ts1") -server = Test.MakeOriginServer("server") - -testName = "" -request_header = { - "headers": "POST /case1 HTTP/1.1\r\nHost: www.example.com\r\nuuid:1\r\n\r\n", - "timestamp": "1469733493.993", - "body": "stuff" -} -response_header = { - "headers": "HTTP/1.1 200 OK\r\nServer: uServer\r\nConnection: close\r\nTransfer-Encoding: chunked\r\n\r\n", - "timestamp": "1469733493.993", - "body": "more stuff" -} - -server.addResponse("sessionlog.json", request_header, response_header) - -ts.Disk.records_config.update({'proxy.config.diags.debug.enabled': 0, 'proxy.config.diags.debug.tags': 'http'}) - -ts.Disk.remap_config.AddLine('map / http://127.0.0.1:{0}'.format(server.Variables.Port)) - -# HTTP1.1 POST: www.example.com/case1 with gzip transfer-encoding -tr = Test.AddTestRun() -tr.TimeOut = 5 -tr.MakeCurlCommand( - '-H "host: example.com" -H "transfer-encoding: gzip" -d "stuff" http://127.0.0.1:{0}/case1 --verbose'.format( - ts.Variables.port), - ts=ts) -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.StartBefore(server) -tr.Processes.Default.StartBefore(ts) -tr.Processes.Default.Streams.All = Testers.ContainsExpression("501 Field not implemented", "Should fail") -tr.Processes.Default.Streams.All = Testers.ExcludesExpression("200 OK", "Should not succeed") -tr.StillRunningAfter = server -tr.StillRunningAfter = ts - -# HTTP1.1 POST: www.example.com/case1 with gzip and chunked transfer-encoding -tr = Test.AddTestRun() -tr.TimeOut = 5 -tr.MakeCurlCommand( - '-H "host: example.com" -H "transfer-encoding: gzip" -H "transfer-encoding: chunked" -d "stuff" http://127.0.0.1:{0}/case1 --verbose' - .format(ts.Variables.port), - ts=ts) -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Streams.All = Testers.ContainsExpression("501 Field not implemented", "Should fail") -tr.Processes.Default.Streams.All = Testers.ExcludesExpression("200 OK", "Should not succeed") -tr.StillRunningAfter = server -tr.StillRunningAfter = ts - - -class HTTP10Test: - chunkedReplayFile = "replays/chunked_in_http_1_0.replay.yaml" - - def __init__(self): - self.setupOriginServer() - self.setupTS() - - def setupOriginServer(self): - self.server = Test.MakeVerifierServerProcess("verifier-server1", self.chunkedReplayFile) - - def setupTS(self): - self.ts = Test.MakeATSProcess("ts2", enable_tls=True, enable_cache=False) - self.ts.addDefaultSSLFiles() - self.ts.Disk.records_config.update( - { - "proxy.config.diags.debug.enabled": 1, - "proxy.config.diags.debug.tags": "http", - "proxy.config.ssl.server.cert.path": f'{self.ts.Variables.SSLDir}', - "proxy.config.ssl.server.private_key.path": f'{self.ts.Variables.SSLDir}', - "proxy.config.ssl.client.verify.server.policy": 'PERMISSIVE', - }) - self.ts.Disk.ssl_multicert_yaml.AddLines( - """ -ssl_multicert: - - dest_ip: "*" - ssl_cert_name: server.pem - ssl_key_name: server.key -""".split("\n")) - self.ts.Disk.remap_config.AddLine(f"map / http://127.0.0.1:{self.server.Variables.http_port}/",) - - def runChunkedTraffic(self): - tr = Test.AddTestRun() - tr.AddVerifierClientProcess( - "client1", self.chunkedReplayFile, http_ports=[self.ts.Variables.port], https_ports=[self.ts.Variables.ssl_port]) - tr.Processes.Default.StartBefore(self.server) - tr.Processes.Default.StartBefore(self.ts) - tr.StillRunningAfter = self.server - tr.StillRunningAfter = self.ts - - def run(self): - self.runChunkedTraffic() - - -HTTP10Test().run() - - -class MalformedChunkHeaderTest: - chunkedReplayFile = "replays/malformed_chunked_header.replay.yaml" - - def __init__(self): - self.setupOriginServer() - self.setupTS() - - def setupOriginServer(self): - self.server = Test.MakeVerifierServerProcess("verifier-server2", self.chunkedReplayFile) - - # The server's responses will fail the first three transactions - # because ATS will close the connection due to the malformed - # chunk headers. - self.server.Streams.stdout += Testers.ContainsExpression( - "Unexpected chunked content for key 1: too small", "Verify that writing the first response failed.") - self.server.Streams.stdout += Testers.ExcludesExpression( - "chunked body of 3 bytes for key 2 with chunk stream", "Verify that writing the second response failed.") - self.server.Streams.stdout += Testers.ContainsExpression( - "Unexpected chunked content for key 3: too small", "Verify that writing the third response failed.") - self.server.Streams.stdout += Testers.ContainsExpression( - "Unexpected chunked content for key 8: too small", "Verify that writing the sixth response failed.") - - # ATS should close the connection before any body gets through. "abcwxyz" - # is the body sent by the client for each of these chunked cases. - self.server.Streams.stdout += Testers.ExcludesExpression("abcwxyz", "Verify that the body never got through.") - - def setupTS(self): - self.ts = Test.MakeATSProcess("ts3", enable_tls=True, enable_cache=False) - self.ts.addDefaultSSLFiles() - self.ts.Disk.records_config.update( - { - "proxy.config.diags.debug.enabled": 1, - "proxy.config.diags.debug.tags": "http", - "proxy.config.ssl.server.cert.path": f'{self.ts.Variables.SSLDir}', - "proxy.config.ssl.server.private_key.path": f'{self.ts.Variables.SSLDir}', - "proxy.config.ssl.client.verify.server.policy": 'PERMISSIVE', - }) - self.ts.Disk.ssl_multicert_yaml.AddLines( - """ -ssl_multicert: - - dest_ip: "*" - ssl_cert_name: server.pem - ssl_key_name: server.key -""".split("\n")) - self.ts.Disk.remap_config.AddLine(f"map / http://127.0.0.1:{self.server.Variables.http_port}/",) - self.ts.Disk.traffic_out.Content += Testers.ContainsExpression( - "user agent post chunk decoding error", "Verify that ATS detected a problem parsing a chunk.") - - def runChunkedTraffic(self): - tr = Test.AddTestRun() - tr.AddVerifierClientProcess( - "client2", self.chunkedReplayFile, http_ports=[self.ts.Variables.port], https_ports=[self.ts.Variables.ssl_port]) - tr.Processes.Default.StartBefore(self.server) - tr.Processes.Default.StartBefore(self.ts) - tr.StillRunningAfter = self.server - tr.StillRunningAfter = self.ts - - # The aborted connections will result in errors and a non-zero return - # code from the verifier client. - tr.Processes.Default.ReturnCode = 1 - tr.Processes.Default.Streams.stdout += Testers.ContainsExpression( - r"(Unexpected chunked content for key 101: too small|Failed HTTP/1 transaction with key: 101)", - "Verify that ATS closed the forth transaction.") - tr.Processes.Default.Streams.stdout += Testers.ContainsExpression( - r"(Unexpected chunked content for key 102: too small|Failed HTTP/1 transaction with key: 102)", - "Verify that ATS closed the fifth transaction.") - tr.Processes.Default.Streams.stdout += Testers.ContainsExpression( - r"(Unexpected chunked content for key 103: too small|Failed HTTP/1 transaction with key: 103)", - "Verify that ATS closed the sixth transaction.") - - tr.Processes.Default.Streams.stdout += Testers.ContainsExpression( - r"Received an HTTP/1 400 response for key 1 with headers", "Verify that ATS returns a response for uuid:1 transaction.") - - tr.Processes.Default.Streams.stdout += Testers.ContainsExpression( - r"Received an HTTP/1 400 response for key 2 with headers", "Verify that ATS returns a response for uuid:2 transaction.") - - tr.Processes.Default.Streams.stdout += Testers.ContainsExpression( - r"Received an HTTP/1 400 response for key 3 with headers", "Verify that ATS returns a response for uuid:3 transaction.") - - tr.Processes.Default.Streams.stdout += Testers.ContainsExpression( - r"Received an HTTP/1 400 response for key 4 with headers", "Verify that ATS returns a response for uuid:4 transaction.") - - tr.Processes.Default.Streams.stdout += Testers.ContainsExpression( - r"Received an HTTP/1 400 response for key 5 with headers", "Verify that ATS returns a response for uuid:5 transaction.") - - tr.Processes.Default.Streams.stdout += Testers.ContainsExpression( - r"Received an HTTP/1 400 response for key 6 with headers", "Verify that ATS returns a response for uuid:6 transaction.") - - tr.Processes.Default.Streams.stdout += Testers.ContainsExpression( - r"Received an HTTP/1 400 response for key 7 with headers", "Verify that ATS returns a response for uuid:7 transaction.") - - # ATS should close the connection before any body gets through. "def" - # is the body sent by the server for each of these chunked cases. - tr.Processes.Default.Streams.stdout += Testers.ExcludesExpression("def", "Verify that the body never got through.") - - def run(self): - self.runChunkedTraffic() - - -MalformedChunkHeaderTest().run() diff --git a/tests/gold_tests/chunked_encoding/chunk_extension_quoted_string.test.py b/tests/gold_tests/chunked_encoding/chunk_extension_quoted_string.test.py deleted file mode 100644 index a670805d367..00000000000 --- a/tests/gold_tests/chunked_encoding/chunk_extension_quoted_string.test.py +++ /dev/null @@ -1,84 +0,0 @@ -''' -Verify that a chunk extension whose quoted-string value contains CR or LF is -rejected. Per RFC 9110 Section 5.6.4 a quoted-string cannot contain a bare CR or -LF, so such a chunk size line is malformed. ATS must reject the request rather -than interpreting the embedded octets as a second, smuggled request: the client -gets a single error response and the embedded GET never reaches the origin. -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -Test.Summary = ''' -A chunk extension quoted-string value containing CR/LF is rejected with 400. -''' - - -class ChunkExtensionQuotedStringTest: - - def __init__(self): - self._setup_origin() - self._setup_ts() - - def _setup_origin(self): - # A proxy-verifier origin parses chunked requests correctly and serves the - # legitimate POST with 200 (keep-alive). If the proxy forwards the embedded - # GET /second, the origin answers it with SECOND-ENDPOINT, so a smuggled - # request shows up as a second response on the client. - self._server = Test.MakeVerifierServerProcess("verifier-server", "replays/chunk_extension_quoted_string.replay.yaml") - - def _setup_ts(self): - self._ts = Test.MakeATSProcess("ts", enable_cache=False) - self._ts.Disk.records_config.update( - { - 'proxy.config.diags.debug.enabled': 0, - 'proxy.config.diags.debug.tags': 'http', - 'proxy.config.http.strict_chunk_parsing': 1, - }) - self._ts.Disk.remap_config.AddLine(f'map / http://127.0.0.1:{self._server.Variables.http_port}') - - def _check(self, tr): - tr.Processes.Default.ReturnCode = 0 - # The malformed chunk size line is rejected, so the client gets exactly one - # error response and the embedded GET /second is never forwarded. A parser - # that ignores the quoted-string would forward that GET as a second request - # and the origin's SECOND-ENDPOINT body would reach the client (two - # responses). Keying on the smuggled body is deterministic; the exact error - # status is not (ATS may relay the origin's response to the forwarded POST - # headers, or generate its own 400). - tr.Processes.Default.Streams.stdout += Testers.ContainsExpression("responses=1", "the client must get exactly one response") - tr.Processes.Default.Streams.stdout += Testers.ExcludesExpression( - "SECOND-ENDPOINT", "the embedded GET must not be smuggled to the origin") - tr.StillRunningAfter = self._server - tr.StillRunningAfter = self._ts - - def run(self): - # Case 1: the whole request arrives in one read. - tr = Test.AddTestRun("Quoted extension value containing CRLF") - tr.Setup.Copy("chunk_extension_client.py") - tr.Processes.Default.StartBefore(self._server) - tr.Processes.Default.StartBefore(self._ts) - tr.Processes.Default.Command = f'python3 chunk_extension_client.py 127.0.0.1 {self._ts.Variables.port}' - self._check(tr) - - # Case 2: the same request split across two writes at a read boundary - # inside the extension. The proxy must resume parsing and still reject it. - tr = Test.AddTestRun("Quoted extension split across reads") - tr.Setup.Copy("chunk_extension_client.py") - tr.Processes.Default.Command = f'python3 chunk_extension_client.py 127.0.0.1 {self._ts.Variables.port} --split' - self._check(tr) - - -ChunkExtensionQuotedStringTest().run() diff --git a/tests/gold_tests/chunked_encoding/chunk_trailer_bare_lf.test.py b/tests/gold_tests/chunked_encoding/chunk_trailer_bare_lf.test.py deleted file mode 100644 index ab56a721638..00000000000 --- a/tests/gold_tests/chunked_encoding/chunk_trailer_bare_lf.test.py +++ /dev/null @@ -1,86 +0,0 @@ -''' -Verify that a chunked request whose trailer section is terminated by a bare LF -instead of CRLF is rejected. Per RFC 9112 Section 7.1 the trailer section ends -with an empty line, "CRLF"; a bare LF blank line is not a valid terminator. ATS -must reject the request rather than ending the body one byte early and -interpreting the trailing octets as a second, smuggled request: the client gets a -single response and the embedded GET never reaches the origin. -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -Test.Summary = ''' -A chunked trailer terminated by a bare LF instead of CRLF is rejected. -''' - - -class ChunkTrailerBareLfTest: - - def __init__(self): - self._setup_origin() - self._setup_ts() - - def _setup_origin(self): - # A proxy-verifier origin parses chunked requests correctly and serves the - # legitimate POST with 200 (keep-alive). If the proxy forwards the embedded - # GET /smuggled, the origin answers it with SECOND-ENDPOINT, so a smuggled - # request shows up as a second response on the client. - self._server = Test.MakeVerifierServerProcess("verifier-server", "replays/chunk_trailer_bare_lf.replay.yaml") - - def _setup_ts(self): - self._ts = Test.MakeATSProcess("ts", enable_cache=False) - self._ts.Disk.records_config.update( - { - 'proxy.config.diags.debug.enabled': 0, - 'proxy.config.diags.debug.tags': 'http', - 'proxy.config.http.strict_chunk_parsing': 1, - }) - self._ts.Disk.remap_config.AddLine(f'map / http://127.0.0.1:{self._server.Variables.http_port}') - - def _check(self, tr): - tr.Processes.Default.ReturnCode = 0 - # The bare-LF trailer terminator is rejected, so the client gets exactly one - # response and the embedded GET /smuggled is never forwarded. A parser that - # accepts the bare LF would forward that GET as a second request and the - # origin's SECOND-ENDPOINT body would reach the client (two responses). - # Keying on the smuggled body is deterministic; the exact error status is - # not (ATS may relay the origin's response to the forwarded POST headers, or - # generate its own 400). - tr.Processes.Default.Streams.stdout += Testers.ContainsExpression("responses=1", "the client must get exactly one response") - tr.Processes.Default.Streams.stdout += Testers.ExcludesExpression( - "SECOND-ENDPOINT", "the embedded GET must not be smuggled to the origin") - tr.StillRunningAfter = self._server - tr.StillRunningAfter = self._ts - - def run(self): - # Case 1: the whole request arrives in one read. - tr = Test.AddTestRun("Chunked trailer terminated by a bare LF") - tr.Setup.Copy("chunk_trailer_client.py") - tr.Processes.Default.StartBefore(self._server) - tr.Processes.Default.StartBefore(self._ts) - tr.Processes.Default.Command = f'python3 chunk_trailer_client.py 127.0.0.1 {self._ts.Variables.port}' - self._check(tr) - - # Case 2: the same request split across two writes at a read boundary right - # after the final "0\r\n". The proxy must resume parsing the trailer and - # still reject the bare-LF terminator. - tr = Test.AddTestRun("Chunked trailer bare LF split across reads") - tr.Setup.Copy("chunk_trailer_client.py") - tr.Processes.Default.Command = f'python3 chunk_trailer_client.py 127.0.0.1 {self._ts.Variables.port} --split' - self._check(tr) - - -ChunkTrailerBareLfTest().run() diff --git a/tests/gold_tests/chunked_encoding/chunked_encoding.test.py b/tests/gold_tests/chunked_encoding/chunked_encoding.test.py deleted file mode 100644 index 1c24420f5cc..00000000000 --- a/tests/gold_tests/chunked_encoding/chunked_encoding.test.py +++ /dev/null @@ -1,263 +0,0 @@ -''' -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import os - -Test.Summary = ''' -Test chunked encoding processing -''' - -Test.SkipUnless(Condition.HasCurlFeature('http2')) -Test.ContinueOnFail = True - -Test.GetTcpPort("upstream_port") - -# Define default ATS -ts = Test.MakeATSProcess("ts", enable_tls=True) -server = Test.MakeOriginServer("server") -server2 = Test.MakeOriginServer("server2", ssl=True) -server3 = Test.MakeOriginServer("server3") - -server4 = Test.Processes.Process( - "server4", "bash -c '" + Test.TestDirectory + "/server4.sh {} outserver4'".format(Test.Variables.upstream_port)) - -testName = "" -request_header = {"headers": "GET / HTTP/1.1\r\nHost: www.example.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -response_header = { - "headers": "HTTP/1.1 200 OK\r\nServer: uServer\r\nConnection: close\r\nTransfer-Encoding: chunked\r\n\r\n", - "timestamp": "1469733493.993", - "body": "" -} - -request_header2 = { - "headers": - "POST / HTTP/1.1\r\nHost: www.anotherexample.com\r\nContent-Type: application/x-www-form-urlencoded\r\nContent-Length: 11\r\n\r\n", - "timestamp": "1415926535.898", - "body": "knock knock" -} -response_header2 = { - "headers": "HTTP/1.1 200 OK\r\nServer: uServer\r\nConnection: close\r\nTransfer-Encoding: chunked\r\n\r\n", - "timestamp": "1415926535.898", - "body": "12345678901234567890" -} - -request_header3 = { - "headers": - "POST / HTTP/1.1\r\nHost: www.yetanotherexample.com\r\nContent-Type: application/x-www-form-urlencoded\r\nContent-Length: 11\r\n\r\n", - "timestamp": "1415926535.898", - "body": "knock knock" -} -response_header3 = { - "headers": "HTTP/1.1 200 OK\r\nServer: uServer\r\nConnection: close\r\nTransfer-Encoding: chunked\r\n\r\n", - "timestamp": "1415926535.898", - "body": "" -} - -server.addResponse("sessionlog.json", request_header, response_header) -server2.addResponse("sessionlog.json", request_header2, response_header2) -server3.addResponse("sessionlog.json", request_header3, response_header3) - -# add ssl materials like key, certificates for the server -ts.addDefaultSSLFiles() - -ts.Disk.records_config.update( - { - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'http', - 'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir), - 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts.Variables.SSLDir), - 'proxy.config.ssl.client.verify.server.policy': 'PERMISSIVE', - }) - -ts.Disk.remap_config.AddLine('map http://www.example.com http://127.0.0.1:{0}'.format(server.Variables.Port)) -ts.Disk.remap_config.AddLine('map http://www.yetanotherexample.com http://127.0.0.1:{0}'.format(server3.Variables.Port)) -ts.Disk.remap_config.AddLine( - 'map https://www.anotherexample.com https://127.0.0.1:{0}'.format(server2.Variables.SSL_Port, ts.Variables.ssl_port)) -ts.Disk.remap_config.AddLine('map / http://127.0.0.1:{0}'.format(Test.Variables.upstream_port)) - -ts.Disk.ssl_multicert_yaml.AddLines( - """ -ssl_multicert: - - dest_ip: "*" - ssl_cert_name: server.pem - ssl_key_name: server.key -""".split("\n")) - -# smuggle-client is built via `make`. Here we copy the built binary down to the -# test directory so that the test runs in this file can use it. -Test.Setup.Copy(os.path.join(Test.Variables.AtsBuildGoldTestsDir, 'chunked_encoding', 'smuggle-client')) - -# HTTP1.1 GET: www.example.com -tr = Test.AddTestRun() -tr.TimeOut = 5 -if Condition.CurlUsingUnixDomainSocket(): - tr.MakeCurlCommand('--http1.1 -H "Host: www.example.com" "http://127.0.0.1:{0}" --verbose'.format(ts.Variables.port), ts=ts) -else: - tr.MakeCurlCommand('--http1.1 --proxy 127.0.0.1:{0} http://www.example.com --verbose'.format(ts.Variables.port), ts=ts) -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.StartBefore(server) -tr.Processes.Default.StartBefore(server2) -tr.Processes.Default.StartBefore(server3) -# Delay on readiness of our ssl ports -tr.Processes.Default.StartBefore(Test.Processes.ts) -if Condition.CurlUsingUnixDomainSocket(): - tr.Processes.Default.Streams.stderr = "gold/chunked_GET_200_uds.gold" -else: - tr.Processes.Default.Streams.stderr = "gold/chunked_GET_200.gold" -tr.StillRunningAfter = server -tr.StillRunningAfter = ts - -if not Condition.CurlUsingUnixDomainSocket(): - # HTTP2 POST: www.example.com Host, chunked body - tr = Test.AddTestRun() - tr.TimeOut = 5 - tr.MakeCurlCommand( - '--http2 -k https://127.0.0.1:{0} --verbose -H "Host: www.anotherexample.com" -d "Knock knock"'.format( - ts.Variables.ssl_port), - ts=ts) - tr.Processes.Default.ReturnCode = 0 - tr.Processes.Default.Streams.stderr = "gold/h2_chunked_POST_200.gold" - -# HTTP1.1 POST: www.yetanotherexample.com Host, explicit size -tr = Test.AddTestRun() -tr.TimeOut = 5 -tr.MakeCurlCommand( - 'http://127.0.0.1:{0} -H "Host: www.yetanotherexample.com" --verbose -d "knock knock"'.format(ts.Variables.port), ts=ts) -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Streams.stderr = "gold/chunked_POST_200.gold" -tr.StillRunningAfter = server - -# HTTP1.1 POST: www.example.com Host, chunked body -tr = Test.AddTestRun() -tr.TimeOut = 5 -tr.MakeCurlCommand( - 'http://127.0.0.1:{0} -H "Host: www.yetanotherexample.com" --verbose -H "Transfer-Encoding: chunked" -d "Knock knock"'.format( - ts.Variables.port), - ts=ts) -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Streams.stderr = "gold/chunked_POST_200.gold" -tr.StillRunningAfter = server - -server4_out = Test.Disk.File("outserver4") -server4_out.Content = Testers.ExcludesExpression("sneaky", "Extra body bytes should not be delivered") - -# HTTP/1.1 Try to smuggle another request to the origin -tr = Test.AddTestRun() -tr.Processes.Default.StartBefore(server4) -tr.TimeOut = 5 -tr.Processes.Default.Command = "./smuggle-client 127.0.0.1 {0}".format(ts.Variables.ssl_port) -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Streams.All = Testers.ExcludesExpression("content-length:", "Response should not include content length") -# Transfer encoding to origin, but no content-length -# No extra bytes in body seen by origin - - -class TestChunkedTrailers: - """Verify chunked trailer proxy behavior.""" - - _chunked_dropped_replay: str = "replays/chunked_trailer_dropped.replay.yaml" - _proxied_dropped_replay: str = "replays/chunked_trailer_proxied.replay.yaml" - - def __init__(self, configure_drop_trailers: bool): - """Create a test to verify chunked trailer behavior. - - :param configure_drop_trailers: Whether to configure ATS to drop - trailers or not. - """ - self._configure_drop_trailers = configure_drop_trailers - self._replay_file = self._chunked_dropped_replay if configure_drop_trailers else self._proxied_dropped_replay - behavior_description = "drop" if configure_drop_trailers else "proxy" - tr = Test.AddTestRun(f'Verify chunked tailers behavior: {behavior_description}') - self._configure_dns(tr) - self._configure_server(tr) - self._configure_ts(tr) - self._configure_client(tr) - - def _configure_dns(self, tr: 'TestRun') -> "Process": - """Configure DNS for the test run. - - :param tr: The TestRun to configure DNS for. - :return: The DNS process. - """ - name = 'dns-drop-trailers' if self._configure_drop_trailers else 'dns-proxy-trailers' - self._dns = tr.MakeDNServer(name, default='127.0.0.1') - return self._dns - - def _configure_server(self, tr: 'TestRun') -> 'Process': - """Configure the origin server for the test run. - - :param tr: The TestRun to configure the server for. - :return: The origin server process. - """ - name = 'server-drop-trailers' if self._configure_drop_trailers else 'server-proxy-trailers' - self._server = tr.AddVerifierServerProcess(name, self._replay_file) - if self._configure_drop_trailers: - self._server.Streams.All += Testers.ExcludesExpression('Client: ATS', 'Verify the Client trailer was dropped.') - self._server.Streams.All += Testers.ExcludesExpression('ETag: "abc"', 'Verify the ETag trailer was dropped.') - else: - self._server.Streams.All += Testers.ContainsExpression('Client: ATS', 'Verify the Client trailer was proxied.') - self._server.Streams.All += Testers.ContainsExpression('ETag: "abc"', 'Verify the ETag trailer was proxied.') - return self._server - - def _configure_ts(self, tr: 'TestRun') -> 'Process': - """Configure ATS for the test run. - - :param tr: The TestRun to configure ATS for. - :return: The ATS process. - """ - name = 'ts-drop-trailers' if self._configure_drop_trailers else 'ts-proxy-trailers' - ts = tr.MakeATSProcess(name, enable_cache=False) - self._ts = ts - port = self._server.Variables.http_port - ts.Disk.remap_config.AddLine(f'map / http://backend.example.com:{port}/') - ts.Disk.records_config.update( - { - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'http', - 'proxy.config.dns.nameservers': f'127.0.0.1:{self._dns.Variables.Port}', - 'proxy.config.dns.resolv_conf': 'NULL' - }) - if not self._configure_drop_trailers: - ts.Disk.records_config.update({ - 'proxy.config.http.drop_chunked_trailers': 0, - }) - return ts - - def _configure_client(self, tr: 'TestRun') -> 'Process': - """Configure the client for the test run. - - :param tr: The TestRun to configure the client for. - :return: The client process. - """ - name = 'client-drop-trailers' if self._configure_drop_trailers else 'client-proxy-trailers' - self._client = tr.AddVerifierClientProcess(name, self._replay_file, http_ports=[self._ts.Variables.port]) - self._client.StartBefore(self._dns) - self._client.StartBefore(self._server) - self._client.StartBefore(self._ts) - - if self._configure_drop_trailers: - self._client.Streams.All += Testers.ExcludesExpression('Sever: ATS', 'Verify the Server trailer was dropped.') - self._client.Streams.All += Testers.ExcludesExpression('ETag: "def"', 'Verify the ETag trailer was dropped.') - else: - self._client.Streams.All += Testers.ContainsExpression('Sever: ATS', 'Verify the Server trailer was proxied.') - self._client.Streams.All += Testers.ContainsExpression('ETag: "def"', 'Verify the ETag trailer was proxied.') - return self._client - - -TestChunkedTrailers(configure_drop_trailers=True) -TestChunkedTrailers(configure_drop_trailers=False) diff --git a/tests/gold_tests/chunked_encoding/chunked_encoding_disabled.test.py b/tests/gold_tests/chunked_encoding/chunked_encoding_disabled.test.py deleted file mode 100644 index f584b906335..00000000000 --- a/tests/gold_tests/chunked_encoding/chunked_encoding_disabled.test.py +++ /dev/null @@ -1,75 +0,0 @@ -''' -Test ATS configured to not respond with chunked content. -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -Test.Summary = 'Test ATS configured to not respond with chunked content.' - - -class ChunkedEncodingDisabled: - chunkedReplayFile = "replays/chunked.replay.yaml" - - def __init__(self): - self.setupOriginServer() - self.setupTS() - - def setupOriginServer(self): - self.server = Test.MakeVerifierServerProcess("server", self.chunkedReplayFile) - - def setupTS(self): - self.ts = Test.MakeATSProcess("ts", enable_tls=True, enable_cache=False) - self.ts.addDefaultSSLFiles() - self.ts.Disk.records_config.update( - { - "proxy.config.diags.debug.enabled": 1, - "proxy.config.diags.debug.tags": "http", - "proxy.config.ssl.server.cert.path": f'{self.ts.Variables.SSLDir}', - "proxy.config.ssl.server.private_key.path": f'{self.ts.Variables.SSLDir}', - "proxy.config.ssl.client.verify.server.policy": 'PERMISSIVE', - - # Never respond with chunked encoding. - "proxy.config.http.chunking_enabled": 0, - }) - self.ts.Disk.ssl_multicert_yaml.AddLines( - """ -ssl_multicert: - - dest_ip: "*" - ssl_cert_name: server.pem - ssl_key_name: server.key -""".split("\n")) - self.ts.Disk.remap_config.AddLines( - [ - f"map /for/http http://127.0.0.1:{self.server.Variables.http_port}/", - f"map /for/tls https://127.0.0.1:{self.server.Variables.https_port}/", - ]) - - def runChunkedTraffic(self): - tr = Test.AddTestRun() - tr.AddVerifierClientProcess( - "client", self.chunkedReplayFile, http_ports=[self.ts.Variables.port], https_ports=[self.ts.Variables.ssl_port]) - tr.Processes.Default.Streams.stdout += "gold/verifier_client_chunked.gold" - - tr.Processes.Default.StartBefore(self.server) - tr.Processes.Default.StartBefore(self.ts) - tr.StillRunningAfter = self.server - tr.StillRunningAfter = self.ts - - def run(self): - self.runChunkedTraffic() - - -ChunkedEncodingDisabled().run() diff --git a/tests/gold_tests/chunked_encoding/chunked_encoding_h2.test.py b/tests/gold_tests/chunked_encoding/chunked_encoding_h2.test.py deleted file mode 100644 index ed43095a6ec..00000000000 --- a/tests/gold_tests/chunked_encoding/chunked_encoding_h2.test.py +++ /dev/null @@ -1,120 +0,0 @@ -''' -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import os -import sys - -Test.Summary = ''' -Test interaction of H2 and chunked encoding -''' -Test.SkipIf(Condition.CurlUsingUnixDomainSocket()) - -Test.SkipUnless( - Condition.HasProgram("nghttp", "Nghttp need to be installed on system for this test to work"), - Condition.HasCurlFeature('http2')) -Test.ContinueOnFail = True - -Test.GetTcpPort("upstream_port") -Test.GetTcpPort("upstream_port2") -Test.GetTcpPort("upstream_port3") - -# Define default ATS -ts = Test.MakeATSProcess("ts", enable_tls=True) - -# add ssl materials like key, certificates for the server -ts.addDefaultSSLFiles() - -origin_server = os.path.join(Test.TestDirectory, "chunked_encoding_h2_server.py") -delay_server = Test.Processes.Process( - "delay-server", f'{sys.executable} "{origin_server}" 127.0.0.1 {Test.Variables.upstream_port} outserver1 delayed-chunked') -server2 = Test.Processes.Process( - "server2", f'{sys.executable} "{origin_server}" 127.0.0.1 {Test.Variables.upstream_port2} outserver2 content-length') -server3 = Test.Processes.Process( - "server3", f'{sys.executable} "{origin_server}" 127.0.0.1 {Test.Variables.upstream_port3} outserver3 chunked') - -ts.Disk.records_config.update( - { - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'http', - 'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir), - 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts.Variables.SSLDir), - 'proxy.config.ssl.client.verify.server.policy': 'PERMISSIVE', - }) - -ts.Disk.remap_config.AddLine('map /delay-chunked-response http://127.0.0.1:{0}'.format(Test.Variables.upstream_port)) -ts.Disk.remap_config.AddLine('map /post-full http://127.0.0.1:{0}'.format(Test.Variables.upstream_port2)) -ts.Disk.remap_config.AddLine('map /post-chunked http://127.0.0.1:{0}'.format(Test.Variables.upstream_port3)) - -ts.Disk.ssl_multicert_yaml.AddLines( - """ -ssl_multicert: - - dest_ip: "*" - ssl_cert_name: server.pem - ssl_key_name: server.key -""".split("\n")) - -# Use a raw origin server in case 1 so the final chunk can be delayed. Use it -# for cases 2 and 3 as well because microserver rejects chunked request headers. - -# H2 GET request -# chunked response without content-length -# delay before final chunk size -server1_out = Test.Disk.File("outserver1") -tr = Test.AddTestRun() -tr.Processes.Default.Command = 'nghttp -vv https://127.0.0.1:{}/delay-chunked-response'.format(ts.Variables.ssl_port) -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.StartBefore(Test.Processes.ts) -tr.Processes.Default.StartBefore(delay_server, ready=When.PortOpen(Test.Variables.upstream_port)) -tr.Processes.Default.Streams.All = Testers.ExcludesExpression("RST_STREAM", "Delayed chunk close should not cause reset") -tr.Processes.Default.Streams.All += Testers.ExcludesExpression("< content-length", "Should return chunked") -tr.Processes.Default.Streams.All += Testers.ContainsExpression(":status: 200", "Should get successful response") -tr.StillRunningAfter = ts -# No resets in the output -# No content lengths in the header - -# HTTP2 POST: www.example.com Host, chunked body -server2_out = Test.Disk.File("outserver2") -tr = Test.AddTestRun() -tr.Processes.Default.StartBefore(server2, ready=When.PortOpen(Test.Variables.upstream_port2)) -tr.MakeCurlCommand( - '--http2 -k https://127.0.0.1:{}/post-full --verbose -H "Transfer-encoding: chunked" -d "Knock knock"'.format( - ts.Variables.ssl_port), - ts=ts) -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Streams.All = Testers.ContainsExpression("HTTP/2 200", "Request should succeed") -tr.Processes.Default.Streams.All += Testers.ContainsExpression("< content-length:", "Response should include content length") -server2_out = Testers.ContainsExpression("Transfer-Encoding: chunked", "Request should be chunked encoded") -# No content-length in header -# Transfer encoding to origin, but no content-length -# Content length on the way back - -# HTTP2 POST: chunked post body and chunked response -server3_out = Test.Disk.File("outserver3") -tr = Test.AddTestRun() -tr.Processes.Default.StartBefore(server3, ready=When.PortOpen(Test.Variables.upstream_port3)) -tr.MakeCurlCommand( - '--http2 -k https://127.0.0.1:{}/post-chunked --verbose -H "Transfer-encoding: chunked" -d "Knock knock"'.format( - ts.Variables.ssl_port), - ts=ts) -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Streams.All = Testers.ContainsExpression("HTTP/2 200", "Request should succeed") -tr.Processes.Default.Streams.All += Testers.ExcludesExpression("< content-length:", "Response should not include content length") -server3_out = Testers.ContainsExpression("Transfer-Encoding: chunked", "Request should be chunked encoded") -# No content length in header -# Transfer encoding to origin, but no content-length -# No content length in the response diff --git a/tests/gold_tests/client_connection/per_client_connection_max.test.py b/tests/gold_tests/client_connection/per_client_connection_max.test.py deleted file mode 100644 index 4bec83d487b..00000000000 --- a/tests/gold_tests/client_connection/per_client_connection_max.test.py +++ /dev/null @@ -1,247 +0,0 @@ -''' -Verify the behavior of proxy.config.net.per_client.connection.max. -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -from enum import Enum -import os - -Test.Summary = __doc__ - - -class Protocol(Enum): - HTTP = 1 - HTTPS = 2 - HTTP2 = 3 - - @classmethod - def to_str(cls, protocol: int) -> str: - """Convert the protocol to a string. - - :param protocol: The protocol to convert. - :return: The string representation of the protocol. - """ - if protocol == cls.HTTP: - return 'http' - elif protocol == cls.HTTPS: - return 'https' - elif protocol == cls.HTTP2: - return 'http2' - else: - raise ValueError(f'Unknown protocol: {protocol}') - - -class PerClientConnectionMaxTest: - """Define an object to test our max client connection behavior.""" - - _process_counter: int = 0 - _max_client_connections: int = 3 - _protocol_to_replay_file = { - Protocol.HTTP: 'http_slow_origins.replay.yaml', - Protocol.HTTPS: 'https_slow_origins.replay.yaml', - Protocol.HTTP2: 'http2_slow_origins.replay.yaml', - } - - def __init__(self, protocol: int, exempt_list: str = '', exempt_list_file: str = '', exempt_list_applies: bool = False) -> None: - """Configure the test processes in preparation for the TestRun. - - :param protocol: The protocol to test. - :param exempt_list: A comma-separated string of IP addresses or ranges to exempt. - The default empty string implies that no exempt list will be configured. - :param exempt_list_file: A file containing a list of IP addresses or ranges to exempt. - The default empty string implies that no exempt list will be configured. - :param exempt_list_applies: If True, the exempt list is assumed to exempt - the test connections. Thus the per client max connections is expected - not to be enforced for the connections. - """ - self._process_counter = PerClientConnectionMaxTest._process_counter - PerClientConnectionMaxTest._process_counter += 1 - self._protocol = protocol - protocol_string = Protocol.to_str(protocol) - self._replay_file = self._protocol_to_replay_file[protocol] - self._exempt_list = exempt_list - self._exempt_list_file = exempt_list_file - self._exempt_list_applies = exempt_list_applies - - exempt_list_description = 'exempted' if exempt_list_applies else 'not exempted' - exempt_description = 'no exempt list' - if exempt_list: - exempt_description = 'exempt list string' - elif exempt_list_file: - exempt_description = 'exempt list file' - tr = Test.AddTestRun( - f'proxy.config.net.per_client.connection.max: {protocol_string}, ' - f'{exempt_description}: {exempt_list_description}') - self._configure_dns(tr) - self._configure_server(tr) - self._configure_trafficserver() - self._configure_client(tr) - self._verify_metrics() - - def _configure_dns(self, tr: 'TestRun') -> None: - """Configure a nameserver for the test. - - :param tr: The TestRun to add the nameserver to. - """ - name = f'dns{self._process_counter}' - self._dns = tr.MakeDNServer(name, default='127.0.0.1') - - def _configure_server(self, tr: 'TestRun') -> None: - """Configure the server to be used in the test. - - :param tr: The TestRun to add the server to. - """ - name = f'server{self._process_counter}' - self._server = tr.AddVerifierServerProcess(name, self._replay_file) - self._server.Streams.All += Testers.ContainsExpression("first-request", "Verify the first request was received.") - self._server.Streams.All += Testers.ContainsExpression("second-request", "Verify the second request was received.") - self._server.Streams.All += Testers.ContainsExpression("third-request", "Verify the third request was received.") - self._server.Streams.All += Testers.ContainsExpression("fifth-request", "Verify the fifth request was received.") - - if self._exempt_list_applies: - # The fourth request should be allowed due to the exempt_list. - self._server.Streams.All += Testers.ContainsExpression("fourth-request", "Verify the fourth request was received.") - else: - # The fourth request should be blocked due to too many connections. - self._server.Streams.All += Testers.ExcludesExpression("fourth-request", "Verify the fourth request was not received.") - - def _configure_trafficserver(self) -> None: - """Configure Traffic Server to be used in the test.""" - # Associate ATS with the Test so that metrics can be verified. - name = f'ts{self._process_counter}' - self._ts = Test.MakeATSProcess(name, enable_cache=False, enable_tls=True) - self._ts.addDefaultSSLFiles() - self._ts.Disk.ssl_multicert_yaml.AddLines( - """ -ssl_multicert: - - dest_ip: "*" - ssl_cert_name: server.pem - ssl_key_name: server.key -""".split("\n")) - if self._protocol == Protocol.HTTP: - server_port = self._server.Variables.http_port - scheme = 'http' - else: - server_port = self._server.Variables.https_port - scheme = 'https' - self._ts.Disk.remap_config.AddLine(f'map / {scheme}://127.0.0.1:{server_port}') - self._ts.Disk.records_config.update( - { - 'proxy.config.ssl.server.cert.path': self._ts.Variables.SSLDir, - 'proxy.config.ssl.server.private_key.path': self._ts.Variables.SSLDir, - 'proxy.config.ssl.client.verify.server.policy': 'PERMISSIVE', - 'proxy.config.dns.nameservers': f"127.0.0.1:{self._dns.Variables.Port}", - 'proxy.config.dns.resolv_conf': 'NULL', - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'socket|http|net_queue|iocore_net|conn_track|cripts', - 'proxy.config.net.per_client.max_connections_in': self._max_client_connections, - # Disable keep-alive so we close the client connections when the - # transactions are done. This allows us to verify cleanup is working - # per the ConnectionTracker metrics. - 'proxy.config.http.keep_alive_enabled_in': 0, - }) - if self._exempt_list_file: - exempt_list_absolute = os.path.join(self._ts.Variables.CONFIGDIR, os.path.basename(self._exempt_list_file)) - self._ts.Setup.Copy(self._exempt_list_file, exempt_list_absolute) - self._ts.Disk.plugin_config.AddLine(f'connection_exempt_list.so {exempt_list_absolute}') - elif self._exempt_list: - self._ts.Disk.records_config.update({ - 'proxy.config.http.per_client.connection.exempt_list': self._exempt_list, - }) - if self._exempt_list_applies: - self._ts.Disk.diags_log.Content += Testers.ExcludesExpression( - f'WARNING:.*too many connections:', 'Connections should not be throttled due to the exempt list.') - else: - self._ts.Disk.diags_log.Content += Testers.ContainsExpression( - f'WARNING:.*too many connections:.*limit={self._max_client_connections}', - 'Verify the user is warned about the connection limit being hit.') - - def _configure_client(self, tr: 'TestRun') -> None: - """Configure the TestRun. - - :param tr: The TestRun to add the client to. - """ - name = f'client{self._process_counter}' - p = tr.AddVerifierClientProcess( - name, - self._replay_file, - http_ports=[self._ts.Variables.port], - https_ports=[self._ts.Variables.ssl_port], - run_parallel=True) - - p.StartBefore(self._dns) - p.StartBefore(self._server) - p.StartBefore(self._ts) - - p.Streams.All += Testers.ContainsExpression("first-request", "Verify the first request was received.") - p.Streams.All += Testers.ContainsExpression("second-request", "Verify the second request was received.") - p.Streams.All += Testers.ContainsExpression("third-request", "Verify the third request was received.") - p.Streams.All += Testers.ContainsExpression("fifth-request", "Verify the fifth request was received.") - if self._exempt_list_applies: - p.ReturnCode = 0 - p.Streams.All += Testers.ContainsExpression("fourth-request", "Verify the fourth request was received.") - else: - # Because the fourth connection will be aborted, the client will have a - # non-zero return code. - p.ReturnCode = 1 - if self._protocol == Protocol.HTTP: - p.Streams.All += Testers.ContainsExpression( - "The peer closed the connection while reading.", - "A connection should be closed due to too many client connections.") - p.Streams.All += Testers.ContainsExpression( - "Failed HTTP/1 transaction with key: fourth-request", "The fourth request should fail.") - else: - p.Streams.All += Testers.ContainsExpression( - "ECONNRESET: Connection reset by peer", "A connection should be closed due to too many client connections.") - p.Streams.All += Testers.ExcludesExpression("fourth-request", "The fourth request should fail.") - - def _verify_metrics(self) -> None: - """Verify the per client connection metrics.""" - tr = Test.AddTestRun("Verify the per client connection metrics.") - tr.Processes.Default.Env = self._ts.Env - tr.Processes.Default.Command = ( - 'traffic_ctl metric get ' - 'proxy.process.net.per_client.connections_throttled_in ' - 'proxy.process.net.per_client.connections_exempt_in ' - 'proxy.process.net.connection_tracker_table_size') - tr.Processes.Default.ReturnCode = 0 - if self._exempt_list_applies: - tr.Processes.Default.Streams.All += Testers.ContainsExpression( - 'proxy.process.net.per_client.connections_throttled_in 0', 'Verify no connections were recorded as throttled.') - tr.Processes.Default.Streams.All += Testers.ContainsExpression( - 'proxy.process.net.per_client.connections_exempt_in 5', - 'Verify that the connections were all recorded as exempted.') - else: - tr.Processes.Default.Streams.All += Testers.ContainsExpression( - 'proxy.process.net.per_client.connections_throttled_in 1', 'Verify the connection was recorded as throttled.') - tr.Processes.Default.Streams.All += Testers.ContainsExpression( - 'proxy.process.net.per_client.connections_exempt_in 0', 'Verify no connections were recorded as exempt.') - tr.Processes.Default.Streams.All += Testers.ContainsExpression( - 'proxy.process.net.connection_tracker_table_size 0', 'Verify the table was cleaned up correctly.') - - -PerClientConnectionMaxTest(Protocol.HTTP) -PerClientConnectionMaxTest(Protocol.HTTPS) -PerClientConnectionMaxTest(Protocol.HTTP2) - -PerClientConnectionMaxTest(Protocol.HTTP, exempt_list='127.0.0.1,::1', exempt_list_applies=True) -PerClientConnectionMaxTest(Protocol.HTTPS, exempt_list='1.2.3.4,5.6.0.0/16', exempt_list_applies=False) -PerClientConnectionMaxTest(Protocol.HTTP2, exempt_list='0/0,::/0', exempt_list_applies=True) - -PerClientConnectionMaxTest(Protocol.HTTP, exempt_list_file='exempt_lists/exempt_localhost.yaml', exempt_list_applies=True) -PerClientConnectionMaxTest(Protocol.HTTP, exempt_list_file='exempt_lists/no_localhost.yaml', exempt_list_applies=False) -PerClientConnectionMaxTest(Protocol.HTTP, exempt_list_file='exempt_lists/exempt_all.yaml', exempt_list_applies=True) diff --git a/tests/gold_tests/command_argument/verify_global_plugin.test.py b/tests/gold_tests/command_argument/verify_global_plugin.test.py deleted file mode 100644 index 05a80af7c8f..00000000000 --- a/tests/gold_tests/command_argument/verify_global_plugin.test.py +++ /dev/null @@ -1,143 +0,0 @@ -''' -Test the verify_global_plugin TrafficServer command. -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import os - -Test.Summary = ''' -Test that the TrafficServer verify_global_plugin command works as expected. -''' - -process_counter = 0 -no_log_data = {'diags': 'stderr', 'error': 'stderr', 'manager': 'stderr'} - - -def create_ts_process(): - """ - Create a unique ATS process with each call to this function. - """ - global process_counter - process_counter += 1 - - ts = Test.MakeATSProcess("ts{counter}".format(counter=process_counter), log_data=no_log_data) - - # Ideally we would set the test run's Processes.Default to ts, but deep - # copy of processes is not currently implemented in autest. Therefore we - # replace the command which ts runs with a dummy command, and pull in - # piecemeal the values from ts that we want into the test run. The helper - # only needs to create the ATS environment and runroot, so let it exit - # normally instead of depending on AuTest's cleanup signal behavior. - ts.Command = "true" - ts.ReturnCode = 0 - # Clear the ready criteria because the helper is ready as soon as it runs. - ts.Ready = None - return ts - - -""" -TEST: verify_global_plugin should complain if an argument is not passed to it. -""" -tr = Test.AddTestRun("Verify the requirement of an argument") -ts = create_ts_process() -tr.Processes.Default.Env = ts.Env -tr.Processes.Default.Command = "traffic_server -C 'verify_global_plugin'" -tr.Processes.Default.ReturnCode = 1 -tr.Processes.Default.StartBefore(ts) -tr.Processes.Default.Streams.stderr = Testers.ContainsExpression( - "ERROR: verifying a plugin requires a plugin SO file path argument", "Should warn about the need for an SO file argument") -""" -TEST: verify_global_plugin should complain if the argument doesn't reference a shared -object file. -""" -tr = Test.AddTestRun("Verify the requirement of a file") -ts = create_ts_process() -tr.Processes.Default.Env = ts.Env -tr.Processes.Default.Command = \ - "traffic_server -C 'verify_global_plugin {filename}'".format( - filename="/this/file/does/not/exist.so") -tr.Processes.Default.ReturnCode = 1 -tr.Processes.Default.StartBefore(ts) -tr.Processes.Default.Streams.stderr = Testers.ContainsExpression( - "ERROR: .*No such file or directory", "Should warn about the non-existent SO file argument") -""" -TEST: verify_global_plugin should complain if the shared object file doesn't -have the expected Plugin symbols. -""" -tr = Test.AddTestRun("Verify the requirement of our Plugin API.") -ts = create_ts_process() -Test.PrepareTestPlugin(os.path.join(Test.Variables.AtsTestPluginsDir, 'missing_ts_plugin_init.so'), ts) -tr.Processes.Default.Env = ts.Env -tr.Processes.Default.Command = \ - "traffic_server -C 'verify_global_plugin {filename}'".format( - filename="${PROXY_CONFIG_PLUGIN_PLUGIN_DIR}/missing_ts_plugin_init.so") -tr.Processes.Default.ReturnCode = 1 -tr.Processes.Default.StartBefore(ts) -tr.Processes.Default.Streams.stderr = Testers.ContainsExpression( - "ERROR: .*unable to find TSPluginInit function in", "Should warn about the need for the TSPluginInit symbol") -ts.Disk.diags_log.Content = [] -""" -TEST: Verify that passing a remap plugin produces a warning because -it doesn't have the global plugin symbols. -""" -tr = Test.AddTestRun("Verify a properly formed plugin works as expected.") -ts = create_ts_process() -Test.PrepareTestPlugin(os.path.join(Test.Variables.AtsTestPluginsDir, 'conf_remap_stripped.so'), ts) -tr.Processes.Default.Env = ts.Env -tr.Processes.Default.Command = \ - "traffic_server -C 'verify_global_plugin {filename}'".format( - filename="${PROXY_CONFIG_PLUGIN_PLUGIN_DIR}/conf_remap_stripped.so") -tr.Processes.Default.ReturnCode = 1 -tr.Processes.Default.StartBefore(ts) -tr.Processes.Default.Streams.stderr = Testers.ContainsExpression( - "ERROR: .*unable to find TSPluginInit function in", "Should warn about the need for the TSPluginInit symbol") -ts.Disk.diags_log.Content = [] -""" -TEST: The happy case: a global plugin shared object file is passed as an -argument that has the definition for the expected Plugin symbols. -""" -tr = Test.AddTestRun("Verify a properly formed plugin works as expected.") -ts = create_ts_process() -Test.PrepareTestPlugin(os.path.join(Test.Variables.AtsTestPluginsDir, 'ssl_hook_test.so'), ts) -tr.Processes.Default.Env = ts.Env -tr.Processes.Default.Command = \ - "traffic_server -C 'verify_global_plugin {filename}'".format( - filename="${PROXY_CONFIG_PLUGIN_PLUGIN_DIR}/ssl_hook_test.so") -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.StartBefore(ts) -tr.Processes.Default.Streams.stderr = Testers.ContainsExpression( - "NOTE: verifying plugin '.*' Success", "Verification should succeed") -""" -TEST: This is a regression test for a shared object file that doesn't have all -of the required symbols defined because of a malformed interaction between C -and C++ files. -""" -tr = Test.AddTestRun("Regression test for an undefined, mangled C++ symbol.") -ts = create_ts_process() -plugin_filename = 'missing_mangled_definition.so' -built_plugin_path = os.path.join(Test.Variables.AtsTestPluginsDir, plugin_filename) -ats_plugin_dir = ts.Env['PROXY_CONFIG_PLUGIN_PLUGIN_DIR'] -ts.Setup.Copy(built_plugin_path, ats_plugin_dir) -tr.Processes.Default.Env = ts.Env -tr.Processes.Default.Command = \ - "traffic_server -C 'verify_global_plugin {plugin_path}'".format( - plugin_path=os.path.join(ats_plugin_dir, plugin_filename)) -tr.Processes.Default.ReturnCode = 1 -tr.Processes.Default.StartBefore(ts) -tr.Processes.Default.Streams.stderr = Testers.ContainsExpression( - "ERROR:.*unable to load", "Should log failure to load shared object") -ts.Disk.diags_log.Content = [] diff --git a/tests/gold_tests/command_argument/verify_remap_plugin.test.py b/tests/gold_tests/command_argument/verify_remap_plugin.test.py deleted file mode 100644 index 554029f871a..00000000000 --- a/tests/gold_tests/command_argument/verify_remap_plugin.test.py +++ /dev/null @@ -1,121 +0,0 @@ -''' -Test the verify_remap_plugin TrafficServer command. -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import os - -Test.Summary = ''' -Test that the TrafficServer verify_remap_plugin command works as expected. -''' - -process_counter = 0 -no_log_data = {'diags': 'stderr', 'error': 'stderr', 'manager': 'stderr'} - - -def create_ts_process(): - """ - Create a unique ATS process with each call to this function. - """ - global process_counter - process_counter += 1 - - ts = Test.MakeATSProcess("ts{counter}".format(counter=process_counter), log_data=no_log_data) - - # Ideally we would set the test run's Processes.Default to ts, but deep - # copy of processes is not currently implemented in autest. Therefore we - # replace the command which ts runs with a dummy command, and pull in - # piecemeal the values from ts that we want into the test run. The helper - # only needs to create the ATS environment and runroot, so let it exit - # normally instead of depending on AuTest's cleanup signal behavior. - ts.Command = "true" - ts.ReturnCode = 0 - # Clear the ready criteria because the helper is ready as soon as it runs. - ts.Ready = None - return ts - - -""" -TEST: verify_remap_plugin should complain if an argument is not passed to it. -""" -tr = Test.AddTestRun("Verify the requirement of an argument") -ts = create_ts_process() -tr.Processes.Default.Env = ts.Env -tr.Processes.Default.Command = "traffic_server -C 'verify_remap_plugin'" -tr.Processes.Default.ReturnCode = 1 -tr.Processes.Default.StartBefore(ts) -tr.Processes.Default.Streams.stderr = Testers.ContainsExpression( - "ERROR: verifying a plugin requires a plugin SO file path argument", "Should warn about the need for an SO file argument") -""" -TEST: verify_remap_plugin should complain if the argument doesn't reference a shared -object file. -""" -tr = Test.AddTestRun("Verify the requirement of a file") -ts = create_ts_process() -tr.Processes.Default.Env = ts.Env -tr.Processes.Default.Command = \ - "traffic_server -C 'verify_remap_plugin {filename}'".format( - filename="/this/file/does/not/exist.so") -tr.Processes.Default.ReturnCode = 1 -tr.Processes.Default.StartBefore(ts) -tr.Processes.Default.Streams.stderr = Testers.ContainsExpression( - "ERROR: .*No such file or directory", "Should warn about the non-existent SO file argument") -""" -TEST: verify_remap_plugin should complain if the shared object file doesn't -have the expected Plugin symbols. -""" -tr = Test.AddTestRun("Verify the requirement of our Plugin API.") -ts = create_ts_process() -Test.PrepareTestPlugin(os.path.join(Test.Variables.AtsTestPluginsDir, 'missing_ts_plugin_init.so'), ts) -tr.Processes.Default.Env = ts.Env -tr.Processes.Default.Command = \ - "traffic_server -C 'verify_remap_plugin {filename}'".format( - filename="${PROXY_CONFIG_PLUGIN_PLUGIN_DIR}/missing_ts_plugin_init.so") -tr.Processes.Default.ReturnCode = 1 -tr.Processes.Default.StartBefore(ts) -tr.Processes.Default.Streams.stderr = Testers.ContainsExpression( - "ERROR: .*missing required function TSRemapInit", "Should warn about the need for the TSRemapInit symbol") -""" -TEST: verify_remap_plugin should complain if the plugin has the global -plugin symbols but not the remap ones. -""" -tr = Test.AddTestRun("Verify a global plugin argument produces warning.") -ts = create_ts_process() -Test.PrepareTestPlugin(os.path.join(Test.Variables.AtsTestPluginsDir, 'ssl_hook_test.so'), ts) -tr.Processes.Default.Env = ts.Env -tr.Processes.Default.Command = \ - "traffic_server -C 'verify_remap_plugin {filename}'".format( - filename="${PROXY_CONFIG_PLUGIN_PLUGIN_DIR}/ssl_hook_test.so") -tr.Processes.Default.ReturnCode = 1 -tr.Processes.Default.StartBefore(ts) -tr.Processes.Default.Streams.stderr = Testers.ContainsExpression( - "ERROR: .*missing required function TSRemapInit", "Should warn about the need for the TSRemapInit symbol") -""" -TEST: The happy case: a remap plugin shared object file is passed as an -argument that has the definition for the expected Plugin symbols. -""" -tr = Test.AddTestRun("Verify a properly formed plugin works as expected.") -ts = create_ts_process() -Test.PrepareTestPlugin(os.path.join(Test.Variables.AtsTestPluginsDir, 'conf_remap_stripped.so'), ts) -tr.Processes.Default.Env = ts.Env -tr.Processes.Default.Command = \ - "traffic_server -C 'verify_remap_plugin {filename}'".format( - filename="${PROXY_CONFIG_PLUGIN_PLUGIN_DIR}/conf_remap_stripped.so") -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.StartBefore(ts) -tr.Processes.Default.Streams.stderr = Testers.ContainsExpression( - "NOTE: verifying plugin '.*' Success", "Verification should succeed") diff --git a/tests/gold_tests/config_processor/config_destroy_thread.test.py b/tests/gold_tests/config_processor/config_destroy_thread.test.py deleted file mode 100644 index 0909c0f3bd3..00000000000 --- a/tests/gold_tests/config_processor/config_destroy_thread.test.py +++ /dev/null @@ -1,77 +0,0 @@ -''' -Verify that a replaced config is destroyed on an ET_TASK thread. -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import os - -Test.Summary = ''' -Verify that a replaced config is destroyed on an ET_TASK thread. -''' - -# ConfigProcessor::set() waits CONFIG_PROCESSOR_RELEASE_SECS before it releases the config that it -# replaced. That timeout is a compile time constant of 60 seconds, so this test needs more than a -# minute of wall clock and does not run in CI. Comment out the next line to run it. -Test.SkipIf(Condition.true("Test takes over 60 seconds to run.")) - -Test.ContinueOnFail = True - -ts = Test.MakeATSProcess("ts") - -ts.Disk.records_config.update({ - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'config', -}) - -ts.Disk.remap_config.AddLine('map / http://127.0.0.1:8080') - -config_dir = ts.Variables.CONFIGDIR - -# Two replacements, reached two different ways. Touching parent.config replaces ParentConfigParams -# through the reload framework, which runs on ET_TASK. Changing an HTTP record replaces -# HttpConfigParams from a network thread, which is the case this test is really about. Neither old -# config is referenced once the test stops sending traffic, so both reach a zero reference count and -# are destroyed when the release timeout expires. -tr = Test.AddTestRun("Mark parent.config for reload") -tr.Processes.Default.StartBefore(ts) -tr.Processes.Default.Command = f"sleep 3 && touch {os.path.join(config_dir, 'parent.config')} && sleep 1" -tr.Processes.Default.ReturnCode = 0 -tr.StillRunningAfter = ts - -Test.AddConfigReload(ts, expect="any", token="config_destroy_thread") - -tr = Test.AddTestRun("Replace the HTTP config from a network thread") -tr.DelayStart = 3 -tr.Processes.Default.Env = ts.Env -tr.Processes.Default.Command = "traffic_ctl config set proxy.config.http.response_server_str probe && sleep 3" -tr.Processes.Default.ReturnCode = 0 -tr.StillRunningAfter = ts - -tr = Test.AddTestRun("Wait for the release timeout to expire") -tr.DelayStart = 3 -tr.Processes.Default.Command = "sleep 80" -tr.Processes.Default.ReturnCode = 0 -tr.TimeOut = 150 -tr.StillRunningAfter = ts - -# The releaser runs on ET_TASK, so it destroys the replaced config there. -ts.Disk.traffic_out.Content = Testers.ContainsExpression( - r"Destroyed config \d+ in \d+ ns on thread \[ET_TASK", "a replaced config should be destroyed on a task thread") - -# Destroying a config on a network thread is the regression this test guards against. -ts.Disk.traffic_out.Content += Testers.ExcludesExpression( - r"Destroyed config \d+ in \d+ ns on thread \[ET_NET", "no config should be destroyed on a network thread") diff --git a/tests/gold_tests/connect/connect.test.py b/tests/gold_tests/connect/connect.test.py deleted file mode 100644 index efdcd21f6f2..00000000000 --- a/tests/gold_tests/connect/connect.test.py +++ /dev/null @@ -1,257 +0,0 @@ -''' -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -from enum import Enum -import os -import re - -Test.Summary = 'Exercise HTTP CONNECT Method' -Test.ContinueOnFail = True -Test.SkipIf(Condition.CurlUsingUnixDomainSocket()) - - -class ConnectTest: - - class State(Enum): - """ - State of process - """ - INIT = 0 - RUNNING = 1 - - def __init__(self): - self.state = self.State.INIT - self.__setupOriginServer() - self.__setupTS() - - def __setupOriginServer(self): - self.httpbin = Test.MakeHttpBinServer("httpbin") - - def __setupTS(self): - self.ts = Test.MakeATSProcess("ts") - - self.ts.Disk.records_config.update( - { - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'http', - 'proxy.config.http.server_ports': f"{self.ts.Variables.port}", - 'proxy.config.http.connect_ports': f"{self.httpbin.Variables.Port}", - }) - - self.ts.Disk.remap_config.AddLines([ - f"map http://foo.com/ http://127.0.0.1:{self.httpbin.Variables.Port}/", - ]) - self.ts.addPrivateConnectAllowYaml() - - self.ts.Disk.logging_yaml.AddLines( - ''' -logging: - formats: - - name: common - format: '% - % [%] "% % %" % %' - logs: - - filename: access - format: common -'''.split("\n")) - - def __checkProcessBefore(self, tr): - if self.state == self.State.RUNNING: - tr.StillRunningBefore = self.httpbin - tr.StillRunningBefore = self.ts - else: - tr.Processes.Default.StartBefore(self.httpbin) - tr.Processes.Default.StartBefore(self.ts) - self.state = self.State.RUNNING - - def __checkProcessAfter(self, tr): - assert (self.state == self.State.RUNNING) - tr.StillRunningAfter = self.httpbin - tr.StillRunningAfter = self.ts - - def __testCase0(self): - tr = Test.AddTestRun() - self.__checkProcessBefore(tr) - tr.MakeCurlCommand(f"-v --fail -s -p -x 127.0.0.1:{self.ts.Variables.port} 'http://foo.com/get'", ts=self.ts) - tr.Processes.Default.Streams.stderr = "gold/connect_0_stderr.gold" - tr.Processes.Default.Streams.stderr = Testers.ContainsExpression( - rf'(Connected to|Established connection to) 127\.0\.0\.1.*{self.ts.Variables.port}', - 'Curl should connect through the ATS proxy port.') - tr.Processes.Default.ReturnCode = 0 - tr.Processes.Default.TimeOut = 3 - self.__checkProcessAfter(tr) - - def __testAccessLog(self): - """Wait for the access log entry to be written.""" - Test.Disk.File(os.path.join(self.ts.Variables.LOGDIR, 'access.log'), exists=True, content='gold/connect_access.gold') - - Test.AddAwaitFileContainsTestRun( - 'Await CONNECT access log entry.', - os.path.join(self.ts.Variables.LOGDIR, 'access.log'), - 'CONNECT', - ) - - def run(self): - self.__testCase0() - self.__testAccessLog() - - -ConnectTest().run() - - -class ConnectViaPVTest: - # This test also executes the CONNECT request but using proxy verifier to - # generate traffic - connectReplayFile = "replays/connect.replay.yaml" - - def __init__(self): - self.setupOriginServer() - self.setupTS() - - def setupOriginServer(self): - self.server = Test.MakeVerifierServerProcess("connect-verifier-server", self.connectReplayFile) - # Verify server output - self.server.Streams.stdout += Testers.ExcludesExpression("uuid: 1", "Verify the CONNECT request doesn't reach the server.") - self.server.Streams.stdout += Testers.ContainsExpression( - "GET /get HTTP/1.1\nuuid: 2", reflags=re.MULTILINE, description="Verify the server gets the second request.") - - def setupTS(self): - self.ts = Test.MakeATSProcess("connect-ts") - - self.ts.Disk.records_config.update( - { - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'http|iocore_net|rec', - 'proxy.config.http.server_ports': f"{self.ts.Variables.port}", - 'proxy.config.http.connect_ports': f"{self.server.Variables.http_port}", - }) - - self.ts.Disk.remap_config.AddLines([ - f"map / http://127.0.0.1:{self.server.Variables.http_port}/", - ]) - self.ts.addPrivateConnectAllowYaml() - # Verify ts logs - self.ts.Disk.traffic_out.Content += Testers.ContainsExpression( - f"Proxy's Request.*\n.*\nCONNECT 127.0.0.1:{self.server.Variables.http_port} HTTP/1.1", - reflags=re.MULTILINE, - description="Verify that ATS recognizes the CONNECT request.") - - def runTraffic(self): - tr = Test.AddTestRun("Verify correct handling of CONNECT request") - tr.AddVerifierClientProcess("connect-client", self.connectReplayFile, http_ports=[self.ts.Variables.port]) - tr.Processes.Default.StartBefore(self.server) - tr.Processes.Default.StartBefore(self.ts) - tr.StillRunningAfter = self.server - tr.StillRunningAfter = self.ts - - def __testMetrics(self): - tr = Test.AddTestRun("Test metrics") - tr.Processes.Default.Command = ( - f"{Test.Variables.AtsTestToolsDir}/stdout_wait" + " 'traffic_ctl metric get" + - " proxy.process.http.total_incoming_connections" + " proxy.process.http.total_client_connections" + - " proxy.process.http.total_client_connections_ipv4" + " proxy.process.http.total_client_connections_ipv6" + - " proxy.process.http.total_server_connections" + " proxy.process.http2.total_client_connections" + - " proxy.process.http.connect_requests" + " proxy.process.tunnel.total_client_connections_blind_tcp" + - " proxy.process.tunnel.current_client_connections_blind_tcp" + - " proxy.process.tunnel.total_server_connections_blind_tcp" + - " proxy.process.tunnel.current_server_connections_blind_tcp" + - " proxy.process.tunnel.total_client_connections_tls_tunnel" + - " proxy.process.tunnel.current_client_connections_tls_tunnel" + - " proxy.process.tunnel.total_client_connections_tls_forward" + - " proxy.process.tunnel.current_client_connections_tls_forward" + - " proxy.process.tunnel.total_client_connections_tls_partial_blind" + - " proxy.process.tunnel.current_client_connections_tls_partial_blind" + - " proxy.process.tunnel.total_client_connections_tls_http" + - " proxy.process.tunnel.current_client_connections_tls_http" + " proxy.process.tunnel.total_server_connections_tls" + - " proxy.process.tunnel.current_server_connections_tls'" + f" {Test.TestDirectory}/gold/metrics.gold") - # Need to copy over the environment so traffic_ctl knows where to find the unix domain socket - tr.Processes.Default.Env = self.ts.Env - tr.Processes.Default.ReturnCode = 0 - tr.StillRunningAfter = self.server - tr.StillRunningAfter = self.ts - - def run(self): - self.runTraffic() - self.__testMetrics() - - -ConnectViaPVTest().run() - - -class ConnectViaPVTest2: - # This test executes a HTTP/2 CONNECT request with Proxy Verifier. - connectReplayFile = "replays/connect_h2.replay.yaml" - - def __init__(self): - self.setupOriginServer() - self.setupTS() - - def setupOriginServer(self): - self.server = Test.MakeVerifierServerProcess("connect-verifier-server2", self.connectReplayFile) - # Verify server output - self.server.Streams.stdout += Testers.ExcludesExpression( - "test: connect-request", "Verify the CONNECT request doesn't reach the server.") - self.server.Streams.stdout += Testers.ContainsExpression( - "GET /get HTTP/1.1\nuuid: 1\ntest: real-request", - reflags=re.MULTILINE, - description="Verify the server gets the second(tunneled) request.") - - def setupTS(self): - self.ts = Test.MakeATSProcess("connect-ts2", enable_tls=True) - - self.ts.Disk.records_config.update( - { - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'http|hpack', - 'proxy.config.ssl.server.cert.path': f'{self.ts.Variables.SSLDir}', - 'proxy.config.ssl.server.private_key.path': f'{self.ts.Variables.SSLDir}', - 'proxy.config.http.server_ports': f"{self.ts.Variables.ssl_port}:ssl", - 'proxy.config.http.connect_ports': f"{self.server.Variables.http_port}", - }) - - self.ts.addDefaultSSLFiles() - self.ts.Disk.ssl_multicert_yaml.AddLines( - """ -ssl_multicert: - - dest_ip: "*" - ssl_cert_name: server.pem - ssl_key_name: server.key -""".split("\n")) - - self.ts.Disk.remap_config.AddLines([ - f"map / http://127.0.0.1:{self.server.Variables.http_port}/", - ]) - self.ts.addPrivateConnectAllowYaml() - # Verify ts logs - self.ts.Disk.traffic_out.Content += Testers.ContainsExpression( - f"Proxy's Request.*\n.*\nCONNECT 127.0.0.1:{self.server.Variables.http_port} HTTP/1.1", - reflags=re.MULTILINE, - description="Verify that ATS recognizes the CONNECT request.") - - def runTraffic(self): - tr = Test.AddTestRun("Verify correct handling of CONNECT request on HTTP/2") - tr.AddVerifierClientProcess("connect-client2", self.connectReplayFile, https_ports=[self.ts.Variables.ssl_port]) - tr.Processes.Default.StartBefore(self.server) - tr.Processes.Default.StartBefore(self.ts) - tr.StillRunningAfter = self.server - tr.StillRunningAfter = self.ts - - def run(self): - self.runTraffic() - - -ConnectViaPVTest2().run() diff --git a/tests/gold_tests/connect/connect_handshake.test.py b/tests/gold_tests/connect/connect_handshake.test.py deleted file mode 100644 index 1d3793dace9..00000000000 --- a/tests/gold_tests/connect/connect_handshake.test.py +++ /dev/null @@ -1,65 +0,0 @@ -'''Verify CONNECT waits for the origin TCP handshake.''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import ports - -Test.Summary = 'Verify CONNECT waits for the origin TCP handshake.' - - -class ConnectHandshakeTest: - '''Verify CONNECT failure is reported before establishing a tunnel.''' - - replay_file: str = 'replays/connect_handshake.replay.yaml' - - def __init__(self) -> None: - '''Configure the test run.''' - tr = Test.AddTestRun('CONNECT to a refused origin port') - self._configure_unavailable_origin(tr) - self._configure_traffic_server(tr) - self._configure_client(tr) - - def _configure_unavailable_origin(self, tr: 'TestRun') -> 'Process': - '''Reserve an origin port without starting a listening server.''' - origin = tr.Processes.Process('unavailable-origin') - ports.get_port(origin, 'Port') - self._origin = origin - return origin - - def _configure_traffic_server(self, tr: 'TestRun') -> 'Process': - '''Configure Traffic Server as an explicit proxy.''' - ts = tr.MakeATSProcess('ts', enable_cache=False) - self._ts = ts - - origin_port = self._origin.Variables.Port - ts.Disk.records_config.update( - { - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'http|iocore_net', - 'proxy.config.http.connect_ports': f'{origin_port}', - }) - ts.Disk.remap_config.AddLine(f'map / http://127.0.0.1:{origin_port}') - ts.addPrivateConnectAllowYaml() - return ts - - def _configure_client(self, tr: 'TestRun') -> 'Process': - '''Configure a Proxy Verifier client that expects the refusal.''' - client = tr.AddVerifierClientProcess('client', self.replay_file, http_ports=[self._ts.Variables.port]) - client.StartBefore(self._ts) - return client - - -ConnectHandshakeTest() diff --git a/tests/gold_tests/connect/connect_parent_error_body.test.py b/tests/gold_tests/connect/connect_parent_error_body.test.py deleted file mode 100644 index 8ac78d9158b..00000000000 --- a/tests/gold_tests/connect/connect_parent_error_body.test.py +++ /dev/null @@ -1,72 +0,0 @@ -''' -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -Test.Summary = 'Verify CONNECT error response bodies from parent proxies' -Test.ContinueOnFail = True - - -class ConnectParentErrorBodyTest: - replay_file = 'replays/connect_parent_error_body.replay.yaml' - - def __init__(self): - self._setupParentProxy() - self._setupTS() - - def _setupParentProxy(self): - self.parent = Test.MakeVerifierServerProcess('parent-proxy', self.replay_file) - self.parent.Streams.stdout += Testers.ContainsExpression( - 'CONNECT www.example.com:443 HTTP/1.1', 'Verify that ATS forwards the CONNECT request to the parent proxy.') - self.parent.Streams.stdout += Testers.ContainsExpression( - 'GET http://www.example.com/next HTTP/1.1', 'Verify that ATS reuses the parent connection for the next request.') - # Each no-DNS-forward request must reach the parent for that request's - # host. The parent receiving the third request with its own Host - # header confirms ATS did not short-circuit to a destination derived - # from earlier State (the dns_info.addr stale-data case). - self.parent.Streams.stdout += Testers.ContainsExpression( - 'GET http://other.example.org/page HTTP/1.1', - 'Verify that ATS forwards a different host on the same session to the parent.') - - def _setupTS(self): - self.ts = Test.MakeATSProcess('ts', enable_cache=False) - - self.ts.Disk.records_config.update( - { - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'http|parent', - 'proxy.config.http.connect_ports': '443', - 'proxy.config.http.no_dns_just_forward_to_parent': 1, - 'proxy.config.http.parent_proxy.self_detect': 0, - 'proxy.config.http.server_ports': f'{self.ts.Variables.port}', - 'proxy.config.http.server_session_sharing.pool': 'global', - 'proxy.config.url_remap.remap_required': 0, - }) - - self.ts.Disk.parent_config.AddLine( - f'dest_domain=. parent="127.0.0.1:{self.parent.Variables.http_port}|1" go_direct=false parent_is_proxy=true') - self.ts.addPrivateConnectAllowYaml(methods='[ CONNECT, GET ]') - - def run(self): - tr = Test.AddTestRun('CONNECT error response body from parent proxy') - tr.AddVerifierClientProcess('client', self.replay_file, http_ports=[self.ts.Variables.port]) - tr.Processes.Default.StartBefore(self.parent) - tr.Processes.Default.StartBefore(self.ts) - tr.StillRunningAfter = self.parent - tr.StillRunningAfter = self.ts - - -ConnectParentErrorBodyTest().run() diff --git a/tests/gold_tests/connect/h2_malformed_request_logging.test.py b/tests/gold_tests/connect/h2_malformed_request_logging.test.py deleted file mode 100644 index de5e1a75d95..00000000000 --- a/tests/gold_tests/connect/h2_malformed_request_logging.test.py +++ /dev/null @@ -1,202 +0,0 @@ -''' -Verify malformed HTTP/2 requests are access logged. -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import os -import re -import sys - -Test.Summary = 'Malformed HTTP/2 requests are logged before transaction creation' - - -class MalformedH2RequestLoggingTest: - """ - Exercise malformed and valid HTTP/2 request logging paths. - """ - - REPLAY_FILE = 'replays/h2_malformed_request_logging.replay.yaml' - MALFORMED_CLIENT = 'malformed_h2_request_client.py' - MALFORMED_CASES = ( - { - 'scenario': 'connect-missing-authority', - 'uuid': 'malformed-connect', - 'method': 'CONNECT', - 'pqu': '/', - 'description': 'Send malformed HTTP/2 CONNECT request', - }, - { - 'scenario': 'get-missing-path', - 'uuid': 'malformed-get-missing-path', - 'method': 'GET', - 'pqu': 'https://missing-path.example/', - 'description': 'Send malformed HTTP/2 GET request without :path', - }, - { - 'scenario': 'get-connection-header', - 'uuid': 'malformed-get-connection', - 'method': 'GET', - 'pqu': 'https://bad-connection.example/bad-connection', - 'description': 'Send malformed HTTP/2 GET request with Connection header', - }, - ) - - def __init__(self): - self._setup_server() - self._setup_ts() - self._processes_started = False - Test.Setup.CopyAs(self.MALFORMED_CLIENT, Test.RunDirectory) - - @property - def _squid_log_path(self) -> str: - return os.path.join(self._ts.Variables.LOGDIR, 'squid.log') - - def _setup_server(self): - self._server = Test.MakeVerifierServerProcess('malformed-request-server', self.REPLAY_FILE) - for case in self.MALFORMED_CASES: - self._server.Streams.stdout += Testers.ExcludesExpression( - f'uuid: {case["uuid"]}', - f'{case["description"]} must not reach the origin server.', - ) - self._server.Streams.stdout += Testers.ContainsExpression( - 'GET /get HTTP/1.1\nuuid: valid-connect', - reflags=re.MULTILINE, - description='A valid CONNECT tunnel should still reach the origin.', - ) - self._server.Streams.stdout += Testers.ContainsExpression( - r'GET /valid-get HTTP/1\.1\n(?:.*\n)*uuid: valid-get', - reflags=re.MULTILINE, - description='A valid non-CONNECT request should still reach the origin.', - ) - - def _setup_ts(self): - self._ts = Test.MakeATSProcess('ts', enable_tls=True, enable_cache=False) - self._ts.addDefaultSSLFiles() - self._ts.Disk.File( - os.path.join(self._ts.Variables.CONFIGDIR, 'storage.config'), - id='storage_config', - typename='ats:config', - ) - self._ts.Disk.storage_config.AddLine('') - self._ts.Disk.ssl_multicert_yaml.AddLines( - """ -ssl_multicert: - - dest_ip: "*" - ssl_cert_name: server.pem - ssl_key_name: server.key -""".split('\n')) - - self._ts.Disk.records_config.update( - { - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'http|hpack|http2', - 'proxy.config.ssl.server.cert.path': self._ts.Variables.SSLDir, - 'proxy.config.ssl.server.private_key.path': self._ts.Variables.SSLDir, - 'proxy.config.http.server_ports': f'{self._ts.Variables.ssl_port}:ssl', - 'proxy.config.http.connect_ports': self._server.Variables.http_port, - }) - self._ts.Disk.remap_config.AddLine(f'map / http://127.0.0.1:{self._server.Variables.http_port}/') - self._ts.addPrivateConnectAllowYaml(methods='[ CONNECT, GET ]') - self._ts.Disk.logging_yaml.AddLines( - """ -logging: - formats: - - name: malformed_h2_request - format: 'uuid=%<{uuid}cqh> cqpv=% cqhm=% crc=% sstc=% pqu=%' - logs: - - filename: squid - format: malformed_h2_request - mode: ascii -""".split('\n')) - self._ts.Disk.traffic_out.Content += Testers.ContainsExpression( - 'recv headers malformed request', - 'ATS should reject malformed requests at the HTTP/2 layer.', - ) - for index, case in enumerate(self.MALFORMED_CASES): - expected = ( - rf'uuid={case["uuid"]} cqpv=http/2 cqhm={case["method"]} ' - rf'crc=ERR_INVALID_REQ sstc=0 pqu={re.escape(case["pqu"])}') - tester = Testers.ContainsExpression( - expected, - f'{case["description"]} should be logged with ERR_INVALID_REQ.', - ) - if index == 0: - self._ts.Disk.squid_log.Content = tester - else: - self._ts.Disk.squid_log.Content += tester - self._ts.Disk.squid_log.Content += Testers.ContainsExpression( - r'uuid=valid-connect cqpv=http/2 cqhm=CONNECT ', - 'A valid HTTP/2 CONNECT should still use the normal transaction log path.', - ) - self._ts.Disk.squid_log.Content += Testers.ContainsExpression( - r'uuid=valid-get cqpv=http/2 cqhm=GET ', - 'A valid HTTP/2 GET should still use the normal transaction log path.', - ) - self._ts.Disk.squid_log.Content += Testers.ExcludesExpression( - r'uuid=valid-connect .*crc=ERR_INVALID_REQ', - 'Valid HTTP/2 CONNECT logging must not be marked as malformed.', - ) - self._ts.Disk.squid_log.Content += Testers.ExcludesExpression( - r'uuid=valid-get .*crc=ERR_INVALID_REQ', - 'Valid HTTP/2 GET logging must not be marked as malformed.', - ) - - def _add_malformed_request_runs(self): - for case in self.MALFORMED_CASES: - tr = Test.AddTestRun(case['description']) - tr.Processes.Default.Command = ( - f'{sys.executable} {self.MALFORMED_CLIENT} {self._ts.Variables.ssl_port} {case["scenario"]}') - tr.Processes.Default.ReturnCode = 0 - self._keep_support_processes_running(tr) - tr.Processes.Default.Streams.stdout += Testers.ContainsExpression( - r'Received (RST_STREAM on stream 1 with error code 1|GOAWAY with error code [01])', - 'ATS should reject the malformed request at the HTTP/2 layer.', - ) - - def _add_valid_request_run(self): - tr = Test.AddTestRun('Send valid HTTP/2 requests') - tr.AddVerifierClientProcess('valid-request-client', self.REPLAY_FILE, https_ports=[self._ts.Variables.ssl_port]) - self._keep_support_processes_running(tr) - - def _await_malformed_log_entries(self): - tr = Test.AddAwaitFileContainsTestRun( - 'Await malformed request squid log entries', - self._squid_log_path, - 'crc=ERR_INVALID_REQ', - desired_count=len(self.MALFORMED_CASES), - ) - self._keep_support_processes_running(tr) - - def _keep_support_processes_running(self, tr): - if self._processes_started: - tr.StillRunningAfter = self._server - tr.StillRunningAfter = self._ts - return - - tr.Processes.Default.StartBefore(self._server) - tr.Processes.Default.StartBefore(self._ts) - tr.StillRunningAfter = self._server - tr.StillRunningAfter = self._ts - self._processes_started = True - - def run(self): - self._add_malformed_request_runs() - self._add_valid_request_run() - self._await_malformed_log_entries() - - -MalformedH2RequestLoggingTest().run() diff --git a/tests/gold_tests/connect_down_policy/connect_down_policy.test.py b/tests/gold_tests/connect_down_policy/connect_down_policy.test.py deleted file mode 100644 index f40af24cc45..00000000000 --- a/tests/gold_tests/connect_down_policy/connect_down_policy.test.py +++ /dev/null @@ -1,113 +0,0 @@ -''' -Verify proxy.config.http.connect.down.policy=3 marks the origin down on -transaction inactive timeout (server goes silent after connection is established), -and that policy=2 does not mark the origin down for the same scenario. -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import os - -Test.Summary = ''' -Verify connect.down.policy=3 marks the origin down on inactive timeout after -connection is established, and policy=2 does not. -''' - -REPLAY_FILE = "replay/inactive_timeout.replay.yaml" - -# Inactivity timeout in seconds. The replay server-response delay (10s) is -# intentionally longer so ATS fires the timeout before the server replies. -INACTIVITY_TIMEOUT = 3 - - -class ConnectDownPolicy3Test: - """ - Test that policy=3 marks the origin server down when the server goes silent - and ATS fires an INACTIVE_TIMEOUT. - - Sequence: - 1. ATS connects to the origin and sends the request. - 2. The origin delays its response beyond INACTIVITY_TIMEOUT. - 3. ATS fires VC_EVENT_INACTIVITY_TIMEOUT, calling track_connect_fail(). - 4. Under policy=3 track_connect_fail() returns true → mark_host_failure(). - 5. With connect_attempts_rr_retries=0, increment_fail_count marks the host - down immediately (fcount >= 0), writing a "marking down" entry to error.log. - """ - - def __init__(self, policy, expect_mark_down): - self._policy = policy - self._expect_mark_down = expect_mark_down - self._name = f"policy{policy}" - self._server = Test.MakeVerifierServerProcess(f"server-{self._name}", REPLAY_FILE) - self._configure_trafficserver() - - def _configure_trafficserver(self): - self._ts = Test.MakeATSProcess(f"ts-{self._name}", enable_cache=False) - - self._ts.Disk.remap_config.AddLine(f"map / http://127.0.0.1:{self._server.Variables.http_port}/") - - self._ts.Disk.records_config.update( - { - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'hostdb|http', - # Use policy under test. - 'proxy.config.http.connect.down.policy': self._policy, - # No connection retries — the single timeout failure is sufficient. - 'proxy.config.http.connect_attempts_max_retries': 0, - # Set rr_retries=0 so it does not exceed max_retries (which would - # emit a Warning). With max_retries=0, increment_fail_count marks - # the host down on the first failure (fcount >= 0 is always true). - 'proxy.config.http.connect_attempts_rr_retries': 0, - # Short server-side inactivity timeout so the test runs quickly. - 'proxy.config.http.transaction_no_activity_timeout_out': INACTIVITY_TIMEOUT, - # Keep the host marked down long enough to verify. - 'proxy.config.hostdb.fail.timeout': 60, - }) - - def _test_inactive_timeout(self): - tr = Test.AddTestRun(f"policy={self._policy}: inactive timeout triggers 504") - tr.Processes.Default.StartBefore(self._server) - tr.Processes.Default.StartBefore(self._ts) - tr.AddVerifierClientProcess(f"client-{self._name}", REPLAY_FILE, http_ports=[self._ts.Variables.port]) - - def _test_mark_down(self): - if self._expect_mark_down: - tr = Test.AddAwaitFileContainsTestRun( - f"policy={self._policy}: check error.log for mark-down", - os.path.join(self._ts.Variables.LOGDIR, 'error.log'), - "marking down", - ) - self._ts.Disk.error_log.Content = Testers.ContainsExpression( - "marking down", f"policy={self._policy}: origin should be marked down after inactive timeout") - else: - # Pre-create error.log so ExcludesExpression can read it even if ATS - # never writes to it (policy=2 should not mark the host down). - error_log_path = os.path.join(self._ts.Variables.LOGDIR, 'error.log') - tr = Test.AddTestRun(f"policy={self._policy}: verify no mark-down in error.log") - tr.Processes.Default.Command = f"touch {error_log_path}" - self._ts.Disk.error_log.Content = Testers.ExcludesExpression( - "marking down", f"policy={self._policy}: origin should NOT be marked down after inactive timeout") - - def run(self): - self._test_inactive_timeout() - self._test_mark_down() - - -# Policy 3: inactive timeout SHOULD mark the origin down. -ConnectDownPolicy3Test(policy=3, expect_mark_down=True).run() - -# Policy 2: inactive timeout should NOT mark the origin down. -ConnectDownPolicy3Test(policy=2, expect_mark_down=False).run() diff --git a/tests/gold_tests/cont_schedule/schedule_every_on_entire_pool.test.py b/tests/gold_tests/cont_schedule/schedule_every_on_entire_pool.test.py deleted file mode 100644 index a63a3f3817c..00000000000 --- a/tests/gold_tests/cont_schedule/schedule_every_on_entire_pool.test.py +++ /dev/null @@ -1,64 +0,0 @@ -''' -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import os -import sys - -Test.Summary = 'Test TSContScheduleEveryOnEntirePool API' -Test.ContinueOnFail = True - -# Define default ATS -ts = Test.MakeATSProcess('ts') - -Test.testName = 'Test TSContScheduleEveryOnEntirePool API' - -ts.Disk.records_config.update( - { - 'proxy.config.exec_thread.autoconfig.enabled': 0, - 'proxy.config.exec_thread.autoconfig.scale': 1.5, - 'proxy.config.exec_thread.limit': 32, - 'proxy.config.accept_threads': 1, - 'proxy.config.task_threads': 2, - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'TSContSchedule_test' - }) - -ts.Setup.Copy('entire_pool.py') - -# Load plugin -Test.PrepareTestPlugin(os.path.join(Test.Variables.AtsTestPluginsDir, 'cont_schedule.so'), ts, 'every_entire') - -# www.example.com Host -tr = Test.AddTestRun() -tr.Processes.Default.Command = 'printf "Test TSContScheduleEveryOnEntirePool API"' -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.StartBefore(ts) - -tr = Test.AddTestRun("Wait traffic.out to be written") -timeout = 30 -watcher = tr.Processes.Process("watcher") -watcher.Command = f"sleep {timeout}" -watcher.Ready = When.FileExists(ts.Disk.traffic_out.AbsPath) -watcher.TimeOut = timeout -tr.TimeOut = timeout -tr.DelayStart = 3 -tr.Processes.Default.StartBefore(watcher) -tr.Processes.Default.Command = f'{sys.executable} entire_pool.py {ts.Disk.traffic_out.AbsPath} ET_NET 32 2' -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Streams.All = "gold/schedule_every_on_entire_pool.gold" -tr.Processes.Default.Streams.All += Testers.ExcludesExpression('fail', 'should not contain "fail"') diff --git a/tests/gold_tests/cont_schedule/schedule_every_on_pool.test.py b/tests/gold_tests/cont_schedule/schedule_every_on_pool.test.py deleted file mode 100644 index d0db9097339..00000000000 --- a/tests/gold_tests/cont_schedule/schedule_every_on_pool.test.py +++ /dev/null @@ -1,62 +0,0 @@ -''' -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import os - -Test.Summary = 'Test TSContScheduleEveryOnPool API' -Test.ContinueOnFail = True - -# Define default ATS -ts = Test.MakeATSProcess('ts') - -Test.testName = 'Test TSContScheduleEveryOnPool API' - -ts.Disk.records_config.update( - { - 'proxy.config.exec_thread.autoconfig.enabled': 0, - 'proxy.config.exec_thread.autoconfig.scale': 1.5, - 'proxy.config.exec_thread.limit': 32, - 'proxy.config.accept_threads': 1, - 'proxy.config.task_threads': 2, - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'TSContSchedule_test' - }) - -# Load plugin -Test.PrepareTestPlugin(os.path.join(Test.Variables.AtsTestPluginsDir, 'cont_schedule.so'), ts, 'every_pool') - -# www.example.com Host -tr = Test.AddTestRun() -tr.Processes.Default.Command = 'printf "Test TSContScheduleEveryOnPool API"' -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.StartBefore(ts) - -tr = Test.AddTestRun("Wait traffic.out to be written") -timeout = 30 -watcher = tr.Processes.Process("watcher") -watcher.Command = f"sleep {timeout}" -watcher.Ready = When.FileExists(ts.Disk.traffic_out.AbsPath) -watcher.TimeOut = timeout -tr.TimeOut = timeout -tr.DelayStart = 3 -tr.Processes.Default.StartBefore(watcher) -tr.Processes.Default.Command = 'printf "await traffic.out"' -tr.Processes.Default.ReturnCode = 0 -ts.Disk.traffic_out.Content = "gold/schedule_every_on_pool.gold" -ts.Disk.traffic_out.Content += Testers.IncludesExpression('pass', 'must contain "pass"') -ts.Disk.traffic_out.Content += Testers.ExcludesExpression('fail', 'should not contain "fail"') diff --git a/tests/gold_tests/cont_schedule/schedule_every_on_thread.test.py b/tests/gold_tests/cont_schedule/schedule_every_on_thread.test.py deleted file mode 100644 index 659a0710735..00000000000 --- a/tests/gold_tests/cont_schedule/schedule_every_on_thread.test.py +++ /dev/null @@ -1,62 +0,0 @@ -''' -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import os - -Test.Summary = 'Test TSContScheduleEveryOnThread API' -Test.ContinueOnFail = True - -# Define default ATS -ts = Test.MakeATSProcess('ts') - -Test.testName = 'Test TSContScheduleEveryOnThread API' - -ts.Disk.records_config.update( - { - 'proxy.config.exec_thread.autoconfig.enabled': 0, - 'proxy.config.exec_thread.autoconfig.scale': 1.5, - 'proxy.config.exec_thread.limit': 32, - 'proxy.config.accept_threads': 1, - 'proxy.config.task_threads': 2, - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'TSContSchedule_test' - }) - -# Load plugin -Test.PrepareTestPlugin(os.path.join(Test.Variables.AtsTestPluginsDir, 'cont_schedule.so'), ts, 'every_thread') - -# www.example.com Host -tr = Test.AddTestRun() -tr.Processes.Default.Command = 'printf "Test TSContScheduleEveryOnThread API"' -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.StartBefore(ts) - -tr = Test.AddTestRun("Wait traffic.out to be written") -timeout = 30 -watcher = tr.Processes.Process("watcher") -watcher.Command = f"sleep {timeout}" -watcher.Ready = When.FileExists(ts.Disk.traffic_out.AbsPath) -watcher.TimeOut = timeout -tr.TimeOut = timeout -tr.DelayStart = 3 -tr.Processes.Default.StartBefore(watcher) -tr.Processes.Default.Command = 'printf "await traffic.out"' -tr.Processes.Default.ReturnCode = 0 -ts.Disk.traffic_out.Content = "gold/schedule_every_on_thread.gold" -ts.Disk.traffic_out.Content += Testers.IncludesExpression('pass', 'must contain "pass"') -ts.Disk.traffic_out.Content += Testers.ExcludesExpression('fail', 'should not contain "fail"') diff --git a/tests/gold_tests/cont_schedule/schedule_on_entire_pool.test.py b/tests/gold_tests/cont_schedule/schedule_on_entire_pool.test.py deleted file mode 100644 index c199db600d6..00000000000 --- a/tests/gold_tests/cont_schedule/schedule_on_entire_pool.test.py +++ /dev/null @@ -1,64 +0,0 @@ -''' -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import os -import sys - -Test.Summary = 'Test TSContScheduleOnEntirePool API' -Test.ContinueOnFail = True - -# Define default ATS -ts = Test.MakeATSProcess('ts') - -Test.testName = 'Test TSContScheduleOnEntirePool API' - -ts.Disk.records_config.update( - { - 'proxy.config.exec_thread.autoconfig.enabled': 0, - 'proxy.config.exec_thread.autoconfig.scale': 1.5, - 'proxy.config.exec_thread.limit': 32, - 'proxy.config.accept_threads': 1, - 'proxy.config.task_threads': 2, - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'TSContSchedule_test' - }) - -ts.Setup.Copy('entire_pool.py') - -# Load plugin -Test.PrepareTestPlugin(os.path.join(Test.Variables.AtsTestPluginsDir, 'cont_schedule.so'), ts, 'entire') - -# www.example.com Host -tr = Test.AddTestRun() -tr.Processes.Default.Command = 'printf "Test TSContScheduleOnEntirePool API"' -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.StartBefore(ts) - -tr = Test.AddTestRun("Wait traffic.out to be written") -timeout = 30 -watcher = tr.Processes.Process("watcher") -watcher.Command = f"sleep {timeout}" -watcher.Ready = When.FileExists(ts.Disk.traffic_out.AbsPath) -watcher.TimeOut = timeout -tr.TimeOut = timeout -tr.DelayStart = 2 -tr.Processes.Default.StartBefore(watcher) -tr.Processes.Default.Command = f'{sys.executable} entire_pool.py {ts.Disk.traffic_out.AbsPath} ET_NET 32 1' -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Streams.All = "gold/schedule_on_entire_pool.gold" -tr.Processes.Default.Streams.All += Testers.ExcludesExpression('fail', 'should not contain "fail"') diff --git a/tests/gold_tests/cont_schedule/schedule_on_pool.test.py b/tests/gold_tests/cont_schedule/schedule_on_pool.test.py deleted file mode 100644 index 8e1d849f697..00000000000 --- a/tests/gold_tests/cont_schedule/schedule_on_pool.test.py +++ /dev/null @@ -1,49 +0,0 @@ -''' -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import os - -Test.Summary = 'Test TSContScheduleOnPool API' -Test.ContinueOnFail = True - -# Define default ATS -ts = Test.MakeATSProcess('ts') - -Test.testName = 'Test TSContScheduleOnPool API' - -ts.Disk.records_config.update( - { - 'proxy.config.exec_thread.autoconfig.enabled': 0, - 'proxy.config.exec_thread.autoconfig.scale': 1.5, - 'proxy.config.exec_thread.limit': 32, - 'proxy.config.accept_threads': 1, - 'proxy.config.task_threads': 2, - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'TSContSchedule_test' - }) - -# Load plugin -Test.PrepareTestPlugin(os.path.join(Test.Variables.AtsTestPluginsDir, 'cont_schedule.so'), ts, 'pool') - -# www.example.com Host -tr = Test.AddTestRun() -tr.Processes.Default.Command = 'printf "Test TSContScheduleOnPool API"' -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.StartBefore(ts) -ts.Disk.traffic_out.Content = "gold/schedule_on_pool.gold" -ts.Disk.traffic_out.Content += Testers.ExcludesExpression('fail', 'should not contain "fail"') diff --git a/tests/gold_tests/cont_schedule/schedule_on_thread.test.py b/tests/gold_tests/cont_schedule/schedule_on_thread.test.py deleted file mode 100644 index 2ee59815f33..00000000000 --- a/tests/gold_tests/cont_schedule/schedule_on_thread.test.py +++ /dev/null @@ -1,49 +0,0 @@ -''' -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import os - -Test.Summary = 'Test TSContScheduleOnThread API' -Test.ContinueOnFail = True - -# Define default ATS -ts = Test.MakeATSProcess('ts') - -Test.testName = 'Test TSContScheduleOnThread API' - -ts.Disk.records_config.update( - { - 'proxy.config.exec_thread.autoconfig.enabled': 0, - 'proxy.config.exec_thread.autoconfig.scale': 1.5, - 'proxy.config.exec_thread.limit': 32, - 'proxy.config.accept_threads': 1, - 'proxy.config.task_threads': 2, - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'TSContSchedule_test' - }) - -# Load plugin -Test.PrepareTestPlugin(os.path.join(Test.Variables.AtsTestPluginsDir, 'cont_schedule.so'), ts, 'thread') - -# www.example.com Host -tr = Test.AddTestRun() -tr.Processes.Default.Command = 'printf "Test TSContScheduleOnThread API"' -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.StartBefore(ts) -ts.Disk.traffic_out.Content = "gold/schedule_on_thread.gold" -ts.Disk.traffic_out.Content += Testers.ExcludesExpression('fail', 'should not contain "fail"') diff --git a/tests/gold_tests/cont_schedule/thread_affinity.test.py b/tests/gold_tests/cont_schedule/thread_affinity.test.py deleted file mode 100644 index d8919d4d4f5..00000000000 --- a/tests/gold_tests/cont_schedule/thread_affinity.test.py +++ /dev/null @@ -1,49 +0,0 @@ -''' -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import os - -Test.Summary = 'Test TSContThreadAffinity APIs' -Test.ContinueOnFail = True - -# Define default ATS -ts = Test.MakeATSProcess('ts') - -Test.testName = 'Test TSContThreadAffinity APIs' - -ts.Disk.records_config.update( - { - 'proxy.config.exec_thread.autoconfig.enabled': 0, - 'proxy.config.exec_thread.autoconfig.scale': 1.5, - 'proxy.config.exec_thread.limit': 32, - 'proxy.config.accept_threads': 1, - 'proxy.config.task_threads': 2, - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'TSContSchedule_test' - }) - -# Load plugin -Test.PrepareTestPlugin(os.path.join(Test.Variables.AtsTestPluginsDir, 'cont_schedule.so'), ts, 'affinity') - -# www.example.com Host -tr = Test.AddTestRun() -tr.Processes.Default.Command = 'printf "Test TSContThreadAffinity API"' -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.StartBefore(ts) -ts.Disk.traffic_out.Content = "gold/thread_affinity.gold" -ts.Disk.traffic_out.Content += Testers.ExcludesExpression('fail', 'should not contain "fail"') diff --git a/tests/gold_tests/continuations/double.test.py b/tests/gold_tests/continuations/double.test.py deleted file mode 100644 index 24053fd1706..00000000000 --- a/tests/gold_tests/continuations/double.test.py +++ /dev/null @@ -1,134 +0,0 @@ -''' -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import os -import subprocess - -Test.Summary = ''' -Test transactions and sessions for http1, making sure the two continuations catch the same number of hooks. -''' - -Test.ContinueOnFail = True -# Define default ATS. Disable the cache to simplify the test. -ts = Test.MakeATSProcess("ts", enable_cache=False) -server = Test.MakeOriginServer("server") - -Test.testName = "" -request_header = {"headers": "GET / HTTP/1.1\r\nHost: double_h2.test\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -# expected response from the origin server -response_header = { - "headers": "HTTP/1.1 200 OK\r\nConnection: close\r\nContent-Length:0\r\n\r\n", - "timestamp": "1469733493.993", - "body": "" -} - -# add response to the server dictionary -server.addResponse("sessionfile.log", request_header, response_header) - -# add port and remap rule -ts.Disk.remap_config.AddLine('map / http://127.0.0.1:{0}'.format(server.Variables.Port)) - -ts.Disk.records_config.update({ - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'continuations_verify.*', -}) - -# add plugin to assist with test metrics -Test.PrepareTestPlugin(os.path.join(Test.Variables.AtsTestPluginsDir, 'continuations_verify.so'), ts) - -comparator_command = ''' -if test "`traffic_ctl metric get continuations_verify.{0}.close.1 | cut -d ' ' -f 2`" -eq "`traffic_ctl metric get continuations_verify.{0}.close.2 | cut -d ' ' -f 2`" ; then\ - echo yes;\ - else \ - echo no; \ - fi; \ - traffic_ctl metric match continuations_verify - ''' - -cmd = '-vs http://127.0.0.1:{0}/'.format(ts.Variables.port) -numberOfRequests = 55 - -tr = Test.AddTestRun() - -# Create a bunch of curl commands to be executed in parallel. Default.Process is set in SpawnCurlCommands. -# On Fedora 28/29, it seems that curl will occasionally timeout after a couple seconds and return exitcode 2 -# Examining the packet capture shows that Traffic Server dutifully sends the response -ps = tr.SpawnCurlCommands(cmdstr=cmd, count=numberOfRequests, retcode=Any(0, 2), ts=ts) -tr.Processes.Default.Env = ts.Env -tr.Processes.Default.ReturnCode = Any(0, 2) - -# Execution order is: ts/server, ps(curl cmds), Default Process. -tr.Processes.Default.StartBefore(server, ready=When.PortOpen(server.Variables.Port)) -tr.Processes.Default.StartBefore(Test.Processes.ts) -ts.StartAfter(*ps) -server.StartAfter(*ps) -tr.StillRunningAfter = ts - -# Signal that all the curl processes have completed and poll for done metric -tr = Test.AddTestRun("Curl Done") -tr.Processes.Default.Command = "traffic_ctl plugin msg done done" -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Env = ts.Env -tr.StillRunningAfter = ts - - -def make_done_stat_ready(tsenv): - - def done_stat_ready(process, hasRunFor, **kw): - retval = subprocess.run( - "traffic_ctl metric get continuations_verify.test.done", - shell=True, - stdout=subprocess.PIPE, - stderr=subprocess.STDOUT, - env=tsenv) - return retval.returncode == 0 and b'1' in retval.stdout - - return done_stat_ready - - -watcher = Test.Processes.Process("watcher", "sleep 20") - -tr = Test.AddTestRun("Wait for stats") -tr.Processes.Default.StartBefore(watcher, ready=make_done_stat_ready(ts.Env)) -tr.Processes.Default.Command = "traffic_ctl metric get continuations_verify.test.done" -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Env = ts.Env - -# number of sessions/transactions opened and closed are equal -tr = Test.AddTestRun("Check Ssn") -tr.Processes.Default.Command = comparator_command.format('ssn') -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Env = ts.Env -tr.Processes.Default.Streams.stdout = Testers.ContainsExpression("yes", 'should verify contents') -# Session and Txn's should be non-zero -tr.Processes.Default.Streams.stdout += Testers.ExcludesExpression("continations_verify.ssn.close.1 0", 'should be nonzero') -tr.Processes.Default.Streams.stdout += Testers.ExcludesExpression("continations_verify.ssn.close.2 0", 'should be nonzero') -tr.Processes.Default.Streams.stdout += Testers.ExcludesExpression("continations_verify.txn.close.1 0", 'should be nonzero') -tr.Processes.Default.Streams.stdout += Testers.ExcludesExpression("continations_verify.txn.close.2 0", 'should be nonzero') -tr.Processes.Default.Streams.stdout += Testers.ContainsExpression( - "continuations_verify.txn.close.1 {}".format(numberOfRequests), 'should be the number of transactions we made') -tr.Processes.Default.Streams.stdout += Testers.ContainsExpression( - "continuations_verify.txn.close.2 {}".format(numberOfRequests), 'should be the number of transactions we made') -tr.StillRunningAfter = ts - -tr = Test.AddTestRun("Check Txn") -tr.Processes.Default.Command = comparator_command.format('txn') -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Env = ts.Env -tr.Processes.Default.Streams.stdout = Testers.ContainsExpression("yes", 'should verify contents') -tr.StillRunningAfter = ts diff --git a/tests/gold_tests/continuations/double_h2.test.py b/tests/gold_tests/continuations/double_h2.test.py deleted file mode 100644 index 8ff4fa0acfa..00000000000 --- a/tests/gold_tests/continuations/double_h2.test.py +++ /dev/null @@ -1,155 +0,0 @@ -''' -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import os -import subprocess - -Test.Summary = ''' -Test transactions and sessions for http2, making sure the two continuations catch the same number of hooks. -''' -Test.SkipUnless(Condition.HasCurlFeature('http2')) -Test.SkipIf(Condition.CurlUsingUnixDomainSocket()) -Test.ContinueOnFail = True -# Define default ATS. Disable the cache to simplify the test. -ts = Test.MakeATSProcess("ts", enable_tls=True, enable_cache=False) -server = Test.MakeOriginServer("server") -server2 = Test.MakeOriginServer("server2") - -Test.testName = "" -request_header = {"headers": "GET / HTTP/1.1\r\nHost: double_h2.test\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -# expected response from the origin server -response_header = { - "headers": "HTTP/1.1 200 OK\r\nConnection: close\r\nContent-Length:0\r\n\r\n", - "timestamp": "1469733493.993", - "body": "" -} - -# add response to the server dictionary -server.addResponse("sessionfile.log", request_header, response_header) - -# add ssl materials like key, certificates for the server -ts.addDefaultSSLFiles() - -# add port and remap rule -ts.Disk.remap_config.AddLine('map / http://127.0.0.1:{0}'.format(server.Variables.Port)) - -ts.Disk.ssl_multicert_yaml.AddLines( - """ -ssl_multicert: - - dest_ip: "*" - ssl_cert_name: server.pem - ssl_key_name: server.key -""".split("\n")) - -ts.Disk.records_config.update( - { - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'continuations_verify', - 'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir), - 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts.Variables.SSLDir), - 'proxy.config.cache.enable_read_while_writer': 0, - 'proxy.config.http2.max_concurrent_streams_in': 65535 - }) - -# add plugin to assist with test metrics -Test.PrepareTestPlugin(os.path.join(Test.Variables.AtsTestPluginsDir, 'continuations_verify.so'), ts) - -comparator_command = ''' -if test "`traffic_ctl metric get continuations_verify.{0}.close.1 | cut -d ' ' -f 2`" -eq "`traffic_ctl metric get continuations_verify.{0}.close.2 | cut -d ' ' -f 2`" ; then\ - echo yes;\ - else \ - echo no; \ - fi; \ - traffic_ctl metric match continuations_verify - ''' - -# curl with http2 -cmd = '--http2 -k -vs https://127.0.0.1:{0}/'.format(ts.Variables.ssl_port) -numberOfRequests = 25 - -tr = Test.AddTestRun() - -# Create a bunch of curl commands to be executed in parallel. Default.Process is set in SpawnCurlCommands. -# On Fedora 28/29, it seems that curl will occasionally timeout after a couple seconds and return exitcode 2 -# Examining the packet capture shows that Traffic Server dutifully sends the response -ps = tr.SpawnCurlCommands(cmdstr=cmd, count=numberOfRequests, retcode=Any(0, 2), ts=ts) -tr.Processes.Default.Env = ts.Env -tr.Processes.Default.ReturnCode = Any(0, 2) - -# Execution order is: ts/server, ps(curl cmds), Default Process. -tr.Processes.Default.StartBefore(server, ready=When.PortOpen(server.Variables.Port)) -# Adds a delay once the ts port is ready. This is because we cannot test the ts state. -tr.Processes.Default.StartBefore(Test.Processes.ts) -ts.StartAfter(*ps) -server.StartAfter(*ps) -tr.StillRunningAfter = ts - -# Signal that all the curl processes have completed -tr = Test.AddTestRun("Curl Done") -tr.Processes.Default.Command = "traffic_ctl plugin msg done done" -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Env = ts.Env -tr.StillRunningAfter = ts - -# Parking this as a ready tester on a meaningless process -# To stall the test runs that check for the stats until the -# stats have propagated and are ready to read. - - -def make_done_stat_ready(tsenv): - - def done_stat_ready(process, hasRunFor, **kw): - retval = subprocess.run( - "traffic_ctl metric get continuations_verify.test.done", - shell=True, - stdout=subprocess.PIPE, - stderr=subprocess.STDOUT, - env=tsenv) - return retval.returncode == 0 and b'1' in retval.stdout - - return done_stat_ready - - -# number of sessions/transactions opened and closed are equal -tr = Test.AddTestRun("Check Ssn") -server2.StartupTimeout = 60 -# Again, here the important thing is the ready function not the server2 process -tr.Processes.Default.StartBefore(server2, ready=make_done_stat_ready(ts.Env)) -tr.Processes.Default.Command = comparator_command.format('ssn') -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Env = ts.Env -tr.Processes.Default.Streams.stdout = Testers.ContainsExpression("yes", 'should verify contents') -# Session and Txn's should be non-zero -tr.Processes.Default.Streams.stdout += Testers.ExcludesExpression("continations_verify.ssn.close.1 0", 'should be nonzero') -tr.Processes.Default.Streams.stdout += Testers.ExcludesExpression("continations_verify.ssn.close.2 0", 'should be nonzero') -tr.Processes.Default.Streams.stdout += Testers.ExcludesExpression("continations_verify.txn.close.1 0", 'should be nonzero') -tr.Processes.Default.Streams.stdout += Testers.ExcludesExpression("continations_verify.txn.close.2 0", 'should be nonzero') -tr.Processes.Default.Streams.stdout += Testers.ContainsExpression( - "continuations_verify.txn.close.1 {}".format(numberOfRequests), 'should be the number of transactions we made') -tr.Processes.Default.Streams.stdout += Testers.ContainsExpression( - "continuations_verify.txn.close.2 {}".format(numberOfRequests), 'should be the number of transactions we made') -tr.StillRunningAfter = ts -tr.StillRunningAfter = server2 - -tr = Test.AddTestRun("Check Txn") -tr.Processes.Default.Command = comparator_command.format('txn') -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Env = ts.Env -tr.Processes.Default.Streams.stdout = Testers.ContainsExpression("yes", 'should verify contents') -tr.StillRunningAfter = ts -tr.StillRunningAfter = server2 diff --git a/tests/gold_tests/continuations/openclose.test.py b/tests/gold_tests/continuations/openclose.test.py deleted file mode 100644 index df7ce0bbdb2..00000000000 --- a/tests/gold_tests/continuations/openclose.test.py +++ /dev/null @@ -1,142 +0,0 @@ -''' -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import os -import subprocess - -Test.Summary = ''' -Test transactions and sessions, making sure they open and close in the proper order. -''' - -# Define default ATS. Disable the cache to simplify the test. -ts = Test.MakeATSProcess("ts", enable_cache=False) - -server = Test.MakeOriginServer("server") -server2 = Test.MakeOriginServer("server2") - -Test.testName = "" -request_header = {"headers": "GET / HTTP/1.1\r\nHost: oc.test\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -# expected response from the origin server -response_header = { - "headers": "HTTP/1.1 200 OK\r\nConnection: close\r\nContent-Length:0\r\n\r\n", - "timestamp": "1469733493.993", - "body": "" -} - -Test.PrepareTestPlugin(os.path.join(Test.Variables.AtsTestPluginsDir, 'ssntxnorder_verify.so'), ts) - -# add response to the server dictionary -server.addResponse("sessionfile.log", request_header, response_header) -ts.Disk.records_config.update( - { - 'proxy.config.diags.debug.enabled': 0, - 'proxy.config.diags.debug.tags': 'ssntxnorder_verify.*', - 'proxy.config.cache.enable_read_while_writer': 0 - }) - -ts.Disk.remap_config.AddLine('map http://oc.test:{0} http://127.0.0.1:{1}'.format(ts.Variables.port, server.Variables.Port)) - -# Add connection close to ensure that the client connection closes promptly after completing the transaction -cmd = '-H "Connection: close" -vs -H "host:oc.test" http://127.0.0.1:{0}'.format(ts.Variables.port) -numberOfRequests = 100 - -tr = Test.AddTestRun() -# Create a bunch of curl commands to be executed in parallel. Default.Process is set in SpawnCurlCommands. -# On Fedora 28/29, it seems that curl will occasionally timeout after a couple seconds and return exitcode 2 -# Examining the packet capture shows that Traffic Server dutifully sends the response -ps = tr.SpawnCurlCommands(cmdstr=cmd, count=numberOfRequests, retcode=Any(0, 2), ts=ts) -tr.Processes.Default.Env = ts.Env -tr.Processes.Default.ReturnCode = Any(0, 2) - -# Execution order is: ts/server, ps(curl cmds), Default Process. -tr.Processes.Default.StartBefore(server, ready=When.PortOpen(server.Variables.Port)) -tr.Processes.Default.StartBefore(Test.Processes.ts) -ts.StartAfter(*ps) -server.StartAfter(*ps) -tr.StillRunningAfter = ts - -# Signal that all the curl processes have completed -tr = Test.AddTestRun("Curl Done") -tr.DelayStart = 2 # Delaying a couple seconds to make sure the global continuation's lock contention resolves. -tr.Processes.Default.Command = "traffic_ctl plugin msg done done" -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Env = ts.Env -tr.StillRunningAfter = ts - -# Parking this as a ready tester on a meaningless process -# To stall the test runs that check for the stats until the -# stats have propagated and are ready to read. - - -def make_done_stat_ready(tsenv): - - def done_stat_ready(process, hasRunFor, **kw): - retval = subprocess.run( - "traffic_ctl metric get ssntxnorder_verify.test.done", - shell=True, - stdout=subprocess.PIPE, - stderr=subprocess.STDOUT, - env=tsenv) - return retval.returncode == 0 and b'1' in retval.stdout - - return done_stat_ready - - -# number of sessions/transactions opened and closed are equal -tr = Test.AddTestRun("Check Ssn order errors") -server2.StartupTimeout = 60 -# Again, here the important thing is the ready function not the server2 process -tr.Processes.Default.StartBefore(server2, ready=make_done_stat_ready(ts.Env)) -tr.Processes.Default.Command = 'traffic_ctl metric get ssntxnorder_verify.err' -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Env = ts.Env -tr.Processes.Default.Streams.All = Testers.ContainsExpression( - 'ssntxnorder_verify.err 0', 'incorrect statistic return, or possible error.') -tr.StillRunningAfter = ts -tr.StillRunningAfter = server - -comparator_command = ''' -if test "`traffic_ctl metric get ssntxnorder_verify.{0}.start | cut -d ' ' -f 2`" -eq "`traffic_ctl metric get ssntxnorder_verify.{0}.close | cut -d ' ' -f 2`" ; then\ - echo yes;\ - else \ - echo no; \ - fi; \ - traffic_ctl metric match ssntxnorder_verify - ''' - -# number of sessions/transactions opened and closed are equal -tr = Test.AddTestRun("Check for ssn open/close") -tr.Processes.Default.Command = comparator_command.format('ssn') -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Env = ts.Env -tr.Processes.Default.Streams.stdout = Testers.ContainsExpression("yes", 'should verify contents') -tr.Processes.Default.Streams.stdout += Testers.ExcludesExpression("ssntxnorder_verify.ssn.start 0", 'should be nonzero') -tr.StillRunningAfter = ts -tr.StillRunningAfter = server - -tr = Test.AddTestRun("Check for txn/open/close") -tr.Processes.Default.Command = comparator_command.format('txn') -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Env = ts.Env -tr.Processes.Default.Streams.stdout = Testers.ContainsExpression("yes", 'should verify contents') -tr.Processes.Default.Streams.stdout += Testers.ExcludesExpression("ssntxnorder_verify.txn.start 0", 'should be nonzero') -# and we receive the same number of transactions as we asked it to make -tr.Processes.Default.Streams.stdout += Testers.ContainsExpression( - "ssntxnorder_verify.txn.start {}".format(numberOfRequests), 'should be the number of transactions we made') -tr.StillRunningAfter = ts -tr.StillRunningAfter = server diff --git a/tests/gold_tests/continuations/openclose_h2.test.py b/tests/gold_tests/continuations/openclose_h2.test.py deleted file mode 100644 index 987636e8845..00000000000 --- a/tests/gold_tests/continuations/openclose_h2.test.py +++ /dev/null @@ -1,158 +0,0 @@ -''' -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import os -import subprocess - -Test.Summary = ''' -Test transactions and sessions over http2, making sure they open and close in the proper order. -''' - -Test.SkipUnless(Condition.HasCurlFeature('http2')) -Test.SkipIf(Condition.CurlUsingUnixDomainSocket()) - -# Define default ATS. Disable the cache to simplify the test. -ts = Test.MakeATSProcess("ts", enable_tls=True, enable_cache=False) - -server = Test.MakeOriginServer("server") -server2 = Test.MakeOriginServer("server2") - -Test.testName = "" -request_header = {"headers": "GET / HTTP/1.1\r\nHost: oc.test\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -# expected response from the origin server -response_header = { - "headers": "HTTP/1.1 200 OK\r\nConnection: close\r\nContent-Length:0\r\n\r\n", - "timestamp": "1469733493.993", - "body": "" -} - -# add ssl materials like key, certificates for the server -ts.addDefaultSSLFiles() - -Test.PrepareTestPlugin(os.path.join(Test.Variables.AtsTestPluginsDir, 'ssntxnorder_verify.so'), ts) - -# add response to the server dictionary -server.addResponse("sessionfile.log", request_header, response_header) -ts.Disk.records_config.update( - { - 'proxy.config.http2.zombie_debug_timeout_in': 10, - 'proxy.config.diags.debug.enabled': 0, - 'proxy.config.diags.debug.tags': 'ssntxnorder_verify', - 'proxy.config.cache.enable_read_while_writer': 0, - 'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir), - 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts.Variables.SSLDir), - }) - -ts.Disk.remap_config.AddLine('map https://oc.test:{0} http://127.0.0.1:{1}'.format(ts.Variables.ssl_port, server.Variables.Port)) -ts.Disk.ssl_multicert_yaml.AddLines( - """ -ssl_multicert: - - dest_ip: "*" - ssl_cert_name: server.pem - ssl_key_name: server.key -""".split("\n")) - -cmd = '-k --resolve oc.test:{0}:127.0.0.1 --http2 https://oc.test:{0}'.format(ts.Variables.ssl_port) -numberOfRequests = 100 - -tr = Test.AddTestRun() -# Create a bunch of curl commands to be executed in parallel. Default.Process is set in SpawnCurlCommands. -# On Fedora 28/29, it seems that curl will occasionally timeout after a couple seconds and return exitcode 2 -# Examining the packet capture shows that Traffic Server dutifully sends the response -ps = tr.SpawnCurlCommands(cmdstr=cmd, count=numberOfRequests, retcode=Any(0, 2), ts=ts) -tr.Processes.Default.Env = ts.Env -tr.Processes.Default.ReturnCode = Any(0, 2) - -# Execution order is: ts/server, ps(curl cmds), Default Process. -tr.Processes.Default.StartBefore(server, ready=When.PortOpen(server.Variables.Port)) -tr.Processes.Default.StartBefore(Test.Processes.ts) -# Don't know why we need both the start before and the start after -ts.StartAfter(*ps) -server.StartAfter(*ps) -tr.StillRunningAfter = ts - -# Signal that all the curl processes have completed -tr = Test.AddTestRun("Curl Done") -tr.DelayStart = 2 # Delaying a couple seconds to make sure the global continuation's lock contention resolves. -tr.Processes.Default.Command = "traffic_ctl plugin msg done done" -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Env = ts.Env -tr.StillRunningAfter = ts - -# Parking this as a ready tester on a meaningless process -# To stall the test runs that check for the stats until the -# stats have propagated and are ready to read. - - -def make_done_stat_ready(tsenv): - - def done_stat_ready(process, hasRunFor, **kw): - retval = subprocess.run( - "traffic_ctl metric get ssntxnorder_verify.test.done", - shell=True, - stdout=subprocess.PIPE, - stderr=subprocess.STDOUT, - env=tsenv) - return retval.returncode == 0 and b'1' in retval.stdout - - return done_stat_ready - - -# number of sessions/transactions opened and closed are equal -tr = Test.AddTestRun("Check Ssn order errors") -server2.StartupTimeout = 60 -# Again, here the important thing is the ready function not the server2 process -tr.Processes.Default.StartBefore(server2, ready=make_done_stat_ready(ts.Env)) -tr.Processes.Default.Command = 'traffic_ctl metric get ssntxnorder_verify.err' -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Env = ts.Env -tr.Processes.Default.Streams.All = Testers.ContainsExpression( - 'ssntxnorder_verify.err 0', 'incorrect statistic return, or possible error.') -tr.StillRunningAfter = ts -tr.StillRunningAfter = server - -comparator_command = ''' -if test "`traffic_ctl metric get ssntxnorder_verify.{0}.start | cut -d ' ' -f 2`" -eq "`traffic_ctl metric get ssntxnorder_verify.{0}.close | cut -d ' ' -f 2`" ; then\ - echo yes;\ - else \ - echo no; \ - fi; \ - traffic_ctl metric match ssntxnorder_verify - ''' - -# number of sessions/transactions opened and closed are equal -tr = Test.AddTestRun("Check for ssn open/close") -tr.Processes.Default.Command = comparator_command.format('ssn') -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Env = ts.Env -tr.Processes.Default.Streams.stdout = Testers.ContainsExpression("yes", 'should verify contents') -tr.Processes.Default.Streams.stdout += Testers.ExcludesExpression("ssntxnorder_verify.ssn.start 0", 'should be nonzero') -tr.StillRunningAfter = ts -tr.StillRunningAfter = server - -tr = Test.AddTestRun("Check for txn/open/close") -tr.Processes.Default.Command = comparator_command.format('txn') -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Env = ts.Env -tr.Processes.Default.Streams.stdout = Testers.ContainsExpression("yes", 'should verify contents') -tr.Processes.Default.Streams.stdout += Testers.ExcludesExpression("ssntxnorder_verify.txn.start 0", 'should be nonzero') -# and we receive the same number of transactions as we asked it to make -tr.Processes.Default.Streams.stdout += Testers.ContainsExpression( - "ssntxnorder_verify.txn.start {}".format(numberOfRequests), 'should be the number of transactions we made') -tr.StillRunningAfter = ts -tr.StillRunningAfter = server diff --git a/tests/gold_tests/continuations/session_id.test.py b/tests/gold_tests/continuations/session_id.test.py deleted file mode 100644 index a10e45b3c2c..00000000000 --- a/tests/gold_tests/continuations/session_id.test.py +++ /dev/null @@ -1,116 +0,0 @@ -''' -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import os - -Test.Summary = ''' -Verify session ID properties. -''' - -Test.SkipUnless(Condition.HasCurlFeature('http2')) - -# Configure the server. -server = Test.MakeOriginServer("server") -request_header = {"headers": "GET / HTTP/1.1\r\nHost: example.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -response_header = { - "headers": "HTTP/1.1 200 OK\r\nConnection: close\r\nContent-Length:0\r\n\r\n", - "timestamp": "1469733493.993", - "body": "" -} -server.addResponse("sessionfile.log", request_header, response_header) - -# Configure ATS. Disable the cache to simplify the test. -ts = Test.MakeATSProcess("ts", enable_tls=True, enable_cache=False) - -ts.addDefaultSSLFiles() - -Test.PrepareTestPlugin( - os.path.join(Test.Variables.AtsBuildGoldTestsDir, 'continuations', 'plugins', '.libs', 'session_id_verify.so'), ts) - -ts.Disk.records_config.update( - { - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'session_id_verify', - 'proxy.config.cache.enable_read_while_writer': 0, - 'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir), - 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts.Variables.SSLDir), - }) - -ts.Disk.remap_config.AddLine('map / http://127.0.0.1:{0}'.format(server.Variables.Port)) - -ts.Disk.ssl_multicert_yaml.AddLines( - """ -ssl_multicert: - - dest_ip: "*" - ssl_cert_name: server.pem - ssl_key_name: server.key -""".split("\n")) - -# -# Run some HTTP/1 traffic. -# -tr = Test.AddTestRun("Perform HTTP/1 transactions") -cmd = '-v -H "host:example.com" http://127.0.0.1:{0}'.format(ts.Variables.port) -numberOfRequests = 100 -# Create a bunch of curl commands to be executed in parallel. Default.Process -# is set in SpawnCurlCommands. On Fedora 28/29, it seems that curl will -# occasionally timeout after a couple seconds and return exitcode 2 -# Examining the packet capture shows that Traffic Server dutifully sends the response -ps = tr.SpawnCurlCommands(cmdstr=cmd, count=numberOfRequests, retcode=Any(0, 2), ts=ts) -tr.Processes.Default.Env = ts.Env -tr.Processes.Default.ReturnCode = Any(0, 2) -tr.Processes.Default.StartBefore(server, ready=When.PortOpen(server.Variables.Port)) -tr.Processes.Default.StartBefore(Test.Processes.ts) -ts.StartAfter(*ps) -server.StartAfter(*ps) - -tr.StillRunningAfter = ts -tr.StillRunningAfter = server - -# -# Run some HTTP/2 traffic. -# -if not Condition.CurlUsingUnixDomainSocket(): - tr = Test.AddTestRun("Perform HTTP/2 transactions") - cmd = '-v -k --http2 -H "host:example.com" https://127.0.0.1:{0}'.format(ts.Variables.ssl_port) - ps = tr.SpawnCurlCommands(cmdstr=cmd, count=numberOfRequests, retcode=Any(0, 2), ts=ts) - tr.Processes.Default.Env = ts.Env - tr.Processes.Default.ReturnCode = Any(0, 2) - -# -# Verify that the session ids are unique. -# - -# AuTest already searches for errors in diags.log and fails if it encounters -# them. The test plugin prints an error to this log if it sees duplicate ids. -# The following is to verify that we encountered the expected ids. - - -def verify_session_count(output): - global numberOfRequests - if Condition.CurlUsingUnixDomainSocket(): - nReq = numberOfRequests - else: - nReq = numberOfRequests * 2 - session_ids = [line[0:line.find("\n")] for line in str(output).split("session id: ")[1:]] - if len(session_ids) != nReq: - return "Found {} session_id's, expected {}".format(len(session_ids), nReq) - return "" - - -ts.Disk.traffic_out.Content += Testers.FileContentCallback(verify_session_count, 'verify_session_count') diff --git a/tests/gold_tests/cripts/cripts.test.py b/tests/gold_tests/cripts/cripts.test.py deleted file mode 100644 index 6c933365478..00000000000 --- a/tests/gold_tests/cripts/cripts.test.py +++ /dev/null @@ -1,142 +0,0 @@ -''' -Test basic cripts functionality -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import os - -# Needed if we want to use sed -i '' on macOS, but autest doesn't like that ... -# import platform - -Test.testName = "cripts: basic functions" -Test.Summary = ''' -Simple cripts test that sets a response header back to the client -''' -Test.ContinueOnFail = True - - -class CriptsBasicTest: - - def __init__(self): - self.setUpOriginServer() - self.setUpTS() - - def setUpOriginServer(self): - self.server = Test.MakeOriginServer("server") - - request_header = {"headers": "GET / HTTP/1.1\r\nHost: www.example.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} - response_header = { - "headers": "HTTP/1.1 200 OK\r\nresponseHeader: unchanged\r\nContent-Length: 0\r\nConnection: close\r\n\r\n", - "timestamp": "1469733493.993", - "body": "" - } - self.server.addResponse("sessionfile.log", request_header, response_header) - - query_request_header = { - "headers": "GET /path/to/resource?foo=1&bar=2&baz=3 HTTP/1.1\r\nHost: does.not.matter\r\n\r\n", - "timestamp": "1469733493.993", - "body": "" - } - query_response_header = { - "headers": "HTTP/1.1 200 OK\r\nContent-Length: 0\r\nConnection: close\r\n\r\n", - "timestamp": "1469733493.993", - "body": "" - } - self.server.addResponse("sessionfile.log", query_request_header, query_response_header) - - def setUpTS(self): - self.ts = Test.MakeATSProcess("ts_in", enable_tls=True, enable_cache=False, enable_cripts=True) - - self.ts.addDefaultSSLFiles() - self.ts.Disk.ssl_multicert_yaml.AddLines( - """ -ssl_multicert: - - dest_ip: "*" - ssl_cert_name: server.pem - ssl_key_name: server.key -""".split("\n")) - - self.ts.Setup.Copy('files/basic.cript', self.ts.Variables.CONFIGDIR) - self.ts.Setup.Copy('files/query_copy.cript', self.ts.Variables.CONFIGDIR) - - self.ts.Disk.records_config.update( - { - 'proxy.config.plugin.dynamic_reload_mode': 1, - "proxy.config.ssl.server.cert.path": f"{self.ts.Variables.SSLDir}", - "proxy.config.ssl.server.private_key.path": f"{self.ts.Variables.SSLDir}", - }) - - self.ts.Disk.remap_config.AddLine( - f'map http://www.example.com http://127.0.0.1:{self.server.Variables.Port} @plugin=basic.cript') - self.ts.Disk.remap_config.AddLine( - f'map https://www.example.com:{self.ts.Variables.ssl_port} http://127.0.0.1:{self.server.Variables.Port} @plugin=basic.cript' - ) - self.ts.Disk.remap_config.AddLine( - f'map http://query.example.com http://127.0.0.1:{self.server.Variables.Port} @plugin=query_copy.cript') - - def runHeaderTest(self): - tr = Test.AddTestRun('Exercise traffic through cripts.') - tr.MakeCurlCommand(f'-v -H "Host: www.example.com" http://127.0.0.1:{self.ts.Variables.port}', ts=self.ts) - tr.Processes.Default.ReturnCode = 0 - tr.Processes.Default.StartBefore(self.server, ready=When.PortOpen(self.server.Variables.Port)) - tr.Processes.Default.StartBefore(self.ts) - tr.Processes.Default.Streams.stderr = "gold/basic_cript.gold" - tr.StillRunningAfter = self.server - - def runCertsTest(self): - tr = Test.AddTestRun('Exercise Cripts certificate introspection.') - tr.MakeCurlCommand( - f'-v --http1.1 -k -H "Host: www.example.com:{self.ts.Variables.ssl_port}" https://127.0.0.1:{self.ts.Variables.ssl_port}', - ts=self.ts) - tr.Processes.Default.ReturnCode = 0 - tr.Processes.Default.Streams.stderr = "gold/certs_cript.gold" - tr.StillRunningAfter = self.server - - def runQueryCopyTest(self): - tr = Test.AddTestRun('Exercise Query/Path copy ctor / copy-assign in cripts.') - tr.MakeCurlCommand( - f'-v -H "Host: query.example.com" "http://127.0.0.1:{self.ts.Variables.port}/path/to/resource?foo=1&bar=2&baz=3"', - ts=self.ts) - tr.Processes.Default.ReturnCode = 0 - # Live URL is untouched: original query is preserved. - tr.Processes.Default.Streams.stderr = Testers.ContainsExpression( - r"X-Original-Query: foo=1&bar=2&baz=3", "Original query must round-trip unchanged") - # Mutating the copy (Erase "foo") must not have leaked into the original. - tr.Processes.Default.Streams.stderr += Testers.ContainsExpression( - r"X-Copy-Query: bar=2&baz=3", "Erase on the copy must drop foo without affecting the original") - # Copy-assignment path produces a snapshot equal to the original. - tr.Processes.Default.Streams.stderr += Testers.ContainsExpression( - r"X-Assigned-Query: foo=1&bar=2&baz=3", "Copy-assigned query must equal the original") - # Live URL path is untouched: original path is preserved. - tr.Processes.Default.Streams.stderr += Testers.ContainsExpression( - r"X-Original-Path: path/to/resource", "Original path must round-trip unchanged") - # Mutating the copy (segment[0] = "edited") must not have leaked into the original. - tr.Processes.Default.Streams.stderr += Testers.ContainsExpression( - r"X-Copy-Path: edited/to/resource", "Segment edit on the copy must not affect the original") - # Copy-assignment path produces a snapshot equal to the original. - tr.Processes.Default.Streams.stderr += Testers.ContainsExpression( - r"X-Assigned-Path: path/to/resource", "Copy-assigned path must equal the original") - tr.StillRunningAfter = self.server - - def run(self): - self.runHeaderTest() - self.runQueryCopyTest() - if not Condition.CurlUsingUnixDomainSocket(): - self.runCertsTest() - - -CriptsBasicTest().run() diff --git a/tests/gold_tests/dns/dns_down_nameserver.test.py b/tests/gold_tests/dns/dns_down_nameserver.test.py deleted file mode 100644 index 495b190a7b6..00000000000 --- a/tests/gold_tests/dns/dns_down_nameserver.test.py +++ /dev/null @@ -1,187 +0,0 @@ -''' -Verify ATS handles down name servers correctly. -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -from ports import get_port - -# This value tracks DNS_PRIMARY_RETRY_PERIOD in P_DNSProcessor.h. -DNS_PRIMARY_RETRY_PERIOD = 5 - -Test.Summary = ''' -Verify ATS handles down name servers correctly. -''' - - -class DownDNSNameserverTest: - """Encapsulate logic for testing down name server functionality. - - Note that this test verifies that ATS behaves correctly with respect to - down *name* servers. This is different than testing down origin servers, - which is an entirely different feature and implementation. - """ - - _replay_file = "replay/multiple_host_requests.replay.yaml" - - def __init__(self): - """Initialize the Test processes for the test runs.""" - self._dns_port = None - self._configure_origin_server() - self._configure_traffic_server() - self._servers_are_running = False - - self._client_counter = 0 - self._dns_counter = 0 - - def _configure_origin_server(self): - """Configure the origin server and the DNS port.""" - self._server = Test.MakeVerifierServerProcess("server", self._replay_file) - - # Associate the DNS port with the long-running server. This way - # ATS can be configured to use it across the multiple test runs - # and DNS servers. - self._dns_port = get_port(self._server, "DNSPort") - - def _configure_traffic_server(self): - """Configure Traffic Server.""" - if not self._dns_port: - raise RuntimeError("The DNS port was not configured.") - - # Since we are testing DNS name resolution and whether these - # transactions reach the origin, disable the caching to ensure none of - # the responses come out of the cache without going to the origin. - self._ts = Test.MakeATSProcess("ts", enable_cache=False) - - self._ts.Disk.records_config.update( - { - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'hostdb|dns', - 'proxy.config.dns.nameservers': f'127.0.0.1:{self._dns_port}', - 'proxy.config.dns.resolv_conf': 'NULL' - }) - - # Cause a name resolution for each, unique path. - self._ts.Disk.remap_config.AddLines( - [ - f'map /first/host http://first.host.com:{self._server.Variables.http_port}/', - f'map /second/host http://second.host.com:{self._server.Variables.http_port}/', - f'map /third/host http://third.host.com:{self._server.Variables.http_port}/', - ]) - - def _run_transaction(self, start_dns: bool, keyname: str): - """Run a transaction with the name server reachable. - - :param start_dns: Whether the TestRun should configure a name server to - be running for ATS to talk to. - - :param keyname: The identifier for the transaction to run. - """ - tr = Test.AddTestRun() - - tr.AddVerifierClientProcess( - f'client{self._client_counter}', - self._replay_file, - http_ports=[self._ts.Variables.port], - other_args=f'--keys {keyname}') - self._client_counter += 1 - - if start_dns: - dns = tr.MakeDNServer(f'dns{self._dns_counter}', default='127.0.0.1', port=self._dns_port) - self._dns_counter += 1 - tr.Processes.Default.StartBefore(dns) - - if not self._servers_are_running: - tr.Processes.Default.StartBefore(self._server) - tr.Processes.Default.StartBefore(self._ts) - self._servers_are_running = True - - # Verify that the client tried to send the transaction. - tr.Processes.Default.Streams.All += Testers.ContainsExpression( - f'uuid: {keyname}', f'The client should have sent a transaction with uuid {keyname}') - - # The client will report an error if ATS could not complete the - # transaction due to DNS resolution issues. - if not start_dns: - tr.Processes.Default.ReturnCode = 1 - - def _delay_for_dns_retry_period(self, start_dns: bool): - """Wait for the failed DNS retry period.""" - tr = Test.AddTestRun() - - if start_dns: - dns = tr.MakeDNServer(f'dns{self._dns_counter}', default='127.0.0.1', port=self._dns_port) - self._dns_counter += 1 - tr.Processes.Default.StartBefore(dns) - - # Exceed the retry period. - tr.Processes.Default.Command = f'sleep {DNS_PRIMARY_RETRY_PERIOD + 1}' - - def _test_dns_reachable(self): - """This is the base case: DNS is reachable. - - This verifies that ATS can use the name server to resolve a name when - the name server is reachable. - """ - self._run_transaction(start_dns=True, keyname='first_host') - - def _test_dns_reachable_within_the_retry_period(self): - """Test nameserver connectivity resolution within the retry period. - - Have the name server be inaccessible, but bring it back online within - the retry period. - """ - # Trigger a name resolution with the name server down. - self._run_transaction(start_dns=False, keyname='second_host') - - # Wait long enough that ATS to retry the name server, with the server - # up. - self._delay_for_dns_retry_period(start_dns=True) - - # ATS should now use the working name server again to resolve the host - # name. - self._run_transaction(start_dns=True, keyname='second_host') - - def _test_dns_reachable_outside_the_retry_period(self): - """Test nameserver connectivity resolution outside the retry period. - - Have the name server be inaccessible, but bring it back online outside - the retry period. This verifies that we handle it coming back online - after the first attempt to re-connect to it. - """ - # Trigger a name resolution with the name server down. - self._run_transaction(start_dns=False, keyname='third_host') - - # Wait long enough that ATS to retry connectivity to the name server, - # with the server down. This will naturally fail. - self._delay_for_dns_retry_period(start_dns=False) - - # Wait long enough that ATS to retry connectivity a second time to name - # server, this time with the server up. - self._delay_for_dns_retry_period(start_dns=True) - - # ATS should now try the name server again, detect that it is up, and - # complete the transaction. - self._run_transaction(start_dns=True, keyname='third_host') - - def run(self): - """Exercise ATS down name server functionality.""" - self._test_dns_reachable() - self._test_dns_reachable_within_the_retry_period() - self._test_dns_reachable_outside_the_retry_period() - - -DownDNSNameserverTest().run() diff --git a/tests/gold_tests/dns/dns_host_down.test.py b/tests/gold_tests/dns/dns_host_down.test.py deleted file mode 100644 index 295100c2fd2..00000000000 --- a/tests/gold_tests/dns/dns_host_down.test.py +++ /dev/null @@ -1,95 +0,0 @@ -''' -Verify ATS handles down origin servers with domain cached correctly. -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. -from ports import get_port -import os - -Test.Summary = ''' -Verify ATS handles down origin servers with cached domain correctly. -''' - - -class DownCachedOriginServerTest: - replay_file = "replay/server_down.replay.yaml" - - def __init__(self): - """Initialize the Test processes for the test runs.""" - self._server = Test.MakeVerifierServerProcess("server", DownCachedOriginServerTest.replay_file) - self._configure_trafficserver() - - def _configure_trafficserver(self): - """Configure Traffic Server.""" - self._ts = Test.MakeATSProcess("ts", enable_cache=False) - - self._ts.Disk.remap_config.AddLine(f"map / http://resolve.this.com:{self._server.Variables.http_port}/") - - self._ts.Disk.records_config.update( - { - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'hostdb|dns|http|socket', - 'proxy.config.http.connect_attempts_max_retries': 0, - 'proxy.config.http.connect_attempts_rr_retries': 0, - 'proxy.config.hostdb.fail.timeout': 10, - 'proxy.config.dns.resolv_conf': 'NULL', - 'proxy.config.hostdb.ttl_mode': 1, - 'proxy.config.hostdb.timeout': 2, - 'proxy.config.hostdb.lookup_timeout': 2, - 'proxy.config.http.transaction_no_activity_timeout_in': 2, - 'proxy.config.http.connect_attempts_timeout': 2, - 'proxy.config.hostdb.host_file.interval': 1, - 'proxy.config.hostdb.host_file.path': os.path.join(Test.TestDirectory, "hosts_file"), - }) - - def _test_host_mark_down(self): - tr = Test.AddTestRun() - - tr.Processes.Default.StartBefore(self._server) - tr.Processes.Default.StartBefore(self._ts) - - tr.AddVerifierClientProcess("client-1", DownCachedOriginServerTest.replay_file, http_ports=[self._ts.Variables.port]) - - # Verify error log marking host down exists - def _test_error_log(self): - tr = Test.AddAwaitFileContainsTestRun( - 'Await error.log mark-down entry.', - os.path.join(self._ts.Variables.LOGDIR, 'error.log'), - "/dns/mark/down' fail_count='1' marking down", - ) - self._ts.Disk.error_log.Content = Testers.ContainsExpression( - "/dns/mark/down' fail_count='1' marking down", "host should be marked down") - - # Verify down_server_no_requests metric is incremented: - # - once when the first request marks the origin DOWN (502) - # - once when the second request is rejected because HostDB has no live address (500) - def _test_down_server_no_requests_metric(self): - tr = Test.AddTestRun('Check proxy.process.http.down_server.no_requests metric') - tr.Processes.Default.Command = 'traffic_ctl metric get proxy.process.http.down_server.no_requests' - tr.Processes.Default.Env = self._ts.Env - tr.Processes.Default.ReturnCode = 0 - tr.Processes.Default.Streams.All = Testers.ContainsExpression( - 'proxy.process.http.down_server.no_requests 2', - 'down_server.no_requests should be 2 (mark-down on first txn, no-live-address on second)') - tr.StillRunningAfter = self._ts - - def run(self): - self._test_host_mark_down() - self._test_error_log() - self._test_down_server_no_requests_metric() - - -DownCachedOriginServerTest().run() diff --git a/tests/gold_tests/dns/dns_reverse_lookup.test.py b/tests/gold_tests/dns/dns_reverse_lookup.test.py deleted file mode 100644 index e56937c6415..00000000000 --- a/tests/gold_tests/dns/dns_reverse_lookup.test.py +++ /dev/null @@ -1,86 +0,0 @@ -''' -Verify that ATS can perform a reverse lookup when necessary -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -from ports import get_port -import os - -Test.Summary = ''' -Verify ATS can perform a reverse lookup when necessary -''' - -# -# This test verifies a fix for a regression that incorrectly marked a hostdb record as failed doing a reverse DNS -# lookup from the hosts file. If a parent.config exists, then ATS will perform a reverse lookup on remaps that -# have an ip address in the map-to url. While this doesn't fail the initil request, subsequent requests that resolve -# to the same host record will see the failed lookup attempt and fail the request with a host not found. -# -# This test verfiies the correct behavior in case future code chages introduce a similar regression -# - - -class DNSReverseLookupTest: - replay_file = "replay/reverse_lookup.replay.yaml" - - def __init__(self): - self._server = Test.MakeVerifierServerProcess("server", DNSReverseLookupTest.replay_file) - self._configure_trafficserver() - - def _configure_trafficserver(self): - self._ts = Test.MakeATSProcess("ts", enable_cache=False) - - # This first rule would trigger the bug - self._ts.Disk.remap_config.AddLine(f"map /test1 http://127.0.0.1:{self._server.Variables.http_port}/",) - # This first rule would fail in the presense of the bug, but this test verifies correct behavior - self._ts.Disk.remap_config.AddLine(f"map /test2 http://localhost:{self._server.Variables.http_port}/",) - self._ts.Disk.parent_config.AddLine( - f'dest_domain=. parent=parent_host:{self._ts.Variables.port} round_robin=consistent_hash go_direct=false') - self._ts.Disk.parent_config.AddLine( - # this doesn't need to match, just exist so ats will do the reverse lookup - f'dest_host=other_host scheme=http parent="parent_host:8080"') - - self._ts.Disk.records_config.update( - { - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'hostdb|dns|http', - 'proxy.config.http.connect_attempts_max_retries': 0, - 'proxy.config.http.connect_attempts_rr_retries': 0, - 'proxy.config.hostdb.fail.timeout': 10, - 'proxy.config.dns.resolv_conf': 'NULL', - 'proxy.config.hostdb.ttl_mode': 1, - 'proxy.config.hostdb.timeout': 2, - 'proxy.config.hostdb.lookup_timeout': 2, - 'proxy.config.http.transaction_no_activity_timeout_in': 2, - 'proxy.config.http.connect_attempts_timeout': 2, - 'proxy.config.hostdb.host_file.interval': 1, - 'proxy.config.hostdb.host_file.path': os.path.join(Test.TestDirectory, "hosts_file"), - }) - - def test_rev_dns(self): - tr = Test.AddTestRun() - - tr.Processes.Default.StartBefore(self._server) - tr.Processes.Default.StartBefore(self._ts) - - tr.AddVerifierClientProcess("client", DNSReverseLookupTest.replay_file, http_ports=[self._ts.Variables.port]) - - def run(self): - self.test_rev_dns() - - -DNSReverseLookupTest().run() diff --git a/tests/gold_tests/dns/dns_ttl.test.py b/tests/gold_tests/dns/dns_ttl.test.py deleted file mode 100644 index f6c32462a9c..00000000000 --- a/tests/gold_tests/dns/dns_ttl.test.py +++ /dev/null @@ -1,143 +0,0 @@ -''' -Test DNS TTL behavior. -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import ports - -Test.Summary = 'Test DNS TTL behavior' - - -class TtlDnsTest: - single_transaction_replay = "replay/single_transaction.replay.yaml" - server_error_replay = "replay/server_error.replay.yaml" - process_counter = 1 - - # The TTL to set for every resolved hostname. - dnsTTL = 1 - - # The DNS query timeout. - queryTimeout = 1 - - def __init__(self, configure_serve_stale=False, exceed_serve_stale=False): - """ - Args: - configure_serve_stale: (bool) Whether the ATS process should be configured to - serve stale DNS entries. - - exceed_serve_stale: (bool) Configure the serve_stale timeout to be low - enough that the timed out DNS response will not be used. - """ - self.configure_serve_stale = configure_serve_stale - self.exceed_serve_stale = exceed_serve_stale - - self.server_process_counter = TtlDnsTest.get_unique_process_counter() - TtlDnsTest.process_counter += 1 - - self.setupOriginServer() - self.setupTS() - - @classmethod - def get_unique_process_counter(cls): - this_counter = cls.process_counter - cls.process_counter += 1 - return this_counter - - def addDNSServerToTestRun(self, test_run): - dns = test_run.MakeDNServer("dns", port=self.dns_port) - dns.addRecords(records={'resolve.this.com': ['127.0.0.1']}) - return dns - - def setupOriginServer(self): - self.server = Test.MakeVerifierServerProcess(f"server-{self.server_process_counter}", TtlDnsTest.single_transaction_replay) - - def setupTS(self): - self.ts = Test.MakeATSProcess(f"ts-{self.server_process_counter}", enable_cache=False) - self.dns_port = ports.get_port(self.ts, 'dns_port') - self.ts.Disk.records_config.update( - { - "proxy.config.diags.debug.enabled": 1, - "proxy.config.diags.debug.tags": "dns", - 'proxy.config.dns.nameservers': f'127.0.0.1:{self.dns_port}', - 'proxy.config.dns.resolv_conf': 'NULL', - - # Configure ATS to treat each resolved name to have a 1 second - # time to live. - "proxy.config.hostdb.ttl_mode": 1, - "proxy.config.hostdb.timeout": self.dnsTTL, - - # MicroDNS will be down for the second transaction. Have ATS give - # up trying to talk to it after one second. - "proxy.config.hostdb.lookup_timeout": self.queryTimeout, - }) - if self.configure_serve_stale: - if self.exceed_serve_stale: - stale_timeout = 1 - else: - stale_timeout = 300 - - self.ts.Disk.records_config.update({"proxy.config.hostdb.serve_stale_for": stale_timeout}) - self.ts.Disk.remap_config.AddLine(f"map / http://resolve.this.com:{self.server.Variables.http_port}/") - - def testRunWithDNS(self): - tr = Test.AddTestRun() - - # Run the DNS server with this test run so it will not be running in - # the next one. - dns = self.addDNSServerToTestRun(tr) - process_number = TtlDnsTest.get_unique_process_counter() - tr.AddVerifierClientProcess( - f"client-{process_number}", TtlDnsTest.single_transaction_replay, http_ports=[self.ts.Variables.port]) - - tr.Processes.Default.StartBefore(dns) - tr.Processes.Default.StartBefore(self.server) - tr.Processes.Default.StartBefore(self.ts) - - tr.StillRunningAfter = dns - tr.StillRunningAfter = self.server - tr.StillRunningAfter = self.ts - - def testRunWithoutDNS(self): - tr = Test.AddTestRun() - - # Delay running the second transaction for long enough to guarantee - # that both the TTL and the DNS query timeout (lookup_timeout) are - # exceeded. - tr.DelayStart = 3 - - # Will the stale resolved DNS response be used? - if self.configure_serve_stale and not self.exceed_serve_stale: - # Yes: expect a proxied transaction with a 200 OK response. - replay_file = TtlDnsTest.single_transaction_replay - else: - # No: expect a 5xx response because the server name could not be - # resolved. - replay_file = TtlDnsTest.server_error_replay - process_number = TtlDnsTest.get_unique_process_counter() - tr.AddVerifierClientProcess(f"client-{process_number}", replay_file, http_ports=[self.ts.Variables.port]) - - tr.StillRunningAfter = self.server - tr.StillRunningAfter = self.ts - - def run(self): - self.testRunWithDNS() - self.testRunWithoutDNS() - - -TtlDnsTest().run() -TtlDnsTest(configure_serve_stale=True, exceed_serve_stale=False).run() -TtlDnsTest(configure_serve_stale=True, exceed_serve_stale=True).run() diff --git a/tests/gold_tests/dns/splitdns.test.py b/tests/gold_tests/dns/splitdns.test.py deleted file mode 100644 index c6f20311763..00000000000 --- a/tests/gold_tests/dns/splitdns.test.py +++ /dev/null @@ -1,77 +0,0 @@ -''' -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -Test.Summary = 'Test Split DNS' - - -class SplitDNSTest: - - def __init__(self): - self.setupDNSServer() - self.setupOriginServer() - self.setupTS() - - def setupDNSServer(self): - self.dns = Test.MakeDNServer("dns") - self.dns.addRecords(records={'foo.ts.a.o.': ['127.0.0.1']}) - - def setupOriginServer(self): - self.origin_server = Test.MakeOriginServer("origin_server") - self.origin_server.addResponse( - "sessionlog.json", {"headers": "GET / HTTP/1.1\r\nHost: localhost\r\n\r\n"}, - {"headers": "HTTP/1.1 200 OK\r\nServer: microserver\r\nConnection: close\r\n\r\n"}) - - def setupTS(self): - self.ts = Test.MakeATSProcess("ts", enable_cache=False) - self.ts.Disk.records_config.update( - { - "proxy.config.dns.splitDNS.enabled": 1, - "proxy.config.diags.debug.enabled": 1, - "proxy.config.diags.debug.tags": "dns|splitdns", - }) - self.ts.Disk.splitdns_config.AddLine(f"dest_domain=foo.ts.a.o named=127.0.0.1:{self.dns.Variables.Port}") - self.ts.Disk.remap_config.AddLine(f"map /foo/ http://foo.ts.a.o:{self.origin_server.Variables.Port}/") - self.ts.Disk.remap_config.AddLine(f"map /bar/ http://127.0.0.1:{self.origin_server.Variables.Port}/") - - def addTestCase0(self): - tr = Test.AddTestRun() - tr.MakeCurlCommand(f"-v http://localhost:{self.ts.Variables.port}/foo/", ts=self.ts) - tr.Processes.Default.ReturnCode = 0 - tr.Processes.Default.Streams.stderr = "gold/test_case_0_stderr.gold" - tr.Processes.Default.StartBefore(self.dns) - tr.Processes.Default.StartBefore(self.origin_server) - tr.Processes.Default.StartBefore(self.ts) - tr.StillRunningAfter = self.dns - tr.StillRunningAfter = self.origin_server - tr.StillRunningAfter = self.ts - - def addTestCase1(self): - tr = Test.AddTestRun() - tr.MakeCurlCommand(f"-v http://localhost:{self.ts.Variables.port}/bar/", ts=self.ts) - tr.Processes.Default.ReturnCode = 0 - tr.Processes.Default.Streams.stderr = "gold/test_case_1_stderr.gold" - tr.StillRunningAfter = self.dns - tr.StillRunningAfter = self.origin_server - tr.StillRunningAfter = self.ts - - def run(self): - self.addTestCase0() - self.addTestCase1() - - -SplitDNSTest().run() diff --git a/tests/gold_tests/dns/splitdns_reload.test.py b/tests/gold_tests/dns/splitdns_reload.test.py deleted file mode 100644 index 0e254e0a179..00000000000 --- a/tests/gold_tests/dns/splitdns_reload.test.py +++ /dev/null @@ -1,80 +0,0 @@ -''' -Test splitdns.config reload via ConfigRegistry. - -Verifies that: -1. splitdns.config reload works after file touch -2. The reload handler is invoked (diags log check) -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import os - -Test.Summary = 'Test splitdns.config reload via ConfigRegistry.' -Test.ContinueOnFail = True - - -class SplitDNSReloadTest: - - def __init__(self): - self.setupDNSServer() - self.setupOriginServer() - self.setupTS() - - def setupDNSServer(self): - self.dns = Test.MakeDNServer("dns") - self.dns.addRecords(records={'foo.ts.a.o.': ['127.0.0.1']}) - - def setupOriginServer(self): - self.origin_server = Test.MakeOriginServer("origin_server") - self.origin_server.addResponse( - "sessionlog.json", {"headers": "GET / HTTP/1.1\r\nHost: localhost\r\n\r\n"}, - {"headers": "HTTP/1.1 200 OK\r\nServer: microserver\r\nConnection: close\r\n\r\n"}) - - def setupTS(self): - self.ts = Test.MakeATSProcess("ts", enable_cache=False) - self.ts.Disk.records_config.update( - { - 'proxy.config.dns.splitDNS.enabled': 1, - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'splitdns|config', - }) - self.ts.Disk.splitdns_config.AddLine(f"dest_domain=foo.ts.a.o named=127.0.0.1:{self.dns.Variables.Port}") - self.ts.Disk.remap_config.AddLine(f"map /foo/ http://foo.ts.a.o:{self.origin_server.Variables.Port}/") - - def run(self): - config_dir = self.ts.Variables.CONFIGDIR - - # Test 1: Verify basic SplitDNS works (startup loads config) - tr = Test.AddTestRun("Verify SplitDNS works at startup") - tr.MakeCurlCommand(f"-v http://localhost:{self.ts.Variables.port}/foo/", ts=self.ts) - tr.Processes.Default.ReturnCode = 0 - tr.Processes.Default.StartBefore(self.dns) - tr.Processes.Default.StartBefore(self.origin_server) - tr.Processes.Default.StartBefore(self.ts) - tr.StillRunningAfter = self.ts - - # Test 2: Touch splitdns.config -> reload -> handler fires - tr = Test.AddTestRun("Touch splitdns.config") - tr.Processes.Default.Command = (f"touch {os.path.join(config_dir, 'splitdns.config')} && sleep 1") - tr.Processes.Default.ReturnCode = 0 - tr.StillRunningAfter = self.ts - - tr = Test.AddConfigReload(self.ts, expect_tasks=["splitdns.config"], description="Reload after splitdns.config touch") - tr.StillRunningAfter = self.ts - - -SplitDNSReloadTest().run() diff --git a/tests/gold_tests/early_hints/early_hints.test.py b/tests/gold_tests/early_hints/early_hints.test.py deleted file mode 100644 index ddcc488be88..00000000000 --- a/tests/gold_tests/early_hints/early_hints.test.py +++ /dev/null @@ -1,175 +0,0 @@ -''' -Verify correct handling of 103 Early Hints responses. -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -from enum import Enum, auto -import os -from ports import get_port -import re -import sys - - -class Protocol(Enum): - HTTP = auto() - HTTPS = auto() - HTTP2 = auto() - HTTP3 = auto() - - @classmethod - def to_string(cls, protocol): - if protocol == cls.HTTP: - return 'HTTP' - elif protocol == cls.HTTPS: - return 'HTTPS' - elif protocol == cls.HTTP2: - return 'HTTP2' - elif protocol == cls.HTTP3: - return 'HTTP3' - else: - return None - - -class TestEarlyHints: - '''Verify that ATS can properly handle a 103 response.''' - - _early_hints_server = 'early_hints_server.py' - - def __init__(self, protocol: Protocol): - '''Create a test run for the given protocol. - :param protocol: The protocol the client will use for the test. - ''' - self._protocol = protocol - self._protocol_str = Protocol.to_string(protocol) - tr = Test.AddTestRun(f'Early hints with client protocol: {self._protocol_str}') - self._configure_dns(tr) - self._configure_server(tr) - ts = self._configure_ts(tr) - self._copy_scripts(tr, ts) - self._configure_client(tr) - - def _configure_dns(self, tr: 'TestRun'): - '''Configure the DNS for the test run. - :param tr: The TestRun for the DNS process. - ''' - dns = tr.MakeDNServer(f'dns_{self._protocol_str}', default='127.0.0.1') - self._dns = dns - return dns - - def _configure_server(self, tr: 'TestRun'): - '''Configure the origin server for the test run. - - :param tr: The TestRun for the origin server. - ''' - tr.Setup.Copy(self._early_hints_server) - server = tr.Processes.Process(f'server_{self._protocol_str}') - server_port = get_port(server, "http_port") - server.Command = \ - f'{sys.executable} {self._early_hints_server} 127.0.0.1 {server_port} ' - server.Ready = When.PortOpenv4(server_port) - - self._server = server - return server - - def _configure_ts(self, tr: 'TestRun'): - '''Configure the traffic server for the test run. - - :param tr: The TestRun for the traffic server. - ''' - ts = Test.MakeATSProcess(f'ts_{self._protocol_str}', enable_tls=True, enable_quic=self._protocol == Protocol.HTTP3) - self._ts = ts - ts.Disk.remap_config.AddLine(f'map / http://backend.server.com:{self._server.Variables.http_port}') - ts.addDefaultSSLFiles() - ts.Disk.ssl_multicert_yaml.AddLines( - """ -ssl_multicert: - - dest_ip: "*" - ssl_cert_name: server.pem - ssl_key_name: server.key -""".split("\n")) - ts.Disk.records_config.update( - { - 'proxy.config.ssl.server.cert.path': ts.Variables.SSLDir, - 'proxy.config.ssl.server.private_key.path': ts.Variables.SSLDir, - 'proxy.config.dns.nameservers': f'127.0.0.1:{self._dns.Variables.Port}', - 'proxy.config.dns.resolv_conf': 'NULL', - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'http', - }) - return ts - - def _copy_scripts(self, tr: 'TestRun', ts: 'TestATSProcess'): - '''Copy the python server and helper files to the test run directory. - :param tr: The TestRun for the server. - :param ts: The TestATSProcess for the traffic server. This is needed - for the ATS tools directory it stores in Variables. - ''' - tr.Setup.Copy(self._early_hints_server) - tools_dir = ts.Variables.AtsTestToolsDir - http_utils = os.path.join(tools_dir, 'http_utils.py') - tr.Setup.CopyAs(http_utils, Test.RunDirectory) - - def _configure_client(self, tr: 'TestRun'): - '''Configure a client to use the given protocol to ATS. - :param tr: The TestRun for the client. - ''' - client = tr.Processes.Default - if self._protocol == Protocol.HTTP: - protocol_arg = '--http1.1' - scheme = 'http' - ts_port = self._ts.Variables.port - elif self._protocol == Protocol.HTTPS: - protocol_arg = '-k --http1.1' - scheme = 'https' - ts_port = self._ts.Variables.ssl_port - elif self._protocol == Protocol.HTTP2: - protocol_arg = '-k --http2' - scheme = 'https' - ts_port = self._ts.Variables.ssl_port - elif self._protocol == Protocol.HTTP3: - protocol_arg = '-k --http3-only' - scheme = 'https' - ts_port = self._ts.Variables.ssl_port - tr.MakeCurlCommand( - f'-v {protocol_arg} ' - f'--resolve "server.com:{ts_port}:127.0.0.1" ' - f'-H "Host: server.com" ' - f'{scheme}://server.com:{ts_port}/{self._protocol_str}', - ts=self._ts) - - client.ReturnCode = 0 - self._ts.StartBefore(self._dns) - self._ts.StartBefore(self._server) - client.StartBefore(self._ts) - - # Note that the server is configured to send two 103 responses. - client.Streams.All += Testers.ContainsExpression( - 'HTTP/.* 103.*HTTP/.* 103', - 'Verify that two 103 Early Hints responses were received.', - reflags=re.MULTILINE | re.DOTALL) - client.Streams.All += Testers.ContainsExpression( - 'ink: ; rel=preload', 'Verify preload link header was received.') - client.Streams.All += Testers.ContainsExpression('HTTP/.* 200', 'Verify 200 OK response was received.') - client.Streams.All += Testers.ContainsExpression('10bytebody', 'Verify the body to the 200 OK was received.') - - -TestEarlyHints(Protocol.HTTP) -if not Condition.CurlUsingUnixDomainSocket(): - TestEarlyHints(Protocol.HTTPS) - TestEarlyHints(Protocol.HTTP2) - if Condition.HasATSFeature('TS_USE_QUIC') and Condition.HasCurlFeature('http3') and Condition.HasCurlOption('--http3-only'): - TestEarlyHints(Protocol.HTTP3) diff --git a/tests/gold_tests/forward_proxy/forward_proxy.test.py b/tests/gold_tests/forward_proxy/forward_proxy.test.py deleted file mode 100644 index 6566414aec8..00000000000 --- a/tests/gold_tests/forward_proxy/forward_proxy.test.py +++ /dev/null @@ -1,115 +0,0 @@ -""" -""" -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -from typing import Union - -Test.Summary = 'Verify ATS can function as a forward proxy' -Test.ContinueOnFail = True -Test.SkipIf(Condition.CurlUsingUnixDomainSocket()) - - -class ForwardProxyTest: - _scheme_proto_mismatch_policy: Union[int, None] - _ts_counter: int = 0 - _server_counter: int = 0 - - def __init__(self, verify_scheme_matches_protocol: Union[int, None]): - """Construct a ForwardProxyTest object. - - :param verify_scheme_matches_protocol: The value with which to - configure Traffic Server's - proxy.config.ssl.client.scheme_proto_mismatch_policy. A value of None - means that no value will be explicitly set in the records.yaml. - :type verify_scheme_matches_protocol: int or None - """ - self._scheme_proto_mismatch_policy = verify_scheme_matches_protocol - self.setupOriginServer() - self.setupTS() - - def setupOriginServer(self): - """Configure the Proxy Verifier server.""" - proc_name = f"server{ForwardProxyTest._server_counter}" - self.server = Test.MakeVerifierServerProcess(proc_name, "forward_proxy.replay.yaml") - ForwardProxyTest._server_counter += 1 - if self._scheme_proto_mismatch_policy in (2, None): - self.server.Streams.All = Testers.ExcludesExpression( - 'Received an HTTP/1 request with key 1', 'Verify that the server did not receive the request.') - else: - self.server.Streams.All = Testers.ContainsExpression( - 'Received an HTTP/1 request with key 1', 'Verify that the server received the request.') - - def setupTS(self): - """Configure the Traffic Server process.""" - proc_name = f"ts{ForwardProxyTest._ts_counter}" - self.ts = Test.MakeATSProcess(proc_name, enable_tls=True, enable_cache=False) - ForwardProxyTest._ts_counter += 1 - self.ts.addDefaultSSLFiles() - self.ts.Disk.ssl_multicert_yaml.AddLines( - """ -ssl_multicert: - - dest_ip: "*" - ssl_cert_name: server.pem - ssl_key_name: server.key -""".split("\n")) - self.ts.Disk.remap_config.AddLine(f"map / http://127.0.0.1:{self.server.Variables.http_port}/") - - self.ts.Disk.records_config.update( - { - 'proxy.config.ssl.server.cert.path': self.ts.Variables.SSLDir, - 'proxy.config.ssl.server.private_key.path': self.ts.Variables.SSLDir, - 'proxy.config.ssl.client.verify.server.policy': 'PERMISSIVE', - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': "http", - }) - - if self._scheme_proto_mismatch_policy is not None: - self.ts.Disk.records_config.update( - { - 'proxy.config.ssl.client.scheme_proto_mismatch_policy': self._scheme_proto_mismatch_policy, - }) - - def addProxyHttpsToHttpCase(self): - """Test ATS as an HTTPS forward proxy behind an HTTP server.""" - tr = Test.AddTestRun() - tr.Processes.Default.StartBefore(self.server) - tr.Processes.Default.StartBefore(self.ts) - tr.MakeCurlCommand( - f'--proxy-insecure -v -H "uuid: 1" ' - f'--proxy "https://127.0.0.1:{self.ts.Variables.ssl_port}/" ' - f'http://example.com/', - ts=self.ts) - tr.Processes.Default.ReturnCode = 0 - tr.StillRunningAfter = self.server - tr.StillRunningAfter = self.ts - - if self._scheme_proto_mismatch_policy in (2, None): - tr.Processes.Default.Streams.All = Testers.ContainsExpression( - '< HTTP/1.1 400 Invalid HTTP Request', 'Verify that the request was rejected.') - else: - tr.Processes.Default.Streams.All = Testers.ContainsExpression( - '< HTTP/1.1 200 OK', 'Verify that curl received a 200 OK response.') - - def run(self): - """Configure the TestRun instances for this set of tests.""" - self.addProxyHttpsToHttpCase() - - -ForwardProxyTest(verify_scheme_matches_protocol=None).run() -ForwardProxyTest(verify_scheme_matches_protocol=0).run() -ForwardProxyTest(verify_scheme_matches_protocol=1).run() -ForwardProxyTest(verify_scheme_matches_protocol=2).run() diff --git a/tests/gold_tests/h2/grpc/grpc.test.py b/tests/gold_tests/h2/grpc/grpc.test.py deleted file mode 100644 index 4c0c19fe97c..00000000000 --- a/tests/gold_tests/h2/grpc/grpc.test.py +++ /dev/null @@ -1,164 +0,0 @@ -"""Test basic gRPC traffic.""" - -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import os -from ports import get_port -import sys - - -class TestGrpc(): - """Test basic gRPC traffic.""" - - num_client_connections = 50 - - def __init__(self, description: str): - """Configure a TestRun for gRPC traffic. - - :param description: The description for the test runs. - """ - self._description = description - - def _configure_dns(self, tr: 'TestRun') -> 'Process': - """Configure a locally running MicroDNS server. - - :param tr: The TestRun with which to associate the MicroDNS server. - :return: The MicroDNS server process. - """ - self._dns = tr.MakeDNServer("dns", default=['127.0.0.1']) - return self._dns - - def _configure_traffic_server(self, tr: 'TestRun', dns_port: int, server_port: int) -> 'Process': - """Configure the traffic server process. - - :param tr: The TestRun with which to associate the traffic server. - :param dns_port: The MicroDNS server port that traffic server should connect to. - :param server_port: The gRPC server port that traffic server should connect to. - :return: The traffic server process. - """ - self._ts = tr.MakeATSProcess("ts", enable_tls=True, enable_cache=False) - - self._ts.addDefaultSSLFiles() - self._ts.Disk.ssl_multicert_yaml.AddLines( - """ -ssl_multicert: - - dest_ip: "*" - ssl_cert_name: server.pem - ssl_key_name: server.key -""".split("\n")) - - self._ts.Disk.remap_config.AddLine(f"map / https://example.com:{server_port}/") - - self._ts.Disk.records_config.update( - { - "proxy.config.ssl.server.cert.path": self._ts.Variables.SSLDir, - "proxy.config.ssl.server.private_key.path": self._ts.Variables.SSLDir, - 'proxy.config.ssl.client.alpn_protocols': 'h2,http/1.1', - 'proxy.config.http.server_session_sharing.pool': 'thread', - 'proxy.config.ssl.client.verify.server.policy': 'PERMISSIVE', - 'proxy.config.dns.nameservers': f"127.0.0.1:{dns_port}", - 'proxy.config.dns.resolv_conf': "NULL", - - # Disable debug logging to avoid excessive log file size. I keep - # it here for convenience of use during manual debugging. - "proxy.config.diags.debug.enabled": 0, - "proxy.config.diags.debug.tags": "http", - - # The Python gRPC module uses many WINDO_UPDATE frames of small - # sizes, so we have to disable the min_avg_window_update to - # avoid ATS generating ERRORS logs and GOAWAY frames for them. - "proxy.config.http2.min_avg_window_update": 0, - }) - return self._ts - - def _configure_grpc_server(self, tr: 'TestRun') -> 'Process': - """Start the gRPC server. - - :param tr: The TestRun with which to associate the gRPC server. - :return: The gRPC server process. - """ - tr.Setup.Copy('grpc_server.py') - self._server = tr.Processes.Process('server') - - server_pem = os.path.join(Test.Variables.AtsTestToolsDir, "ssl", "server.pem") - server_key = os.path.join(Test.Variables.AtsTestToolsDir, "ssl", "server.key") - self._server.Setup.Copy(server_pem) - self._server.Setup.Copy(server_key) - - port = get_port(self._server, 'port') - # Each connection performs two requests, so multiply the number of - # connections by 2 to get the expected number of transactions. - command = ( - f'{sys.executable} {tr.RunDirectory}/grpc_server.py {port} ' - f'server.pem server.key {TestGrpc.num_client_connections * 2}') - self._server.Command = command - self._server.ReturnCode = 0 - return self._server - - def _configure_grpc_client(self, tr: 'TestRun', proxy_port: int) -> None: - """Start the gRPC client. - - :param tr: The TestRun with which to associate the gRPC client. - :param proxy_port: The proxy_port to which to connect. - """ - tr.Setup.Copy('grpc_client.py') - ts_cert = os.path.join(self._ts.Variables.SSLDir, 'server.pem') - # The cert is for example.com, so we must use that domain. - hostname = 'example.com' - command = ( - f'{sys.executable} {tr.RunDirectory}/grpc_client.py ' - f'{hostname} {proxy_port} {ts_cert} {TestGrpc.num_client_connections}') - tr.Processes.Default.Command = command - tr.Processes.Default.ReturnCode = 0 - tr.TimeOut = 10 - - def _compile_protobuf_files(self) -> None: - """Compile the protobuf files.""" - tr = Test.AddTestRun(f'{self._description}: compile the protobuf files.') - tr.Setup.Copy('simple.proto') - command = ( - f'{sys.executable} -m grpc_tools.protoc -I{tr.RunDirectory} ' - f'--python_out={tr.RunDirectory} --grpc_python_out={tr.RunDirectory} simple.proto') - tr.Processes.Default.Command = command - pb2_file = os.path.join(tr.RunDirectory, 'simple_pb2.py') - tr.Disk.File(pb2_file, id='pb2', exists=True) - - pb2_grpc_file = os.path.join(tr.RunDirectory, 'simple_pb2_grpc.py') - tr.Disk.File(pb2_grpc_file, id='pb2_grpc', exists=True) - - def _run_test_traffic(self) -> None: - """Configure the TestRun for the client and servers.""" - tr = Test.AddTestRun(f'{self._description}: run the gRPC traffic.') - - dns = self._configure_dns(tr) - server = self._configure_grpc_server(tr) - ts = self._configure_traffic_server(tr, dns.Variables.Port, server.Variables.port) - - tr.Processes.Default.StartBefore(dns) - tr.Processes.Default.StartBefore(server) - tr.Processes.Default.StartBefore(ts) - - self._configure_grpc_client(tr, ts.Variables.ssl_port) - - def run(self) -> None: - """Configure the various test runs for the gRPC test.""" - self._compile_protobuf_files() - self._run_test_traffic() - - -test = TestGrpc("Test basic gRPC traffic") -test.run() diff --git a/tests/gold_tests/h2/h2_origin_cert_reverify.test.py b/tests/gold_tests/h2/h2_origin_cert_reverify.test.py deleted file mode 100644 index cd983526a82..00000000000 --- a/tests/gold_tests/h2/h2_origin_cert_reverify.test.py +++ /dev/null @@ -1,78 +0,0 @@ -''' -Verify HTTP/2 origin session reuse re-checks the origin certificate name. -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -Test.Summary = ''' -Verify HTTP/2 origin session reuse re-checks the origin certificate name. -''' - -ts = Test.MakeATSProcess("ts", enable_tls=True) -server = Test.MakeVerifierServerProcess( - "h2-origin", "replay_h2_origin_cert_reverify.yaml", ssl_cert="../tls/ssl/signed-foo.pem", ca_cert="../tls/ssl/signer.pem") - -ts.addDefaultSSLFiles() -ts.addSSLfile("../tls/ssl/signer.pem") - -ts.Disk.records_config.update( - { - 'proxy.config.ssl.server.cert.path': ts.Variables.SSLDir, - 'proxy.config.ssl.server.private_key.path': ts.Variables.SSLDir, - 'proxy.config.ssl.client.CA.cert.path': ts.Variables.SSLDir, - 'proxy.config.ssl.client.CA.cert.filename': 'signer.pem', - 'proxy.config.ssl.client.alpn_protocols': 'h2,http/1.1', - 'proxy.config.ssl.client.verify.server.policy': 'ENFORCED', - 'proxy.config.ssl.client.verify.server.properties': 'ALL', - 'proxy.config.url_remap.pristine_host_hdr': 1, - 'proxy.config.http.server_session_sharing.pool': 'thread', - 'proxy.config.http.server_session_sharing.match': 'ip', - 'proxy.config.exec_thread.autoconfig.enabled': 0, - 'proxy.config.exec_thread.limit': 1, - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'http|ssl_verify', - }) - -ts.Disk.ssl_multicert_yaml.AddLines( - """ -ssl_multicert: - - dest_ip: "*" - ssl_cert_name: server.pem - ssl_key_name: server.key -""".split("\n")) - -ts.Disk.remap_config.AddLine(f"map / https://127.0.0.1:{server.Variables.https_port}/") - -tr = Test.AddTestRun("Prime an H2 origin connection for foo.com") -tr.Processes.Default.StartBefore(server) -tr.Processes.Default.StartBefore(ts) -tr.MakeCurlCommand(f"-v -H 'Host: foo.com' -H 'uuid: foo' http://127.0.0.1:{ts.Variables.port}/foo", ts=ts) -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Streams.stdout = Testers.ContainsExpression("foo-response", "foo.com should receive the origin response") -tr.StillRunningAfter = server -tr.StillRunningAfter = ts - -tr = Test.AddTestRun("Reject reuse for bar.com") -tr.MakeCurlCommand(f"-v -H 'Host: bar.com' http://127.0.0.1:{ts.Variables.port}/bar", ts=ts) -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Streams.stdout = Testers.ContainsExpression( - "Could Not Connect", "bar.com should fail certificate name verification") -tr.StillRunningAfter = server -tr.StillRunningAfter = ts - -ts.Disk.diags_log.Content = Testers.ContainsExpression( - r"WARNING: Origin hostname \(bar.com\) not in certificate. Action=Terminate", - "The pooled H2 origin session should be rejected for bar.com.") diff --git a/tests/gold_tests/h2/h2_origin_trailers_h1.test.py b/tests/gold_tests/h2/h2_origin_trailers_h1.test.py deleted file mode 100644 index 120549eba70..00000000000 --- a/tests/gold_tests/h2/h2_origin_trailers_h1.test.py +++ /dev/null @@ -1,94 +0,0 @@ -''' -Verify HTTP/2 origin trailers are not forwarded to HTTP/1 clients. -''' - -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import sys - -Test.Summary = __doc__ - -Test.SkipUnless(Condition.HasProxyVerifierVersion('2.8.0')) - - -class TestOriginTrailers: - """Verify HTTP/2 origin trailers are handled safely by client protocol.""" - - _replay_file = 'h2_origin_trailers_h1.replay.yaml' - - def __init__(self): - self._h1_server = self._configure_server('h2-origin-h1') - self._h1_ts = self._configure_ats('ts-h1', self._h1_server) - self._configure_h1_client() - - self._h2_server = self._configure_server('h2-origin-h2') - self._h2_ts = self._configure_ats('ts-h2', self._h2_server) - self._configure_h2_client() - - def _configure_server(self, name): - return Test.MakeVerifierServerProcess(name, self._replay_file) - - def _configure_ats(self, name, server): - ts = Test.MakeATSProcess(name, enable_tls=True, enable_cache=False) - ts.addDefaultSSLFiles() - - ts.Disk.records_config.update( - { - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'http|http2', - 'proxy.config.exec_thread.autoconfig.enabled': 0, - 'proxy.config.exec_thread.limit': 1, - 'proxy.config.http.server_session_sharing.pool': 'thread', - 'proxy.config.http.server_session_sharing.match': 'ip,sni,cert', - 'proxy.config.ssl.client.alpn_protocols': 'h2,http/1.1', - 'proxy.config.ssl.client.verify.server.policy': 'PERMISSIVE', - 'proxy.config.ssl.server.cert.path': ts.Variables.SSLDir, - 'proxy.config.ssl.server.private_key.path': ts.Variables.SSLDir, - }) - - ts.Disk.ssl_multicert_yaml.AddLines( - """ -ssl_multicert: - - dest_ip: "*" - ssl_cert_name: server.pem - ssl_key_name: server.key -""".split("\n")) - - ts.Disk.remap_config.AddLine(f'map / https://127.0.0.1:{server.Variables.https_port}') - return ts - - def _configure_h1_client(self): - Test.Setup.CopyAs('h1_trailer_client.py', Test.RunDirectory) - - tr = Test.AddTestRun('HTTP/2 origin trailers are dropped for HTTP/1 clients') - tr.Processes.Default.StartBefore(self._h1_server) - tr.Processes.Default.StartBefore(self._h1_ts) - tr.Processes.Default.Command = f'{sys.executable} h1_trailer_client.py 127.0.0.1 {self._h1_ts.Variables.port}' - tr.Processes.Default.ReturnCode = 0 - tr.Processes.Default.Streams.All += Testers.ContainsExpression( - 'No H2 origin trailers were forwarded to the HTTP/1 client.', 'The HTTP/1 response must end at its terminal chunk.') - - def _configure_h2_client(self): - tr = Test.AddTestRun('HTTP/2 origin trailers are forwarded to HTTP/2 clients') - client = tr.AddVerifierClientProcess('h2-client', self._replay_file, https_ports=[self._h2_ts.Variables.ssl_port]) - client.StartBefore(self._h2_server) - client.StartBefore(self._h2_ts) - client.Streams.All += Testers.ContainsExpression( - 'x-ats-h2-trailer: smuggled', 'The HTTP/2 client must receive the origin trailer.') - - -TestOriginTrailers() diff --git a/tests/gold_tests/h2/h2disable.test.py b/tests/gold_tests/h2/h2disable.test.py deleted file mode 100644 index 7946228fabc..00000000000 --- a/tests/gold_tests/h2/h2disable.test.py +++ /dev/null @@ -1,96 +0,0 @@ -''' -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -Test.Summary = ''' -Test disabling H2 on a per domain basis -''' - -Test.SkipUnless(Condition.HasCurlFeature('http2')) - -# Define default ATS -ts = Test.MakeATSProcess("ts", enable_tls=True) -server = Test.MakeOriginServer("server") - -request_header = {"headers": "GET / HTTP/1.1\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -server.addResponse("sessionlog.json", request_header, response_header) - -# add ssl materials like key, certificates for the server -ts.addDefaultSSLFiles() - -ts.Disk.remap_config.AddLine('map / http://127.0.0.1:{0}'.format(server.Variables.Port)) - -ts.Disk.ssl_multicert_yaml.AddLines( - """ -ssl_multicert: - - dest_ip: "*" - ssl_cert_name: server.pem - ssl_key_name: server.key -""".split("\n")) - -# Case 1, global config policy=permissive properties=signature -# override for foo.com policy=enforced properties=all -ts.Disk.records_config.update( - { - 'proxy.config.diags.debug.enabled': 0, - 'proxy.config.diags.debug.tags': 'http|ssl', - 'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir), - 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts.Variables.SSLDir), - 'proxy.config.url_remap.pristine_host_hdr': 1, - 'proxy.config.accept_threads': 1 - }) - -ts.Disk.sni_yaml.AddLines([ - 'sni:', - '- fqdn: bar.com', - ' http2: off', - '- fqdn: "*.foo.com"', - ' http2: off', -]) - -tr = Test.AddTestRun("Negotiate-h2") -tr.MakeCurlCommand("-v -k --resolve 'foo.com:{0}:127.0.0.1' https://foo.com:{0}".format(ts.Variables.ssl_port), ts=ts) -tr.ReturnCode = 0 -tr.Processes.Default.StartBefore(server) -tr.Processes.Default.StartBefore(Test.Processes.ts) -tr.StillRunningAfter = server -tr.StillRunningAfter = ts -tr.Processes.Default.TimeOut = 5 -tr.Processes.Default.Streams.All = Testers.ExcludesExpression("Could Not Connect", "Curl attempt should have succeeded") -tr.Processes.Default.Streams.All += Testers.ContainsExpression("[Uu]sing HTTP/?2", "Curl should negotiate HTTP2") -tr.TimeOut = 5 - -tr2 = Test.AddTestRun("Do not negotiate h2") -tr2.MakeCurlCommand("-v -k --resolve 'bar.com:{0}:127.0.0.1' https://bar.com:{0}".format(ts.Variables.ssl_port), ts=ts) -tr2.ReturnCode = 0 -tr2.StillRunningAfter = server -tr2.Processes.Default.TimeOut = 5 -tr2.StillRunningAfter = ts -tr2.Processes.Default.Streams.All = Testers.ExcludesExpression("Could Not Connect", "Curl attempt should have succeeded") -tr2.Processes.Default.Streams.All += Testers.ExcludesExpression("[Uu]sing HTTP/?2", "Curl should not negotiate HTTP2") -tr2.TimeOut = 5 - -tr2 = Test.AddTestRun("Do not negotiate h2") -tr2.MakeCurlCommand("-v -k --resolve 'bob.foo.com:{0}:127.0.0.1' https://bob.foo.com:{0}".format(ts.Variables.ssl_port), ts=ts) -tr2.ReturnCode = 0 -tr2.StillRunningAfter = server -tr2.Processes.Default.TimeOut = 5 -tr2.StillRunningAfter = ts -tr2.Processes.Default.Streams.All = Testers.ExcludesExpression("Could Not Connect", "Curl attempt should have succeeded") -tr2.Processes.Default.Streams.All += Testers.ExcludesExpression("[Uu]sing HTTP/?2", "Curl should not negotiate HTTP2") -tr2.TimeOut = 5 diff --git a/tests/gold_tests/h2/h2disable_no_accept_threads.test.py b/tests/gold_tests/h2/h2disable_no_accept_threads.test.py deleted file mode 100644 index e431858b908..00000000000 --- a/tests/gold_tests/h2/h2disable_no_accept_threads.test.py +++ /dev/null @@ -1,96 +0,0 @@ -''' -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -Test.Summary = ''' -Test disabling H2 on a per domain basis -''' - -Test.SkipUnless(Condition.HasCurlFeature('http2')) - -# Define default ATS -ts = Test.MakeATSProcess("ts", enable_tls=True) -server = Test.MakeOriginServer("server") - -request_header = {"headers": "GET / HTTP/1.1\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -server.addResponse("sessionlog.json", request_header, response_header) - -# add ssl materials like key, certificates for the server -ts.addDefaultSSLFiles() - -ts.Disk.remap_config.AddLine('map / http://127.0.0.1:{0}'.format(server.Variables.Port)) - -ts.Disk.ssl_multicert_yaml.AddLines( - """ -ssl_multicert: - - dest_ip: "*" - ssl_cert_name: server.pem - ssl_key_name: server.key -""".split("\n")) - -# Case 1, global config policy=permissive properties=signature -# override for foo.com policy=enforced properties=all -ts.Disk.records_config.update( - { - 'proxy.config.diags.debug.enabled': 0, - 'proxy.config.diags.debug.tags': 'http|ssl', - 'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir), - 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts.Variables.SSLDir), - 'proxy.config.url_remap.pristine_host_hdr': 1, - 'proxy.config.accept_threads': 0 - }) - -ts.Disk.sni_yaml.AddLines([ - 'sni:', - '- fqdn: bar.com', - ' http2: off', - '- fqdn: "*.foo.com"', - ' http2: off', -]) - -tr = Test.AddTestRun("Negotiate-h2") -tr.MakeCurlCommand("-v -k --resolve 'foo.com:{0}:127.0.0.1' https://foo.com:{0}".format(ts.Variables.ssl_port), ts=ts) -tr.ReturnCode = 0 -tr.Processes.Default.StartBefore(server) -tr.Processes.Default.StartBefore(Test.Processes.ts) -tr.StillRunningAfter = server -tr.StillRunningAfter = ts -tr.Processes.Default.TimeOut = 5 -tr.Processes.Default.Streams.All = Testers.ExcludesExpression("Could Not Connect", "Curl attempt should have succeeded") -tr.Processes.Default.Streams.All += Testers.ContainsExpression("[Uu]sing HTTP/?2", "Curl should negotiate HTTP2") -tr.TimeOut = 5 - -tr2 = Test.AddTestRun("Do not negotiate h2") -tr2.MakeCurlCommand("-v -k --resolve 'bar.com:{0}:127.0.0.1' https://bar.com:{0}".format(ts.Variables.ssl_port), ts=ts) -tr2.ReturnCode = 0 -tr2.StillRunningAfter = server -tr2.Processes.Default.TimeOut = 5 -tr2.StillRunningAfter = ts -tr2.Processes.Default.Streams.All = Testers.ExcludesExpression("Could Not Connect", "Curl attempt should have succeeded") -tr2.Processes.Default.Streams.All += Testers.ExcludesExpression("[Uu]sing HTTP/?2", "Curl should not negotiate HTTP2") -tr2.TimeOut = 5 - -tr2 = Test.AddTestRun("Do not negotiate h2") -tr2.MakeCurlCommand("-v -k --resolve 'bob.foo.com:{0}:127.0.0.1' https://bob.foo.com:{0}".format(ts.Variables.ssl_port), ts=ts) -tr2.ReturnCode = 0 -tr2.StillRunningAfter = server -tr2.Processes.Default.TimeOut = 5 -tr2.StillRunningAfter = ts -tr2.Processes.Default.Streams.All = Testers.ExcludesExpression("Could Not Connect", "Curl attempt should have succeeded") -tr2.Processes.Default.Streams.All += Testers.ExcludesExpression("[Uu]sing HTTP/?2", "Curl should not negotiate HTTP2") -tr2.TimeOut = 5 diff --git a/tests/gold_tests/h2/h2enable.test.py b/tests/gold_tests/h2/h2enable.test.py deleted file mode 100644 index 1aaf0168555..00000000000 --- a/tests/gold_tests/h2/h2enable.test.py +++ /dev/null @@ -1,97 +0,0 @@ -''' -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -Test.Summary = ''' -Test enabling H2 on a per domain basis -''' - -Test.SkipUnless(Condition.HasCurlFeature('http2')) - -# Define default ATS -ts = Test.MakeATSProcess("ts", enable_tls=True) -server = Test.MakeOriginServer("server") - -request_header = {"headers": "GET / HTTP/1.1\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -server.addResponse("sessionlog.json", request_header, response_header) - -# add ssl materials like key, certificates for the server -ts.addDefaultSSLFiles() - -ts.Disk.remap_config.AddLine('map / http://127.0.0.1:{0}'.format(server.Variables.Port)) - -ts.Disk.ssl_multicert_yaml.AddLines( - """ -ssl_multicert: - - dest_ip: "*" - ssl_cert_name: server.pem - ssl_key_name: server.key -""".split("\n")) - -# Set up port 4444 with HTTP1 only, no HTTP/2 -ts.Disk.records_config.update( - { - 'proxy.config.diags.debug.enabled': 0, - 'proxy.config.diags.debug.tags': 'http|ssl', - 'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir), - 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts.Variables.SSLDir), - 'proxy.config.url_remap.pristine_host_hdr': 1, - 'proxy.config.accept_threads': 1, - 'proxy.config.http.server_ports': '{0}:ssl:proto=http {1}'.format(ts.Variables.ssl_port, ts.Variables.port) - }) - -ts.Disk.sni_yaml.AddLines([ - 'sni:', - '- fqdn: bar.com', - ' http2: on', - '- fqdn: "*.foo.com"', - ' http2: on', -]) - -tr = Test.AddTestRun("Do-not-Negotiate-h2") -tr.MakeCurlCommand("-v -k --ipv4 --resolve 'foo.com:{0}:127.0.0.1' https://foo.com:{0}".format(ts.Variables.ssl_port), ts=ts) -tr.ReturnCode = 0 -tr.Processes.Default.StartBefore(server, ready=When.PortOpen(server.Variables.Port)) -tr.Processes.Default.StartBefore(Test.Processes.ts) -tr.StillRunningAfter = server -tr.StillRunningAfter = ts -tr.Processes.Default.TimeOut = 10 -tr.Processes.Default.Streams.All = Testers.ExcludesExpression("Could Not Connect", "Curl attempt should have succeeded") -tr.Processes.Default.Streams.All += Testers.ExcludesExpression("[Uu]sing HTTP/?2", "Curl should negotiate HTTP2") -tr.TimeOut = 10 - -tr2 = Test.AddTestRun("Do negotiate h2") -tr2.MakeCurlCommand("-v -k --ipv4 --resolve 'bar.com:{0}:127.0.0.1' https://bar.com:{0}".format(ts.Variables.ssl_port), ts=ts) -tr2.ReturnCode = 0 -tr2.StillRunningAfter = server -tr2.Processes.Default.TimeOut = 10 -tr2.StillRunningAfter = ts -tr2.Processes.Default.Streams.All = Testers.ExcludesExpression("Could Not Connect", "Curl attempt should have succeeded") -tr2.Processes.Default.Streams.All += Testers.ContainsExpression("[Uu]sing HTTP/?2", "Curl should not negotiate HTTP2") -tr2.TimeOut = 10 - -tr2 = Test.AddTestRun("Do negotiate h2") -tr2.MakeCurlCommand( - "-v -k --ipv4 --resolve 'bob.foo.com:{0}:127.0.0.1' https://bob.foo.com:{0}".format(ts.Variables.ssl_port), ts=ts) -tr2.ReturnCode = 0 -tr2.StillRunningAfter = server -tr2.Processes.Default.TimeOut = 10 -tr2.StillRunningAfter = ts -tr2.Processes.Default.Streams.All = Testers.ExcludesExpression("Could Not Connect", "Curl attempt should have succeeded") -tr2.Processes.Default.Streams.All += Testers.ContainsExpression("[Uu]sing HTTP/?2", "Curl should not negotiate HTTP2") -tr2.TimeOut = 10 diff --git a/tests/gold_tests/h2/h2enable_no_accept_threads.test.py b/tests/gold_tests/h2/h2enable_no_accept_threads.test.py deleted file mode 100644 index c6818be4a4d..00000000000 --- a/tests/gold_tests/h2/h2enable_no_accept_threads.test.py +++ /dev/null @@ -1,96 +0,0 @@ -''' -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -Test.Summary = ''' -Test enabling H2 on a per domain basis -''' - -Test.SkipUnless(Condition.HasCurlFeature('http2')) - -# Define default ATS -ts = Test.MakeATSProcess("ts", enable_tls=True) -server = Test.MakeOriginServer("server") - -request_header = {"headers": "GET / HTTP/1.1\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -server.addResponse("sessionlog.json", request_header, response_header) - -# add ssl materials like key, certificates for the server -ts.addDefaultSSLFiles() - -ts.Disk.remap_config.AddLine('map / http://127.0.0.1:{0}'.format(server.Variables.Port)) - -ts.Disk.ssl_multicert_yaml.AddLines( - """ -ssl_multicert: - - dest_ip: "*" - ssl_cert_name: server.pem - ssl_key_name: server.key -""".split("\n")) - -# Set up port 4444 with HTTP1 only, no HTTP/2 -ts.Disk.records_config.update( - { - 'proxy.config.diags.debug.enabled': 0, - 'proxy.config.diags.debug.tags': 'http|ssl', - 'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir), - 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts.Variables.SSLDir), - 'proxy.config.url_remap.pristine_host_hdr': 1, - 'proxy.config.http.server_ports': '{0}:ssl:proto=http {1}'.format(ts.Variables.ssl_port, ts.Variables.port), - 'proxy.config.accept_threads': 0 - }) - -ts.Disk.sni_yaml.AddLines([ - 'sni:', - '- fqdn: bar.com', - ' http2: on', - '- fqdn: "*.foo.com"', - ' http2: on', -]) - -tr = Test.AddTestRun("Do-not-Negotiate-h2") -tr.MakeCurlCommand("-v -k --resolve 'foo.com:{0}:127.0.0.1' https://foo.com:{0}".format(ts.Variables.ssl_port), ts=ts) -tr.ReturnCode = 0 -tr.Processes.Default.StartBefore(server, ready=When.PortOpen(server.Variables.Port)) -tr.Processes.Default.StartBefore(Test.Processes.ts) -tr.StillRunningAfter = server -tr.StillRunningAfter = ts -tr.Processes.Default.TimeOut = 5 -tr.Processes.Default.Streams.All = Testers.ExcludesExpression("Could Not Connect", "Curl attempt should have succeeded") -tr.Processes.Default.Streams.All += Testers.ExcludesExpression("[Uu]sing HTTP/?2", "Curl should negotiate HTTP2") -tr.TimeOut = 5 - -tr2 = Test.AddTestRun("Do negotiate h2") -tr2.MakeCurlCommand("-v -k --resolve 'bar.com:{0}:127.0.0.1' https://bar.com:{0}".format(ts.Variables.ssl_port), ts=ts) -tr2.ReturnCode = 0 -tr2.StillRunningAfter = server -tr2.Processes.Default.TimeOut = 5 -tr2.StillRunningAfter = ts -tr2.Processes.Default.Streams.All = Testers.ExcludesExpression("Could Not Connect", "Curl attempt should have succeeded") -tr2.Processes.Default.Streams.All += Testers.ContainsExpression("[Uu]sing HTTP/?2", "Curl should not negotiate HTTP2") -tr2.TimeOut = 5 - -tr2 = Test.AddTestRun("Do negotiate h2") -tr2.MakeCurlCommand("-v -k --resolve 'bob.foo.com:{0}:127.0.0.1' https://bob.foo.com:{0}".format(ts.Variables.ssl_port), ts=ts) -tr2.ReturnCode = 0 -tr2.StillRunningAfter = server -tr2.Processes.Default.TimeOut = 5 -tr2.StillRunningAfter = ts -tr2.Processes.Default.Streams.All = Testers.ExcludesExpression("Could Not Connect", "Curl attempt should have succeeded") -tr2.Processes.Default.Streams.All += Testers.ContainsExpression("[Uu]sing HTTP/?2", "Curl should not negotiate HTTP2") -tr2.TimeOut = 5 diff --git a/tests/gold_tests/h2/h2get_with_body.test.py b/tests/gold_tests/h2/h2get_with_body.test.py deleted file mode 100644 index 89661b6362f..00000000000 --- a/tests/gold_tests/h2/h2get_with_body.test.py +++ /dev/null @@ -1,71 +0,0 @@ -''' -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import os - -Test.Summary = ''' -Test http/2 GET method that has a body -''' - -Test.SkipUnless(Condition.HasProxyVerifierVersion('2.8.0')) - -pv_server = Test.MakeVerifierServerProcess("pv_server", "h2get_with_body.yaml") - -ts = Test.MakeATSProcess('ts', select_ports=True, enable_tls=True, enable_cache=True) - -ts.addDefaultSSLFiles() -ts.Disk.ssl_multicert_yaml.AddLines( - """ -ssl_multicert: - - dest_ip: "*" - ssl_cert_name: server.pem - ssl_key_name: server.key -""".split("\n")) -ts.Disk.records_config.update( - { - "proxy.config.http.server_ports": f"{ts.Variables.port} {ts.Variables.ssl_port}:ssl", - "proxy.config.http.background_fill_active_timeout": "0", - "proxy.config.http.background_fill_completed_threshold": "0.0", - "proxy.config.http.cache.required_headers": 0, # Force cache - "proxy.config.http.insert_response_via_str": 2, - 'proxy.config.http.server_session_sharing.pool': 'thread', - 'proxy.config.http.server_session_sharing.match': 'ip,sni,cert', - 'proxy.config.exec_thread.autoconfig.enabled': 0, - 'proxy.config.exec_thread.limit': 1, - 'proxy.config.ssl.server.cert.path': f"{ts.Variables.SSLDir}", - 'proxy.config.ssl.server.private_key.path': f"{ts.Variables.SSLDir}", - 'proxy.config.ssl.client.alpn_protocols': 'h2,http/1.1', - 'proxy.config.ssl.client.verify.server.policy': 'PERMISSIVE', - "proxy.config.diags.debug.enabled": 3, - "proxy.config.diags.debug.tags": "http", - }) - -ts.Disk.remap_config.AddLines([f'map / https://127.0.0.1:{pv_server.Variables.https_port}']) - -tr = Test.AddTestRun() -tr.Processes.Default.StartBefore(pv_server) -tr.Processes.Default.StartBefore(ts) -tr.AddVerifierClientProcess( - "pv_client", "h2get_with_body.yaml", http_ports=[ts.Variables.port], https_ports=[ts.Variables.ssl_port]) -tr.Processes.Default.ReturnCode = 0 - -tr.Processes.Default.Streams.All += Testers.ContainsExpression( - 'Equals Success: Key: "1", Content Data: "body", Value: "server_test"', 'Response check') - -pv_server.Streams.All += Testers.ContainsExpression( - 'Equals Success: Key: "1", Content Data: "body", Value: "client_test"', 'Request check') diff --git a/tests/gold_tests/h2/h2origin.test.py b/tests/gold_tests/h2/h2origin.test.py deleted file mode 100644 index 41c9c5bae6b..00000000000 --- a/tests/gold_tests/h2/h2origin.test.py +++ /dev/null @@ -1,123 +0,0 @@ -''' -Test communication to origin with H2 -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -Test.Summary = ''' -Test communication to origin with H2 -''' - -Test.ContinueOnFail = True - -# -# Communicate to origin with HTTP/2 -# -ts = Test.MakeATSProcess("ts", enable_tls="true") - -# add ssl materials like key, certificates for the server -ts.addDefaultSSLFiles() -replay_file = "replay_h2origin/" -server = Test.MakeVerifierServerProcess("h2-origin", replay_file) -server_expect = Test.MakeVerifierServerProcess("server-expect", "expect_100_continue.yaml") -ts.Disk.records_config.update( - { - 'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir), - 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts.Variables.SSLDir), - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'http', - 'proxy.config.exec_thread.autoconfig.enabled': 0, - # Allow for more parallelism - 'proxy.config.exec_thread.limit': 4, - 'proxy.config.ssl.client.alpn_protocols': 'h2,http/1.1', - # Sticking with thread pool because global pool does not work with h2 - 'proxy.config.http.server_session_sharing.pool': 'thread', - 'proxy.config.http.server_session_sharing.match': 'ip,sni,cert', - 'proxy.config.ssl.client.verify.server.policy': 'PERMISSIVE', - }) - -ts.Disk.remap_config.AddLines( - [f'map /expect http://127.0.0.1:{server_expect.Variables.http_port}', f'map / https://127.0.0.1:{server.Variables.https_port}']) -ts.Disk.ssl_multicert_yaml.AddLines( - """ -ssl_multicert: - - dest_ip: "*" - ssl_cert_name: server.pem - ssl_key_name: server.key -""".split("\n")) - -ts.Disk.logging_yaml.AddLines( - ''' -logging: - formats: - - name: testformat - format: '% % % % %<{uuid}cqh> % % % % % %' - logs: - - mode: ascii - format: testformat - filename: squid -'''.split("\n")) - -tr = Test.AddTestRun("Test traffic to origin using HTTP/2") -tr.Processes.Default.StartBefore(server) -tr.Processes.Default.StartBefore(ts) -tr.AddVerifierClientProcess("client", replay_file, http_ports=[ts.Variables.port], https_ports=[ts.Variables.ssl_port]) -tr.StillRunningAfter = ts -tr.TimeOut = 60 - -# A regression test for #9857. -tr = Test.AddTestRun("Test an empty body POST request with an Expect: 100-continue header") -tr.AddVerifierClientProcess( - "client-expect", "expect_100_continue.yaml", http_ports=[ts.Variables.port], https_ports=[ts.Variables.ssl_port]) -tr.Processes.Default.StartBefore(server_expect) -tr.Processes.Default.ReturnCode = 0 - -tr = Test.AddTestRun("Wait for the squid.log to be written") -timeout = 30 -watcher = tr.Processes.Process("watcher") -watcher.Command = f"sleep {timeout}" -watcher.Ready = When.FileContains(ts.Disk.squid_log.Name, r'16 http/2 http/2') -watcher.TimeOut = timeout -tr.StillRunningAfter = ts -tr.StillRunningAfter = server -tr.TimeOut = timeout -tr.Processes.Default.StartBefore(watcher) -tr.Processes.Default.Command = 'echo await_squid_log' -tr.Processes.Default.ReturnCode = 0 - -# UUIDs 1-4 should be http/1.1 clients and H2 origin -# UUIDs 5-11 and 15-16 should be http/2 clients and H2 origins -ts.Disk.squid_log.Content = Testers.ContainsExpression(" [1-4] http/1.1 http/2", "cases 1-4 request http/1.1") -ts.Disk.squid_log.Content += Testers.ExcludesExpression(" [1-4] http/2 http/2", "cases 1-4 request http/1.1") -ts.Disk.squid_log.Content += Testers.ContainsExpression(" 1[1-4] http/1.1 http/2", "cases 12-14 request http/1.1") -ts.Disk.squid_log.Content += Testers.ExcludesExpression(" 1[2-4] http/2 http/2", "cases 12-14 request http/1.1") -ts.Disk.squid_log.Content += Testers.ContainsExpression(" [5-9] http/2 http/2", "cases 5-11 request http/2") -ts.Disk.squid_log.Content += Testers.ExcludesExpression(" [5-9] http/1.1 http/2", "cases 5-11 request http/2") -ts.Disk.squid_log.Content += Testers.ContainsExpression(" 1[0-1] http/2 http/2", "cases 5-11 request http/2") -ts.Disk.squid_log.Content += Testers.ExcludesExpression(" 1[0-1] http/1.1 http/2", "cases 5-11 request http/2") -ts.Disk.squid_log.Content += Testers.ContainsExpression(" 1[5-6] http/2 http/2", "cases 15-16 request http/2") -ts.Disk.squid_log.Content += Testers.ExcludesExpression(" 1[5-6] http/1.1 http/2", "cases 15-16 request http/2") - -tr = Test.AddTestRun("Test HTTP method Metrics") -tr.Processes.Default.Command = ( - f"{Test.Variables.AtsTestToolsDir}/stdout_wait" + " 'traffic_ctl metric get" + " proxy.process.http.get_requests" + - " proxy.process.http.post_requests" + " proxy.process.http.put_requests'" + - f" {Test.TestDirectory}/gold/http-request-method-metrics.gold") -# Need to copy over the environment so traffic_ctl knows where to find the unix -# domain socket -tr.Processes.Default.Env = ts.Env -tr.Processes.Default.ReturnCode = 0 -tr.StillRunningAfter = ts diff --git a/tests/gold_tests/h2/h2origin_single_thread.test.py b/tests/gold_tests/h2/h2origin_single_thread.test.py deleted file mode 100644 index ba0b3389537..00000000000 --- a/tests/gold_tests/h2/h2origin_single_thread.test.py +++ /dev/null @@ -1,92 +0,0 @@ -''' -Test communication to origin with H2 -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -Test.Summary = ''' -Test communication to origin with H2 -''' - -Test.ContinueOnFail = True - -# -# Communicate to origin with HTTP/2 -# -ts = Test.MakeATSProcess("ts", enable_tls="true") - -# add ssl materials like key, certificates for the server -ts.addDefaultSSLFiles() -replay_file = "replay_h2origin" -server = Test.MakeVerifierServerProcess("h2-origin", replay_file) -ts.Disk.records_config.update( - { - 'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir), - 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts.Variables.SSLDir), - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'http', - 'proxy.config.exec_thread.autoconfig.enabled': 0, - # Limiting ourselves to 1 thread to exercise origin reuse - 'proxy.config.exec_thread.limit': 1, - 'proxy.config.ssl.client.alpn_protocols': 'h2,http/1.1', - # Sticking with hybrid pool because global pool does not work with h2 - 'proxy.config.http.server_session_sharing.pool': 'hybrid', - 'proxy.config.http.server_session_sharing.match': 'hostonly', - 'proxy.config.ssl.client.verify.server.policy': 'PERMISSIVE', - }) - -ts.Disk.remap_config.AddLine('map / https://127.0.0.1:{0}'.format(server.Variables.https_port)) -ts.Disk.ssl_multicert_yaml.AddLines( - """ -ssl_multicert: - - dest_ip: "*" - ssl_cert_name: server.pem - ssl_key_name: server.key -""".split("\n")) - -ts.Disk.logging_yaml.AddLines( - ''' -logging: - formats: - - name: testformat - format: '% % % % %<{uuid}cqh> % % % % % %' - logs: - - mode: ascii - format: testformat - filename: squid -'''.split("\n")) - -tr = Test.AddTestRun("Test traffic to origin using HTTP/2") -tr.Processes.Default.StartBefore(server) -tr.Processes.Default.StartBefore(ts) -tr.AddVerifierClientProcess("client", replay_file, http_ports=[ts.Variables.port], https_ports=[ts.Variables.ssl_port]) -tr.StillRunningAfter = ts - -# Just a check to flush out the traffic log until we have a clean shutdown for traffic_server -tr = Test.AddTestRun("Wait for the access log to write out") -tr.DelayStart = 10 -tr.StillRunningAfter = ts -tr.StillRunningAfter = server -tr.Processes.Default.Command = 'ls' -tr.Processes.Default.ReturnCode = 0 - -# UUIDs 1-4 should be http/1.1 clients and H2 origin -# UUIDs 5-9 should be http/2 clients and H2 origins -ts.Disk.squid_log.Content = Testers.ContainsExpression(" [1-4] http/1.1 http/2", "cases 1-4 request http/1.1") -ts.Disk.squid_log.Content += Testers.ExcludesExpression(" [1-4] http/2 http/2", "cases 1-4 request http/1.1") -ts.Disk.squid_log.Content += Testers.ContainsExpression(" [5-9] http/2 http/2", "cases 5-9 request http/2") -ts.Disk.squid_log.Content += Testers.ExcludesExpression(" [5-9] http/1.1 http/2", "cases 5-9 request http/2") -ts.Disk.squid_log.Content += Testers.ContainsExpression(" http/2 1 1 1 [2-9]", "At least one case of origin reuse") diff --git a/tests/gold_tests/h2/h2spec.test.py b/tests/gold_tests/h2/h2spec.test.py deleted file mode 100644 index ba841153980..00000000000 --- a/tests/gold_tests/h2/h2spec.test.py +++ /dev/null @@ -1,74 +0,0 @@ -''' -Test HTTP/2 with h2spec -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -Test.Summary = ''' -Test HTTP/2 with httpspec -''' - -Test.SkipUnless(Condition.HasProgram("h2spec", "h2spec need to be installed on system for this test to work"),) -Test.ContinueOnFail = True - -# ---- -# Setup httpbin Origin Server -# ---- -httpbin = Test.MakeHttpBinServer("httpbin") - -# ---- -# Setup ATS. Disable the cache to simplify the test. -# ---- -ts = Test.MakeATSProcess("ts", enable_tls=True, enable_cache=False) - -# add ssl materials like key, certificates for the server -ts.addDefaultSSLFiles() - -ts.Disk.remap_config.AddLine('map / http://127.0.0.1:{0}'.format(httpbin.Variables.Port)) -ts.Disk.ssl_multicert_yaml.AddLines( - """ -ssl_multicert: - - dest_ip: "*" - ssl_cert_name: server.pem - ssl_key_name: server.key -""".split("\n")) -ts.Disk.records_config.update( - { - 'proxy.config.http.insert_request_via_str': 1, - 'proxy.config.http.insert_response_via_str': 1, - 'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir), - 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts.Variables.SSLDir), - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'http', - }) - -# ---- -# Test Cases -# ---- - -# In case you need to disable some of the tests, you can specify sections like http2/6.4. -h2spec_targets = "generic http2/3 http2/4 http2/5 http2/6 http2/7 http2/8 hpack" - -test_run = Test.AddTestRun() -test_run.Processes.Default.Command = 'h2spec {0} -t -k --timeout 10 -p {1}'.format(h2spec_targets, ts.Variables.ssl_port) -test_run.Processes.Default.ReturnCode = 0 -test_run.Processes.Default.StartBefore(httpbin) -test_run.Processes.Default.StartBefore(Test.Processes.ts) -test_run.Processes.Default.Streams.stdout = "gold/h2spec_stdout.gold" -test_run.StillRunningAfter = httpbin - -# Over riding the built in ERROR check since we expect some error cases -ts.Disk.diags_log.Content = Testers.ContainsExpression("ERROR: HTTP/2", "h2spec tests should have error log") diff --git a/tests/gold_tests/h2/http2.test.py b/tests/gold_tests/h2/http2.test.py deleted file mode 100644 index 65cfbe81079..00000000000 --- a/tests/gold_tests/h2/http2.test.py +++ /dev/null @@ -1,286 +0,0 @@ -''' -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import os -import sys - -Test.Summary = ''' -Test a basic remap of a http/2 connection -''' - -Test.SkipUnless(Condition.HasCurlFeature('http2')) -Test.ContinueOnFail = True - -# ---- -# Setup Origin Server -# ---- -server = Test.MakeOriginServer("server") - -# For Test Case 1 & 5 - / -server.addResponse( - "sessionlog.json", { - "headers": "GET / HTTP/1.1\r\nHost: www.example.com\r\n\r\n", - "timestamp": "1469733493.993", - "body": "" - }, { - "headers": "HTTP/1.1 200 OK\r\nServer: microserver\r\nConnection: close\r\n\r\n", - "timestamp": "1469733493.993", - "body": "" - }) - -# For Test Case 2 - /bigfile -# Add info for the large H2 download test -server.addResponse( - "sessionlog.json", { - "headers": "GET /bigfile HTTP/1.1\r\nHost: www.example.com\r\n\r\n", - "timestamp": "1469733493.993", - "body": "" - }, { - "headers": - "HTTP/1.1 200 OK\r\nServer: microserver\r\nConnection: close\r\nCache-Control: max-age=3600\r\nContent-Length: 191414\r\n\r\n", - "timestamp": "1469733493.993", - "body": "" - }) - -# For Test Case 3 - /test2 -server.addResponse( - "sessionlog.json", { - "headers": "GET /test2 HTTP/1.1\r\nHost: www.example.com\r\n\r\n", - "timestamp": "1469733493.993", - "body": "" - }, { - "headers": "HTTP/1.1 200 OK\r\nServer: microserver\r\nTransfer-Encoding: chunked\r\nConnection: close\r\n\r\n", - "timestamp": "1469733493.993", - "body": "" - }) - -# For Test Case 6 - /postchunked -post_body = "12345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890" -server.addResponse( - "sessionlog.json", { - "headers": "POST /postchunked HTTP/1.1\r\nHost: www.example.com\r\n\r\n", - "timestamp": "1469733493.993", - "body": post_body - }, { - "headers": "HTTP/1.1 200 OK\r\nServer: microserver\r\nConnection: close\r\nContent-Length: 10\r\n\r\n", - "timestamp": "1469733493.993", - "body": "0123456789" - }) - -# For Test Case 7 - /bigpostchunked -# Make a post body that will be split across at least two frames -big_post_body = "0123456789" * 131070 -server.addResponse( - "sessionlog.json", { - "headers": "POST /bigpostchunked HTTP/1.1\r\nHost: www.example.com\r\n\r\n", - "timestamp": "1469733493.993", - "body": big_post_body - }, { - "headers": "HTTP/1.1 200 OK\r\nServer: microserver\r\nConnection: close\r\nContent-Length: 10\r\n\r\n", - "timestamp": "1469733493.993", - "body": "0123456789" - }) - -big_post_body_file = open(os.path.join(Test.RunDirectory, "big_post_body"), "w") -big_post_body_file.write(big_post_body) -big_post_body_file.close() - -# For Test Case 8 - /huge_resp_hdrs -server.addResponse( - "sessionlog.json", { - "headers": "GET /huge_resp_hdrs HTTP/1.1\r\nHost: www.example.com\r\n\r\n", - "timestamp": "1469733493.993", - "body": "" - }, { - "headers": "HTTP/1.1 200 OK\r\nServer: microserver\r\nConnection: close\r\nContent-Length: 6\r\n\r\n", - "timestamp": "1469733493.993", - "body": "200 OK" - }) - -# For Test Case 9 - /status/204 -server.addResponse( - "sessionlog.json", { - "headers": "GET /status/204 HTTP/1.1\r\nHost: www.example.com\r\n\r\n", - "timestamp": "1469733493.993", - "body": "" - }, { - "headers": "HTTP/1.1 204 No Content\r\nServer: microserver\r\nConnection: close\r\n\r\n", - "timestamp": "1469733493.993", - "body": "" - }) - -# ---- -# Setup ATS -# ---- -ts = Test.MakeATSProcess("ts", enable_tls=True, enable_cache=False) - -# add ssl materials like key, certificates for the server -ts.addDefaultSSLFiles() - -ts.Setup.CopyAs('rules/huge_resp_hdrs.conf', Test.RunDirectory) -ts.Disk.remap_config.AddLine( - 'map /huge_resp_hdrs http://127.0.0.1:{0}/huge_resp_hdrs @plugin=header_rewrite.so @pparam={1}/huge_resp_hdrs.conf '.format( - server.Variables.Port, Test.RunDirectory)) - -ts.Disk.remap_config.AddLine('map / http://127.0.0.1:{0}'.format(server.Variables.Port)) - -ts.Disk.ssl_multicert_yaml.AddLines( - """ -ssl_multicert: - - dest_ip: "*" - ssl_cert_name: server.pem - ssl_key_name: server.key -""".split("\n")) -ts.Disk.records_config.update( - { - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'http', - 'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir), - 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts.Variables.SSLDir), - 'proxy.config.http2.active_timeout_in': 3, - 'proxy.config.http2.max_concurrent_streams_in': 65535, - }) - -ts.Setup.CopyAs('h2client.py', Test.RunDirectory) -ts.Setup.CopyAs('h2active_timeout.py', Test.RunDirectory) -ts.Setup.CopyAs('clients/h2_extension_settings.py', Test.RunDirectory) - -settings_limit_ts = Test.MakeATSProcess("ts_settings_limit", enable_tls=True, enable_cache=False) -settings_limit_ts.addDefaultSSLFiles() -settings_limit_ts.Setup.CopyAs('clients/h2_max_settings_per_minute.py', Test.RunDirectory) -settings_limit_ts.Disk.records_config.update( - { - 'proxy.config.ssl.server.cert.path': f'{settings_limit_ts.Variables.SSLDir}', - 'proxy.config.ssl.server.private_key.path': f'{settings_limit_ts.Variables.SSLDir}', - 'proxy.config.http2.max_settings_per_frame': -1, - 'proxy.config.http2.max_settings_per_minute': 1, - 'proxy.config.http2.max_settings_frames_per_minute': 100, - }) -settings_limit_ts.Disk.ssl_multicert_yaml.AddLines( - """ -ssl_multicert: - - dest_ip: "*" - ssl_cert_name: server.pem - ssl_key_name: server.key -""".split("\n")) -settings_limit_ts.Disk.diags_log.Content = Testers.ContainsExpression( - "ERROR: HTTP/2 connection error.*recv settings too frequent setting changes", - "ATS should log the SETTINGS limit connection error.") - -# ---- -# Test Cases -# ---- - -# Test Case 1: basic H2 interaction -tr = Test.AddTestRun("basic H2 interaction") -tr.Processes.Default.Command = f'{sys.executable} h2client.py {ts.Variables.ssl_port} / --verify_default_body' -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.StartBefore(server) -tr.Processes.Default.StartBefore(Test.Processes.ts) -tr.Processes.Default.Streams.stdout = "gold/remap-200.gold" -tr.StillRunningAfter = server - -# Test Case 2: Make sure all the big file gets back. Regression test for issue 1646 -tr = Test.AddTestRun("big file download") -tr.Processes.Default.Command = f'{sys.executable} h2client.py {ts.Variables.ssl_port} /bigfile --repeat 2 --verify_default_body' -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Streams.stdout = "gold/bigfile.gold" -tr.StillRunningAfter = server - -# Test Case 3: Chunked content -tr = Test.AddTestRun("chunked content") -tr.Processes.Default.Command = f'{sys.executable} h2client.py {ts.Variables.ssl_port} /test2 --print_body' -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Streams.stdout = "gold/chunked.gold" -tr.StillRunningAfter = server - -# NOTE: Skipping this test run because traffic-replay doesn't currently support H2 -# Test Case 4: Multiple request -# client_path = os.path.join(Test.Variables.AtsTestToolsDir, 'traffic-replay/') -# tr = Test.AddTestRun("multiple request") -# tr.Processes.Default.Command = \ -# (f"{sys.executable} {client_path} -type h2 -log_dir {server.Variables.DataDir} " -# f"-port {ts.Variables.port} -host '127.0.0.1' -s_port {ts.Variables.ssl_port} -v -colorize False") -# tr.Processes.Default.ReturnCode = 0 -# tr.Processes.Default.Streams.stdout = "gold/replay.gold" -# tr.StillRunningAfter = server - -# Test Case 5: h2_active_timeout -tr = Test.AddTestRun("h2_active_timeout") -tr.Processes.Default.Command = f'{sys.executable} h2active_timeout.py {ts.Variables.ssl_port} / 4' -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Streams.All = "gold/active_timeout.gold" -tr.StillRunningAfter = server - -# Test Case 6: Post with chunked body -# While HTTP/2 does not support Transfer-encoding we pass that into curl to encourage it to not set the content length -# on the post body -tr = Test.AddTestRun("post with chunked body") -tr.MakeCurlCommand( - '-s -k -H "Transfer-Encoding: chunked" -d "{0}" https://127.0.0.1:{1}/postchunked'.format(post_body, ts.Variables.ssl_port), - ts=ts) -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Streams.All = "gold/post_chunked.gold" -tr.StillRunningAfter = server - -# Test Case 7: Post with big chunked body -# While HTTP/2 does not support Transfer-encoding we pass that into curl to encourage it to not set the content length -# on the post body -tr = Test.AddTestRun("post with big chunked body") -tr.MakeCurlCommand( - '-s -k -H "Transfer-Encoding: chunked" -d @big_post_body https://127.0.0.1:{0}/bigpostchunked'.format(ts.Variables.ssl_port), - ts=ts) -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Streams.All = "gold/post_chunked.gold" -tr.StillRunningAfter = server - -# Test Case 8: Huge response header -tr = Test.AddTestRun("huge response header") -# Different versions of curl have "bytes data" at various places in the output. -# Normalize them by simply filtering out those lines since they are not -# important to this test. -tr.MakeCurlCommand(f'-vs -k --http2 https://127.0.0.1:{ts.Variables.ssl_port}/huge_resp_hdrs |& grep -v "bytes data"', ts=ts) -tr.Processes.Default.ReturnCode = 0 -# Different versions of curl will have different cases for HTTP/2 field names. -tr.Processes.Default.Streams.stdout = Testers.GoldFile("gold/http2_8_stdout.gold", case_insensitive=True) -tr.StillRunningAfter = server - -# Test Case 9: Header Only Response - e.g. 204 -tr = Test.AddTestRun("header only response") -tr.MakeCurlCommand('-vs -k --http2 https://127.0.0.1:{0}/status/204'.format(ts.Variables.ssl_port), ts=ts) -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Streams.stdout = "gold/http2_9_stdout.gold" -# Different versions of curl will have different cases for HTTP/2 field names. -tr.Processes.Default.Streams.stderr = Testers.GoldFile("gold/http2_9_stderr.gold", case_insensitive=True) -tr.StillRunningAfter = server - -# Test Case 10: max_settings_per_minute with max_settings_per_frame disabled -tr = Test.AddTestRun("max_settings_per_minute with max_settings_per_frame disabled") -tr.Processes.Default.Command = f'{sys.executable} h2_max_settings_per_minute.py {settings_limit_ts.Variables.ssl_port}' -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.StartBefore(settings_limit_ts) -tr.Processes.Default.Streams.stdout += Testers.ContainsExpression( - "Received GOAWAY with error code 11", "Received ENHANCE_YOUR_CALM GOAWAY.") - -# Test Case 11: Extension settings fit within the default SETTINGS limits. -tr = Test.AddTestRun("HTTP/2 extension settings") -tr.Processes.Default.Command = f'{sys.executable} h2_extension_settings.py {ts.Variables.ssl_port}' -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Streams.stdout = Testers.ContainsExpression( - "Received 200 response", "The request following the extension settings should succeed.") -tr.StillRunningAfter = server diff --git a/tests/gold_tests/h2/http2_close_connection.test.py b/tests/gold_tests/h2/http2_close_connection.test.py deleted file mode 100644 index 5a16bd4cd0a..00000000000 --- a/tests/gold_tests/h2/http2_close_connection.test.py +++ /dev/null @@ -1,70 +0,0 @@ -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import os - -Test.Summary = ''' -Check whether the HTTP/2 grace shutdown works as intended. -''' - -Test.SkipUnless(Condition.HasProxyVerifierVersion('2.8.0')) - -pv_server = Test.MakeVerifierServerProcess("pv_server", "http2_close_connection.yaml") - -ts = Test.MakeATSProcess('ts', select_ports=True, enable_tls=True) - -ts.addDefaultSSLFiles() -ts.Disk.ssl_multicert_yaml.AddLines( - """ -ssl_multicert: - - dest_ip: "*" - ssl_cert_name: server.pem - ssl_key_name: server.key -""".split("\n")) -ts.Disk.records_config.update( - { - "proxy.config.http.server_ports": f"{ts.Variables.port} {ts.Variables.ssl_port}:ssl", - 'proxy.config.ssl.server.cert.path': f'{ts.Variables.SSLDir}', - 'proxy.config.ssl.server.private_key.path': f'{ts.Variables.SSLDir}', - 'proxy.config.ssl.client.verify.server.policy': 'PERMISSIVE', - 'proxy.config.diags.debug.enabled': 3, - 'proxy.config.diags.debug.tags': 'http', - 'proxy.config.exec_thread.autoconfig.enabled': 0, - 'proxy.config.exec_thread.limit': 4, - 'proxy.config.ssl.client.alpn_protocols': 'h2,http/1.1', - 'proxy.config.http.server_session_sharing.pool': 'thread', - 'proxy.config.http.server_session_sharing.match': 'ip,sni,cert', - }) - -ts.Disk.remap_config.AddLines([f'map / https://127.0.0.1:{pv_server.Variables.https_port}']) - -Test.PrepareTestPlugin(os.path.join(Test.Variables.AtsTestPluginsDir, 'http2_close_connection.so'), ts) - -tr = Test.AddTestRun() -tr.Processes.Default.StartBefore(pv_server) -tr.Processes.Default.StartBefore(ts) -tr.AddVerifierClientProcess( - "pv_client", "http2_close_connection.yaml", http_ports=[ts.Variables.port], https_ports=[ts.Variables.ssl_port]) -tr.Processes.Default.ReturnCode = 0 - -tr.Processes.Default.Streams.All += Testers.ContainsExpression( - 'Equals Success: Key: "1", Content Data: "body", Value: "server_test_1"', 'Response check') -tr.Processes.Default.Streams.All += Testers.ContainsExpression( - 'Received GOAWAY frame with last stream id 2147483647, error code 0', 'initial GOAWAY frame with last stream id set to max') -tr.Processes.Default.Streams.All += Testers.ContainsExpression( - 'Received GOAWAY frame with last stream id 1, error code 0', 'updated GOAWAY frame with last stream id set to 1') - -pv_server.Streams.All += Testers.ExcludesExpression('server_test_2', 'Only one response should be sent') diff --git a/tests/gold_tests/h2/http2_concurrent_streams.test.py b/tests/gold_tests/h2/http2_concurrent_streams.test.py deleted file mode 100644 index 79e6b9d088d..00000000000 --- a/tests/gold_tests/h2/http2_concurrent_streams.test.py +++ /dev/null @@ -1,64 +0,0 @@ -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import sys - -Test.Sumary = ''' -Verify Concurrent Streams Handling -''' - - -class Http2ConcurrentStreamsTest: - replayFile = "replay/http2_concurrent_streams.replay.yaml" - - def __init__(self): - self.__setupOriginServer() - self.__setupTS() - - def __setupOriginServer(self): - self._server = Test.MakeVerifierServerProcess("verifier-server", self.replayFile) - - def __setupTS(self): - self._ts = Test.MakeATSProcess(f"ts", enable_tls=True, enable_cache=True) - self._ts.addDefaultSSLFiles() - self._ts.Disk.records_config.update( - { - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'http2', - 'proxy.config.ssl.server.cert.path': f"{self._ts.Variables.SSLDir}", - 'proxy.config.ssl.server.private_key.path': f"{self._ts.Variables.SSLDir}", - 'proxy.config.http.insert_response_via_str': 2, - }) - self._ts.Disk.remap_config.AddLine(f"map / http://127.0.0.1:{self._server.Variables.http_port}") - self._ts.Disk.ssl_multicert_yaml.AddLines( - """ -ssl_multicert: - - dest_ip: "*" - ssl_cert_name: server.pem - ssl_key_name: server.key -""".split("\n")) - - def run(self): - tr = Test.AddTestRun() - tr.AddVerifierClientProcess( - "verifier-client", self.replayFile, http_ports=[self._ts.Variables.port], https_ports=[self._ts.Variables.ssl_port]) - tr.Processes.Default.StartBefore(self._ts) - tr.Processes.Default.StartBefore(self._server) - tr.StillRunningAfter = self._ts - tr.StillRunningAfter = self._server - - -Http2ConcurrentStreamsTest().run() diff --git a/tests/gold_tests/h2/http2_crlf_header_validation.test.py b/tests/gold_tests/h2/http2_crlf_header_validation.test.py deleted file mode 100644 index a37e0d6e4cc..00000000000 --- a/tests/gold_tests/h2/http2_crlf_header_validation.test.py +++ /dev/null @@ -1,97 +0,0 @@ -''' -Verify HTTP/2 requests with NUL, CR, or LF in header values are rejected. -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import sys - -Test.Summary = 'HTTP/2 requests with NUL, CR, or LF in header values are rejected' -Test.ContinueOnFail = True - -MALFORMED_CLIENT = 'malformed_h2_request_client.py' - -CONTROL_CHARACTER_CASES = ( - { - 'scenario': 'crlf-in-header-value', - 'description': 'HTTP/2 request with CRLF in header value', - }, - { - 'scenario': 'cr-in-header-value', - 'description': 'HTTP/2 request with bare CR in header value', - }, - { - 'scenario': 'lf-in-header-value', - 'description': 'HTTP/2 request with bare LF in header value', - }, - { - 'scenario': 'nul-in-header-value', - 'description': 'HTTP/2 request with NUL in header value', - }, -) - -server = Test.MakeOriginServer('server') -server.Streams.All = Testers.ExcludesExpression( - 'x-injected', - 'Malformed control-character requests must not reach the origin server.', -) -server.Streams.All += Testers.ExcludesExpression( - 'malformed-nul-value', - 'Malformed NUL request must not reach the origin server.', -) -server.addResponse( - 'sessionlog.json', { - 'headers': 'GET / HTTP/1.1\r\nHost: www.example.com\r\n\r\n', - 'timestamp': '1469733493.993', - 'body': '', - }, { - 'headers': 'HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n', - 'timestamp': '1469733493.993', - 'body': '', - }) - -ts = Test.MakeATSProcess('ts', enable_tls=True, enable_cache=False) -ts.addDefaultSSLFiles() -ts.Disk.ssl_multicert_yaml.AddLines( - """ -ssl_multicert: - - dest_ip: "*" - ssl_cert_name: server.pem - ssl_key_name: server.key -""".split('\n')) -ts.Disk.records_config.update( - { - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'http', - 'proxy.config.ssl.server.cert.path': ts.Variables.SSLDir, - 'proxy.config.ssl.server.private_key.path': ts.Variables.SSLDir, - }) -ts.Disk.remap_config.AddLine(f'map / http://127.0.0.1:{server.Variables.Port}/') - -Test.Setup.CopyAs('../connect/' + MALFORMED_CLIENT, Test.RunDirectory) - -for i, case in enumerate(CONTROL_CHARACTER_CASES): - tr = Test.AddTestRun(case['description']) - tr.Processes.Default.Command = (f'{sys.executable} {MALFORMED_CLIENT} {ts.Variables.ssl_port} {case["scenario"]}') - tr.Processes.Default.ReturnCode = 0 - if i == 0: - tr.Processes.Default.StartBefore(server) - tr.Processes.Default.StartBefore(ts) - tr.StillRunningAfter = ts - tr.Processes.Default.Streams.stdout += Testers.ContainsExpression( - r'Received (RST_STREAM|GOAWAY|HTTP/2 response with status 4\d\d)', - f'ATS should reject the request: {case["description"]}', - ) diff --git a/tests/gold_tests/h2/http2_empty_data_frame.test.py b/tests/gold_tests/h2/http2_empty_data_frame.test.py deleted file mode 100644 index ae2caf7cadc..00000000000 --- a/tests/gold_tests/h2/http2_empty_data_frame.test.py +++ /dev/null @@ -1,77 +0,0 @@ -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import sys - -Test.Sumary = ''' -Verify Empty DATA Frame Handling -''' - - -class Http2EmptyDataFrameTest: - - def __init__(self): - self.__setupOriginServer() - self.__setupTS() - self.__setupClient() - - def __setupOriginServer(self): - self._server = Test.MakeHttpBinServer("httpbin") - - def __setupTS(self): - self._ts = Test.MakeATSProcess(f"ts", enable_tls=True, enable_cache=True) - self._ts.addDefaultSSLFiles() - self._ts.Disk.records_config.update( - { - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'http2', - 'proxy.config.ssl.server.cert.path': f"{self._ts.Variables.SSLDir}", - 'proxy.config.ssl.server.private_key.path': f"{self._ts.Variables.SSLDir}", - 'proxy.config.http.insert_response_via_str': 2, - 'proxy.config.http2.active_timeout_in': 3, - 'proxy.config.http2.stream_error_rate_threshold': 0.1 # default - }) - self._ts.Disk.remap_config.AddLine(f"map / http://127.0.0.1:{self._server.Variables.Port}") - self._ts.Disk.ssl_multicert_yaml.AddLines( - """ -ssl_multicert: - - dest_ip: "*" - ssl_cert_name: server.pem - ssl_key_name: server.key -""".split("\n")) - - def __setupClient(self): - self._ts.Setup.CopyAs("clients/h2empty_data_frame.py", Test.RunDirectory) - - def run(self): - tr = Test.AddTestRun("warm-up cache") - - tr.Processes.Default.StartBefore(self._ts) - tr.Processes.Default.StartBefore(self._server) - - # warm up the cache - tr.Processes.Default.Command = f"{sys.executable} h2empty_data_frame.py {self._ts.Variables.ssl_port} /cache/10 -n 1" - tr.Processes.Default.ReturnCode = 0 - - # verify 20 streams doesn't hit `proxy.config.http2.stream_error_rate_threshold` - tr = Test.AddTestRun("open 20 streams") - tr.Processes.Default.Command = f"{sys.executable} h2empty_data_frame.py {self._ts.Variables.ssl_port} /cache/10 -n 20" - tr.Processes.Default.ReturnCode = 0 - - tr.StillRunningAfter = self._ts - - -Http2EmptyDataFrameTest().run() diff --git a/tests/gold_tests/h2/http2_flow_control.test.py b/tests/gold_tests/h2/http2_flow_control.test.py deleted file mode 100644 index 8b676b1c221..00000000000 --- a/tests/gold_tests/h2/http2_flow_control.test.py +++ /dev/null @@ -1,429 +0,0 @@ -"""Verify HTTP/2 flow control behavior.""" - -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import re -import sys -from enum import Enum -from typing import List, Optional - -Test.Summary = __doc__ - - -class Http2FlowControlTest: - """Define an object to test HTTP/2 flow control behavior.""" - - _replay_file: str = 'http2_flow_control.replay.yaml' - _replay_chunked_file: str = 'http2_flow_control_chunked.replay.yaml' - _valid_policy_values: List[int] = list(range(0, 3)) - _flow_control_policy: Optional[int] = None - _flow_control_policy_is_malformed: bool = False - - _default_initial_window_size: int = 65535 - _default_max_concurrent_streams: int = 100 - _default_flow_control_policy: int = 0 - - _dns_counter: int = 0 - _server_counter: int = 0 - _ts_counter: int = 0 - _client_counter: int = 0 - - IS_OUTBOUND = True - IS_INBOUND = False - - IS_HTTP2_TO_ORIGIN = True - IS_HTTP1_TO_ORIGIN = False - - class ServerType(Enum): - """Define the type of server to use in a TestRun.""" - - HTTP1_CONTENT_LENGTH = 0 - HTTP1_CHUNKED = 1 - HTTP2 = 2 - - def __init__( - self, - description: str, - initial_window_size: Optional[int] = None, - max_concurrent_streams: Optional[int] = None, - flow_control_policy: Optional[int] = None): - """Declare the various test Processes. - - :param description: A description of the test. - - :param initial_window_size: The value with which to configure the - proxy.config.http2.initial_window_size_(in|out) ATS parameter in the - records.yaml file. If the paramenter is None, then no window size - will be explicitly set and ATS will use the default value. - - :param max_concurrent_streams: The value with which to configure the - proxy.config.http2.max_concurrent_streams_(in|out) ATS parameter in the - records.yaml file. If the paramenter is None, then no window size - will be explicitly set and ATS will use the default value. - - :param flow_control_policy: The value with which to configure the - proxy.config.http2.flow_control.policy_(in|out) ATS parameter the - records.yaml file. If the paramenter is None, then no policy - configuration will be explicitly set and ATS will use the default - value. - """ - self._description = description - - self._initial_window_size = initial_window_size - self._expected_initial_stream_window_size = ( - initial_window_size if initial_window_size is not None else self._default_initial_window_size) - - self._max_concurrent_streams = max_concurrent_streams - self._expected_max_concurrent_streams = ( - max_concurrent_streams if max_concurrent_streams is not None else self._default_max_concurrent_streams) - - self._flow_control_policy = flow_control_policy - self._expected_flow_control_policy = ( - flow_control_policy if flow_control_policy is not None else self._default_flow_control_policy) - - self._flow_control_policy_is_malformed = ( - self._flow_control_policy is not None and self._flow_control_policy not in self._valid_policy_values) - - def _configure_dns(self, tr: 'TestRun') -> 'Process': - """Configure the DNS.""" - dns = tr.MakeDNServer(f'dns-{Http2FlowControlTest._dns_counter}') - Http2FlowControlTest._dns_counter += 1 - return dns - - def _configure_server(self, tr: 'TestRun', server_type: ServerType) -> Optional['Process']: - """Configure the test server.""" - if self._flow_control_policy_is_malformed: - return None - if server_type == self.ServerType.HTTP1_CHUNKED: - replay_file = self._replay_chunked_file - else: - replay_file = self._replay_file - - server = tr.AddVerifierServerProcess(f'server-{Http2FlowControlTest._server_counter}', replay_file) - Http2FlowControlTest._server_counter += 1 - return server - - def _configure_trafficserver(self, tr: 'TestRun', is_outbound: bool, server_type: ServerType) -> 'Process': - """Configure a Traffic Server process.""" - ts = tr.MakeATSProcess(f'ts-{Http2FlowControlTest._ts_counter}', enable_tls=True) - Http2FlowControlTest._ts_counter += 1 - - ts.addDefaultSSLFiles() - ts.Disk.records_config.update( - { - 'proxy.config.ssl.server.cert.path': f'{ts.Variables.SSLDir}', - 'proxy.config.ssl.server.private_key.path': f'{ts.Variables.SSLDir}', - 'proxy.config.ssl.client.verify.server.policy': 'PERMISSIVE', - 'proxy.config.dns.nameservers': '127.0.0.1:{0}'.format(self._dns.Variables.Port), - 'proxy.config.dns.resolv_conf': 'NULL', - 'proxy.config.http.insert_age_in_response': 0, - 'proxy.config.diags.debug.enabled': 3, - 'proxy.config.diags.debug.tags': 'http', - }) - - if server_type == self.ServerType.HTTP2: - ts.Disk.records_config.update({ - 'proxy.config.ssl.client.alpn_protocols': 'h2,http/1.1', - }) - - if self._initial_window_size is not None: - if is_outbound: - configuration = 'proxy.config.http2.initial_window_size_out' - else: - configuration = 'proxy.config.http2.initial_window_size_in' - ts.Disk.records_config.update({ - configuration: self._initial_window_size, - }) - - if self._flow_control_policy is not None: - if is_outbound: - configuration = 'proxy.config.http2.flow_control.policy_out' - else: - configuration = 'proxy.config.http2.flow_control.policy_in' - ts.Disk.records_config.update({ - configuration: self._flow_control_policy, - }) - - if self._max_concurrent_streams is not None: - if is_outbound: - configuration = 'proxy.config.http2.max_concurrent_streams_out' - else: - configuration = 'proxy.config.http2.max_concurrent_streams_in' - ts.Disk.records_config.update({ - configuration: self._max_concurrent_streams, - }) - - ts.Disk.ssl_multicert_yaml.AddLines( - """ -ssl_multicert: - - dest_ip: "*" - ssl_cert_name: server.pem - ssl_key_name: server.key -""".split("\n")) - - if self._server is not None: - ts.Disk.remap_config.AddLine(f'map / https://127.0.0.1:{self._server.Variables.https_port}') - - if self._flow_control_policy_is_malformed: - if is_outbound: - configuration = 'proxy.config.http2.flow_control.policy_out' - else: - configuration = 'proxy.config.http2.flow_control.policy_in' - ts.Disk.diags_log.Content = Testers.ContainsExpression( - f"ERROR.*{configuration}", "Expected an error about an invalid flow control policy.") - - return ts - - def _configure_client( - self, - tr, - ): - """Configure a client process. - - :param tr: The TestRun to associate the client with. - """ - tr.AddVerifierClientProcess( - f'client-{Http2FlowControlTest._client_counter}', self._replay_file, https_ports=[self._ts.Variables.ssl_port]) - Http2FlowControlTest._client_counter += 1 - - def _configure_log_expectations(self, host): - """Configure the log expectations for the client or server.""" - hostname = "server" if host == self._server else "client" - if self._flow_control_policy_is_malformed: - # Since we're just testing ATS configuration errors, there's no - # need to set up client expectations. - return - - # ATS currently always sends a MAX_CONCURRENT_STREAMS setting. - host.Streams.stdout += Testers.ContainsExpression( - f'MAX_CONCURRENT_STREAMS:{self._expected_max_concurrent_streams}', - f"{hostname} should receive a MAX_CONCURRENT_STREAMS setting.") - - if self._initial_window_size is not None: - host.Streams.stdout += Testers.ContainsExpression( - f'INITIAL_WINDOW_SIZE:{self._expected_initial_stream_window_size}', - f"{hostname} should receive an INITIAL_WINDOW_SIZE setting.") - - if self._expected_flow_control_policy == 0: - update_window_size = (self._expected_initial_stream_window_size - self._default_initial_window_size) - if update_window_size > 0: - host.Streams.stdout += Testers.ContainsExpression( - f'WINDOW_UPDATE.*id 0: {update_window_size}', f"{hostname} should receive a session WINDOW_UPDATE.") - - if self._expected_flow_control_policy in (1, 2): - # Verify the larger window size. - - session_window_size = (self._expected_initial_stream_window_size * self._expected_max_concurrent_streams) - - # ATS will send a WINDOW_UPDATE frame to the client to increase - # the session window size to the configured value from the default - # value. - update_window_size = (session_window_size - self._expected_initial_stream_window_size) - - # A WINDOW_UPDATE can only increase the window size. So make sure that - # the new window size is greater than the default window size. - if update_window_size > Http2FlowControlTest._default_initial_window_size: - host.Streams.stdout += Testers.ContainsExpression( - f'WINDOW_UPDATE.*id 0: {update_window_size}', f"{hostname} should receive an initial session WINDOW_UPDATE.") - else: - # Our test traffic is large enough that eventually we should - # send a session WINDOW_UPDATE frame for the smaller window. - # It's not clear what it will be in advance though. A 100 byte - # session window may not receive a 100 byte WINDOW_UPDATE frame - # if the client is sending DATA frames in 10 byte chunks due to - # a smaller stream window. - host.Streams.stdout += Testers.ContainsExpression( - 'WINDOW_UPDATE.*id 0: ', f"{hostname} should receive a session WINDOW_UPDATE.") - - if self._expected_flow_control_policy == 2: - # Verify the streams window sizes get updated. - stream_window_1 = session_window_size - stream_window_2 = int(session_window_size / 2) - stream_window_3 = int(session_window_size / 3) - if self._server: - # Toward the server, there is a potential race condition - # between sending of first-request and the sending of the - # SETTINGS frame which reduces the stream window size. - # Allow for either scenario. - host.Streams.stdout += Testers.ContainsExpression( - (f'INITIAL_WINDOW_SIZE:{stream_window_1}.*' - f'INITIAL_WINDOW_SIZE:{stream_window_2}.*'), - f"{hostname} should stream receive window updates", - reflags=re.DOTALL | re.MULTILINE) - else: - host.Streams.stdout += Testers.ContainsExpression( - ( - f'INITIAL_WINDOW_SIZE:{stream_window_1}.*' - f'INITIAL_WINDOW_SIZE:{stream_window_2}.*' - f'INITIAL_WINDOW_SIZE:{stream_window_3}'), - f"{hostname} should stream receive window updates", - reflags=re.DOTALL | re.MULTILINE) - - if self._expected_initial_stream_window_size < 1000: - first_id = 5 if self._server else 3 - - if self._server and self._expected_flow_control_policy == 2: - # Toward the server, there is a potential race condition - # between sending of first-request and the sending of the - # SETTINGS frame which reduces the stream window size. Allow - # for either scenario. - window_update_size = f'33|{self._expected_initial_stream_window_size}' - else: - window_update_size = f'{self._expected_initial_stream_window_size}' - # For the smaller session window sizes, we expect WINDOW_UPDATE frames. - host.Streams.stdout += Testers.ContainsExpression( - f'WINDOW_UPDATE.*id {first_id}: {window_update_size}', f"{hostname} should receive a stream WINDOW_UPDATE.") - - host.Streams.stdout += Testers.ContainsExpression( - f'WINDOW_UPDATE.*id {first_id + 2}: {window_update_size}', f"{hostname} should receive a stream WINDOW_UPDATE.") - - host.Streams.stdout += Testers.ContainsExpression( - f'WINDOW_UPDATE.*id {first_id + 4}: {window_update_size}', f"{hostname} should receive a stream WINDOW_UPDATE.") - - def _configure_test_run_common(self, tr, is_outbound: bool, server_type: ServerType) -> None: - """Perform the common Process configuration.""" - self._dns = self._configure_dns(tr) - self._server = self._configure_server(tr, server_type) - self._ts = self._configure_trafficserver(tr, is_outbound, server_type) - if not self._flow_control_policy_is_malformed: - self._configure_client(tr) - tr.Processes.Default.StartBefore(self._dns) - tr.Processes.Default.StartBefore(self._server) - else: - tr.Processes.Default.Command = "true" - tr.Processes.Default.StartBefore(self._ts) - tr.TimeOut = 20 - - def _configure_inbound_http1_to_origin_test_run(self) -> None: - """Configure the TestRun for inbound stream configuration.""" - tr = Test.AddTestRun(f'{self._description} - inbound, ' - 'HTTP/1 Content-Length origin') - self._configure_test_run_common(tr, self.IS_INBOUND, self.ServerType.HTTP1_CONTENT_LENGTH) - self._configure_log_expectations(tr.Processes.Default) - - tr = Test.AddTestRun(f'{self._description} - inbound, ' - 'HTTP/1 chunked origin') - self._configure_test_run_common(tr, self.IS_INBOUND, self.ServerType.HTTP1_CHUNKED) - self._configure_log_expectations(tr.Processes.Default) - - def _configure_inbound_http2_to_origin_test_run(self) -> None: - """Configure the TestRun for inbound stream configuration.""" - tr = Test.AddTestRun(f'{self._description} - inbound, HTTP/2 origin') - self._configure_test_run_common(tr, self.IS_INBOUND, self.ServerType.HTTP2) - self._configure_log_expectations(tr.Processes.Default) - - def _configure_outbound_test_run(self) -> None: - """Configure the TestRun outbound stream configuration.""" - tr = Test.AddTestRun(f'{self._description} - outbound, HTTP/2 origin') - self._configure_test_run_common(tr, self.IS_OUTBOUND, self.ServerType.HTTP2) - self._configure_log_expectations(self._server) - - def run(self) -> None: - """Configure the test run for various origin side configurations.""" - self._configure_inbound_http1_to_origin_test_run() - self._configure_inbound_http2_to_origin_test_run() - self._configure_outbound_test_run() - - -class Http2DynamicWindowSettingsCapTest: - """Verify dynamic stream windows cap unacknowledged SETTINGS frames.""" - - _replay_file: str = 'replay/http2_settings_ack_stall.replay.yaml' - - def run(self) -> None: - """Configure the test run.""" - tr = Test.AddTestRun('Dynamic stream windows cap unacknowledged SETTINGS') - server = tr.AddVerifierServerProcess('server-settings-cap', self._replay_file) - ts = tr.MakeATSProcess('ts-settings-cap', enable_tls=True, enable_cache=False) - - ts.addDefaultSSLFiles() - ts.Setup.CopyAs('clients/h2_settings_ack_stall.py', Test.RunDirectory) - ts.Disk.records_config.update( - { - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'http2', - 'proxy.config.ssl.server.cert.path': f'{ts.Variables.SSLDir}', - 'proxy.config.ssl.server.private_key.path': f'{ts.Variables.SSLDir}', - 'proxy.config.http.insert_response_via_str': 2, - 'proxy.config.http2.active_timeout_in': 5, - 'proxy.config.http2.flow_control.policy_in': 2, - 'proxy.config.http2.max_concurrent_streams_in': 2, - }) - ts.Disk.remap_config.AddLine(f'map / http://127.0.0.1:{server.Variables.http_port}') - ts.Disk.ssl_multicert_yaml.AddLines( - """ -ssl_multicert: - - dest_ip: "*" - ssl_cert_name: server.pem - ssl_key_name: server.key -""".split("\n")) - - tr.Processes.Default.StartBefore(server) - tr.Processes.Default.StartBefore(ts) - tr.Processes.Default.Command = f'{sys.executable} h2_settings_ack_stall.py {ts.Variables.ssl_port}' - tr.Processes.Default.ReturnCode = 0 - tr.Processes.Default.Streams.stdout += Testers.ContainsExpression( - 'GOAWAY error_code=4', 'ATS should close the connection with SETTINGS_TIMEOUT.') - ts.Disk.diags_log.Content = Testers.ContainsExpression( - 'ERROR: HTTP/2 connection error code=0x04.*send settings too many outstanding SETTINGS frames', - 'ATS should log the expected SETTINGS_TIMEOUT connection error.') - - -# -# Default configuration. -# -test = Http2FlowControlTest("Default Configurations") -test.run() - -# -# Configuring max_concurrent_streams_(in|out). -# -test = Http2FlowControlTest(description="Configure max_concurrent_streams", max_concurrent_streams=53) -test.run() - -# -# Configuring initial_window_size. -# -test = Http2FlowControlTest(description="Configure a larger initial_window_size_(in|out)", initial_window_size=100123) -test.run() - -# -# Configuring flow_control_policy. -# -test = Http2FlowControlTest(description="Configure an unrecognized flow_control.in.policy", flow_control_policy=23) -test.run() - -test = Http2FlowControlTest( - description="Flow control policy 0 (default): small initial_window_size", - initial_window_size=500, # The default is 65 KB. - flow_control_policy=0) -test.run() -test = Http2FlowControlTest( - description="Flow control policy 1: 100 byte session, 10 byte streams", - max_concurrent_streams=10, - initial_window_size=10, - flow_control_policy=1) -test.run() -test = Http2FlowControlTest( - description="Flow control policy 2: 100 byte session, dynamic streams", - max_concurrent_streams=10, - initial_window_size=10, - flow_control_policy=2) -test.run() - -test = Http2DynamicWindowSettingsCapTest() -test.run() diff --git a/tests/gold_tests/h2/http2_max_active_streams.test.py b/tests/gold_tests/h2/http2_max_active_streams.test.py deleted file mode 100644 index 42fb1d627f8..00000000000 --- a/tests/gold_tests/h2/http2_max_active_streams.test.py +++ /dev/null @@ -1,86 +0,0 @@ -''' -Verify proxy.config.http2.max_active_streams_policy_in enforces the global -active-streams cap and that HPACK stays in sync across refused streams. -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import sys - -Test.Summary = ''' -HTTP/2 max_active_streams_policy_in enforcement and HPACK sync test. -''' - -CLIENT_SCRIPT = 'h2_max_active_streams.py' - - -class Http2MaxActiveStreamsTest: - """Drive concurrent inbound streams past max_active_streams_in.""" - - def __init__(self, name: str, replay_file: str, policy: int): - self._name = name - self._replay_file = replay_file - self._policy = policy - - def run(self) -> None: - tr = Test.AddTestRun(self._name) - server = tr.AddVerifierServerProcess(f'server-{self._name}', self._replay_file) - ts = tr.MakeATSProcess(f'ts-{self._name}', enable_tls=True, enable_cache=False) - - ts.addDefaultSSLFiles() - ts.Setup.CopyAs(f'clients/{CLIENT_SCRIPT}', Test.RunDirectory) - ts.Disk.records_config.update( - { - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'http2', - 'proxy.config.ssl.server.cert.path': f'{ts.Variables.SSLDir}', - 'proxy.config.ssl.server.private_key.path': f'{ts.Variables.SSLDir}', - 'proxy.config.http2.max_active_streams_in': 2, - 'proxy.config.http2.max_active_streams_policy_in': self._policy, - 'proxy.config.http2.max_concurrent_streams_in': 100, - }) - ts.Disk.remap_config.AddLine(f'map / http://127.0.0.1:{server.Variables.http_port}') - ts.Disk.ssl_multicert_yaml.AddLines( - """ -ssl_multicert: - - dest_ip: "*" - ssl_cert_name: server.pem - ssl_key_name: server.key -""".split('\n')) - - tr.Processes.Default.StartBefore(server) - tr.Processes.Default.StartBefore(ts) - tr.Processes.Default.Command = (f'{sys.executable} {CLIENT_SCRIPT} {ts.Variables.ssl_port} --streams 4 --probe-from 5') - tr.Processes.Default.ReturnCode = 0 - tr.Processes.Default.Streams.stdout += Testers.ExcludesExpression( - 'GOAWAY', 'ATS must not tear down the connection; HPACK dynamic table must stay in sync.') - - if self._policy == 1: - tr.Processes.Default.Streams.stdout += Testers.ContainsExpression( - r'stream 5: RST_STREAM error_code=7', 'stream 5 must be refused with REFUSED_STREAM under enforce policy.') - tr.Processes.Default.Streams.stdout += Testers.ContainsExpression( - r'stream 7: RST_STREAM error_code=7', - 'stream 7 must also be refused with REFUSED_STREAM, proving HPACK decode happened on stream 5.') - ts.Disk.traffic_out.Content += Testers.ContainsExpression( - r'HTTP/2 stream error code=0x07.*active streams cap reached', - 'ATS should log the cap-reached stream error under enforce policy.') - else: - tr.Processes.Default.Streams.stdout += Testers.ExcludesExpression( - r'RST_STREAM error_code=7', 'No stream should be refused under advisory policy.') - - -Http2MaxActiveStreamsTest('enforce', 'replay/http2_max_active_streams_enforce.replay.yaml', policy=1).run() -Http2MaxActiveStreamsTest('advisory', 'replay/http2_max_active_streams_advisory.replay.yaml', policy=0).run() diff --git a/tests/gold_tests/h2/http2_priority.test.py b/tests/gold_tests/h2/http2_priority.test.py deleted file mode 100644 index 8b8168ca550..00000000000 --- a/tests/gold_tests/h2/http2_priority.test.py +++ /dev/null @@ -1,86 +0,0 @@ -''' -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -Test.Summary = ''' -Test a basic remap of a http connection with Stream Priority Feature -''' - -Test.SkipUnless( - Condition.HasCurlFeature('http2'), - Condition.HasProgram("cksum", "cksum need to be installed on system for this test to work"), -) -Test.ContinueOnFail = True - -# ---- -# Setup Origin Server -# ---- -server = Test.MakeOriginServer("server") - -# Test Case 0: -server.addResponse( - "sessionlog.json", { - "headers": "GET /bigfile HTTP/1.1\r\nHost: www.example.com\r\n\r\n", - "timestamp": "1469733493.993", - "body": "" - }, { - "headers": - "HTTP/1.1 200 OK\r\nServer: microserver\r\nConnection: close\r\nCache-Control: max-age=3600\r\nContent-Length: 1048576\r\n\r\n", - "timestamp": "1469733493.993", - "body": "" - }) - -# ---- -# Setup ATS -# ---- -ts = Test.MakeATSProcess("ts", enable_tls=True, enable_cache=False) - -ts.addDefaultSSLFiles() - -ts.Disk.remap_config.AddLine('map / http://127.0.0.1:{0}'.format(server.Variables.Port)) -ts.Disk.ssl_multicert_yaml.AddLines( - """ -ssl_multicert: - - dest_ip: "*" - ssl_cert_name: server.pem - ssl_key_name: server.key -""".split("\n")) -ts.Disk.records_config.update( - { - 'proxy.config.http2.stream_priority_enabled': 1, - 'proxy.config.http2.no_activity_timeout_in': 3, - 'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir), - 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts.Variables.SSLDir), - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'http2', - }) - -# ---- -# Test Cases -# ---- - -# Test Case 0: -tr = Test.AddTestRun() -tr.MakeCurlCommand('-vs -k --http2 https://127.0.0.1:{0}/bigfile | cksum'.format(ts.Variables.ssl_port), ts=ts) -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.TimeOut = 5 -tr.Processes.Default.StartBefore(server, ready=When.PortOpen(server.Variables.Port)) -tr.Processes.Default.StartBefore(Test.Processes.ts) -tr.Processes.Default.Streams.stdout = "gold/priority_0_stdout.gold" -# Different versions of curl will have different cases for HTTP/2 field names. -tr.Processes.Default.Streams.stderr = Testers.GoldFile("gold/priority_0_stderr.gold", case_insensitive=True) -tr.StillRunningAfter = server diff --git a/tests/gold_tests/h2/http2_rst_stream.test.py b/tests/gold_tests/h2/http2_rst_stream.test.py deleted file mode 100644 index 339e0dec60b..00000000000 --- a/tests/gold_tests/h2/http2_rst_stream.test.py +++ /dev/null @@ -1,170 +0,0 @@ -''' -Abort HTTP/2 connection using RST_STREAM frame. -''' - -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -Test.Summary = ''' -Abort HTTP/2 connection using RST_STREAM frame. -''' - -Test.SkipUnless(Condition.HasOpenSSLVersion('1.1.1'), Condition.HasProxyVerifierVersion('2.8.0')) - -# -# Client sends RST_STREAM after DATA frame -# -ts = Test.MakeATSProcess("ts0", enable_tls=True) -replay_file = "replay_rst_stream/http2_rst_stream_client_after_data.yaml" -server = Test.MakeVerifierServerProcess("server0", replay_file) -ts.addDefaultSSLFiles() -ts.Disk.records_config.update( - { - 'proxy.config.ssl.server.cert.path': f'{ts.Variables.SSLDir}', - 'proxy.config.ssl.server.private_key.path': f'{ts.Variables.SSLDir}', - 'proxy.config.ssl.client.verify.server.policy': 'PERMISSIVE', - 'proxy.config.diags.debug.enabled': 3, - 'proxy.config.diags.debug.tags': 'http', - 'proxy.config.exec_thread.autoconfig.enabled': 0, - 'proxy.config.exec_thread.limit': 4, - 'proxy.config.ssl.client.alpn_protocols': 'h2,http/1.1', - 'proxy.config.http.server_session_sharing.pool': 'thread', - 'proxy.config.http.server_session_sharing.match': 'ip,sni,cert', - }) -ts.Disk.remap_config.AddLine(f'map / https://127.0.0.1:{server.Variables.https_port}') -ts.Disk.ssl_multicert_yaml.AddLines( - """ -ssl_multicert: - - dest_ip: "*" - ssl_cert_name: server.pem - ssl_key_name: server.key -""".split("\n")) - -tr = Test.AddTestRun('Client sends RST_STREAM after DATA frame') -tr.Processes.Default.StartBefore(server) -tr.Processes.Default.StartBefore(ts) -tr.AddVerifierClientProcess("client0", replay_file, http_ports=[ts.Variables.port], https_ports=[ts.Variables.ssl_port]) - -tr.Processes.Default.Streams.All += Testers.ContainsExpression( - 'Submitting RST_STREAM frame for key 1 after DATA frame with error code INTERNAL_ERROR.', 'Detect client abort flag.') - -tr.Processes.Default.Streams.All += Testers.ContainsExpression( - 'Submitted RST_STREAM frame for key 1 on stream 1.', 'Send RST_STREAM frame.') - -server.Streams.All += Testers.ExcludesExpression('RST_STREAM', 'Server is not affected.') - -ts.Disk.traffic_out.Content += Testers.ContainsExpression('Received HEADERS frame', 'Received HEADERS frame.') - -ts.Disk.traffic_out.Content += Testers.ContainsExpression('Received DATA frame', 'Received DATA frame.') - -ts.Disk.traffic_out.Content += Testers.ContainsExpression('Received RST_STREAM frame', 'Received RST_STREAM frame.') - -ts.Disk.traffic_out.Content += Testers.ContainsExpression('Parsed RST_STREAM frame: Error Code: 2', 'Error Code: ') - -# -# Client sends RST_STREAM after HEADERS frame -# -ts = Test.MakeATSProcess("ts1", enable_tls=True) -replay_file = "replay_rst_stream/http2_rst_stream_client_after_headers.yaml" -server = Test.MakeVerifierServerProcess("server1", replay_file) -ts.addDefaultSSLFiles() -ts.Disk.records_config.update( - { - 'proxy.config.ssl.server.cert.path': f'{ts.Variables.SSLDir}', - 'proxy.config.ssl.server.private_key.path': f'{ts.Variables.SSLDir}', - 'proxy.config.ssl.client.verify.server.policy': 'PERMISSIVE', - 'proxy.config.diags.debug.enabled': 3, - 'proxy.config.diags.debug.tags': 'http', - 'proxy.config.exec_thread.autoconfig.enabled': 0, - 'proxy.config.exec_thread.limit': 4, - 'proxy.config.ssl.client.alpn_protocols': 'h2,http/1.1', - 'proxy.config.http.server_session_sharing.pool': 'thread', - 'proxy.config.http.server_session_sharing.match': 'ip,sni,cert', - }) -ts.Disk.remap_config.AddLine(f'map / https://127.0.0.1:{server.Variables.https_port}') -ts.Disk.ssl_multicert_yaml.AddLines( - """ -ssl_multicert: - - dest_ip: "*" - ssl_cert_name: server.pem - ssl_key_name: server.key -""".split("\n")) - -tr = Test.AddTestRun('Client sends RST_STREAM after HEADERS frame') -tr.Processes.Default.StartBefore(server) -tr.Processes.Default.StartBefore(ts) -tr.AddVerifierClientProcess("client1", replay_file, http_ports=[ts.Variables.port], https_ports=[ts.Variables.ssl_port]) - -tr.Processes.Default.Streams.All += Testers.ContainsExpression( - 'Submitting RST_STREAM frame for key 1 after HEADERS frame with error code STREAM_CLOSED.', 'Detect client abort flag.') - -tr.Processes.Default.Streams.All += Testers.ContainsExpression( - 'Submitted RST_STREAM frame for key 1 on stream 1', 'Send RST_STREAM frame.') - -server.Streams.All += Testers.ExcludesExpression('RST_STREAM', 'Server is not affected.') - -ts.Disk.traffic_out.Content += Testers.ContainsExpression('Received HEADERS frame', 'Received HEADERS frame.') - -ts.Disk.traffic_out.Content += Testers.ExcludesExpression('Received DATA frame', 'Received DATA frame.') - -ts.Disk.traffic_out.Content += Testers.ContainsExpression('Received RST_STREAM frame', 'Received RST_STREAM frame.') - -ts.Disk.traffic_out.Content += Testers.ContainsExpression('Parsed RST_STREAM frame: Error Code: 5', 'Error Code: ') - -# -# Server sends RST_STREAM after HEADERS frame -# -ts = Test.MakeATSProcess("ts2", enable_tls=True) -replay_file = "replay_rst_stream/http2_rst_stream_server_after_headers.yaml" -server = Test.MakeVerifierServerProcess("server2", replay_file) -ts.addDefaultSSLFiles() -ts.Disk.records_config.update( - { - 'proxy.config.ssl.server.cert.path': f'{ts.Variables.SSLDir}', - 'proxy.config.ssl.server.private_key.path': f'{ts.Variables.SSLDir}', - 'proxy.config.ssl.client.verify.server.policy': 'PERMISSIVE', - 'proxy.config.diags.debug.enabled': 3, - 'proxy.config.diags.debug.tags': 'http', - 'proxy.config.exec_thread.autoconfig.enabled': 0, - 'proxy.config.exec_thread.limit': 1, - 'proxy.config.ssl.client.alpn_protocols': 'h2,http/1.1', - 'proxy.config.http.server_session_sharing.pool': 'thread', - 'proxy.config.http.server_session_sharing.match': 'ip,sni,cert', - }) -ts.Disk.remap_config.AddLine(f'map / https://127.0.0.1:{server.Variables.https_port}') -ts.Disk.ssl_multicert_yaml.AddLines( - """ -ssl_multicert: - - dest_ip: "*" - ssl_cert_name: server.pem - ssl_key_name: server.key -""".split("\n")) - -tr = Test.AddTestRun('Server sends RST_STREAM after HEADERS frame') -tr.Processes.Default.StartBefore(server) -tr.Processes.Default.StartBefore(ts) -tr.AddVerifierClientProcess("client2", replay_file, http_ports=[ts.Variables.port], https_ports=[ts.Variables.ssl_port]) - -tr.Processes.Default.Streams.All += Testers.ContainsExpression( - 'Received RST_STREAM frame with stream id 1, error code 0', 'Client received RST_STREAM frame.') - -server.Streams.All += "gold/server_after_headers.gold" - -ts.Disk.traffic_out.Content += Testers.ContainsExpression('Received RST_STREAM frame', 'Received RST_STREAM frame.') - -ts.Disk.traffic_out.Content += Testers.ContainsExpression('Parsed RST_STREAM frame: Error Code: 11', 'Error Code: ') - -ts.Disk.traffic_out.Content += Testers.ContainsExpression('Send RST_STREAM frame: Error Code: 0', 'Error Code: ') diff --git a/tests/gold_tests/h2/http2_txn_start_read_gate.test.py b/tests/gold_tests/h2/http2_txn_start_read_gate.test.py deleted file mode 100644 index 8ad8aea333b..00000000000 --- a/tests/gold_tests/h2/http2_txn_start_read_gate.test.py +++ /dev/null @@ -1,63 +0,0 @@ -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import os - -Test.Summary = ''' -Verify HTTP/2 read events do not bypass async TXN_START hooks. -''' - -Test.SkipUnless(Condition.HasProxyVerifierVersion('2.8.0')) - -replay_file = "replay/http2_txn_start_read_gate.replay.yaml" -server = Test.MakeVerifierServerProcess("server", replay_file) - -ts = Test.MakeATSProcess("ts", enable_tls=True, enable_cache=False) -ts.addDefaultSSLFiles() -ts.Disk.remap_config.AddLine(f"map / http://127.0.0.1:{server.Variables.http_port}") -ts.Disk.ssl_multicert_yaml.AddLines( - """ -ssl_multicert: - - dest_ip: "*" - ssl_cert_name: server.pem - ssl_key_name: server.key -""".split("\n")) -ts.Disk.records_config.update( - { - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'delay_txn_start', - 'proxy.config.ssl.server.cert.path': f"{ts.Variables.SSLDir}", - 'proxy.config.ssl.server.private_key.path': f"{ts.Variables.SSLDir}", - }) - -Test.PrepareTestPlugin(os.path.join(Test.Variables.AtsTestPluginsDir, 'delay_txn_start.so'), ts, '500') - -tr = Test.AddTestRun() -tr.Processes.Default.StartBefore(server) -tr.Processes.Default.StartBefore(ts) -tr.AddVerifierClientProcess("client", replay_file, http_ports=[ts.Variables.port], https_ports=[ts.Variables.ssl_port]) -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Streams.All += Testers.ExcludesExpression(r'\[ERROR\]', 'Proxy Verifier should not report errors.') -tr.Processes.Default.Streams.All += Testers.ContainsExpression( - 'Equals Success: Key: "1", Content Data: "body", Value: "response-body"', 'Client should receive the response body.') - -server.Streams.All += Testers.ContainsExpression( - 'Equals Success: Key: "1", Content Data: "body", Value: "request-body"', 'Origin should receive the request body.') - -ts.Disk.traffic_out.Content += Testers.ContainsExpression( - "delayed TXN_START reenable", "The test plugin should delay and then resume TXN_START.") -ts.Disk.traffic_out.Content += Testers.ExcludesExpression( - "READ_REQUEST_HDR before delayed TXN_START reenable", "READ_REQUEST_HDR must wait for TXN_START reenable.") diff --git a/tests/gold_tests/h2/http2_write_threshold.test.py b/tests/gold_tests/h2/http2_write_threshold.test.py deleted file mode 100644 index 53bfcae865e..00000000000 --- a/tests/gold_tests/h2/http2_write_threshold.test.py +++ /dev/null @@ -1,143 +0,0 @@ -"""Test proxy.config.http2.write_size_threshold.""" - -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import os -from ports import get_port -import sys - - -class TestGrpc(): - """Test proxy.config.http2.write_size_threshold and its associated timeout.""" - - def __init__(self, description: str, write_threshold: int, write_timeout: int) -> None: - """Configure a TestRun for gRPC traffic. - - :param description: The description for the test runs. - """ - self._description = description - tr = Test.AddTestRun(self._description) - dns = self._configure_dns(tr) - server = self._configure_h2_server(tr, write_timeout) - ts = self._configure_traffic_server(tr, dns.Variables.Port, server.Variables.port, write_threshold, write_timeout) - - ts.StartBefore(dns) - ts.StartBefore(server) - tr.Processes.Default.StartBefore(ts) - - tr.TimeOut = 10 - - self._configure_h2_client(tr, ts.Variables.ssl_port, write_timeout) - - def _configure_dns(self, tr: 'TestRun') -> 'Process': - """Configure a locally running MicroDNS server. - - :param tr: The TestRun with which to associate the MicroDNS server. - :return: The MicroDNS server process. - """ - self._dns = tr.MakeDNServer("dns", default=['127.0.0.1']) - return self._dns - - def _configure_h2_server(self, tr: 'TestRun', write_timeout: int) -> 'Process': - """Set up the go HTTP/2 server. - - :param tr: The TestRun with which to associate the server. - :param write_timeout: The expected maximum amount of time frames should be delivered. - :return: The server process. - """ - tr.Setup.Copy('trickle_server.py') - self._server = tr.Processes.Process('server') - - server_pem = os.path.join(Test.Variables.AtsTestToolsDir, "ssl", "server.pem") - server_key = os.path.join(Test.Variables.AtsTestToolsDir, "ssl", "server.key") - self._server.Setup.Copy(server_pem) - self._server.Setup.Copy(server_key) - - port = get_port(self._server, 'port') - command = (f'{sys.executable} {tr.RunDirectory}/trickle_server.py {port} ' - f'server.pem server.key {write_timeout}') - self._server.Command = command - self._server.ReturnCode = 0 - self._server.Ready = When.PortOpen(port) - return self._server - - def _configure_traffic_server( - self, tr: 'TestRun', dns_port: int, server_port: int, write_threshold: int, write_timeout: int) -> 'Process': - """Configure the traffic server process. - - :param tr: The TestRun with which to associate the traffic server. - :param dns_port: The MicroDNS server port that traffic server should connect to. - :param server_port: The server port that traffic server should connect to. - :param write_threshold: The value to set for proxy.config.http2.write_size_threshold. - :param write_timeout: The value to set for proxy.config.http2.write_time_threshold. - :return: The traffic server process. - """ - self._ts = tr.MakeATSProcess("ts", enable_tls=True, enable_cache=False) - - self._ts.addDefaultSSLFiles() - self._ts.Disk.ssl_multicert_yaml.AddLines( - """ -ssl_multicert: - - dest_ip: "*" - ssl_cert_name: server.pem - ssl_key_name: server.key -""".split("\n")) - - self._ts.Disk.remap_config.AddLine(f"map / https://example.com:{server_port}/") - - self._ts.Disk.records_config.update( - { - "proxy.config.ssl.server.cert.path": self._ts.Variables.SSLDir, - "proxy.config.ssl.server.private_key.path": self._ts.Variables.SSLDir, - 'proxy.config.ssl.client.alpn_protocols': 'h2,http/1.1', - 'proxy.config.http.server_session_sharing.pool': 'thread', - 'proxy.config.ssl.client.verify.server.policy': 'PERMISSIVE', - 'proxy.config.dns.nameservers': f"127.0.0.1:{dns_port}", - 'proxy.config.dns.resolv_conf': "NULL", - 'proxy.config.http2.write_size_threshold': write_threshold, - 'proxy.config.http2.write_time_threshold': write_timeout, - - # Only enable debug logging during manual exectution. All the - # DATA frames get multiple logs and it makes the traffic.out too - # unwieldy. - "proxy.config.diags.debug.enabled": 0, - "proxy.config.diags.debug.tags": "http", - }) - return self._ts - - def _configure_h2_client(self, tr: 'TestRun', proxy_port: int, write_timeout: int) -> None: - """Start the HTTP/2 client. - - :param tr: The TestRun with which to associate the client. - :param proxy_port: The proxy_port to which to connect. - """ - tr.Setup.Copy('trickle_client.py') - ca = os.path.join(Test.Variables.AtsTestToolsDir, "ssl", "server.pem") - key = os.path.join(Test.Variables.AtsTestToolsDir, "ssl", "server.key") - - self._server.Setup.Copy(ca) - self._server.Setup.Copy(key) - # The cert is for example.com, so we must use that domain. - hostname = 'example.com' - command = (f'{sys.executable} {tr.RunDirectory}/trickle_client.py ' - f'{hostname} {proxy_port} server.pem {write_timeout}') - p = tr.Processes.Default - p.Command = command - p.ReturnCode = 0 - - -test = TestGrpc("Test proxy.config.http2.write_size_threshold", 0.5, 10) diff --git a/tests/gold_tests/h2/httpbin.test.py b/tests/gold_tests/h2/httpbin.test.py deleted file mode 100644 index 4af4c0bf7d9..00000000000 --- a/tests/gold_tests/h2/httpbin.test.py +++ /dev/null @@ -1,136 +0,0 @@ -''' -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import os -import sys - -# ---- -# Setup Test -# ---- -Test.Summary = ''' -Test HTTP/2 with httpbin origin server -''' -# Require HTTP/2 enabled Curl -Test.SkipUnless( - Condition.HasCurlFeature('http2'), - Condition.HasProgram("cksum", "cksum need to be installed on system for this test to work"), -) -# Test.ContinueOnFail = True - -# ---- -# Setup httpbin Origin Server -# ---- -httpbin = Test.MakeHttpBinServer("httpbin") - -# ---- -# Setup ATS -# ---- -ts = Test.MakeATSProcess("ts", enable_tls=True, enable_cache=False) - -# add ssl materials like key, certificates for the server -ts.addDefaultSSLFiles() - -ts.Disk.remap_config.AddLine('map / http://127.0.0.1:{0}'.format(httpbin.Variables.Port)) -ts.Disk.ssl_multicert_yaml.AddLines( - """ -ssl_multicert: - - dest_ip: "*" - ssl_cert_name: server.pem - ssl_key_name: server.key -""".split("\n")) -ts.Disk.records_config.update( - { - 'proxy.config.http.insert_request_via_str': 1, - 'proxy.config.http.insert_response_via_str': 1, - 'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir), - 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts.Variables.SSLDir), - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'http', - }) -ts.Disk.logging_yaml.AddLines( - ''' -logging: - formats: - - name: access - format: '[%] % % % % % % % %' - - logs: - - filename: access - format: access -'''.split("\n")) - -Test.Disk.File(os.path.join(ts.Variables.LOGDIR, 'access.log'), exists=True, content='gold/httpbin_access.gold') - -# TODO: when httpbin 0.8.0 or later is released, remove below json pretty print hack -json_printer = f''' -{sys.executable} -c "import sys,json; print(json.dumps(json.load(sys.stdin), indent=2, separators=(',', ': ')))" -''' - -# ---- -# Test Cases -# ---- - -# Test Case 0: Basic request and response -test_run = Test.AddTestRun() -test_run.MakeCurlCommand("-vs -k --http2 https://127.0.0.1:{0}/get | {1}".format(ts.Variables.ssl_port, json_printer), ts=ts) -test_run.Processes.Default.ReturnCode = 0 -test_run.Processes.Default.StartBefore(httpbin) -test_run.Processes.Default.StartBefore(Test.Processes.ts) -test_run.Processes.Default.Streams.stdout = "gold/httpbin_0_stdout.gold" -# Different versions of curl will have different cases for HTTP/2 field names. -test_run.Processes.Default.Streams.stderr = Testers.GoldFile("gold/httpbin_0_stderr.gold", case_insensitive=True) -test_run.StillRunningAfter = httpbin - -# Test Case 1: Attempt an empty response body. -# This test case requires go-httpbin@v2.6.0 or later. -test_run = Test.AddTestRun() -test_run.MakeCurlCommand('-vs -k --http2 https://127.0.0.1:{0}/bytes/0'.format(ts.Variables.ssl_port), ts=ts) -test_run.Processes.Default.ReturnCode = 0 -test_run.Processes.Default.Streams.stdout = "gold/httpbin_1_stdout.gold" -# Different versions of curl will have different cases for HTTP/2 field names. -test_run.Processes.Default.Streams.stderr = Testers.GoldFile("gold/httpbin_1_stderr.gold", case_insensitive=True) -test_run.StillRunningAfter = httpbin - -# Test Case 2: Chunked -test_run = Test.AddTestRun() -test_run.MakeCurlCommand( - '-vs -k --http2 https://127.0.0.1:{0}/stream-bytes/102400?seed=0 | cksum'.format(ts.Variables.ssl_port), ts=ts) -test_run.Processes.Default.ReturnCode = 0 -test_run.Processes.Default.Streams.stdout = "gold/httpbin_2_stdout.gold" -# Different versions of curl will have different cases for HTTP/2 field names. -test_run.Processes.Default.Streams.stderr = Testers.GoldFile("gold/httpbin_2_stderr.gold", case_insensitive=True) -test_run.StillRunningAfter = httpbin - -# Test Case 3: Expect 100-Continue -test_run = Test.AddTestRun() -test_run.MakeCurlCommand( - "-vs -k --http2 https://127.0.0.1:{0}/post --data 'key=value' -H 'Expect: 100-continue' --max-time 5 | {1}".format( - ts.Variables.ssl_port, json_printer), - ts=ts) -test_run.Processes.Default.ReturnCode = 0 -test_run.Processes.Default.Streams.stdout = "gold/httpbin_3_stdout.gold" -# Different versions of curl will have different cases for HTTP/2 field names. -test_run.Processes.Default.Streams.stderr = Testers.GoldFile("gold/httpbin_3_stderr.gold", case_insensitive=True) -test_run.StillRunningAfter = httpbin - -# Wait for the POST transaction to be logged. -Test.AddAwaitFileContainsTestRun( - 'Await POST access log entry.', - os.path.join(ts.Variables.LOGDIR, 'access.log'), - r'POST .*?/post', -) diff --git a/tests/gold_tests/h2/nghttp.test.py b/tests/gold_tests/h2/nghttp.test.py deleted file mode 100644 index b465e2766fc..00000000000 --- a/tests/gold_tests/h2/nghttp.test.py +++ /dev/null @@ -1,97 +0,0 @@ -''' -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import os - -Test.Summary = ''' -Test with nghttp -''' - -Test.SkipUnless(Condition.HasProgram("nghttp", "Nghttp need to be installed on system for this test to work"),) -Test.ContinueOnFail = True - -# ---- -# Setup Origin Server -# ---- -httpbin = Test.MakeHttpBinServer("httpbin") - -# 128KB -post_body = "0123456789abcdef" * 8192 -post_body_file = open(os.path.join(Test.RunDirectory, "post_body"), "w") -post_body_file.write(post_body) -post_body_file.close() - -# ---- -# Setup ATS -# ---- -ts = Test.MakeATSProcess("ts", enable_tls=True, enable_cache=False) - -# add ssl materials like key, certificates for the server -ts.addDefaultSSLFiles() - -ts.Setup.CopyAs('rules/graceful_shutdown.conf', Test.RunDirectory) - -ts.Disk.remap_config.AddLines( - [ - 'map /httpbin/ http://127.0.0.1:{0}/ @plugin=header_rewrite.so @pparam={1}/graceful_shutdown.conf'.format( - httpbin.Variables.Port, Test.RunDirectory) - ]) - -ts.Disk.ssl_multicert_yaml.AddLines( - """ -ssl_multicert: - - dest_ip: "*" - ssl_cert_name: server.pem - ssl_key_name: server.key -""".split("\n")) - -ts.Disk.records_config.update( - { - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'http2_cs', - 'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir), - 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts.Variables.SSLDir) - }) - -# ---- -# Test Cases -# ---- - -# Test Case 0: Trailer -tr = Test.AddTestRun() -tr.TimeOut = 10 -tr.Processes.Default.Command = f"nghttp -vn --no-dep 'https://127.0.0.1:{ts.Variables.ssl_port}/httpbin/post' --trailer 'foo: bar' -d 'post_body'" -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.StartBefore(httpbin) -tr.Processes.Default.StartBefore(Test.Processes.ts) -tr.Processes.Default.Streams.stdout = "gold/nghttp_0_stdout.gold" -tr.StillRunningAfter = httpbin -tr.StillRunningAfter = ts - -# Test Case 1: Graceful Shutdown -# - This test takes 3 seconds to make sure receiving 2 GOAWAY frames -# - TODO: add a test case of a client keeps the connection open ( -e.g. keep sending PING frame) -tr = Test.AddTestRun() -tr.TimeOut = 10 -tr.Processes.Default.Command = f"nghttp -vn --no-dep 'https://127.0.0.1:{ts.Variables.ssl_port}/httpbin/drip?duration=3'" -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Streams.stdout = "gold/nghttp_1_stdout.gold" -tr.StillRunningAfter = httpbin -tr.StillRunningAfter = ts - -ts.Disk.traffic_out.Content = "gold/nghttp_ts_stderr.gold" diff --git a/tests/gold_tests/h3/h3_curl.test.py b/tests/gold_tests/h3/h3_curl.test.py deleted file mode 100644 index 0fffd6b3d10..00000000000 --- a/tests/gold_tests/h3/h3_curl.test.py +++ /dev/null @@ -1,143 +0,0 @@ -''' -Verify HTTP/3 client interop with curl. -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information regarding -# copyright ownership. The ASF licenses this file to you under -# the Apache License, Version 2.0 (the "License"); you may not -# use this file except in compliance with the License. You may -# obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import os - -Test.Summary = ''' -This test is written specifically to verify that an HTTP/3 curl client can -complete a request through ATS. -''' - -Test.SkipUnless( - Condition.HasATSFeature('TS_USE_QUIC'), - Condition.HasCurlFeature('http3'), - Condition.HasCurlOption('--http3-only'), -) -Test.SkipIf(Condition.CurlUsingUnixDomainSocket()) - - -class TestHttp3Curl: - """Configure a test to verify HTTP/3 curl client interoperability.""" - - response_body = "0123456789" * 30000 - - def __init__(self, name: str): - """Initialize the test. - - :param name: The name of the test. - """ - self.name = name - self._body_path = os.path.join(Test.RunDirectory, "h3_curl_body.txt") - self._configure_server() - self._configure_traffic_server() - self._configure_client() - - def _configure_server(self): - """Configure the origin server.""" - server = Test.MakeOriginServer("server") - server.addResponse( - "sessionlog.json", { - "headers": "GET /h3-curl HTTP/1.1\r\nHost: localhost\r\n\r\n", - "timestamp": "1469733493.993", - "body": "" - }, { - "headers": f"HTTP/1.1 200 OK\r\nConnection: close\r\nContent-Length: {len(self.response_body)}\r\n\r\n", - "timestamp": "1469733493.993", - "body": self.response_body - }) - - self._server = server - - def _configure_traffic_server(self): - """Configure Traffic Server.""" - ts = Test.MakeATSProcess("ts", enable_tls=True, enable_quic=True, enable_cache=False) - ts.StartupTimeout = 60 - ts.addDefaultSSLFiles() - ts.addSSLfile("../tls/ssl/signed-foo.pem") - ts.addSSLfile("../tls/ssl/signed-foo.key") - ts.Disk.ssl_multicert_yaml.AddLines( - ''' -ssl_multicert: - - ssl_cert_name: signed-foo.pem - ssl_key_name: signed-foo.key - - ssl_cert_name: server.pem - ssl_key_name: server.key - dest_ip: "*" -'''.split('\n')) - ts.Disk.records_config.update( - { - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'quic|http3', - 'proxy.config.quic.server.stateless_retry_enabled': 0, - 'proxy.config.ssl.server.cert.path': ts.Variables.SSLDir, - 'proxy.config.ssl.server.private_key.path': ts.Variables.SSLDir, - }) - ts.Disk.remap_config.AddLine(f'map / http://127.0.0.1:{self._server.Variables.Port}') - ts.Disk.logging_yaml.AddLines( - ''' -logging: - formats: - - name: h3_access - format: 'c_alpn=% client_version=% c_ssl_version=% c_method=% c_url=%' - - logs: - - filename: h3_access - format: h3_access -'''.split("\n")) - - self._access_log = Test.Disk.File(os.path.join(ts.Variables.LOGDIR, 'h3_access.log'), exists=True) - self._access_log.Content = Testers.ContainsExpression( - r'c_alpn=h3 client_version=http/3 c_ssl_version=[^ ]+ c_method=GET c_url=https://foo.com:[0-9]+/h3-curl', - "ATS should log the curl request as HTTP/3") - - self._ts = ts - - def _check_curl_response(self, tr): - """Verify that curl received the response over HTTP/3.""" - tr.Processes.Default.ReturnCode = 0 - tr.Processes.Default.Streams.stdout = Testers.ContainsExpression( - f"size_download={len(self.response_body)}", "curl should receive the complete HTTP/3 response body") - tr.Processes.Default.Streams.stdout += Testers.ContainsExpression("http_version=3", "curl should report HTTP/3") - - def _configure_client(self): - """Configure the curl client test runs.""" - tr = Test.AddTestRun(self.name) - tr.Processes.Default.StartBefore(self._server) - tr.Processes.Default.StartBefore(self._ts) - tr.MakeCurlCommand( - '--silent --show-error --fail --ipv4 --http3-only --insecure ' - f'--resolve "foo.com:{self._ts.Variables.ssl_port}:127.0.0.1" ' - f'--output "{self._body_path}" ' - '--write-out "\\nhttp_version=%{http_version}\\nsize_download=%{size_download}\\n" ' - f'https://foo.com:{self._ts.Variables.ssl_port}/h3-curl', - ts=self._ts) - self._check_curl_response(tr) - tr.StillRunningAfter = self._server - tr.StillRunningAfter = self._ts - - tr = Test.AddTestRun("Wait for HTTP/3 access log") - tr.Processes.Default.Command = ( - os.path.join(Test.Variables.AtsTestToolsDir, 'condwait') + ' 60 1 -f ' + - os.path.join(self._ts.Variables.LOGDIR, 'h3_access.log')) - tr.Processes.Default.ReturnCode = 0 - tr.StillRunningAfter = self._server - tr.StillRunningAfter = self._ts - - -TestHttp3Curl("curl forced HTTP/3 request") diff --git a/tests/gold_tests/h3/h3_go_client.test.py b/tests/gold_tests/h3/h3_go_client.test.py deleted file mode 100644 index 5feee435000..00000000000 --- a/tests/gold_tests/h3/h3_go_client.test.py +++ /dev/null @@ -1,130 +0,0 @@ -''' -Verify HTTP/3 client interop with a quic-go client. -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information regarding -# copyright ownership. The ASF licenses this file to you under -# the Apache License, Version 2.0 (the "License"); you may not -# use this file except in compliance with the License. You may -# obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import os - -Test.Summary = ''' -Verify that a quic-go HTTP/3 client can complete sequential and concurrent -transactions through ATS. -''' - -Test.SkipUnless( - Condition.HasATSFeature('TS_USE_QUIC'), - Condition.HasGoVersion('1.24'), -) - - -def add_default_ssl_multicert(ts): - """Configure the default server certificate.""" - ts.Disk.ssl_multicert_yaml.AddLines( - """ -ssl_multicert: - - dest_ip: "*" - ssl_cert_name: server.pem - ssl_key_name: server.key -""".split("\n")) - - -class TestHttp3GoClient: - """Configure a test to verify HTTP/3 quic-go client interoperability.""" - - replay_file = "replays/h3_server_for_go_client.replay.yaml" - - def __init__(self, name: str): - """Initialize the test.""" - self.name = name - self._configure_server() - self._configure_traffic_server() - self._configure_client() - - def _configure_server(self): - """Configure the Proxy Verifier origin server.""" - self._server = Test.MakeVerifierServerProcess( - "server-go-h3-client", self.replay_file, verbose=False, other_args="--poll-timeout 30000") - - def _configure_traffic_server(self): - """Configure Traffic Server.""" - ts = Test.MakeATSProcess("ts-go-h3-client", enable_tls=True, enable_quic=True, enable_cache=False) - ts.StartupTimeout = 60 - ts.addDefaultSSLFiles() - add_default_ssl_multicert(ts) - ts.Disk.records_config.update( - { - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'quic|http3', - 'proxy.config.quic.initial_max_data_in': 1000000, - 'proxy.config.quic.initial_max_stream_data_bidi_remote_in': 1000000, - 'proxy.config.quic.server.stateless_retry_enabled': 0, - 'proxy.config.ssl.server.cert.path': ts.Variables.SSLDir, - 'proxy.config.ssl.server.private_key.path': ts.Variables.SSLDir, - }) - ts.Disk.remap_config.AddLine(f'map / http://127.0.0.1:{self._server.Variables.http_port}') - ts.Disk.logging_yaml.AddLines( - ''' -logging: - formats: - - name: h3_go_access - format: 'c_alpn=% client_version=% c_method=% c_url=%' - - logs: - - filename: h3_go_access - format: h3_go_access -'''.split("\n")) - - self._access_log = Test.Disk.File(os.path.join(ts.Variables.LOGDIR, 'h3_go_access.log'), exists=True) - self._access_log.Content = Testers.ContainsExpression( - r'c_alpn=h3 client_version=http/3 c_method=GET c_url=https://go\.example\.com:[0-9]+/go-get-empty', - "ATS should log the quic-go request as HTTP/3") - self._access_log.Content += Testers.ContainsExpression( - r'c_alpn=h3 client_version=http/3 c_method=POST c_url=https://go\.example\.com:[0-9]+/go-post-large', - "ATS should log the quic-go large POST as HTTP/3") - - self._ts = ts - - def _configure_client(self): - """Configure the quic-go client test runs.""" - tr = Test.AddTestRun(self.name) - tr.Setup.Copy("go_h3_client") - tr.Processes.Default.StartBefore(self._server) - tr.Processes.Default.StartBefore(self._ts) - tr.Processes.Default.Env['GOFLAGS'] = '-mod=readonly -modcacherw' - tr.Processes.Default.Env['GOCACHE'] = os.path.join(tr.RunDirectory, 'gocache') - tr.Processes.Default.Env['GOMODCACHE'] = os.path.join(tr.RunDirectory, 'gomodcache') - tr.Processes.Default.Env['GOTOOLCHAIN'] = 'local' - tr.Processes.Default.Command = ( - f'cd "{os.path.join(tr.RunDirectory, "go_h3_client")}" && ' - f'go run . --addr 127.0.0.1:{self._ts.Variables.ssl_port} ' - f'--authority go.example.com:{self._ts.Variables.ssl_port} ' - '--server-name go.example.com') - tr.Processes.Default.ReturnCode = 0 - tr.Processes.Default.Streams.stdout = Testers.ContainsExpression( - "completed 13 HTTP/3 requests", "The quic-go client should complete all HTTP/3 requests.") - tr.StillRunningAfter = self._server - tr.StillRunningAfter = self._ts - - tr = Test.AddTestRun("Wait for quic-go HTTP/3 access log") - tr.Processes.Default.Command = ( - os.path.join(Test.Variables.AtsTestToolsDir, 'condwait') + ' 60 1 -f ' + - os.path.join(self._ts.Variables.LOGDIR, 'h3_go_access.log')) - tr.Processes.Default.ReturnCode = 0 - tr.StillRunningAfter = self._server - tr.StillRunningAfter = self._ts - - -TestHttp3GoClient("quic-go HTTP/3 client requests") diff --git a/tests/gold_tests/h3/h3_python_client.test.py b/tests/gold_tests/h3/h3_python_client.test.py deleted file mode 100644 index 2d809977ef4..00000000000 --- a/tests/gold_tests/h3/h3_python_client.test.py +++ /dev/null @@ -1,132 +0,0 @@ -''' -Verify HTTP/3 client interop with an aioquic Python client. -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information regarding -# copyright ownership. The ASF licenses this file to you under -# the Apache License, Version 2.0 (the "License"); you may not -# use this file except in compliance with the License. You may -# obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import os -import sys - -Test.Summary = ''' -Verify that an aioquic HTTP/3 client can complete normal requests and selected -HTTP/3 edge-case probes through ATS. -''' - -Test.SkipUnless( - Condition.HasATSFeature('TS_USE_QUIC'), - Condition.HasProgram("python3", "python3 is required for the aioquic HTTP/3 client"), -) - - -def add_default_ssl_multicert(ts): - """Configure the default server certificate.""" - if hasattr(ts.Disk, "ssl_multicert_yaml"): - ts.Disk.ssl_multicert_yaml.AddLines( - """ -ssl_multicert: - - dest_ip: "*" - ssl_cert_name: server.pem - ssl_key_name: server.key -""".split("\n")) - else: - ts.Disk.ssl_multicert_config.AddLine("dest_ip=* ssl_cert_name=server.pem ssl_key_name=server.key") - - -class TestHttp3PythonClient: - """Configure a test to verify HTTP/3 aioquic client interoperability.""" - - replay_file = "replays/h3_server_for_python_client.replay.yaml" - - def __init__(self, name: str): - """Initialize the test.""" - self.name = name - self._configure_server() - self._configure_traffic_server() - self._configure_client() - - def _configure_server(self): - """Configure the Proxy Verifier origin server.""" - self._server = Test.MakeVerifierServerProcess( - "server-python-h3-client", self.replay_file, verbose=False, other_args="--poll-timeout 30000") - - def _configure_traffic_server(self): - """Configure Traffic Server.""" - ts = Test.MakeATSProcess("ts-python-h3-client", enable_tls=True, enable_quic=True, enable_cache=False) - ts.StartupTimeout = 60 - ts.addDefaultSSLFiles() - add_default_ssl_multicert(ts) - ts.Disk.records_config.update( - { - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'quic|http3', - 'proxy.config.quic.initial_max_data_in': 1000000, - 'proxy.config.quic.initial_max_stream_data_bidi_remote_in': 1000000, - 'proxy.config.quic.max_send_udp_payload_size_in': 1200, - 'proxy.config.quic.server.stateless_retry_enabled': 0, - 'proxy.config.ssl.server.cert.path': ts.Variables.SSLDir, - 'proxy.config.ssl.server.private_key.path': ts.Variables.SSLDir, - }) - ts.Disk.remap_config.AddLine(f'map / http://127.0.0.1:{self._server.Variables.http_port}') - ts.Disk.logging_yaml.AddLines( - ''' -logging: - formats: - - name: h3_python_access - format: 'c_alpn=% client_version=% c_method=% c_url=%' - - logs: - - filename: h3_python_access - format: h3_python_access -'''.split("\n")) - - self._access_log = Test.Disk.File(os.path.join(ts.Variables.LOGDIR, 'h3_python_access.log'), exists=True) - self._access_log.Content = Testers.ContainsExpression( - r'c_alpn=h3 client_version=http/3 c_method=GET c_url=https://py\.example\.com:[0-9]+/py-get-empty', - "ATS should log the aioquic request as HTTP/3") - self._access_log.Content += Testers.ContainsExpression( - r'c_alpn=h3 client_version=http/3 c_method=PUT c_url=https://py\.example\.com:[0-9]+/py-put-large', - "ATS should log the aioquic large PUT as HTTP/3") - - self._ts = ts - - def _configure_client(self): - """Configure the aioquic client test runs.""" - tr = Test.AddTestRun(self.name) - tr.Setup.Copy("py_h3_client") - tr.Processes.Default.StartBefore(self._server) - tr.Processes.Default.StartBefore(self._ts) - client_dir = os.path.join(tr.RunDirectory, "py_h3_client") - tr.Processes.Default.Command = ( - f'"{sys.executable}" "{os.path.join(client_dir, "h3_client.py")}" ' - f'--addr 127.0.0.1:{self._ts.Variables.ssl_port} ' - f'--authority py.example.com:{self._ts.Variables.ssl_port} ' - '--server-name py.example.com') - tr.Processes.Default.ReturnCode = 0 - tr.Processes.Default.Streams.stdout = Testers.ContainsExpression( - "completed 18 Python HTTP/3 checks", "The aioquic client should complete all HTTP/3 checks.") - tr.StillRunningAfter = self._server - tr.StillRunningAfter = self._ts - - tr = Test.AddTestRun("Wait for aioquic HTTP/3 access log") - tr.Processes.Default.Command = ( - os.path.join(Test.Variables.AtsTestToolsDir, 'condwait') + ' 60 1 -f ' + - os.path.join(self._ts.Variables.LOGDIR, 'h3_python_access.log')) - tr.Processes.Default.ReturnCode = 0 - tr.StillRunningAfter = self._server - tr.StillRunningAfter = self._ts - - -TestHttp3PythonClient("aioquic HTTP/3 client requests") diff --git a/tests/gold_tests/h3/h3_range_cache.test.py b/tests/gold_tests/h3/h3_range_cache.test.py deleted file mode 100644 index 2222e2e47dc..00000000000 --- a/tests/gold_tests/h3/h3_range_cache.test.py +++ /dev/null @@ -1,140 +0,0 @@ -''' -Verify HTTP/3 range requests over cached content. -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information regarding -# copyright ownership. The ASF licenses this file to you under -# the Apache License, Version 2.0 (the "License"); you may not -# use this file except in compliance with the License. You may -# obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import os - -Test.Summary = ''' -Verify that HTTP/3 clients can populate cache and receive range responses from -cached objects. -''' - -Test.SkipUnless( - Condition.HasATSFeature('TS_USE_QUIC'), - Condition.HasCurlFeature('http3'), - Condition.HasCurlOption('--http3-only'), -) -Test.SkipIf(Condition.CurlUsingUnixDomainSocket()) - - -def add_default_ssl_multicert(ts): - """Configure the default server certificate.""" - if hasattr(ts.Disk, "ssl_multicert_yaml"): - ts.Disk.ssl_multicert_yaml.AddLines( - """ -ssl_multicert: - - dest_ip: "*" - ssl_cert_name: server.pem - ssl_key_name: server.key -""".split("\n")) - else: - ts.Disk.ssl_multicert_config.AddLine("dest_ip=* ssl_cert_name=server.pem ssl_key_name=server.key") - - -class TestHttp3RangeCache: - """Configure an HTTP/3 range-over-cache test.""" - - response_body = "0123456789" * 30000 - range_body = "6789012345678901" - - def __init__(self): - """Initialize the test.""" - self._configure_server() - self._configure_traffic_server() - self._configure_clients() - - def _configure_server(self): - """Configure the origin server.""" - server = Test.MakeOriginServer("server-h3-range-cache") - server.addResponse( - "sessionlog.json", { - "headers": "GET /h3-range-cache HTTP/1.1\r\nHost: localhost\r\n\r\n", - "timestamp": "1469733493.993", - "body": "" - }, { - "headers": - ( - "HTTP/1.1 200 OK\r\n" - "Connection: close\r\n" - "Cache-Control: public, max-age=60\r\n" - f"Content-Length: {len(self.response_body)}\r\n\r\n"), - "timestamp": "1469733493.993", - "body": self.response_body - }) - self._server = server - - def _configure_traffic_server(self): - """Configure Traffic Server.""" - ts = Test.MakeATSProcess("ts-h3-range-cache", enable_tls=True, enable_quic=True, enable_cache=True) - ts.StartupTimeout = 60 - ts.addDefaultSSLFiles() - add_default_ssl_multicert(ts) - ts.Disk.records_config.update( - { - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'quic|http3|http', - 'proxy.config.quic.server.stateless_retry_enabled': 0, - 'proxy.config.ssl.server.cert.path': ts.Variables.SSLDir, - 'proxy.config.ssl.server.private_key.path': ts.Variables.SSLDir, - }) - ts.Disk.remap_config.AddLine(f'map / http://127.0.0.1:{self._server.Variables.Port}') - self._ts = ts - - def _curl_base(self): - """Build the shared curl arguments.""" - return ( - '--silent --show-error --fail --ipv4 --http3-only --insecure ' - f'--resolve "range.example.com:{self._ts.Variables.ssl_port}:127.0.0.1" ' - f'https://range.example.com:{self._ts.Variables.ssl_port}/h3-range-cache') - - def _configure_clients(self): - """Configure the cache fill and range request clients.""" - full_body_path = os.path.join(Test.RunDirectory, "h3-range-full.txt") - range_body_path = os.path.join(Test.RunDirectory, "h3-range-part.txt") - - tr = Test.AddTestRun("HTTP/3 cache fill") - tr.Processes.Default.StartBefore(self._server) - tr.Processes.Default.StartBefore(self._ts) - tr.MakeCurlCommand( - f'{self._curl_base()} --output "{full_body_path}" ' - '--write-out "\\nhttp_code=%{http_code}\\nsize_download=%{size_download}\\n"', - ts=self._ts) - tr.Processes.Default.ReturnCode = 0 - tr.Processes.Default.Streams.stdout = Testers.ContainsExpression("http_code=200", "The fill request should return 200.") - tr.Processes.Default.Streams.stdout += Testers.ContainsExpression( - f"size_download={len(self.response_body)}", "The fill request should receive the full object.") - tr.StillRunningAfter = self._server - tr.StillRunningAfter = self._ts - - tr = Test.AddTestRun("HTTP/3 cached range request") - tr.MakeCurlCommand( - f'{self._curl_base()} --header "Range: bytes=16-31" --output "{range_body_path}" ' - '--write-out "\\nhttp_code=%{http_code}\\nsize_download=%{size_download}\\n"', - ts=self._ts) - tr.Processes.Default.ReturnCode = 0 - tr.Processes.Default.Streams.stdout = Testers.ContainsExpression("http_code=206", "The range request should return 206.") - tr.Processes.Default.Streams.stdout += Testers.ContainsExpression( - "size_download=16", "The range request should receive 16 bytes.") - Test.Disk.File( - range_body_path, exists=True).Content = Testers.ContainsExpression( - self.range_body, "The cached range response body should match the requested byte range.") - tr.StillRunningAfter = self._server - tr.StillRunningAfter = self._ts - - -TestHttp3RangeCache() diff --git a/tests/gold_tests/h3/h3_session_ticket.test.py b/tests/gold_tests/h3/h3_session_ticket.test.py deleted file mode 100644 index fd839aad1cb..00000000000 --- a/tests/gold_tests/h3/h3_session_ticket.test.py +++ /dev/null @@ -1,110 +0,0 @@ -''' -Verify HTTP/3 QUIC TLS session ticket handling. -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information regarding -# copyright ownership. The ASF licenses this file to you under -# the Apache License, Version 2.0 (the "License"); you may not -# use this file except in compliance with the License. You may -# obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import os -import shlex - -Test.Summary = ''' -Verify that HTTP/3 QUIC connections can receive and offer TLS session tickets. -''' - -Test.SkipUnless( - Condition.HasATSFeature('TS_USE_QUIC'), - Condition.HasOpenSSLVersion('3.5.0'), - Condition.HasOpenSSLQuicClient(), -) -Test.Setup.Copy('../tls/file.ticket') - - -def add_default_ssl_multicert(ts): - """Configure the default server certificate.""" - if hasattr(ts.Disk, "ssl_multicert_yaml"): - ts.Disk.ssl_multicert_yaml.AddLines( - """ -ssl_multicert: - - dest_ip: "*" - ssl_cert_name: server.pem - ssl_key_name: server.key -""".split("\n")) - else: - ts.Disk.ssl_multicert_config.AddLine("dest_ip=* ssl_cert_name=server.pem ssl_key_name=server.key") - - -class TestHttp3SessionTicket: - """Configure an HTTP/3 QUIC TLS session ticket test.""" - - def __init__(self, name: str): - """Initialize the test.""" - self.name = name - self.session_file = os.path.join(Test.RunDirectory, "h3-quic-session.pem") - self.ticket_file = os.path.join(Test.RunDirectory, "file.ticket") - self._configure_traffic_server() - self._configure_ticket_save() - self._configure_ticket_reuse() - - def _configure_traffic_server(self): - """Configure Traffic Server.""" - ts = Test.MakeATSProcess("ts", enable_tls=True, enable_quic=True, enable_cache=False) - ts.StartupTimeout = 60 - ts.addDefaultSSLFiles() - add_default_ssl_multicert(ts) - ts.Disk.records_config.update( - { - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'quic|ssl', - 'proxy.config.quic.server.stateless_retry_enabled': 0, - 'proxy.config.ssl.server.cert.path': ts.Variables.SSLDir, - 'proxy.config.ssl.server.private_key.path': ts.Variables.SSLDir, - 'proxy.config.ssl.server.session_ticket.enable': 1, - 'proxy.config.ssl.server.session_ticket.number': 2, - 'proxy.config.ssl.server.ticket_key.filename': self.ticket_file, - }) - - self._ts = ts - - def _s_client_command(self, session_option: str): - """Build an OpenSSL QUIC client command for ticket save or reuse.""" - script = os.path.join(Test.TestDirectory, "h3_session_ticket.sh") - return f"{shlex.quote(script)} {session_option} {shlex.quote(self.session_file)} {self._ts.Variables.ssl_port}" - - def _check_s_client_handshake(self, tr): - """Verify that OpenSSL completed the QUIC handshake.""" - tr.Processes.Default.ReturnCode = 0 - tr.Processes.Default.Streams.All = Testers.ContainsExpression( - "CONNECTION ESTABLISHED", "OpenSSL should complete the QUIC handshake.") - tr.Processes.Default.Streams.All += Testers.ContainsExpression( - "Protocol version: QUICv1", "OpenSSL should negotiate QUICv1.") - - def _configure_ticket_save(self): - """Configure the ticket save test run.""" - tr = Test.AddTestRun(self.name) - tr.Processes.Default.StartBefore(self._ts) - tr.Processes.Default.Command = f"rm -f {shlex.quote(self.session_file)}; {self._s_client_command('-sess_out')}" - self._check_s_client_handshake(tr) - tr.StillRunningAfter = self._ts - - def _configure_ticket_reuse(self): - """Configure the ticket reuse test run.""" - tr = Test.AddTestRun("OpenSSL QUIC offers saved session ticket") - tr.Processes.Default.Command = self._s_client_command("-sess_in") - self._check_s_client_handshake(tr) - tr.StillRunningAfter = self._ts - - -TestHttp3SessionTicket("OpenSSL QUIC saves session ticket") diff --git a/tests/gold_tests/h3/h3_sni_check.test.py b/tests/gold_tests/h3/h3_sni_check.test.py deleted file mode 100644 index 6b5dcb8e56f..00000000000 --- a/tests/gold_tests/h3/h3_sni_check.test.py +++ /dev/null @@ -1,131 +0,0 @@ -''' -Verify h3 SNI checking behavior. -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -Test.Summary = ''' -Verify h3 SNI checking behavior. -''' - -Test.SkipUnless(Condition.HasATSFeature('TS_USE_QUIC')) - -Test.ContinueOnFail = True - - -class Test_sni_check: - """Configure a test to verify SNI checking behavior for h3 connections.""" - - replay_file = "replays/h3_sni.replay.yaml" - client_counter: int = 0 - ts_counter: int = 0 - server_counter: int = 0 - - def __init__(self, name: str, gold_file="", replay_keys="", expect_request_rejected=False): - """Initialize the test. - - :param name: The name of the test. - :param gold_file: (Optional) Gold file to be checked. - :param replay_keys: (Optional) Keys to be used by pv. - :param expect_request_rejected: (Optional) Whether or not the client request is expected to be rejected. - """ - self.name = name - self.gold_file = gold_file - self.replay_keys = replay_keys - self.expect_request_rejected = expect_request_rejected - - def _configure_server(self, tr: 'TestRun'): - """Configure the server. - - :param tr: The TestRun object to associate the server process with. - """ - server = tr.AddVerifierServerProcess(f"server_{Test_sni_check.server_counter}", self.replay_file) - Test_sni_check.server_counter += 1 - self._server = server - - def _configure_traffic_server(self, tr: 'TestRun'): - """Configure Traffic Server. - - :param tr: The TestRun object to associate the ts process with. - """ - ts = tr.MakeATSProcess(f"ts-{Test_sni_check.ts_counter}", enable_quic=True, enable_tls=True) - ts.StartupTimeout = 60 - - Test_sni_check.ts_counter += 1 - self._ts = ts - # Configure TLS for Traffic Server. - self._ts.addDefaultSSLFiles() - self._ts.addSSLfile("../tls/ssl/signed-foo.pem") - self._ts.addSSLfile("../tls/ssl/signed-foo.key") - self._ts.Disk.ssl_multicert_yaml.AddLines( - """ -ssl_multicert: - - ssl_cert_name: signed-foo.pem - ssl_key_name: signed-foo.key - - dest_ip: "*" - ssl_cert_name: server.pem - ssl_key_name: server.key -""".split("\n")) - self._ts.Disk.records_config.update( - { - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'http', - 'proxy.config.ssl.server.cert.path': ts.Variables.SSLDir, - 'proxy.config.quic.no_activity_timeout_in': 0, - 'proxy.config.ssl.server.private_key.path': ts.Variables.SSLDir, - 'proxy.config.ssl.client.verify.server.policy': 'PERMISSIVE', - }) - - self._ts.Disk.remap_config.AddLine(f'map / http://127.0.0.1:{self._server.Variables.http_port}') - - def run(self): - """Run the test.""" - tr = Test.AddTestRun(self.name) - self._configure_server(tr) - self._configure_traffic_server(tr) - - tr.Processes.Default.StartBefore(self._server) - tr.Processes.Default.StartBefore(self._ts) - - tr.AddVerifierClientProcess( - f'client-{Test_sni_check.client_counter}', - self.replay_file, - http3_ports=[self._ts.Variables.ssl_port], - keys=self.replay_keys) - Test_sni_check.client_counter += 1 - - if self.expect_request_rejected: - # The client request should time out because ATS rejects it and does - # not send a response. - tr.Processes.Default.ReturnCode = 1 - self._ts.Disk.traffic_out.Content += Testers.ContainsExpression("SNI not found", "ATS should detect the missing SNI.") - else: - # Verify the client request is successful. - tr.Processes.Default.ReturnCode = 0 - self._ts.Disk.traffic_out.Content += Testers.ExcludesExpression( - "SNI not found", "ATS should see the SNI presented by client.") - - if self.gold_file: - tr.Processes.Default.Streams.all = self.gold_file - - -# TEST 1: Client request with SNI. -test0 = Test_sni_check("", replay_keys="has_sni", expect_request_rejected=False) -test0.run() - -# TEST 2: Client request without SNI. -test1 = Test_sni_check("", replay_keys="no_sni", expect_request_rejected=True) -test1.run() diff --git a/tests/gold_tests/headers/cache_and_req_body.test.py b/tests/gold_tests/headers/cache_and_req_body.test.py deleted file mode 100644 index b7102056b5e..00000000000 --- a/tests/gold_tests/headers/cache_and_req_body.test.py +++ /dev/null @@ -1,159 +0,0 @@ -''' -Test cached responses and requests with bodies using CurlHeader tester -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -Test.Summary = ''' -Test cached responses and requests with bodies using CurlHeader tester -''' -Test.SkipIf(Condition.CurlUsingUnixDomainSocket()) -Test.ContinueOnFail = True - -# Define default ATS -ts = Test.MakeATSProcess("ts") -server = Test.MakeOriginServer("server") - -# **testname is required** -testName = "" -request_header = {"headers": "GET / HTTP/1.1\r\nHost: www.example.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -response_header = { - "headers": - "HTTP/1.1 200 OK\r\nConnection: close\r\nLast-Modified: Tue, 08 May 2018 15:49:41 GMT\r\nCache-Control: max-age=1\r\n\r\n", - "timestamp": "1469733493.993", - "body": "xxx" -} -server.addResponse("sessionlog.json", request_header, response_header) - -# ATS Configuration -ts.Disk.plugin_config.AddLine('xdebug.so --enable=x-cache,x-cache-key,via') -ts.Disk.records_config.update( - { - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'http', - 'proxy.config.http.response_via_str': 3, - }) - -ts.Disk.remap_config.AddLine('map / http://127.0.0.1:{0}'.format(server.Variables.Port)) - -cache_and_req_body_miss = { - 'Connection': 'keep-alive', - 'Via': { - 'equal_re': None - }, - 'Server': { - 'equal_re': '.*' - }, - 'X-Cache-Key': { - 'equal_re': 'http://127.0.0.1.*' - }, - 'X-Cache': 'miss', - 'Last-Modified': { - 'equal_re': '.*' - }, - 'cache-control': 'max-age=1', - 'Content-Length': '3', - 'Date': { - 'equal_re': '.*' - }, - 'Age': { - 'equal_re': '.*' - } -} - -cache_and_req_body_hit = { - 'Last-Modified': { - 'equal_re': '.*' - }, - 'cache-control': 'max-age=1', - 'Content-Length': '3', - 'Date': { - 'equal_re': '.*' - }, - 'Age': { - 'equal_re': '.*' - }, - 'Connection': 'keep-alive', - 'Via': { - 'equal_re': '.*' - }, - 'Server': { - 'equal_re': '.*' - }, - 'X-Cache': 'hit-fresh', - 'HTTP/1.1 200 OK': '' -} - -cache_and_req_body_hit_close = { - 'Last-Modified': { - 'equal_re': '.*' - }, - 'cache-control': 'max-age=1', - 'Content-Length': '3', - 'Date': { - 'equal_re': '.*' - }, - 'Age': { - 'equal_re': '.*' - }, - 'Connection': 'close', - 'Via': { - 'equal_re': '.*' - }, - 'Server': { - 'equal_re': '.*' - }, - 'X-Cache': 'hit-fresh', - 'HTTP/1.1 200 OK': '' -} - -# Test 1 - 200 response and cache fill -tr = Test.AddTestRun() -tr.Processes.Default.StartBefore(server) -tr.Processes.Default.StartBefore(ts) -tr.MakeCurlCommand( - '-s -D - -v --ipv4 --http1.1 -H "x-debug: x-cache,x-cache-key,via" -H "Host: www.example.com" http://localhost:{port}/'.format( - port=ts.Variables.port), - ts=ts) -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Streams.stdout = Testers.CurlHeader(cache_and_req_body_miss) -tr.StillRunningAfter = ts - -# Test 2 - 200 cached response and using netcat -tr = Test.AddTestRun() -tr.MakeCurlCommand( - '-s -D - -v --ipv4 --http1.1 -H "x-debug: x-cache,x-cache-key,via" -H "Host: www.example.com" http://localhost:{}'.format( - ts.Variables.port), - ts=ts) -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Streams.stdout = Testers.CurlHeader(cache_and_req_body_hit) -tr.StillRunningAfter = ts - -# Test 3 - 200 cached response and trying to hide a request in the body -tr = Test.AddTestRun() -tr.Processes.Default.Command = "printf 'GET / HTTP/1.1\r\n''x-debug: x-cache,x-cache-key,via\r\n''Host: www.example.com\r\n''Content-Length: 71\r\n''\r\n''GET /index.html?evil=zorg810 HTTP/1.1\r\n''Host: dummy-host.example.com\r\n''\r\n'|nc 127.0.0.1 -w 1 {port}".format( - port=ts.Variables.port) -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Streams.stdout = Testers.CurlHeader(cache_and_req_body_hit) -tr.StillRunningAfter = ts - -# Test 4 - 200 cached response and Content-Length larger than bytes sent, MUST close -tr = Test.AddTestRun() -tr.Processes.Default.Command = "printf 'GET / HTTP/1.1\r\n''x-debug: x-cache,x-cache-key,via\r\n''Host: dummy-host.example.com\r\n''Cache-control: max-age=300\r\n''Content-Length: 100\r\n''\r\n''GET /index.html?evil=zorg810 HTTP/1.1\r\n''Host: dummy-host.example.com\r\n''\r\n'|nc 127.0.0.1 -w 1 {port}".format( - port=ts.Variables.port) -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Streams.stdout = Testers.CurlHeader(cache_and_req_body_hit_close) -tr.StillRunningAfter = ts diff --git a/tests/gold_tests/headers/field_name_space.test.py b/tests/gold_tests/headers/field_name_space.test.py deleted file mode 100644 index cd76ee74b01..00000000000 --- a/tests/gold_tests/headers/field_name_space.test.py +++ /dev/null @@ -1,53 +0,0 @@ -''' -Test on handling spaces after the field name and before the colon -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -Test.Summary = ''' -Checking on handling spaces after the field name and before the colon -''' - -Test.ContinueOnFail = True - -# Define default ATS -ts = Test.MakeATSProcess("ts") -server = Test.MakeOriginServer("server") - -testName = "field_name_space" -request_header = {"headers": "GET / HTTP/1.1\r\nHost: www.example.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -response_header = { - "headers": "HTTP/1.1 200 OK\r\nConnection: close\r\nFoo : 123\r\nFoo: 456\r\n", - "timestamp": "1469733493.993", - "body": "xxx" -} -server.addResponse("sessionlog.json", request_header, response_header) - -ts.Disk.remap_config.AddLine('map http://www.example.com http://127.0.0.1:{0}'.format(server.Variables.Port)) - -ipv4flag = "" -if not Condition.CurlUsingUnixDomainSocket(): - ipv4flag = "--ipv4" - -# Test spaces at the end of the field name and before the : -tr = Test.AddTestRun() -tr.Processes.Default.StartBefore(server, ready=When.PortOpen(server.Variables.Port)) -tr.Processes.Default.StartBefore(Test.Processes.ts) -tr.MakeCurlCommand( - '-s -D - -v {0} --http1.1 -H "Host: www.example.com" http://localhost:{1}/'.format(ipv4flag, ts.Variables.port), ts=ts) -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Streams.stderr = "gold/field_name_space.gold" -tr.StillRunningAfter = ts diff --git a/tests/gold_tests/headers/forwarded.test.py b/tests/gold_tests/headers/forwarded.test.py deleted file mode 100644 index 958e27a7c06..00000000000 --- a/tests/gold_tests/headers/forwarded.test.py +++ /dev/null @@ -1,267 +0,0 @@ -''' -Test the Forwarded header and related configuration.. -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import os - -Test.Summary = ''' -Test FORWARDED header. -''' -Test.SkipIf(Condition.CurlUsingUnixDomainSocket()) - -Test.SkipUnless( - Condition.HasCurlFeature('http2'), - Condition.HasCurlFeature('IPv6'), -) -Test.ContinueOnFail = True - -testName = "FORWARDED" - -server = Test.MakeOriginServer("server", options={'--load': os.path.join(Test.TestDirectory, 'forwarded-observer.py')}) - -request_header = {"headers": "GET / HTTP/1.1\r\nHost: www.no-oride.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -server.addResponse("sessionlog.json", request_header, response_header) -request_header = {"headers": "GET / HTTP/1.1\r\nHost: www.forwarded-none.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -server.addResponse("sessionlog.json", request_header, response_header) -request_header = {"headers": "GET / HTTP/1.1\r\nHost: www.forwarded-for.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -server.addResponse("sessionlog.json", request_header, response_header) -request_header = {"headers": "GET / HTTP/1.1\r\nHost: www.forwarded-by-ip.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -server.addResponse("sessionlog.json", request_header, response_header) -request_header = { - "headers": "GET / HTTP/1.1\r\nHost: www.forwarded-by-unknown.com\r\n\r\n", - "timestamp": "1469733493.993", - "body": "" -} -server.addResponse("sessionlog.json", request_header, response_header) -request_header = { - "headers": "GET / HTTP/1.1\r\nHost: www.forwarded-by-server-name.com\r\n\r\n", - "timestamp": "1469733493.993", - "body": "" -} -server.addResponse("sessionlog.json", request_header, response_header) -request_header = {"headers": "GET / HTTP/1.1\r\nHost: www.forwarded-by-uuid.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -server.addResponse("sessionlog.json", request_header, response_header) -request_header = {"headers": "GET / HTTP/1.1\r\nHost: www.forwarded-proto.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -server.addResponse("sessionlog.json", request_header, response_header) -request_header = {"headers": "GET / HTTP/1.1\r\nHost: www.forwarded-host.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -server.addResponse("sessionlog.json", request_header, response_header) -request_header = { - "headers": "GET / HTTP/1.1\r\nHost: www.forwarded-connection-compact.com\r\n\r\n", - "timestamp": "1469733493.993", - "body": "" -} -server.addResponse("sessionlog.json", request_header, response_header) -request_header = { - "headers": "GET / HTTP/1.1\r\nHost: www.forwarded-connection-std.com\r\n\r\n", - "timestamp": "1469733493.993", - "body": "" -} -server.addResponse("sessionlog.json", request_header, response_header) -request_header = { - "headers": "GET / HTTP/1.1\r\nHost: www.forwarded-connection-full.com\r\n\r\n", - "timestamp": "1469733493.993", - "body": "" -} -server.addResponse("sessionlog.json", request_header, response_header) - -# Set up to check the output after the tests have run. -# -forwarded_log_id = Test.Disk.File("forwarded.log") -forwarded_log_id.Content = "forwarded.gold" - - -def baselineTsSetup(ts): - - ts.addDefaultSSLFiles() - - ts.Disk.records_config.update( - { - # 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.url_remap.pristine_host_hdr': 1, # Retain Host header in original incoming client request. - 'proxy.config.proxy_name': 'Poxy_Proxy', # This will be the server name. - 'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir), - 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts.Variables.SSLDir) - }) - - ts.Disk.ssl_multicert_yaml.AddLines( - """ - ssl_multicert: - - dest_ip: "*" - ssl_cert_name: server.pem - ssl_key_name: server.key - """.split("\n")) - - ts.Disk.remap_config.AddLine('map http://www.no-oride.com http://127.0.0.1:{0}'.format(server.Variables.Port)) - - -# Disable the cache to make sure each request is forwarded to the origin -# server. -ts = Test.MakeATSProcess("ts", enable_tls=True, enable_cache=False) - -baselineTsSetup(ts) - -ts.Disk.remap_config.AddLine( - 'map http://www.forwarded-none.com http://127.0.0.1:{0}'.format(server.Variables.Port) + - ' @plugin=conf_remap.so @pparam=proxy.config.http.insert_forwarded=none') -ts.Disk.remap_config.AddLine( - 'map http://www.forwarded-for.com http://127.0.0.1:{0}'.format(server.Variables.Port) + - ' @plugin=conf_remap.so @pparam=proxy.config.http.insert_forwarded=for') -ts.Disk.remap_config.AddLine( - 'map http://www.forwarded-by-ip.com http://127.0.0.1:{0}'.format(server.Variables.Port) + - ' @plugin=conf_remap.so @pparam=proxy.config.http.insert_forwarded=by=ip') -ts.Disk.remap_config.AddLine( - 'map http://www.forwarded-by-unknown.com http://127.0.0.1:{0}'.format(server.Variables.Port) + - ' @plugin=conf_remap.so @pparam=proxy.config.http.insert_forwarded=by=unknown') -ts.Disk.remap_config.AddLine( - 'map http://www.forwarded-by-server-name.com http://127.0.0.1:{0}'.format(server.Variables.Port) + - ' @plugin=conf_remap.so @pparam=proxy.config.http.insert_forwarded=by=serverName') -ts.Disk.remap_config.AddLine( - 'map http://www.forwarded-by-uuid.com http://127.0.0.1:{0}'.format(server.Variables.Port) + - ' @plugin=conf_remap.so @pparam=proxy.config.http.insert_forwarded=by=uuid') -ts.Disk.remap_config.AddLine( - 'map http://www.forwarded-proto.com http://127.0.0.1:{0}'.format(server.Variables.Port) + - ' @plugin=conf_remap.so @pparam=proxy.config.http.insert_forwarded=proto') -ts.Disk.remap_config.AddLine( - 'map http://www.forwarded-host.com http://127.0.0.1:{0}'.format(server.Variables.Port) + - ' @plugin=conf_remap.so @pparam=proxy.config.http.insert_forwarded=host') -ts.Disk.remap_config.AddLine( - 'map http://www.forwarded-connection-compact.com http://127.0.0.1:{0}'.format(server.Variables.Port) + - ' @plugin=conf_remap.so @pparam=proxy.config.http.insert_forwarded=connection=compact') -ts.Disk.remap_config.AddLine( - 'map http://www.forwarded-connection-std.com http://127.0.0.1:{0}'.format(server.Variables.Port) + - ' @plugin=conf_remap.so @pparam=proxy.config.http.insert_forwarded=connection=std') -ts.Disk.remap_config.AddLine( - 'map http://www.forwarded-connection-full.com http://127.0.0.1:{0}'.format(server.Variables.Port) + - ' @plugin=conf_remap.so @pparam=proxy.config.http.insert_forwarded=connection=full') - -# Basic HTTP 1.1 -- No Forwarded by default -tr = Test.AddTestRun() -# Wait for the micro server -tr.Processes.Default.StartBefore(server, ready=When.PortOpen(server.Variables.Port)) -# Delay on readiness of our ssl ports -tr.Processes.Default.StartBefore(Test.Processes.ts) -# -tr.MakeCurlCommand('--verbose --ipv4 --http1.1 --proxy localhost:{} http://www.no-oride.com'.format(ts.Variables.port), ts=ts) -tr.Processes.Default.ReturnCode = 0 - - -def TestHttp1_1(host): - - tr = Test.AddTestRun() - tr.MakeCurlCommand('--verbose --ipv4 --http1.1 --proxy localhost:{} http://{}'.format(ts.Variables.port, host), ts=ts) - tr.Processes.Default.ReturnCode = 0 - - -# Basic HTTP 1.1 -- No Forwarded -- explicit configuration. -# -TestHttp1_1('www.forwarded-none.com') - -# Test enabling of each forwarded parameter singly. - -TestHttp1_1('www.forwarded-for.com') - -# Note: forwarded-observer.py counts on the "by" tests being done in the order below. - -TestHttp1_1('www.forwarded-by-ip.com') -TestHttp1_1('www.forwarded-by-unknown.com') -TestHttp1_1('www.forwarded-by-server-name.com') -TestHttp1_1('www.forwarded-by-uuid.com') - -TestHttp1_1('www.forwarded-proto.com') -TestHttp1_1('www.forwarded-host.com') -TestHttp1_1('www.forwarded-connection-compact.com') -TestHttp1_1('www.forwarded-connection-std.com') -TestHttp1_1('www.forwarded-connection-full.com') - -ts2 = Test.MakeATSProcess("ts2", enable_tls=True) - -baselineTsSetup(ts2) - -ts2.Disk.records_config.update( - { - # 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.url_remap.pristine_host_hdr': 1, # Retain Host header in original incoming client request. - 'proxy.config.http.insert_forwarded': 'by=uuid' - }) - -ts2.Disk.remap_config.AddLine('map https://www.no-oride.com http://127.0.0.1:{0}'.format(server.Variables.Port)) - -# Forwarded header with UUID of 2nd ATS. -tr = Test.AddTestRun() -# Delay on readiness of our ssl ports -tr.Processes.Default.StartBefore(Test.Processes.ts2) -# -tr.MakeCurlCommand('--verbose --ipv4 --http1.1 --proxy localhost:{} http://www.no-oride.com'.format(ts2.Variables.port), ts=ts2) -tr.Processes.Default.ReturnCode = 0 - -# Call traffic_ctrl to set insert_forwarded -tr = Test.AddTestRun() -tr.Processes.Default.Command = ( - 'traffic_ctl --debug config set proxy.config.http.insert_forwarded' + - ' "for|by=ip|by=unknown|by=servername|by=uuid|proto|host|connection=compact|connection=std|connection=full"') -tr.Processes.Default.ForceUseShell = False -tr.Processes.Default.Env = ts2.Env -tr.Processes.Default.ReturnCode = 0 - -# HTTP 1.1 -tr = Test.AddTestRun() -# Delay to give traffic_ctl config change time to take effect. -tr.DelayStart = 15 -tr.MakeCurlCommand('--verbose --ipv4 --http1.1 --proxy localhost:{} http://www.no-oride.com'.format(ts2.Variables.port), ts=ts2) -tr.Processes.Default.ReturnCode = 0 - -# HTTP 1.0 -tr = Test.AddTestRun() -tr.MakeCurlCommand('--verbose --ipv4 --http1.0 --proxy localhost:{} http://www.no-oride.com'.format(ts2.Variables.port), ts=ts2) -tr.Processes.Default.ReturnCode = 0 - -# HTTP 1.0 -- Forwarded headers already present -tr = Test.AddTestRun() -tr.MakeCurlCommand( - "--verbose -H 'forwarded:for=0.6.6.6' -H 'forwarded:for=_argh' --ipv4 --http1.0" + - " --proxy localhost:{} http://www.no-oride.com".format(ts2.Variables.port), - ts=ts2) -tr.Processes.Default.ReturnCode = 0 - -# HTTP 2 -tr = Test.AddTestRun() -tr.MakeCurlCommand( - '--verbose --ipv4 --http2 --insecure --header "Host: www.no-oride.com"' + - ' https://localhost:{}'.format(ts2.Variables.ssl_port), - ts=ts2) -tr.Processes.Default.ReturnCode = 0 - -# TLS -tr = Test.AddTestRun() -tr.MakeCurlCommand( - '--verbose --ipv4 --http1.1 --insecure --header "Host: www.no-oride.com" https://localhost:{}'.format(ts2.Variables.ssl_port), - ts=ts2) -tr.Processes.Default.ReturnCode = 0 - -# IPv6 - -tr = Test.AddTestRun() -tr.MakeCurlCommand('--verbose --ipv6 --http1.1 --proxy localhost:{} http://www.no-oride.com'.format(ts2.Variables.portv6), ts=ts2) -tr.Processes.Default.ReturnCode = 0 - -tr = Test.AddTestRun() -tr.MakeCurlCommand( - '--verbose --ipv6 --http1.1 --insecure --header "Host: www.no-oride.com" https://localhost:{}'.format(ts2.Variables.ssl_portv6), - ts=ts2) -tr.Processes.Default.ReturnCode = 0 diff --git a/tests/gold_tests/headers/general-connection-failure-502.test.py b/tests/gold_tests/headers/general-connection-failure-502.test.py deleted file mode 100644 index fd7c80cf91d..00000000000 --- a/tests/gold_tests/headers/general-connection-failure-502.test.py +++ /dev/null @@ -1,45 +0,0 @@ -''' -Test response when connection to origin fails -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import os -import sys - -Test.Summary = ''' -Test response when connection to origin fails -''' -ts = Test.MakeATSProcess("ts") - -HOST = 'www.connectfail502.test' -server = Test.MakeOriginServer("server", ssl=False) # Reserves a port across autest. - -ts.Disk.remap_config.AddLine('map http://{host} http://{ip}:{uport}'.format(host=HOST, ip='127.0.0.1', uport=server.Variables.Port)) - -Test.Setup.Copy(os.path.join(Test.Variables.AtsTestToolsDir, 'tcp_client.py')) - -data_file = Test.Disk.File("www.connectfail502.test-get.txt", id="datafile") -data_file.WriteOn("GET / HTTP/1.1\r\nHost: {host}\r\n\r\n".format(host=HOST)) - -tr = Test.AddTestRun() -tr.Processes.Default.StartBefore(Test.Processes.ts) -# Do not start the origin server: We wish to simulate connection refused while hopefully no one else uses this port. -tr.Processes.Default.Command = \ - (f"{sys.executable} tcp_client.py 127.0.0.1 {ts.Variables.port} www.connectfail502.test-get.txt | " - r"sed -e '/^Date: /d' -e '/^Server: ATS\//d'") -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Streams.stdout = 'general-connection-failure-502.gold' diff --git a/tests/gold_tests/headers/good_request_after_bad.test.py b/tests/gold_tests/headers/good_request_after_bad.test.py deleted file mode 100644 index 44aeb4e2855..00000000000 --- a/tests/gold_tests/headers/good_request_after_bad.test.py +++ /dev/null @@ -1,192 +0,0 @@ -''' -Verify that request following a ill-formed request is not processed -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import os - -Test.Summary = ''' -Verify that request following a ill-formed request is not processed -''' -Test.ContinueOnFail = True -ts = Test.MakeATSProcess("ts") -Test.ContinueOnFail = True -ts.Disk.records_config.update( - { - 'proxy.config.diags.debug.tags': 'http', - 'proxy.config.diags.debug.enabled': 0, - 'proxy.config.http.strict_uri_parsing': 1 - }) - -ts2 = Test.MakeATSProcess("ts2") - -ts2.Disk.records_config.update( - { - 'proxy.config.diags.debug.tags': 'http', - 'proxy.config.diags.debug.enabled': 0, - 'proxy.config.http.strict_uri_parsing': 2 - }) - -server = Test.MakeOriginServer("server") -request_header = {"headers": "GET / HTTP/1.1\r\nHost: www.example.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -response_header = { - "headers": - "HTTP/1.1 200 OK\r\nConnection: close\r\nLast-Modified: Tue, 08 May 2018 15:49:41 GMT\r\nCache-Control: max-age=1000\r\n\r\n", - "timestamp": "1469733493.993", - "body": "xxx" -} -server.addResponse("sessionlog.json", request_header, response_header) - -ts.Disk.remap_config.AddLine('map / http://127.0.0.1:{0}'.format(server.Variables.Port)) -ts.Disk.remap_config.AddLine('map /bob<> http://127.0.0.1:{0}'.format(server.Variables.Port)) -ts2.Disk.remap_config.AddLine('map / http://127.0.0.1:{0}'.format(server.Variables.Port)) -ts2.Disk.remap_config.AddLine('map /bob<> http://127.0.0.1:{0}'.format(server.Variables.Port)) - -trace_out = Test.Disk.File("trace_curl.txt") - -# Make a good request to get item in the cache for later tests -tr = Test.AddTestRun("Good control") -tr.Processes.Default.StartBefore(server) -tr.Processes.Default.StartBefore(Test.Processes.ts) -tr.Processes.Default.Command = 'printf "GET / HTTP/1.1\r\nHost: bob\r\n\r\n" | nc 127.0.0.1 {}'.format(ts.Variables.port) -tr.Processes.Default.ReturnCode = 0 - -tr = Test.AddTestRun("Good control") -tr.Processes.Default.StartBefore(server) -tr.Processes.Default.StartBefore(Test.Processes.ts2) -tr.Processes.Default.Command = 'printf "GET / HTTP/1.1\r\nHost: bob\r\n\r\n" | nc 127.0.0.1 {}'.format(ts2.Variables.port) -tr.Processes.Default.ReturnCode = 0 - -tr = Test.AddTestRun("space after header name") -tr.Processes.Default.Command = 'printf "GET / HTTP/1.1\r\nHost : bob\r\n\r\nGET / HTTP/1.1\r\nHost: boa\r\n\r\n" | nc 127.0.0.1 {}'.format( - ts.Variables.port) -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Streams.stdout = 'gold/bad_good_request.gold' - -tr = Test.AddTestRun("Bad protocol number") -tr.Processes.Default.Command = 'printf "GET / HTTP/11.1\r\nhost: bob\r\n\r\nGET / HTTP/1.1\r\nHost: boa\r\n\r\n" | nc 127.0.0.1 {}'.format( - ts.Variables.port) -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Streams.stdout = 'gold/bad_protocol_number.gold' - -tr = Test.AddTestRun("Unsupported Transfer Encoding value") -tr.Processes.Default.Command = 'printf "GET / HTTP/1.1\r\nhost: bob\r\ntransfer-encoding: random\r\n\r\nGET / HTTP/1.1\r\nHost: boa\r\n\r\n" | nc 127.0.0.1 {}'.format( - ts.Variables.port) -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Streams.stdout = 'gold/bad_te_value.gold' - -tr = Test.AddTestRun("Another unsupported Transfer Encoding value") -tr.Processes.Default.Command = 'printf "GET / HTTP/1.1\r\nhost: bob\r\ntransfer-encoding: \x08chunked\r\n\r\nGET / HTTP/1.1\r\nHost: boa\r\n\r\n" | nc 127.0.0.1 {}'.format( - ts.Variables.port) -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Streams.stdout = 'gold/invalid_character_in_te_value.gold' - -tr = Test.AddTestRun("Extra characters in content-length") -tr.Processes.Default.Command = 'printf "GET / HTTP/1.1\r\nhost: bob\r\ncontent-length:+3\r\n\r\nGET / HTTP/1.1\r\nHost: boa\r\n\r\n" | nc 127.0.0.1 {}'.format( - ts.Variables.port) -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Streams.stdout = 'gold/bad_good_request_header.gold' - -tr = Test.AddTestRun("Different extra characters in content-length") -tr.Processes.Default.Command = 'printf "GET / HTTP/1.1\r\nhost: bob\r\ncontent-length:\x0c3\r\n\r\nGET / HTTP/1.1\r\nHost: boa\r\n\r\n" | nc 127.0.0.1 {}'.format( - ts.Variables.port) -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Streams.stdout = 'gold/bad_good_request_header.gold' - -# TRACE request with a body -tr = Test.AddTestRun("Trace request with a body") -tr.Processes.Default.Command = 'printf "TRACE /foo HTTP/1.1\r\nHost: bob\r\nContent-length:2\r\n\r\nokGET / HTTP/1.1\r\nHost: boa\r\n\r\n" | nc 127.0.0.1 {}'.format( - ts.Variables.port) -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Streams.stdout = 'gold/bad_good_request.gold' - -tr = Test.AddTestRun("Trace request with a chunked body") -tr.Processes.Default.Command = 'printf "TRACE /foo HTTP/1.1\r\nHost: bob\r\ntransfer-encoding: chunked\r\n\r\n2\r\nokGGET / HTTP/1.1\r\nHost: boa\r\n\r\n" | nc 127.0.0.1 {}'.format( - ts.Variables.port) -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Streams.stdout = 'gold/bad_good_request.gold' - -tr = Test.AddTestRun("Trace request with a chunked body via curl") -tr.MakeCurlCommand( - '-v --http1.1 --header "Transfer-Encoding: chunked" -d aaa -X TRACE -o trace_curl.txt -k http://127.0.0.1:{}/foo'.format( - ts.Variables.port), - ts=ts) -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Streams.All = 'gold/bad_good_request_header.gold' -trace_out.Content = Testers.ContainsExpression("Bad Request", "ATS error msg") -trace_out.Content += Testers.ContainsExpression("Description: Could not process this request.", "ATS error msg") - -tr = Test.AddTestRun("Trace request via curl") -tr.MakeCurlCommand('-v --http1.1 -X TRACE -k http://127.0.0.1:{}/bar'.format(ts.Variables.port), ts=ts) -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Streams.All = Testers.ContainsExpression( - r"HTTP/1.1 501 Unsupported method \('TRACE'\)", "microserver does not support TRACE") - -# Methods are case sensitive. Verify that "gET" is not confused with "GET". -tr = Test.AddTestRun("mixed case method") -tr.Processes.Default.Command = 'printf "gET / HTTP/1.1\r\nHost:bob\r\n\r\nGET / HTTP/1.1\r\nHost: boa\r\n\r\n" | nc 127.0.0.1 {}'.format( - ts.Variables.port) -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Streams.stdout = 'gold/bad_method.gold' - -# mangled termination -tr = Test.AddTestRun("mangled line termination") -tr.Processes.Default.Command = 'printf "GET / HTTP/1.1\r\nHost:bob\r\n \r\nGET / HTTP/1.1\r\nHost: boa\r\n\r\n" | nc 127.0.0.1 {}'.format( - ts.Variables.port) -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Streams.stdout = 'gold/bad_good_request.gold' - -tr = Test.AddTestRun("Catch bad URL characters") -tr.Processes.Default.Command = 'printf "GET /bob<> HTTP/1.1\r\nhost: bob\r\n\r\nGET / HTTP/1.1\r\nHost: boa\r\n\r\n" | nc 127.0.0.1 {}'.format( - ts.Variables.port) -tr.Processes.Default.ReturnCode = 0 -# Since the request line is messsed up ATS will reply with HTTP/1.0 -tr.Processes.Default.Streams.stdout = 'gold/bad_good_request_http1.gold' - -tr = Test.AddTestRun("Catch whitespace in URL") -tr.Processes.Default.Command = 'printf "GET /bob foo HTTP/1.1\r\nhost: bob\r\n\r\nGET / HTTP/1.1\r\nHost: boa\r\n\r\n" | nc 127.0.0.1 {}'.format( - ts.Variables.port) -tr.Processes.Default.ReturnCode = 0 -# Since the request line is messsed up ATS will reply with HTTP/1.0 -tr.Processes.Default.Streams.stdout = 'gold/bad_good_request_http1.gold' - -tr = Test.AddTestRun("Extra characters in protocol") -tr.Processes.Default.Command = 'printf "GET / HTP/1.1\r\nhost: bob\r\n\r\nGET / HTTP/1.1\r\nHost: boa\r\n\r\n" | nc 127.0.0.1 {}'.format( - ts.Variables.port) -tr.Processes.Default.ReturnCode = 0 -# Since the request line is messsed up ATS will reply with HTTP/1.0 -tr.Processes.Default.Streams.stdout = 'gold/bad_good_request_http1.gold' - -tr = Test.AddTestRun("Characters that are strict but not case 2 bad") -tr.Processes.Default.Command = 'printf "GET /bob<> HTTP/1.1\r\nhost: bob\r\n\r\nGET / HTTP/1.1\r\nHost: boa\r\n\r\n" | nc 127.0.0.1 {}'.format( - ts2.Variables.port) -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Streams.All = Testers.ContainsExpression("HTTP/1.1 200 OK", "Success") - -tr = Test.AddTestRun("Catch whitespace in URL") -tr.Processes.Default.Command = 'printf "GET /bob foo HTTP/1.1\r\nhost: bob\r\n\r\nGET / HTTP/1.1\r\nHost: boa\r\n\r\n" | nc 127.0.0.1 {}'.format( - ts2.Variables.port) -tr.Processes.Default.ReturnCode = 0 -# Since the request line is messsed up ATS will reply with HTTP/1.0 -tr.Processes.Default.Streams.stdout = 'gold/bad_good_request_http1.gold' - -tr = Test.AddTestRun("Extra characters in protocol") -tr.Processes.Default.Command = 'printf "GET / HTP/1.1\r\nhost: bob\r\n\r\nGET / HTTP/1.1\r\nHost: boa\r\n\r\n" | nc 127.0.0.1 {}'.format( - ts2.Variables.port) -tr.Processes.Default.ReturnCode = 0 -# Since the request line is messsed up ATS will reply with HTTP/1.0 -tr.Processes.Default.Streams.stdout = 'gold/bad_good_request_http1.gold' diff --git a/tests/gold_tests/headers/http408.test.py b/tests/gold_tests/headers/http408.test.py deleted file mode 100644 index 21626738c43..00000000000 --- a/tests/gold_tests/headers/http408.test.py +++ /dev/null @@ -1,56 +0,0 @@ -''' -Test the 408 response header. -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import os -import sys - -Test.Summary = ''' -Check 408 response header for protocol stack data. -''' - -Test.ContinueOnFail = True - -# Define default ATS -ts = Test.MakeATSProcess("ts") -server = Test.MakeOriginServer("server") - -HTTP_408_HOST = 'www.http408.test' - -request_header = {"headers": "GET / HTTP/1.1\r\nHost: {}\r\n\r\n".format(HTTP_408_HOST), "timestamp": "1469733493.993", "body": ""} -response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -server.addResponse("sessionlog.json", request_header, response_header) - -ts.Disk.remap_config.AddLine('map http://{0} http://127.0.0.1:{1}'.format(HTTP_408_HOST, server.Variables.Port)) - -TIMEOUT = 2 -ts.Disk.records_config.update({ - 'proxy.config.http.transaction_no_activity_timeout_in': TIMEOUT, -}) - -Test.Setup.Copy(os.path.join(os.pardir, os.pardir, 'tools', 'tcp_client.py')) -Test.Setup.Copy('data') - -tr = Test.AddTestRun() -tr.Processes.Default.StartBefore(server) -tr.Processes.Default.StartBefore(Test.Processes.ts) -DELAY = TIMEOUT + 2 -tr.Processes.Default.Command = f'{sys.executable} tcp_client.py 127.0.0.1 {ts.Variables.port} data/{HTTP_408_HOST}.txt --delay-after-send {DELAY}' -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.TimeOut = 10 -tr.Processes.Default.Streams.stdout = "http408.gold" diff --git a/tests/gold_tests/headers/normalize_ae.test.py b/tests/gold_tests/headers/normalize_ae.test.py deleted file mode 100644 index ca38755bfe9..00000000000 --- a/tests/gold_tests/headers/normalize_ae.test.py +++ /dev/null @@ -1,200 +0,0 @@ -''' -Test normalizations of the Accept-Encoding header field. -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import os - -Test.Summary = ''' -Test normalizations of the Accept-Encoding header field. -''' - -Test.SkipUnless(Condition.HasATSFeature('TS_HAS_BROTLI')) - -Test.ContinueOnFail = True - -server = Test.MakeOriginServer("server", options={'--load': os.path.join(Test.TestDirectory, 'normalize_ae_observer.py')}) - -testName = "NORMALIZE_AE" - -request_header = {"headers": "GET / HTTP/1.1\r\nHost: www.no-oride.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -server.addResponse("sessionlog.json", request_header, response_header) -request_header = {"headers": "GET / HTTP/1.1\r\nHost: www.ae-0.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -server.addResponse("sessionlog.json", request_header, response_header) -request_header = {"headers": "GET / HTTP/1.1\r\nHost: www.ae-1.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -server.addResponse("sessionlog.json", request_header, response_header) -request_header = {"headers": "GET / HTTP/1.1\r\nHost: www.ae-2.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -server.addResponse("sessionlog.json", request_header, response_header) -request_header = {"headers": "GET / HTTP/1.1\r\nHost: www.ae-4.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -server.addResponse("sessionlog.json", request_header, response_header) -request_header = {"headers": "GET / HTTP/1.1\r\nHost: www.ae-5.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -server.addResponse("sessionlog.json", request_header, response_header) - -# Define first ATS. Disable the cache to make sure each request is sent to the -# origin server. -ts = Test.MakeATSProcess("ts", enable_cache=False) - - -def baselineTsSetup(ts): - - ts.Disk.records_config.update({ - # 'proxy.config.diags.debug.enabled': 1, - }) - - ts.Disk.remap_config.AddLine('map http://www.no-oride.com http://127.0.0.1:{0}'.format(server.Variables.Port)) - ts.Disk.remap_config.AddLine( - 'map http://www.ae-0.com http://127.0.0.1:{0}'.format(server.Variables.Port) + - ' @plugin=conf_remap.so @pparam=proxy.config.http.normalize_ae=0') - ts.Disk.remap_config.AddLine( - 'map http://www.ae-1.com http://127.0.0.1:{0}'.format(server.Variables.Port) + - ' @plugin=conf_remap.so @pparam=proxy.config.http.normalize_ae=1') - ts.Disk.remap_config.AddLine( - 'map http://www.ae-2.com http://127.0.0.1:{0}'.format(server.Variables.Port) + - ' @plugin=conf_remap.so @pparam=proxy.config.http.normalize_ae=2') - ts.Disk.remap_config.AddLine( - 'map http://www.ae-3.com http://127.0.0.1:{0}'.format(server.Variables.Port) + - ' @plugin=conf_remap.so @pparam=proxy.config.http.normalize_ae=3') - ts.Disk.remap_config.AddLine( - 'map http://www.ae-4.com http://127.0.0.1:{0}'.format(server.Variables.Port) + - ' @plugin=conf_remap.so @pparam=proxy.config.http.normalize_ae=4') - ts.Disk.remap_config.AddLine( - 'map http://www.ae-5.com http://127.0.0.1:{0}'.format(server.Variables.Port) + - ' @plugin=conf_remap.so @pparam=proxy.config.http.normalize_ae=5') - - -baselineTsSetup(ts) - -# set up to check the output after the tests have run. -# -normalize_ae_log_id = Test.Disk.File("normalize_ae.log") -normalize_ae_log_id.Content = "normalize_ae.gold" - -# Try various Accept-Encoding header fields for a particular traffic server and host. - -ipv4flag = "" -if not Condition.CurlUsingUnixDomainSocket(): - ipv4flag = "--ipv4" - - -def allAEHdrs(shouldWaitForUServer, shouldWaitForTs, ts, host): - - tr = test.AddTestRun() - - if shouldWaitForUServer: - # wait for the micro server - tr.Processes.Default.StartBefore(server) - - if shouldWaitForTs: - # wait for the micro server - # delay on readiness of port - tr.Processes.Default.StartBefore(ts) - - baseCurl = '--verbose {0} --http1.1 --proxy localhost:{1} '.format(ipv4flag, ts.Variables.port) - - # No Accept-Encoding header. - # - tr.MakeCurlCommand(baseCurl + '--header "X-Au-Test: {0}" http://{0}'.format(host), ts=ts) - tr.Processes.Default.ReturnCode = 0 - - def curlTail(hdrValue): - return '--header "Accept-Encoding: {}" http://'.format(hdrValue) + host - - tr = test.AddTestRun() - tr.MakeCurlCommand(baseCurl + curlTail('gzip'), ts=ts) - tr.Processes.Default.ReturnCode = 0 - - tr = test.AddTestRun() - tr.MakeCurlCommand(baseCurl + curlTail('x-gzip'), ts=ts) - tr.Processes.Default.ReturnCode = 0 - - tr = test.AddTestRun() - tr.MakeCurlCommand(baseCurl + curlTail('br'), ts=ts) - tr.Processes.Default.ReturnCode = 0 - - tr = test.AddTestRun() - tr.MakeCurlCommand(baseCurl + curlTail('gzip, br'), ts=ts) - tr.Processes.Default.ReturnCode = 0 - - tr = test.AddTestRun() - tr.MakeCurlCommand(baseCurl + curlTail('gzip;q=0.3, whatever;q=0.666, br;q=0.7'), ts=ts) - tr.Processes.Default.ReturnCode = 0 - - # ZSTD-related tests for normalize_ae modes 4 and 5 - tr = test.AddTestRun() - tr.MakeCurlCommand(baseCurl + curlTail('zstd')) - tr.Processes.Default.ReturnCode = 0 - - tr = test.AddTestRun() - tr.MakeCurlCommand(baseCurl + curlTail('zstd, gzip')) - tr.Processes.Default.ReturnCode = 0 - - tr = test.AddTestRun() - tr.MakeCurlCommand(baseCurl + curlTail('zstd, br')) - tr.Processes.Default.ReturnCode = 0 - - tr = test.AddTestRun() - tr.MakeCurlCommand(baseCurl + curlTail('zstd, br, gzip')) - tr.Processes.Default.ReturnCode = 0 - - tr = test.AddTestRun() - tr.MakeCurlCommand(baseCurl + curlTail('gzip, zstd, br')) - tr.Processes.Default.ReturnCode = 0 - - tr = test.AddTestRun() - tr.MakeCurlCommand(baseCurl + curlTail('br, zstd')) - tr.Processes.Default.ReturnCode = 0 - - tr = test.AddTestRun() - tr.MakeCurlCommand(baseCurl + curlTail('zstd;q=0.8, br;q=0.7, gzip;q=0.6')) - tr.Processes.Default.ReturnCode = 0 - - tr = test.AddTestRun() - tr.MakeCurlCommand(baseCurl + curlTail('deflate, zstd')) - tr.Processes.Default.ReturnCode = 0 - - tr = test.AddTestRun() - tr.MakeCurlCommand(baseCurl + curlTail('identity, zstd, compress')) - tr.Processes.Default.ReturnCode = 0 - - tr = test.AddTestRun() - tr.MakeCurlCommand(baseCurl + curlTail('br, compress')) - tr.Processes.Default.ReturnCode = 0 - - -def perTsTest(shouldWaitForUServer, ts): - allAEHdrs(shouldWaitForUServer, True, ts, 'www.no-oride.com') - allAEHdrs(False, False, ts, 'www.ae-0.com') - allAEHdrs(False, False, ts, 'www.ae-1.com') - allAEHdrs(False, False, ts, 'www.ae-2.com') - allAEHdrs(False, False, ts, 'www.ae-3.com') - allAEHdrs(False, False, ts, 'www.ae-4.com') - allAEHdrs(False, False, ts, 'www.ae-5.com') - - -perTsTest(True, ts) - -# Define second ATS -ts2 = Test.MakeATSProcess("ts2") - -baselineTsSetup(ts2) - -ts2.Disk.records_config.update({ - 'proxy.config.http.normalize_ae': 0, -}) - -perTsTest(False, ts2) diff --git a/tests/gold_tests/headers/oversized_field_h2.test.py b/tests/gold_tests/headers/oversized_field_h2.test.py deleted file mode 100644 index 11d11e98bce..00000000000 --- a/tests/gold_tests/headers/oversized_field_h2.test.py +++ /dev/null @@ -1,142 +0,0 @@ -''' -Verify that an HTTP/2 header whose name or value length exceeds the uint16_t -field-length limit (>= 65535 bytes) is treated as an HPACK connection error -(GOAWAY with COMPRESSION_ERROR, code 0x9) and is never forwarded to the origin. -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import sys - -Test.Summary = ''' -Verify that an HTTP/2 header field whose name or value length exceeds the -uint16_t field-length limit (>= 65535 bytes) is rejected as an HPACK connection -error (GOAWAY COMPRESSION_ERROR, code 0x9) rather than stored with a truncated -length, and is never forwarded to the origin. -''' - -Test.SkipUnless(Condition.HasOpenSSLVersion('1.1.1'), Condition.HasProxyVerifierVersion('2.8.0')) - - -class OversizedFieldH2Test: - '''Drive an oversized HTTP/2 header with a raw HPACK client.''' - - replayFile = "replay/oversized_field_h2.replay.yaml" - clientScript = "oversized_field_h2_client.py" - - # Header sizes large enough to exceed the uint16_t (65535) field-length limit. - oversizedSize = 70000 - - def __init__(self): - self.__setupOriginServer() - self.__setupTS() - self.__setupClient() - - def __setupOriginServer(self): - self._server = Test.MakeVerifierServerProcess("verifier-server", self.replayFile) - # The origin must never receive the oversized requests. If ATS forwarded - # them (the pre-fix behavior), the verifier server would log a request - # for these paths / serve the marker bodies. - self._server.Streams.All += Testers.ExcludesExpression( - 'h2-oversized-value', 'Origin must not receive the oversized-value request.') - self._server.Streams.All += Testers.ExcludesExpression( - 'h2-oversized-name', 'Origin must not receive the oversized-name request.') - # Regression guard: the normal, under-limit request MUST reach the origin. - self._server.Streams.All += Testers.ContainsExpression('h2-normal', 'Origin must receive the normal under-limit request.') - - def __setupTS(self): - self._ts = Test.MakeATSProcess("ts", enable_tls=True, enable_cache=False) - self._ts.addDefaultSSLFiles() - self._ts.Disk.records_config.update( - { - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'http2|hpack', - 'proxy.config.ssl.server.cert.path': f"{self._ts.Variables.SSLDir}", - 'proxy.config.ssl.server.private_key.path': f"{self._ts.Variables.SSLDir}", - # max_header_list_size is raised well above the oversized field so the - # request is not rejected at the HTTP/2 header-list-size level first. - # header_field_max_size is set to its maximum (65535, the uint16_t - # ceiling enforced by the records range check); a field larger than that - # is rejected by the configured field-size limit with a COMPRESSION_ERROR. - # The uint16_t storage limit in the MIME setters is exercised directly by - # the HpackIndexingTable unit test: header_field_max_size can no longer be - # configured above 65535, so an oversized field can no longer reach the - # storage path end to end through config. - 'proxy.config.http.header_field_max_size': 65535, - 'proxy.config.http2.max_header_list_size': 8 * 1024 * 1024, - }) - # Rejecting the oversized field is an HPACK connection error, so ATS - # intentionally logs "ERROR: HTTP/2 connection error code=0x09 ... - # compression error". Whitelist exactly that line; the default check - # treats any "ERROR:" in diags.log as a failure, so assert this - # expected line is present instead. - self._ts.Disk.diags_log.Content = Testers.ContainsExpression( - r"ERROR: HTTP/2 connection error code=0x09 .* compression error", - "ATS must log the expected HTTP/2 COMPRESSION_ERROR for the oversized field.") - self._ts.Disk.remap_config.AddLine(f"map / http://127.0.0.1:{self._server.Variables.http_port}") - self._ts.Disk.ssl_multicert_yaml.AddLines( - """ -ssl_multicert: - - dest_ip: "*" - ssl_cert_name: server.pem - ssl_key_name: server.key -""".split("\n")) - - def __setupClient(self): - self._ts.Setup.CopyAs(f"clients/{self.clientScript}", Test.RunDirectory) - - def run(self): - # Case 1: oversized header VALUE. - tr = Test.AddTestRun("oversized H2 header value") - tr.Processes.Default.StartBefore(self._server) - tr.Processes.Default.StartBefore(self._ts) - port = self._ts.Variables.ssl_port - tr.Processes.Default.Command = ( - f"{sys.executable} {self.clientScript} /h2-oversized-value 0 {self.oversizedSize} 127.0.0.1 {port} example.com") - tr.Processes.Default.ReturnCode = 0 - # No :status (connection error, not a response) and GOAWAY COMPRESSION_ERROR (0x9). - tr.Processes.Default.Streams.All += Testers.ContainsExpression( - 'status=None', 'Client must not get an HTTP response for the oversized header.') - tr.Processes.Default.Streams.All += Testers.ContainsExpression( - 'goaway_error=9', 'ATS must send GOAWAY with COMPRESSION_ERROR (0x9).') - tr.StillRunningAfter = self._ts - tr.StillRunningAfter = self._server - - # Case 2: oversized header NAME. - tr = Test.AddTestRun("oversized H2 header name") - tr.Processes.Default.Command = ( - f"{sys.executable} {self.clientScript} /h2-oversized-name {self.oversizedSize} 0 127.0.0.1 {port} example.com") - tr.Processes.Default.ReturnCode = 0 - tr.Processes.Default.Streams.All += Testers.ContainsExpression( - 'status=None', 'Client must not get an HTTP response for the oversized header.') - tr.Processes.Default.Streams.All += Testers.ContainsExpression( - 'goaway_error=9', 'ATS must send GOAWAY with COMPRESSION_ERROR (0x9).') - tr.StillRunningAfter = self._ts - tr.StillRunningAfter = self._server - - # Case 3: NORMAL, under-limit request (regression guard against - # over-rejection). Sanity mode "0 0" sends a plain GET; the client must - # get a 200 and the origin must receive it. - tr = Test.AddTestRun("normal under-limit H2 request") - tr.Processes.Default.Command = (f"{sys.executable} {self.clientScript} /h2-normal 0 0 127.0.0.1 {port} example.com") - tr.Processes.Default.ReturnCode = 0 - tr.Processes.Default.Streams.All += Testers.ContainsExpression( - 'status=200', 'Client must get a 200 for the normal under-limit request.') - tr.StillRunningAfter = self._ts - tr.StillRunningAfter = self._server - - -OversizedFieldH2Test().run() diff --git a/tests/gold_tests/headers/syntax.test.py b/tests/gold_tests/headers/syntax.test.py deleted file mode 100644 index 00d30cb260e..00000000000 --- a/tests/gold_tests/headers/syntax.test.py +++ /dev/null @@ -1,99 +0,0 @@ -''' -Test whitespace between field name and colon in the header -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -Test.Summary = ''' -Test whitespace between field name and colon in the header -''' - -Test.ContinueOnFail = True - -# Define default ATS -ts = Test.MakeATSProcess("ts") -server = Test.MakeOriginServer("server") - -# **testname is required** -testName = "" -request_header = {"headers": "GET / HTTP/1.1\r\nHost: www.example.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -server.addResponse("sessionlog.json", request_header, response_header) - -ts.Disk.remap_config.AddLine('map http://www.example.com http://127.0.0.1:{0}'.format(server.Variables.Port)) - -ipv4flag = "" -if not Condition.CurlUsingUnixDomainSocket(): - ipv4flag = "--ipv4" - -# Test 0 - 200 Response -tr = Test.AddTestRun() -tr.Processes.Default.StartBefore(server, ready=When.PortOpen(server.Variables.Port)) -tr.Processes.Default.StartBefore(Test.Processes.ts) -tr.MakeCurlCommand( - '-s -D - -v {0} --http1.1 -H " foo: bar" -H "Host: www.example.com" http://localhost:{1}/'.format(ipv4flag, ts.Variables.port), - ts=ts) -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Streams.stdout = "syntax.200.gold" -tr.StillRunningAfter = ts - -# Test 1 - 400 Response - Single space after field name -tr = Test.AddTestRun() -tr.MakeCurlCommand( - '-s -D - -v {0} --http1.1 -H "foo : bar" -H "Host: www.example.com" http://localhost:{1}/'.format(ipv4flag, ts.Variables.port), - ts=ts) -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Streams.stdout = "syntax.400.gold" -tr.StillRunningAfter = ts - -# Test 2 - 400 Response - Double space after field name -tr = Test.AddTestRun() -tr.MakeCurlCommand( - '-s -D - -v {0} --http1.1 -H "foo : bar" -H "Host: www.example.com" http://localhost:{1}/'.format(ipv4flag, ts.Variables.port), - ts=ts) -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Streams.stdout = "syntax.400.gold" -tr.StillRunningAfter = ts - -# Test 3 - 400 Response - Three different Content-Length headers -tr = Test.AddTestRun() -tr.MakeCurlCommand( - '-s -D - -v {0} --http1.1 -d "hello world" -H "Content-Length: 11" -H "Content-Length: 10" -H "Content-Length: 9" -H "Host: www.example.com" http://localhost:{1}/' - .format(ipv4flag, ts.Variables.port), - ts=ts) -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Streams.stdout = "syntax.400.gold" -tr.StillRunningAfter = ts - -# Test 4 - 200 Response - Three same Content-Length headers -tr = Test.AddTestRun() -tr.MakeCurlCommand( - '-s -D - -v {0} --http1.1 -d "hello world" -H "Content-Length: 11" -H "Content-Length: 11" -H "Content-Length: 11" -H "Host: www.example.com" http://localhost:{1}/' - .format(ipv4flag, ts.Variables.port), - ts=ts) -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Streams.stdout = "syntax.200.gold" -tr.StillRunningAfter = ts - -# Test 5 - 200 Response - Three different Content-Length headers with a Transfer encoding header -tr = Test.AddTestRun() -tr.MakeCurlCommand( - '-s -D - -v {0} --http1.1 -d "hello world" -H "Transfer-Encoding: chunked" -H "Content-Length: 11" -H "Content-Length: 10" -H "Content-Length: 9" -H "Host: www.example.com" http://localhost:{1}/' - .format(ipv4flag, ts.Variables.port), - ts=ts) -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Streams.stdout = "syntax.200.gold" -tr.StillRunningAfter = ts diff --git a/tests/gold_tests/headers/via.test.py b/tests/gold_tests/headers/via.test.py deleted file mode 100644 index 0a9f527cf07..00000000000 --- a/tests/gold_tests/headers/via.test.py +++ /dev/null @@ -1,155 +0,0 @@ -''' -Test the VIA header. This runs several requests through ATS and extracts the upstream VIA headers. -Those are then checked against a gold file to verify the protocol stack based output is correct. -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import os - -Test.Summary = ''' -Check VIA header for protocol stack data. -''' - -Test.SkipUnless(Condition.HasCurlFeature('http2'), Condition.HasCurlFeature('IPv6')) -Test.ContinueOnFail = True - -# Define default ATS -if Condition.HasATSFeature('TS_USE_QUIC') and Condition.HasCurlFeature('http3'): - ts = Test.MakeATSProcess("ts", enable_tls=True, enable_quic=True) -else: - ts = Test.MakeATSProcess("ts", enable_tls=True) - -server = Test.MakeOriginServer("server", options={'--load': os.path.join(Test.TestDirectory, 'via-observer.py')}) - -testName = "VIA" - -# We only need one transaction as only the VIA header will be checked. -request_header = {"headers": "GET / HTTP/1.1\r\nHost: www.example.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -server.addResponse("sessionlog.json", request_header, response_header) - -# These should be promoted rather than other tests like this reaching around. -ts.addDefaultSSLFiles() - -ts.Disk.records_config.update( - { - 'proxy.config.http.insert_request_via_str': 4, - 'proxy.config.http.insert_response_via_str': 4, - 'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir), - 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts.Variables.SSLDir), - }) - -ts.Disk.remap_config.AddLine('map http://www.example.com http://127.0.0.1:{0}'.format(server.Variables.Port)) -ts.Disk.remap_config.AddLine( - 'map https://www.example.com http://127.0.0.1:{0}'.format(server.Variables.Port, ts.Variables.ssl_port)) - -ts.Disk.ssl_multicert_yaml.AddLines( - """ -ssl_multicert: - - dest_ip: "*" - ssl_cert_name: server.pem - ssl_key_name: server.key -""".split("\n")) - -# Set up to check the output after the tests have run. -via_log_id = Test.Disk.File("via.log") -if Condition.HasATSFeature('TS_USE_QUIC') and Condition.HasCurlFeature('http3'): - via_log_id.Content = "via_h3.gold" -elif Condition.CurlUsingUnixDomainSocket(): - via_log_id.Content = "via_uds.gold" -else: - via_log_id.Content = "via.gold" - -ipv4flag = "" -ipv6flag = "" -if not Condition.CurlUsingUnixDomainSocket(): - ipv4flag = "--ipv4" - ipv6flag = "--ipv6" - -# Basic HTTP 1.1 -tr = Test.AddTestRun() -# Wait for the micro server -tr.Processes.Default.StartBefore(server, ready=When.PortOpen(server.Variables.Port)) -# Delay on readiness of our ssl ports -tr.Processes.Default.StartBefore(Test.Processes.ts) - -tr.MakeCurlCommand( - '--verbose {0} --http1.1 --proxy localhost:{1} http://www.example.com'.format(ipv4flag, ts.Variables.port), ts=ts) -tr.Processes.Default.ReturnCode = 0 - -tr.StillRunningAfter = server -tr.StillRunningAfter = ts - -# HTTP 1.0 -tr = Test.AddTestRun() -tr.MakeCurlCommand( - '--verbose {0} --http1.0 --proxy localhost:{1} http://www.example.com'.format(ipv4flag, ts.Variables.port), ts=ts) -tr.Processes.Default.ReturnCode = 0 - -tr.StillRunningAfter = server -tr.StillRunningAfter = ts - -if not Condition.CurlUsingUnixDomainSocket(): - # HTTP 2 - tr = Test.AddTestRun() - tr.MakeCurlCommand( - '--verbose --ipv4 --http2 --insecure --header "Host: www.example.com" https://localhost:{}'.format(ts.Variables.ssl_port), - ts=ts) - tr.Processes.Default.ReturnCode = 0 - - tr.StillRunningAfter = server - tr.StillRunningAfter = ts - - # HTTP 3 - if Condition.HasATSFeature('TS_USE_QUIC') and Condition.HasCurlFeature('http3'): - tr = Test.AddTestRun() - tr.MakeCurlCommand( - '--verbose --ipv4 --http3 --insecure --header "Host: www.example.com" https://localhost:{}'.format( - ts.Variables.ssl_port), - ts=ts) - tr.Processes.Default.ReturnCode = 0 - tr.StillRunningAfter = server - tr.StillRunningAfter = ts - - # TLS - tr = Test.AddTestRun() - tr.MakeCurlCommand( - '--verbose --ipv4 --http1.1 --insecure --header "Host: www.example.com" https://localhost:{}'.format(ts.Variables.ssl_port), - ts=ts) - tr.Processes.Default.ReturnCode = 0 - - tr.StillRunningAfter = server - tr.StillRunningAfter = ts - -# IPv6 -tr = Test.AddTestRun() -tr.MakeCurlCommand( - '--verbose {0} --http1.1 --proxy localhost:{1} http://www.example.com'.format(ipv6flag, ts.Variables.portv6), ts=ts) -tr.Processes.Default.ReturnCode = 0 -tr.StillRunningAfter = server -tr.StillRunningAfter = ts - -if not Condition.CurlUsingUnixDomainSocket(): - tr = Test.AddTestRun() - tr.MakeCurlCommand( - '--verbose --ipv6 --http1.1 --insecure --header "Host: www.example.com" https://localhost:{}'.format( - ts.Variables.ssl_portv6), - ts=ts) - tr.Processes.Default.ReturnCode = 0 - - tr.StillRunningAfter = server - tr.StillRunningAfter = ts diff --git a/tests/gold_tests/ip_allow/connect_destination_acl.test.py b/tests/gold_tests/ip_allow/connect_destination_acl.test.py deleted file mode 100644 index 586a946e597..00000000000 --- a/tests/gold_tests/ip_allow/connect_destination_acl.test.py +++ /dev/null @@ -1,174 +0,0 @@ -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. -''' -Verify outbound ip_allow filtering for CONNECT destinations. -''' - -Test.Summary = ''' -Verify outbound ip_allow filtering for CONNECT destinations. -''' - -Test.SkipIf(Condition.CurlUsingUnixDomainSocket()) -Test.ContinueOnFail = True - -server = Test.MakeOriginServer("server", ssl=True) - -request = { - "headers": f"GET / HTTP/1.1\r\nHost: 127.0.0.1:{server.Variables.SSL_Port}\r\n\r\n", - "timestamp": "1469733493.993", - "body": "", -} -response = { - "headers": "HTTP/1.1 200 OK\r\nContent-Length: 0\r\nConnection: close\r\n\r\n", - "timestamp": "1469733493.993", - "body": "", -} -server.addResponse("sessionlog.json", request, response) - - -def configure_ts(name): - ts = Test.MakeATSProcess(name, enable_cache=False) - ts.Disk.records_config.update( - { - "proxy.config.diags.debug.enabled": 1, - "proxy.config.diags.debug.tags": "http|ip_allow", - "proxy.config.http.connect_ports": f"{server.Variables.SSL_Port}", - "proxy.config.url_remap.remap_required": 0, - }) - return ts - - -ts_default = configure_ts("ts-default") -ts_allowed = configure_ts("ts-allowed") -ts_allowed.Disk.ip_allow_yaml.AddLines( - [ - "ip_allow:", - " - apply: in", - " ip_addrs: 127.0.0.1", - " action: allow", - " methods: ALL", - " - apply: out", - " ip_addrs: 127.0.0.1", - " action: allow", - " methods: CONNECT", - " - apply: out", - " ip_addrs:", - " - 0.0.0.0/8", - " - 127.0.0.0/8", - " - \"::\"", - " - ::1", - " - 10.0.0.0/8", - " - 172.16.0.0/12", - " - 192.168.0.0/16", - " - 169.254.0.0/16", - " - ::/96", - " - fc00::/7", - " - fe80::/10", - " - ::ffff:0:0/96", - " action: deny", - " methods: CONNECT", - ]) - -ts_sni_default = Test.MakeATSProcess("ts-sni-default", enable_cache=False, enable_tls=True) -ts_sni_default.addDefaultSSLFiles() -ts_sni_default.Disk.records_config.update( - { - "proxy.config.diags.debug.enabled": 1, - "proxy.config.diags.debug.tags": "http|ip_allow|ssl|sni", - "proxy.config.http.connect_ports": f"{server.Variables.SSL_Port}", - "proxy.config.ssl.server.cert.path": ts_sni_default.Variables.SSLDir, - "proxy.config.ssl.server.private_key.path": ts_sni_default.Variables.SSLDir, - }) -ts_sni_default.Disk.ssl_multicert_yaml.AddLines( - """ -ssl_multicert: - - dest_ip: "*" - ssl_cert_name: server.pem - ssl_key_name: server.key -""".split("\n")) -ts_sni_default.Disk.sni_yaml.AddLines( - [ - "sni:", - "- fqdn: sni-denied.example.com", - f" tunnel_route: 127.0.0.1:{server.Variables.SSL_Port}", - ]) -ts_sni_default.Disk.diags_log.Content += Testers.ContainsExpression( - r"server '127\.0\.0\.1.*' prohibited by ip-allow policy", "SNI tunnel_route should be denied by outbound ip_allow.") - -loopback_url = f"https://127.0.0.1:{server.Variables.SSL_Port}/" -unspecified_url = f"https://0.0.0.0:{server.Variables.SSL_Port}/" -reserved_ipv4_url = f"https://0.1.2.3:{server.Variables.SSL_Port}/" -unspecified_v6_url = f"https://[::]:{server.Variables.SSL_Port}/" -compatible_loopback_url = f"https://[::7f00:1]:{server.Variables.SSL_Port}/" -mapped_loopback_url = f"https://[::ffff:127.0.0.1]:{server.Variables.SSL_Port}/" -mapped_loopback_hex_url = f"https://[::ffff:7f00:1]:{server.Variables.SSL_Port}/" - - -def add_denied_connect_run(name, url, http_connect="403"): - tr = Test.AddTestRun(name) - tr.MakeCurlCommand( - f'-sk --noproxy does-not-match --proxy http://127.0.0.1:{ts_default.Variables.port} ' - f'-o /dev/null -w "http_code=%{{http_code}} http_connect=%{{http_connect}}\\n" {url}', - ts=ts_default) - tr.Processes.Default.ReturnCode = Any(7, 56) - tr.Processes.Default.Streams.stdout = Testers.ContainsExpression( - f"http_code=000 http_connect={http_connect}", "CONNECT should be rejected before tunneling.") - tr.StillRunningAfter = server - tr.StillRunningAfter = ts_default - return tr - - -tr = Test.AddTestRun("Default policy denies CONNECT to loopback") -tr.Processes.Default.StartBefore(server, ready=When.PortOpen(server.Variables.SSL_Port)) -tr.Processes.Default.StartBefore(ts_default) -tr.MakeCurlCommand( - f'-sk --noproxy does-not-match --proxy http://127.0.0.1:{ts_default.Variables.port} ' - f'-o /dev/null -w "http_code=%{{http_code}} http_connect=%{{http_connect}}\\n" {loopback_url}', - ts=ts_default) -tr.Processes.Default.ReturnCode = Any(7, 56) -tr.Processes.Default.Streams.stdout = Testers.ContainsExpression( - "http_code=000 http_connect=403", "CONNECT to loopback should be rejected by the default outbound policy.") -tr.StillRunningAfter = server -tr.StillRunningAfter = ts_default - -add_denied_connect_run("ATS rejects CONNECT to unspecified IPv4 before outbound policy", unspecified_url, http_connect="400") -add_denied_connect_run("Default policy denies CONNECT to 0.0.0.0/8", reserved_ipv4_url) -add_denied_connect_run("ATS rejects CONNECT to unspecified IPv6 before outbound policy", unspecified_v6_url, http_connect="400") -add_denied_connect_run("Default policy denies CONNECT to IPv4-compatible loopback", compatible_loopback_url) -add_denied_connect_run("Default policy denies CONNECT to IPv4-mapped loopback", mapped_loopback_url) -add_denied_connect_run("Default policy denies CONNECT to hex IPv4-mapped loopback", mapped_loopback_hex_url) - -tr = Test.AddTestRun("Default policy denies SNI tunnel_route to loopback") -tr.Processes.Default.StartBefore(ts_sni_default) -tr.MakeCurlCommand( - f"-skv --resolve sni-denied.example.com:{ts_sni_default.Variables.ssl_port}:127.0.0.1 " - f"https://sni-denied.example.com:{ts_sni_default.Variables.ssl_port}/", - ts=ts_sni_default) -tr.Processes.Default.ReturnCode = Any(35, 52, 56) -tr.StillRunningAfter = server -tr.StillRunningAfter = ts_sni_default - -tr = Test.AddTestRun("Explicit outbound allow permits CONNECT to loopback") -tr.Processes.Default.StartBefore(ts_allowed) -tr.MakeCurlCommand( - f'-sk --noproxy does-not-match --proxy http://127.0.0.1:{ts_allowed.Variables.port} ' - f'-o /dev/null -w "http_code=%{{http_code}} http_connect=%{{http_connect}}\\n" {loopback_url}', - ts=ts_allowed) -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Streams.stdout = Testers.ContainsExpression( - "http_code=200 http_connect=200", "Explicit outbound allow should permit the CONNECT tunnel.") -tr.StillRunningAfter = server -tr.StillRunningAfter = ts_allowed diff --git a/tests/gold_tests/ip_allow/ip_allow.test.py b/tests/gold_tests/ip_allow/ip_allow.test.py deleted file mode 100644 index 0020e62d6b0..00000000000 --- a/tests/gold_tests/ip_allow/ip_allow.test.py +++ /dev/null @@ -1,366 +0,0 @@ -''' -Verify ip_allow filtering behavior. -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import os - -Test.Summary = ''' -Verify ip_allow filtering behavior. -''' -Test.SkipIf(Condition.CurlUsingUnixDomainSocket()) -Test.ContinueOnFail = True - -# Define default ATS -ts = Test.MakeATSProcess("ts", enable_tls=True, enable_cache=False) -server = Test.MakeOriginServer("server", ssl=True) - -testName = "" -request = { - "headers": "GET /get HTTP/1.1\r\n" - "Host: www.example.com:80\r\n\r\n", - "timestamp": "1469733493.993", - "body": "" -} -response = { - "headers": "HTTP/1.1 200 OK\r\n" - "Content-Length: 3\r\n" - "Connection: close\r\n\r\n", - "timestamp": "1469733493.993", - "body": "xxx" -} -server.addResponse("sessionlog.json", request, response) - -# The following shouldn't come to the server, but in the event that there is a -# bug in ip_allow and they are sent through, have them return a 200 OK. This -# will fail the match with the gold file which expects a 403. -request = { - "headers": "CONNECT www.example.com:80/connect HTTP/1.1\r\n" - "Host: www.example.com:80\r\n\r\n", - "timestamp": "1469733493.993", - "body": "" -} -response = { - "headers": "HTTP/1.1 200 OK\r\n" - "Content-Length: 3\r\n" - "Connection: close\r\n\r\n", - "timestamp": "1469733493.993", - "body": "xxx" -} -server.addResponse("sessionlog.json", request, response) -request = { - "headers": "PUSH www.example.com:80/h2_push HTTP/2\r\n" - "Host: www.example.com:80\r\n\r\n", - "timestamp": "1469733493.993", - "body": "" -} -response = { - "headers": "HTTP/2 200 OK\r\n" - "Content-Length: 3\r\n" - "Connection: close\r\n\r\n", - "timestamp": "1469733493.993", - "body": "xxx" -} -server.addResponse("sessionlog.json", request, response) - -# Configure TLS for Traffic Server for HTTP/2. -ts.addDefaultSSLFiles() - -ts.Disk.ssl_multicert_yaml.AddLines( - """ -ssl_multicert: - - dest_ip: "*" - ssl_cert_name: server.pem - ssl_key_name: server.key -""".split("\n")) - -ts.Disk.records_config.update( - { - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'ip_allow|http|url_rewrite', - 'proxy.config.http.push_method_enabled': 1, - 'proxy.config.http.connect_ports': '{0}'.format(server.Variables.SSL_Port), - 'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir), - 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts.Variables.SSLDir), - 'proxy.config.ssl.client.verify.server.policy': 'PERMISSIVE', - 'proxy.config.http2.active_timeout_in': 3, - 'proxy.config.http2.max_concurrent_streams_in': 65535, - }) - -format_string = ( - 'scheme=% %-% % % % %/% % ' - '% % % % %<{Y-RID}pqh> ' - '%<{Y-YPCS}pqh> %<{Host}cqh> %<{CHAD}pqh> ' - 'sftover=%<{x-safet-overlimit-rules}cqh> sftmat=%<{x-safet-matched-rules}cqh> ' - 'sftcls=%<{x-safet-classification}cqh> ' - 'sftbadclf=%<{x-safet-bad-classifiers}cqh> yra=%<{Y-RA}cqh> status_setter=%') - -ts.Disk.logging_yaml.AddLines( - f''' logging: - formats: - - name: custom - format: '{format_string}' - logs: - - filename: squid.log - format: custom -'''.split("\n")) - -ts.Disk.remap_config.AddLine('map / https://127.0.0.1:{0}'.format(server.Variables.SSL_Port)) - -# Note that CONNECT is not in the allowed list. -ts.Disk.ip_allow_yaml.AddLines( - '''ip_allow: - - apply: in - ip_addrs: 0/0 - action: allow - methods: [GET, HEAD, POST ] - - apply: in - ip_addrs: ::/0 - action: allow - methods: [GET, HEAD, POST ] - -'''.split("\n")) - -ts.Disk.traffic_out.Content += Testers.ContainsExpression( - "Line 1 denial for 'CONNECT' from 127.0.0.1", "The CONNECT request should be denied by ip_allow") -ts.Disk.traffic_out.Content += Testers.ContainsExpression( - "Line 1 denial for 'PUSH' from 127.0.0.1", "The PUSH request should be denied by ip_allow") - -# -# TEST 1: Perform a GET request. Should be allowed because GET is in the allowlist. -# -tr = Test.AddTestRun('Allowed GET request') -tr.Processes.Default.StartBefore(server, ready=When.PortOpen(server.Variables.SSL_Port)) -tr.Processes.Default.StartBefore(Test.Processes.ts) - -tr.MakeCurlCommand('--verbose -H "Host: www.example.com" http://localhost:{ts_port}/get'.format(ts_port=ts.Variables.port), ts=ts) -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Streams.stderr = 'gold/200.gold' -tr.StillRunningAfter = ts -tr.StillRunningAfter = server - -# -# TEST 2: Perform a CONNECT request. Should not be allowed because CONNECT is -# not in the allowlist. -# -tr = Test.AddTestRun('Denied CONNECT request') -tr.MakeCurlCommand( - '--verbose -X CONNECT -H "Host: localhost" http://localhost:{ts_port}/connect'.format(ts_port=ts.Variables.port), ts=ts) -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Streams.stderr = 'gold/403.gold' -tr.StillRunningAfter = ts -tr.StillRunningAfter = server - -# -# TEST 3: Perform a PUSH request over HTTP/2. Should not be allowed because -# PUSH is not in the allowlist. -# -tr = Test.AddTestRun('Denied PUSH request over HTTP/2') -tr.MakeCurlCommand( - '--http2 --verbose -k -X PUSH -H "Host: localhost" https://localhost:{ts_port}/h2_push'.format(ts_port=ts.Variables.ssl_port), - ts=ts) -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Streams.stderr = 'gold/403_h2.gold' -tr.StillRunningAfter = ts -tr.StillRunningAfter = server - -tr = Test.AddTestRun('Await and verify the transaction log file') -squid_log = os.path.join(ts.Variables.LOGDIR, 'squid.log') -tr.Processes.Default.Command = ( - os.path.join(Test.Variables.AtsTestToolsDir, 'stdout_wait') + ' 60 "{} {}" {}'.format( - os.path.join(Test.TestDirectory, 'run_sed.sh'), squid_log, os.path.join(Test.TestDirectory, 'gold/log.gold'))) -tr.Processes.Default.ReturnCode = 0 - -IP_ALLOW_CONFIG_ALLOW_ALL = '''ip_allow: - - apply: in - ip_addrs: 0/0 - action: allow - methods: ALL -''' - -IP_ALLOW_CONFIG_DENY_ALL = '''ip_allow: - - apply: in - ip_addrs: 0/0 - action: deny - methods: ALL -''' - - -class Test_ip_allow: - """Configure a test to verify ip_allow behavior.""" - - client_counter: int = 0 - ts_counter: int = 0 - server_counter: int = 0 - - def __init__( - self, - name: str, - replay_file: str, - ip_allow_config: str, - gold_file="", - replay_keys="", - is_h3=False, - expect_request_rejected=False): - """Initialize the test. - - :param name: The name of the test. - :param ip_allow_config: The ip_allow configuration to be used. - :param replay_file: The replay file to be used. - :param gold_file: (Optional) Gold file to be checked. - :param replay_keys: (Optional) Keys to be used by pv. - :param expect_request_rejected: (Optional) Whether or not the client request is expected to be rejected. - """ - self.name = name - self.replay_file = replay_file - self.ip_allow_config = ip_allow_config - self.gold_file = gold_file - self.replay_keys = replay_keys - self.is_h3 = is_h3 - self.expect_request_rejected = expect_request_rejected - - def _configure_server(self, tr: 'TestRun'): - """Configure the server. - - :param tr: The TestRun object to associate the server process with. - """ - server = tr.AddVerifierServerProcess(f"server_{Test_ip_allow.server_counter}", self.replay_file) - Test_ip_allow.server_counter += 1 - self._server = server - - def _configure_traffic_server(self, tr: 'TestRun'): - """Configure Traffic Server. - - :param tr: The TestRun object to associate the ts process with. - """ - ts = tr.MakeATSProcess( - f"ts-{Test_ip_allow.ts_counter}", enable_quic=self.is_h3, enable_tls=True, enable_proxy_protocol=True) - - Test_ip_allow.ts_counter += 1 - self._ts = ts - # Configure TLS for Traffic Server. - self._ts.addDefaultSSLFiles() - self._ts.Disk.ssl_multicert_yaml.AddLines( - """ -ssl_multicert: - - dest_ip: "*" - ssl_cert_name: server.pem - ssl_key_name: server.key -""".split("\n")) - self._ts.Disk.records_config.update( - { - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'v_quic|quic|http|ip_allow|proxyprotocol', - 'proxy.config.http.push_method_enabled': 1, - 'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir), - 'proxy.config.quic.no_activity_timeout_in': 0, - 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts.Variables.SSLDir), - 'proxy.config.ssl.client.verify.server.policy': 'PERMISSIVE', - 'proxy.config.http.connect_ports': f"{self._server.Variables.http_port}", - 'proxy.config.acl.subjects': 'PROXY,PEER', - }) - - self._ts.Disk.remap_config.AddLine(f'map / http://127.0.0.1:{self._server.Variables.http_port}') - - # Set ip_allow policy based on the input configuration. - self._ts.Disk.ip_allow_yaml.AddLines(self.ip_allow_config.split("\n")) - - def run(self): - """Run the test.""" - tr = Test.AddTestRun(self.name) - self._configure_server(tr) - self._configure_traffic_server(tr) - - tr.Processes.Default.StartBefore(self._server) - tr.Processes.Default.StartBefore(self._ts) - - tr.AddVerifierClientProcess( - f'client-{Test_ip_allow.client_counter}', - self.replay_file, - http_ports=[self._ts.Variables.proxy_protocol_port], - https_ports=[self._ts.Variables.ssl_port], - http3_ports=[self._ts.Variables.ssl_port], - keys=self.replay_keys) - Test_ip_allow.client_counter += 1 - - if self.expect_request_rejected: - # The client request should time out because ATS rejects it and does - # not send a response. - tr.Processes.Default.ReturnCode = 1 - self._ts.Disk.diags_log.Content += Testers.ContainsExpression( - "client.*prohibited by ip-allow policy", "Request should be rejected by ip_allow") - else: - # Verify the client request is successful. - tr.Processes.Default.ReturnCode = 0 - self._ts.Disk.diags_log.Content += Testers.ExcludesExpression( - "client.*allowed by ip-allow policy", "Request should be allowed by ip_allow") - - if self.gold_file: - tr.Processes.Default.Streams.all = self.gold_file - - -# ip_allow tests for h3. -if Condition.HasATSFeature('TS_USE_QUIC') and Condition.HasCurlFeature('http3'): - - # TEST 4: Perform a request in h3 with ip_allow configured to allow all IPs. - test0 = Test_ip_allow( - "h3_allow_all", - replay_file='replays/h3.replay.yaml', - ip_allow_config=IP_ALLOW_CONFIG_ALLOW_ALL, - is_h3=True, - expect_request_rejected=False) - test0.run() - - # TEST 5: Perform a request in h3 with ip_allow configured to deny all IPs. - test1 = Test_ip_allow( - "h3_deny_all", - replay_file='replays/h3.replay.yaml', - ip_allow_config=IP_ALLOW_CONFIG_DENY_ALL, - is_h3=True, - expect_request_rejected=True) - test1.run() - -# TEST 6: Verify rules are applied to all methods if methods is not specified. -IP_ALLOW_CONFIG_METHODS_UNSPECIFIED = '''ip_allow: - - apply: in - ip_addrs: 0/0 - action: allow -''' -test_ip_allow_optional_methods = Test_ip_allow( - "ip_allow_optional_methods", - replay_file='replays/https_multiple_methods.replay.yaml', - ip_allow_config=IP_ALLOW_CONFIG_METHODS_UNSPECIFIED, - is_h3=False, - expect_request_rejected=False) -test_ip_allow_optional_methods.run() - -# TEST 7: Verify IP address from PROXY protocol is used. -IP_ALLOW_CONFIG_PROXY_PROTOCOL = '''ip_allow: - - apply: in - ip_addrs: 1.2.3.4 - action: allow - - apply: in - ip_addrs: 0/0 - action: deny -''' -test_ip_allow_proxy_protocol = Test_ip_allow( - "ip_allow_proxy_protocol", - replay_file='replays/http_proxy_protocol.replay.yaml', - ip_allow_config=IP_ALLOW_CONFIG_PROXY_PROTOCOL, - is_h3=False, - expect_request_rejected=False) -test_ip_allow_proxy_protocol.run() diff --git a/tests/gold_tests/ip_allow/ip_allow_reload_triggered.test.py b/tests/gold_tests/ip_allow/ip_allow_reload_triggered.test.py deleted file mode 100644 index 4a8195c0371..00000000000 --- a/tests/gold_tests/ip_allow/ip_allow_reload_triggered.test.py +++ /dev/null @@ -1,218 +0,0 @@ -''' -Test ip_allow and ip_categories reload via ConfigRegistry. - -Verifies that: -1. ip_allow.yaml touch triggers ip_allow reload -2. ip_categories touch triggers ip_allow reload (via add_file_dependency) -3. Unrelated config touch (hosting.config) does NOT trigger ip_allow reload -4. ip_categories content change causes actual behavior change after reload -5. Changing ip_categories record value (traffic_ctl config set) triggers ip_allow reload -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import os -import shutil - -Test.Summary = ''' -Test ip_allow and ip_categories reload via ConfigRegistry add_file_dependency. -''' - -Test.ContinueOnFail = True - -# --- Setup: origin server --- -server = Test.MakeOriginServer("server", ssl=False) -request = {"headers": "GET /test HTTP/1.1\r\nHost: www.example.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -response = { - "headers": "HTTP/1.1 200 OK\r\nContent-Length: 2\r\nConnection: close\r\n\r\n", - "timestamp": "1469733493.993", - "body": "ok" -} -server.addResponse("sessionlog.json", request, response) - -# --- Setup: ip_categories file variants --- - -# Version A: 127.0.0.1 is INTERNAL (requests allowed) -categories_allow = os.path.join(Test.RunDirectory, 'categories_allow.yaml') -with open(categories_allow, 'w') as f: - f.write('ip_categories:\n - name: INTERNAL\n ip_addrs: 127.0.0.1\n') - -# Version B: 127.0.0.1 is NOT INTERNAL (GET denied, only HEAD allowed by catch-all) -categories_deny = os.path.join(Test.RunDirectory, 'categories_deny.yaml') -with open(categories_deny, 'w') as f: - f.write('ip_categories:\n - name: INTERNAL\n ip_addrs: 1.2.3.4\n') - -# Version C: 127.0.0.1 back in INTERNAL (for record value change test) -categories_restore = os.path.join(Test.RunDirectory, 'categories_restore.yaml') -with open(categories_restore, 'w') as f: - f.write('ip_categories:\n - name: INTERNAL\n ip_addrs: 127.0.0.1\n') - -# Active ip_categories file that the record points to (start with allow version) -categories_file = os.path.join(Test.RunDirectory, 'ip_categories.yaml') -shutil.copy(categories_allow, categories_file) - -# --- Setup: ATS --- -ts = Test.MakeATSProcess("ts", enable_cache=True) - -ts.Disk.records_config.update( - { - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'ip_allow|config', - 'proxy.config.cache.ip_categories.filename': categories_file, - }) - -# ip_allow config: -# Rule 1: INTERNAL category → allow ALL methods -# Rule 2: catch-all 0/0 → allow only HEAD -# -# Effect: -# 127.0.0.1 in INTERNAL → GET /test → 200 (rule 1 matches) -# 127.0.0.1 NOT in INTERNAL → GET /test → 403 (rule 2 matches, GET not allowed) -ts.Disk.ip_allow_yaml.AddLines( - '''ip_allow: - - apply: in - ip_categories: INTERNAL - action: allow - methods: ALL - - apply: in - ip_addrs: 0/0 - action: allow - methods: - - HEAD -'''.split("\n")) - -ts.Disk.remap_config.AddLine(f'map / http://127.0.0.1:{server.Variables.Port}') - -config_dir = ts.Variables.CONFIGDIR -reload_counter = 0 - -# ================================================================ -# Test 1: Touch ip_allow.yaml → reload → ip_allow reloads (count 2) -# ================================================================ - -tr = Test.AddTestRun("Touch ip_allow.yaml") -tr.Processes.Default.StartBefore(server, ready=When.PortOpen(server.Variables.Port)) -tr.Processes.Default.StartBefore(ts) -tr.Processes.Default.Command = f"sleep 3 && touch {os.path.join(config_dir, 'ip_allow.yaml')} && sleep 1" -tr.Processes.Default.ReturnCode = 0 -tr.StillRunningAfter = ts -tr.StillRunningAfter = server - -reload_counter += 1 -tr = Test.AddConfigReload(ts, expect_tasks=["ip_allow.yaml"], description="Reload after ip_allow.yaml touch") -tr.StillRunningAfter = ts - -# ================================================================ -# Test 2: Touch ip_categories → reload → ip_allow reloads (count 3) -# Verifies add_file_dependency() correctly wired ip_categories -# to trigger ip_allow reload via FileManager mtime detection. -# ================================================================ - -tr = Test.AddTestRun("Touch ip_categories") -tr.Processes.Default.Command = f"touch {categories_file} && sleep 1" -tr.Processes.Default.ReturnCode = 0 -tr.StillRunningAfter = ts - -reload_counter += 1 -tr = Test.AddConfigReload(ts, expect_tasks=["ip_allow.yaml"], description="Reload after ip_categories touch") -tr.StillRunningAfter = ts - -# ================================================================ -# Test 3: Touch hosting.config → reload → ip_allow NOT triggered -# Verifies the fix for the false trigger bug where changing -# any file in the config directory spuriously triggered -# ip_allow reload (due to FileManager watching the directory -# instead of ip_categories.yaml when the record was ""). -# ================================================================ - -tr = Test.AddTestRun("Touch hosting.config before reload") -tr.Processes.Default.Command = f"touch {os.path.join(config_dir, 'hosting.config')}" -tr.Processes.Default.ReturnCode = 0 -tr.StillRunningAfter = ts - -tr = Test.AddConfigReload(ts, expect_absent_tasks=["ip_allow.yaml"], description="Reload (should NOT trigger ip_allow)") - -# ================================================================ -# Test 4: Functional — change ip_categories content, verify behavior -# Proves that add_file_dependency() not only triggers the -# reload but that the handler actually re-reads the updated -# ip_categories file. -# ================================================================ - -# 4a: Verify initial state: GET → 200 (127.0.0.1 is in INTERNAL) -tr = Test.AddTestRun("GET should succeed (127.0.0.1 in INTERNAL category)") -tr.Processes.Default.Command = (f"curl -s -o /dev/null -w '%{{http_code}}' " - f"http://127.0.0.1:{ts.Variables.port}/test") -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Streams.stdout = Testers.ContainsExpression("200", "Should get 200 when 127.0.0.1 is in INTERNAL category") -tr.StillRunningAfter = ts -tr.StillRunningAfter = server - -# 4b: Swap ip_categories to deny version (127.0.0.1 NOT in INTERNAL) -tr = Test.AddTestRun("Change ip_categories to deny 127.0.0.1") -tr.Processes.Default.Command = f"cp {categories_deny} {categories_file}" -tr.Processes.Default.ReturnCode = 0 -tr.StillRunningAfter = ts - -# 4c: Reload and wait for ip_allow to pick up the change -reload_counter += 1 -tr = Test.AddConfigReload(ts, expect_tasks=["ip_allow.yaml"], description="Reload after ip_categories content change") -tr.StillRunningAfter = ts - -# 4d: GET should now be denied (falls to catch-all: only HEAD allowed) -tr = Test.AddTestRun("GET should be denied (127.0.0.1 NOT in INTERNAL)") -tr.Processes.Default.Command = (f"curl -s -o /dev/null -w '%{{http_code}}' " - f"http://127.0.0.1:{ts.Variables.port}/test") -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Streams.stdout = Testers.ContainsExpression("403", "Should get 403 when 127.0.0.1 is not in INTERNAL category") -tr.StillRunningAfter = ts -tr.StillRunningAfter = server - -# ================================================================ -# Test 5: Record value change triggers ip_allow reload -# Changes the ip_categories filename record via traffic_ctl -# config set (no explicit config reload). The callback -# registered by add_file_dependency() fires when the record -# value changes, triggering ip_allow reload with the new file. -# ================================================================ - -# 5a: Change the ip_categories filename record to point to categories_restore -# (which has 127.0.0.1 back in INTERNAL). -# No traffic_ctl config reload — the RecRegisterConfigUpdateCb fires -# automatically via config_update_cont when the record value changes. -reload_counter += 1 -tr = Test.AddTestRun("Change ip_categories record value to new file") -p = tr.Processes.Process(f"reload-{reload_counter}") -p.Command = (f"traffic_ctl config set proxy.config.cache.ip_categories.filename " - f"'{categories_restore}'; sleep 30") -p.Env = ts.Env -p.ReturnCode = Any(0, -2) -p.Ready = When.FileContains(ts.Disk.diags_log.Name, "ip_allow.yaml finished loading", 1 + reload_counter) -p.Timeout = 20 -tr.Processes.Default.StartBefore(p) -tr.Processes.Default.Command = 'echo "waiting for ip_allow reload after record value change"' -tr.TimeOut = 25 -tr.StillRunningAfter = ts - -# 5b: GET should succeed again (new file has 127.0.0.1 in INTERNAL) -tr = Test.AddTestRun("GET should succeed after record value change") -tr.Processes.Default.Command = (f"curl -s -o /dev/null -w '%{{http_code}}' " - f"http://127.0.0.1:{ts.Variables.port}/test") -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Streams.stdout = Testers.ContainsExpression( - "200", "Should get 200 after restoring INTERNAL category via record change") -tr.StillRunningAfter = ts -tr.StillRunningAfter = server diff --git a/tests/gold_tests/ip_allow/ip_category.test.py b/tests/gold_tests/ip_allow/ip_category.test.py deleted file mode 100644 index 77a8069f992..00000000000 --- a/tests/gold_tests/ip_allow/ip_category.test.py +++ /dev/null @@ -1,323 +0,0 @@ -''' -Verify IP allow ip_category behavior. -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import os -import re - -from typing import List - -Test.Summary = ''' -Verify IP allow ip_category behavior. -''' - - -class CategoryFile: - """Encapsulate the various ip_category.yaml contents.""" - - contents: List['CategoryFile'] = [] - parent_directory: str = Test.RunDirectory - _index: int = 0 - - def __init__(self, content: str): - """Initialize the object. - - :param content: The content of the ip_category.yaml file. - """ - self._content = content - self._index = len(CategoryFile.contents) - self._filename = os.path.join(CategoryFile.parent_directory, f'categories{self._index}.yaml') - CategoryFile.contents.append(self) - - def _write(self): - with open(self._filename, 'w') as f: - f.write(self._content) - - def get_path(self): - return self._filename - - @classmethod - def write_all(cls): - for content in cls.contents: - content._write() - - -localhost_is_internal_and_external = CategoryFile( - ''' -ip_categories: - - name: ACME_INTERNAL - ip_addrs: 127.0.0.1 - - name: ACME_EXTERNAL - ip_addrs: 127.0.0.1 - - name: ACME_ALL - ip_addrs: 127.0.0.1 - - name: ALL - ip_addrs: 127.0.0.1 -''') - -localhost_is_external = CategoryFile( - ''' -ip_categories: - - name: ACME_INTERNAL - ip_addrs: 1.2.3.4 - - name: ACME_REMAP_EXTERNAL - ip_addrs: 127.0.0.1 - - name: ACME_EXTERNAL - ip_addrs: 127.0.0.1 - - name: ACME_ALL - ip_addrs: - - 1.2.3.4 - - 127.0.0.1 - - name: ALL - ip_addrs: - - 1.2.3.4 - - 127.0.0.1 -''') - -localhost_is_neither = CategoryFile( - ''' -ip_categories: - - name: ACME_INTERNAL - ip_addrs: 1.2.3.4 - - name: ACME_EXTERNAL - ip_addrs: 1.2.3.4 - - name: ACME_ALL - ip_addrs: 1.2.3.4 - - name: ALL - ip_addrs: - - 1.2.3.4 - - 127.0.0.1 -''') - -# Keep this below the above content instantiations. -CategoryFile.write_all() - - -class Test_ip_category: - """Configure a test to verify ip_category behavior.""" - - _client_counter: int = 0 - _ts_is_started: bool = False - _reload_server_is_started: bool = False - _server_is_started: bool = False - _reload_counter: int = 0 - - _ts: 'TestProcess' = None - _server: 'TestProcess' = None - _reload_server: 'TestProcess' = None - - _categories_filename: str = f'{Test.RunDirectory}/categories.yaml' - _category_files_are_written: bool = False - - _server_replay = 'replays/https_categories_server.replay.yaml' - - def __init__( - self, name: str, replay_file: str, ip_allow_config: str, ip_category_config: 'CategoryFile', acl_configuration: str, - expected_responses: List[int]): - """Initialize the test. - - :param name: The name of the test. - :param replay_file: The replay file to be used. - :param ip_allow_config: The ip_allow configuration to be used. - :param ip_category_config: The ip_category.yaml configuration to be used. - :param acl_configuration: The ACL configuration to be used. - :param expect_responses: The in-order expected responses from the proxy. - """ - self._replay_file = replay_file - self._ip_allow_config = ip_allow_config - self._acl_configuration = acl_configuration - self._expected_responses = expected_responses - - self._update_categories_file(ip_category_config) - self._update_remap_with_acl() - - self._configure_server() - self._configure_traffic_server() - - tr = Test.AddTestRun(name) - self._configure_client(tr) - - def _update_remap_with_acl(self) -> None: - """Update the remap.config file with the ACL configuration.""" - if Test_ip_category._ts: - if self._acl_configuration: - tr = Test.AddTestRun(f"remap.config file update with acl: {self._acl_configuration}") - p = tr.Processes.Default - destination = os.path.join(Test_ip_category._ts.Variables.CONFIGDIR, 'remap.config') - common = f'map / http://127.0.0.1:{Test_ip_category._server.Variables.http_port} ' - p.Command = f'echo {common} {self._acl_configuration} > {destination}; cat {destination}' - p.ReturnCode = 0 - else: - tr = Test.AddTestRun(f"remap.config file update with no acl") - p = tr.Processes.Default - destination = os.path.join(Test_ip_category._ts.Variables.CONFIGDIR, 'remap.config') - common = f'map / http://127.0.0.1:{Test_ip_category._server.Variables.http_port} ' - p.Command = f'echo {common} > {destination}; cat {destination}' - p.ReturnCode = 0 - - def _update_categories_file(self, category_content: 'CategoryFile') -> None: - """Update the categories file. - - :param category_content: The content of the categories file. - """ - tr = Test.AddTestRun(f"Categories file update: {category_content.get_path()}") - p = tr.Processes.Default - destination = Test_ip_category._categories_filename - p.Command = f'cp {category_content.get_path()} {destination}; cat {destination}; ls -ltr {destination}' - p.ReturnCode = 0 - - def _configure_server(self) -> None: - """Configure the server.""" - if Test_ip_category._server: - # All test runs share a single server instance. - return - server = Test.MakeVerifierServerProcess(f"server", self._server_replay) - Test_ip_category._server = server - - def _configure_traffic_server(self) -> None: - """Configure Traffic Server.""" - - if Test_ip_category._ts: - # All test runs share a single Traffic Server instance. - - # Reload the ip_allow.yaml file. - ts = Test_ip_category._ts - Test_ip_category._reload_counter += 1 - tr = Test.AddConfigReload(ts, expect_tasks=["ip_allow.yaml"], description="Reload the configuration") - tr.StillRunningAfter = ts - - return - ts = Test.MakeATSProcess("ts", enable_cache=False, enable_tls=True) - Test_ip_category._ts = ts - - ts.addDefaultSSLFiles() - ts.Disk.ssl_multicert_yaml.AddLines( - """ -ssl_multicert: - - dest_ip: "*" - ssl_cert_name: server.pem - ssl_key_name: server.key -""".split("\n")) - ts.Disk.records_config.update( - { - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'http|ip_allow|config.reload', - 'proxy.config.cache.ip_categories.filename': Test_ip_category._categories_filename, - 'proxy.config.http.push_method_enabled': 1, - 'proxy.config.ssl.server.cert.path': ts.Variables.SSLDir, - 'proxy.config.quic.no_activity_timeout_in': 0, - 'proxy.config.ssl.server.private_key.path': ts.Variables.SSLDir, - 'proxy.config.ssl.client.verify.server.policy': 'PERMISSIVE', - 'proxy.config.http.connect_ports': Test_ip_category._server.Variables.http_port, - 'proxy.config.url_remap.acl_behavior_policy': 1, # TODO: adjust expected_responses with the default config - }) - - ts.Disk.remap_config.AddLine( - f'map / http://127.0.0.1:{Test_ip_category._server.Variables.http_port} {self._acl_configuration}') - ts.Disk.ip_allow_yaml.AddLines(self._ip_allow_config.split("\n")) - - def _configure_client(self, tr: 'TestRun') -> None: - """Run the test. - - :param tr: The TestRun object to associate the client process with. - """ - - if not Test_ip_category._server_is_started: - tr.Processes.Default.StartBefore(Test_ip_category._server) - Test_ip_category._server_is_started = True - if not Test_ip_category._ts_is_started: - tr.Processes.Default.StartBefore(Test_ip_category._ts) - Test_ip_category._ts_is_started = True - - p = tr.AddVerifierClientProcess( - f'client-{Test_ip_category._client_counter}', self._replay_file, https_ports=[Test_ip_category._ts.Variables.ssl_port]) - Test_ip_category._client_counter += 1 - - if self._expected_responses: - codes = [str(code) for code in self._expected_responses] - p.Streams.stdout += Testers.ContainsExpression( - '.*'.join(codes), "Verifying the expected order of responses", reflags=re.DOTALL | re.MULTILINE) - else: - # If there are no expected responses, expect the Warning about the rejected ip. - self._ts.Disk.diags_log.Content += Testers.ContainsExpression( - "client '127.0.0.1' prohibited by ip-allow policy", "Verify the client rejection warning message.") - - # Also, the client will complain about the broken connections. - p.ReturnCode = 1 - - -IP_ALLOW_CONTENT = f''' -ip_allow: - - apply: in - ip_categories: ACME_INTERNAL - action: set_allow - methods: - - GET - - HEAD - - POST - - PUSH - - apply: in - ip_categories: ACME_EXTERNAL - action: set_allow - methods: - - GET - - HEAD - - apply: in - ip_categories: ACME_ALL - action: set_allow - methods: - - HEAD - - apply: in - ip_categories: ALL - action: set_deny -''' - -test_ip_allow_optional_methods = Test_ip_category( - "IP Category: INTERNAL", - replay_file='replays/https_categories_internal.replay.yaml', - ip_allow_config=IP_ALLOW_CONTENT, - ip_category_config=localhost_is_internal_and_external, - acl_configuration='', - expected_responses=[200, 200, 400, 403]) - -test_ip_allow_optional_methods = Test_ip_category( - "IP Category: EXTERNAL", - replay_file='replays/https_categories_external.replay.yaml', - ip_allow_config=IP_ALLOW_CONTENT, - ip_category_config=localhost_is_external, - acl_configuration='', - expected_responses=[200, 403, 403]) - -# Because all requests are outright rejected for 127.0.0.1, ATS will -# reject all incoming transactions and not even give a 403 response. -test_ip_allow_optional_methods = Test_ip_category( - "IP Category: ALL", - replay_file='replays/https_categories_all.replay.yaml', - ip_allow_config=IP_ALLOW_CONTENT, - ip_category_config=localhost_is_neither, - acl_configuration='', - expected_responses=None) - -# Deny GET via remap.config ACL. -test_ip_allow_optional_methods = Test_ip_category( - "IP Category: EXTERNAL", - replay_file='replays/https_categories_external_remap.replay.yaml', - ip_allow_config=IP_ALLOW_CONTENT, - ip_category_config=localhost_is_external, - acl_configuration='@action=set_deny @src_ip_category=ACME_REMAP_EXTERNAL @method=GET', - expected_responses=[403, 200, 200]) diff --git a/tests/gold_tests/jsonrpc/basic_plugin_handler.test.py b/tests/gold_tests/jsonrpc/basic_plugin_handler.test.py deleted file mode 100644 index 222dbfa3c92..00000000000 --- a/tests/gold_tests/jsonrpc/basic_plugin_handler.test.py +++ /dev/null @@ -1,186 +0,0 @@ -''' -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import os -import sys -from jsonrpc import Notification, Request, Response - -Test.Summary = 'Test JSONRPC Handler inside a plugin' -Test.ContinueOnFail = True -# Define default ATS, we will use the runroot file for traffic_ctl -ts = Test.MakeATSProcess('ts', dump_runroot=True) - -Test.testName = 'Basic plugin handler test' - -ts.Disk.records_config.update( - { - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'rpc|jsonrpc_plugin_handler_test' - }) - -# Load plugin -Test.PrepareTestPlugin( - os.path.join(Test.Variables.AtsBuildGoldTestsDir, 'jsonrpc', 'plugins', '.libs', 'jsonrpc_plugin_handler_test.so'), ts) - -tr = Test.AddTestRun() -tr.Processes.Default.StartBefore(ts) -tr.Processes.Default.Command = "echo run jsonrpc_plugin_handler_test plugin" -tr.Processes.Default.ReturnCode = 0 -tr.Processes.StillRunningAfter = ts -ts.Ready = 0 -ts.Disk.traffic_out.Content = All( - Testers.IncludesExpression('Test Plugin Initialized.', 'plugin should be properly initialized'), - Testers.IncludesExpression( - 'test_join_hosts_method successfully registered', 'test_join_hosts_method should be properly registered'), - Testers.IncludesExpression( - 'test_join_hosts_notification successfully registered', 'test_join_hosts_notification should be properly registered')) - -# 1 - now let's try the registered api. -tr = Test.AddTestRun("Test registered API") -tr.AddJsonRPCClientRequest(ts, Request.show_registered_handlers()) -tr.DelayStart = 2 -# Ok we can just check like this: -tr.Processes.Default.Streams.stdout = All( - Testers.IncludesExpression('test_join_hosts_method', 'Should be listed'), - Testers.IncludesExpression('test_join_hosts_notification', 'Should be listed')) - -# 2 - We perform the same as above but without implicit 'show_registered_handlers()' call. -tr = Test.AddTestRun("Test registered API - using AddJsonRPCShowRegisterHandlerRequest") -tr.AddJsonRPCShowRegisterHandlerRequest(ts) - -# Ok we can just check like this: -tr.Processes.Default.Streams.stdout = All( - Testers.IncludesExpression('test_join_hosts_method', 'Should be listed'), - Testers.IncludesExpression('test_join_hosts_notification', 'Should be listed')) - -# 3 - Call the actual plugin method handler: -tr = Test.AddTestRun("Test JSONRPC test_join_hosts_method") -tr.AddJsonRPCClientRequest(ts, Request.test_join_hosts_method(hosts=["yahoo.com", "aol.com", "vz.com"])) - - -def validate_host_response(resp: Response): - ''' - Custom check for a particular response values. Also error validation. - ''' - if resp.is_error(): - return (False, resp.error_as_str()) - - join_str = resp.result['join'] - expected = "yahoo.comaol.comvz.com" - if join_str != expected: - return (False, f"Invalid response, expected yahoo.comaol.comvz.com, got {join_str}") - - return (True, "All good") - - -tr.Processes.Default.Streams.stdout = Testers.CustomJSONRPCResponse(validate_host_response) - -# Custom response validator: - - -def _validate_response_for_test_io_on_et_task(added, updated, resp: Response): - ''' - Custom check for a particular response values. Also error validation. - ''' - if resp.is_error(): - return (False, resp.error_as_str()) - - addedHosts = resp.result['addedHosts'] - updatedHosts = resp.result['updatedHosts'] - - if addedHosts == added and updatedHosts == updated: - return (True, "All good") - - return ( - False, - f"Invalid response, expected addedHosts == {added} and updatedHosts == {updated}, got {addedHosts} and {updatedHosts}") - - -# 4 - Call plugin handler to perform a IO task on the ET_TASK thread. -tr = Test.AddTestRun("Test test_io_on_et_task") -newHosts = [ - { - 'name': 'brbzull', - 'status': 'up' - }, { - 'name': 'brbzull1', - 'status': 'down' - }, { - 'name': 'brbzull3', - 'status': 'up' - }, { - 'name': 'brbzull4', - 'status': 'down' - }, { - 'name': 'yahoo', - 'status': 'down' - }, { - 'name': 'trafficserver', - 'status': 'down' - } -] - -# the jsonrpc query -tr.AddJsonRPCClientRequest(ts, Request.test_io_on_et_task(hosts=newHosts)) - - -def validate_response_for_test_io_on_et_task_1(resp: Response): - return _validate_response_for_test_io_on_et_task('6', '0', resp) - - -tr.Processes.Default.Streams.stdout = Testers.CustomJSONRPCResponse(validate_response_for_test_io_on_et_task_1) - -# 5 -tr = Test.AddTestRun("Test test_io_on_et_task - update, yahoo up") -updateYahoo = [{'name': 'yahoo', 'status': 'up'}] - -# the jsonrpc query -tr.AddJsonRPCClientRequest(ts, Request.test_io_on_et_task(hosts=updateYahoo)) - - -def validate_response_for_test_io_on_et_task_2(resp: Response): - return _validate_response_for_test_io_on_et_task('0', '1', resp) - - -tr.Processes.Default.Streams.stdout = Testers.CustomJSONRPCResponse(validate_response_for_test_io_on_et_task_2) - -# 6 -tr = Test.AddTestRun("Test privileged field from plugin handlers") -tr.AddJsonRPCClientRequest(ts, Request.get_service_descriptor()) - - -def validate_privileged_field_for_method(resp: Response, params): - for (name, val) in params: - if resp.is_error(): - return (False, resp.error_as_str()) - - data = resp.result['methods'] - e = list(filter(lambda x: x['name'] == name, data))[0] - # Method should be registered. - if e is None or e['privileged'] != val: - return (False, f"{name} privileged != {val}") - - return (True, "All good") - - -def validate_privileged_field(resp: Response): - return validate_privileged_field_for_method( - resp, [('test_join_hosts_method', "1"), ('test_io_on_et_task', '1'), ('test_join_hosts_notification', "0")]) - - -tr.Processes.Default.Streams.stdout = Testers.CustomJSONRPCResponse(validate_privileged_field) diff --git a/tests/gold_tests/jsonrpc/config_reload_dedup.test.py b/tests/gold_tests/jsonrpc/config_reload_dedup.test.py deleted file mode 100644 index 1426aed481b..00000000000 --- a/tests/gold_tests/jsonrpc/config_reload_dedup.test.py +++ /dev/null @@ -1,160 +0,0 @@ -''' -Test that a trigger-record value change in records.yaml is handled correctly -during a reload, including deduplication when the 3s config_update_cont timer -fires the same on_record_change callback again. - -Scenario: - 1. Change proxy.config.ssl.server.session_ticket.enable in records.yaml - via --cold. This is a trigger record for ssl_client_coordinator - (registered via register_record_config with 11 trigger records). - - 2. Touch sni.yaml — this also triggers ssl_client_coordinator via - add_file_and_node_dependency (proxy.config.ssl.servername.filename). - So ssl_client_coordinator gets hit from TWO paths: the value change - callback AND the file-mtime callback. - - 3. Touch other config files (ip_allow, logging) for broader coverage. - - 4. Trigger a reload and wait for completion. - - 5. Wait long enough (>6s) for the 3s config_update_cont timer to fire - at least once during or after the reload. When it fires: - a. If the reload is still in progress — create_config_context() - finds the existing subtask and logs "Duplicate reload …skipping". - b. If the reload already completed (SUCCESS) — create_config_context() - may create a new subtask, the handler runs again, and the task - re-settles to SUCCESS. - Either way, no crash, no stuck tasks, no state-transition conflicts. - - 6. Verify: - - "Reserved subtask" messages present (reserve_subtask worked) - - ssl_client_coordinator subtask completed successfully - - No "ignoring transition from" messages (no state corruption) - - Final status is success (even if briefly flickered) -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -Test.Summary = 'Verify trigger-record value change + file-mtime dedup during reload' -Test.ContinueOnFail = True - -# --- Setup --- -ts = Test.MakeATSProcess("ts", enable_cache=True) -ts.StartupTimeout = 30 - -ts.Disk.records_config.update( - { - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'rpc|config|config.reload|configproc', - }) - -# Provide valid content for config files whose handlers reject empty input. -ts.Disk.ip_allow_yaml.AddLines([ - 'ip_allow:', - '- apply: in', - ' ip_addrs: 0/0', - ' action: allow', - ' methods: ALL', -]) -ts.Disk.logging_yaml.AddLines([ - 'logging:', - ' formats:', - ' - name: dedup_test', - ' format: "%"', -]) -ts.Disk.sni_yaml.AddLines([ - 'sni:', - '- fqdn: "*.example.com"', - ' verify_client: NONE', -]) - -# Files to touch — sni.yaml is key because it also triggers -# ssl_client_coordinator via add_file_and_node_dependency. -files_to_touch = [ - ts.Disk.ip_allow_yaml, - ts.Disk.logging_yaml, - ts.Disk.sni_yaml, -] -touch_cmd = "touch " + " ".join([f.AbsRunTimePath for f in files_to_touch]) - -# ============================================================================ -# Test 1: Change a trigger-record VALUE in records.yaml via --cold -# -# proxy.config.ssl.server.session_ticket.enable is a trigger record for -# ssl_client_coordinator. Toggling it from 1→0 modifies records.yaml on -# disk. When the reload fires, rereadConfig → RecReadYamlConfigFile → -# RecSetRecord sets update_required on this record. RecFlushConfigUpdateCbs -# fires the on_record_change callback. Later the 3s timer may fire it again. -# ============================================================================ -tr = Test.AddTestRun("Change trigger record value in records.yaml via --cold") -tr.Processes.Default.StartBefore(ts) -tr.Processes.Default.Command = ('sleep 3 && traffic_ctl config set proxy.config.ssl.server.session_ticket.enable 0 --cold') -tr.Processes.Default.Env = ts.Env -tr.Processes.Default.ReturnCode = 0 -tr.StillRunningAfter = ts - -# ============================================================================ -# Test 2: Touch config files — sni.yaml is the critical one -# -# sni.yaml's mtime change triggers RecSetSyncRequired on -# proxy.config.ssl.servername.filename, which is wired to -# ssl_client_coordinator. So ssl_client_coordinator gets hit from two paths: -# 1. proxy.config.ssl.server.session_ticket.enable value change (from records.yaml) -# 2. proxy.config.ssl.servername.filename sync-required (from sni.yaml mtime) -# This exercises the fan-in deduplication in on_record_change / reserve_subtask. -# ============================================================================ -tr = Test.AddTestRun("Touch config files including sni.yaml") -tr.Processes.Default.Command = touch_cmd -tr.Processes.Default.ReturnCode = 0 -tr.StillRunningAfter = ts - -# ============================================================================ -# Test 3: Trigger reload -# ============================================================================ -tr = Test.AddTestRun("Trigger reload with named token") -tr.Processes.Default.Command = "traffic_ctl config reload -t dedup_test" -tr.Processes.Default.Env = ts.Env -tr.Processes.Default.ReturnCode = 0 -tr.StillRunningAfter = ts - -# ============================================================================ -# Test 4: Wait long enough for the 3s timer to fire at least twice -# -# The config_update_cont timer fires every 3s. We wait 15s to ensure: -# - All subtasks complete (ip_allow, logging, remap, ssl, etc.) -# - The 3s timer fires at least 4 times during/after the reload -# - Any duplicate on_record_change callbacks have been processed -# ============================================================================ -tr = Test.AddTestRun("Verify reload completed — no tasks stuck in progress") -tr.DelayStart = 15 -tr.Processes.Default.Command = "traffic_ctl config status -t dedup_test" -tr.Processes.Default.Env = ts.Env -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Streams.stdout += Testers.ExcludesExpression( - "in_progress", "No task should remain in progress after 15s delay") -# ssl_client_coordinator must be present in the output (it was triggered) -tr.Processes.Default.Streams.stdout += Testers.ContainsExpression( - "ssl_client_coordinator", "ssl_client_coordinator subtask must appear in status") -tr.Processes.Default.Streams.stdout += Testers.ContainsExpression("success", "Final status must be success") -tr.StillRunningAfter = ts - -# ============================================================================ -# Test 5: No state-transition conflicts anywhere in the logs -# ============================================================================ -ts.Disk.traffic_out.Content += Testers.ExcludesExpression( - "ignoring transition from", "No state-transition conflicts should appear in traffic.out") -ts.Disk.traffic_out.Content += Testers.ContainsExpression( - "Reserved subtask", "reserve_subtask() must log pre-registration messages") diff --git a/tests/gold_tests/jsonrpc/config_reload_failures.test.py b/tests/gold_tests/jsonrpc/config_reload_failures.test.py deleted file mode 100644 index b000176f4dc..00000000000 --- a/tests/gold_tests/jsonrpc/config_reload_failures.test.py +++ /dev/null @@ -1,429 +0,0 @@ -''' -Test config reload failure scenarios. - -Tests: -1. Failed tasks (invalid config content) -2. Failed subtasks (invalid SSL certificates) -3. Incomplete subtasks (timeout scenarios) -4. Status propagation when subtasks fail -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -from jsonrpc import Request, Response -import os - -Test.Summary = 'Test config reload failure scenarios and status propagation' -Test.ContinueOnFail = True - -ts = Test.MakeATSProcess('ts', dump_runroot=True, enable_tls=True) - -Test.testName = 'config_reload_failures' - -# Enable debugging -ts.Disk.records_config.update( - { - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'config|ssl|ip_allow', - 'proxy.config.ssl.server.cert.path': ts.Variables.SSLDir, - 'proxy.config.ssl.server.private_key.path': ts.Variables.SSLDir, - }) - -# Add valid SSL certs for baseline -ts.addDefaultSSLFiles() - -ts.Disk.ssl_multicert_yaml.AddLines( - [ - 'ssl_multicert:', - ' - dest_ip: "*"', - ' ssl_cert_name: server.pem', - ' ssl_key_name: server.key', - ]) - -# Override default diags check — this test intentionally triggers SSL errors -ts.Disk.diags_log.Content = Testers.ContainsExpression("ERROR", "Expected errors from invalid SSL cert injection") - -# ============================================================================ -# Test 1: Baseline - successful reload -# ============================================================================ -tr = Test.AddTestRun("Baseline - successful reload") -tr.Processes.Default.StartBefore(ts) -tr.DelayStart = 2 -tr.AddJsonRPCClientRequest(ts, Request.admin_config_reload(force=True)) - - -def validate_baseline(resp: Response): - '''Verify baseline reload succeeds''' - if resp.is_error(): - return (False, f"Baseline failed: {resp.error_as_str()}") - - result = resp.result - token = result.get('token', '') - return (True, f"Baseline succeeded: token={token}") - - -tr.Processes.Default.Streams.stdout = Testers.CustomJSONRPCResponse(validate_baseline) -tr.StillRunningAfter = ts - -# ============================================================================ -# Test 2: Wait and check baseline completed successfully -# ============================================================================ -tr = Test.AddTestRun("Verify baseline completed with success status") -tr.DelayStart = 3 -tr.AddJsonRPCClientRequest(ts, Request.admin_config_reload()) - - -def validate_baseline_status(resp: Response): - '''Check baseline reload status''' - if resp.is_error(): - error = resp.error_as_str() - if 'in progress' in error.lower(): - return (True, f"Still in progress: {error}") - return (True, f"Query result: {error}") - - result = resp.result - tasks = result.get('tasks', []) - - # Check for any failed tasks - def find_failures(task_list): - failures = [] - for t in task_list: - if t.get('status') == 'fail': - failures.append(t.get('description', 'unknown')) - failures.extend(find_failures(t.get('sub_tasks', []))) - return failures - - failures = find_failures(tasks) - if failures: - return (True, f"Found failures (may be expected): {failures}") - - return (True, f"Baseline status OK, {len(tasks)} tasks") - - -tr.Processes.Default.Streams.stdout = Testers.CustomJSONRPCResponse(validate_baseline_status) -tr.StillRunningAfter = ts - -# ============================================================================ -# Test 3: Inject invalid ip_allow config (should fail) -# ============================================================================ -tr = Test.AddTestRun("Inject invalid ip_allow config") -tr.DelayStart = 2 - -# Invalid ip_allow YAML - missing required fields -invalid_ip_allow = """ip_allow: - - apply: invalid_value - action: not_a_valid_action -""" - -# This should trigger a validation error in IpAllow -# Note: The actual behavior depends on how strict the parser is -tr.AddJsonRPCClientRequest(ts, Request.admin_config_reload(force=True)) - - -def validate_after_invalid_config(resp: Response): - '''Check reload after invalid config injection''' - if resp.is_error(): - return (True, f"Reload error (may be expected): {resp.error_as_str()}") - - result = resp.result - token = result.get('token', '') - errors = result.get('error', []) - - if errors: - return (True, f"Reload reported errors: {errors}") - - return (True, f"Reload started: {token}") - - -tr.Processes.Default.Streams.stdout = Testers.CustomJSONRPCResponse(validate_after_invalid_config) -tr.StillRunningAfter = ts - -# ============================================================================ -# Test 4: Add invalid SSL cert reference (subtask failure) -# ============================================================================ -tr = Test.AddTestRun("Configure invalid SSL cert path") -tr.DelayStart = 2 - -# Add a bad cert reference to ssl_multicert.yaml -# This should cause the SSL subtask to fail -sslcertpath = ts.Disk.ssl_multicert_yaml.AbsPath -tr.Disk.File(sslcertpath, id="ssl_multicert_yaml", typename="ats:config") -tr.Disk.ssl_multicert_yaml.AddLines( - [ - 'ssl_multicert:', - ' - dest_ip: "*"', - ' ssl_cert_name: server.pem', - ' ssl_key_name: server.key', - ' - dest_ip: 1.2.3.4', - ' ssl_cert_name: /nonexistent/bad.pem', - ' ssl_key_name: /nonexistent/bad.key', - ]) - -tr.AddJsonRPCClientRequest(ts, Request.admin_config_reload(force=True)) - - -def validate_ssl_failure(resp: Response): - '''Check reload with bad SSL config''' - if resp.is_error(): - return (True, f"SSL reload error: {resp.error_as_str()}") - - result = resp.result - token = result.get('token', '') - return (True, f"SSL reload started: {token}") - - -tr.Processes.Default.Streams.stdout = Testers.CustomJSONRPCResponse(validate_ssl_failure) -tr.StillRunningAfter = ts - -# ============================================================================ -# Test 5: Check for failed subtasks after SSL reload -# ============================================================================ -tr = Test.AddTestRun("Check for failed SSL subtasks") -tr.DelayStart = 3 -tr.AddJsonRPCClientRequest(ts, Request.admin_config_reload()) - - -def validate_failed_subtasks(resp: Response): - '''Check if SSL subtasks show failure''' - if resp.is_error(): - error = resp.error_as_str() - if 'in progress' in error.lower(): - return (True, f"Still in progress: {error}") - return (True, f"Query: {error}") - - result = resp.result - tasks = result.get('tasks', []) - - def analyze_tasks(task_list, depth=0): - analysis = [] - for t in task_list: - desc = t.get('description', 'unknown') - status = t.get('status', 'unknown') - logs = t.get('logs', []) - - info = f"{' '*depth}{desc}: {status}" - if status == 'fail' and logs: - info += f" - {logs[0][:50]}..." - - analysis.append(info) - - # Recurse into subtasks - analysis.extend(analyze_tasks(t.get('sub_tasks', []), depth + 1)) - - return analysis - - task_info = analyze_tasks(tasks) - return (True, f"Task analysis:\n" + "\n".join(task_info[:10])) - - -tr.Processes.Default.Streams.stdout = Testers.CustomJSONRPCResponse(validate_failed_subtasks) -tr.StillRunningAfter = ts - -# ============================================================================ -# Test 6: Verify parent status reflects subtask failure -# ============================================================================ -tr = Test.AddTestRun("Verify status propagation from subtask to parent") -tr.DelayStart = 1 -tr.AddJsonRPCClientRequest(ts, Request.admin_config_reload()) - - -def validate_status_propagation(resp: Response): - '''Verify failed subtask propagates to parent''' - if resp.is_error(): - error = resp.error_as_str() - if 'in progress' in error.lower(): - return (True, f"In progress: {error}") - return (True, f"Query: {error}") - - result = resp.result - tasks = result.get('tasks', []) - - def check_propagation(task_list): - """ - For each task with subtasks, verify: - - If any subtask is 'fail', parent should be 'fail' - - If any subtask is 'in_progress', parent should be 'in_progress' - - If all subtasks are 'success', parent should be 'success' - """ - issues = [] - for t in task_list: - sub_tasks = t.get('sub_tasks', []) - if not sub_tasks: - continue - - parent_status = t.get('status', '') - sub_statuses = [st.get('status', '') for st in sub_tasks] - - if 'fail' in sub_statuses and parent_status != 'fail': - issues.append(f"{t.get('description')}: has failed subtask but parent is '{parent_status}'") - - if 'in_progress' in sub_statuses and parent_status != 'in_progress': - issues.append(f"{t.get('description')}: has in_progress subtask but parent is '{parent_status}'") - - # Recurse - issues.extend(check_propagation(sub_tasks)) - - return issues - - issues = check_propagation(tasks) - if issues: - return (True, f"Propagation issues found: {issues}") - - return (True, "Status propagation verified correctly") - - -tr.Processes.Default.Streams.stdout = Testers.CustomJSONRPCResponse(validate_status_propagation) -tr.StillRunningAfter = ts - -# ============================================================================ -# Test 7: Check task logs contain error details -# ============================================================================ -tr = Test.AddTestRun("Check failed tasks have error logs") -tr.DelayStart = 1 -tr.AddJsonRPCClientRequest(ts, Request.admin_config_reload()) - - -def validate_error_logs(resp: Response): - '''Verify failed tasks have descriptive logs''' - if resp.is_error(): - return (True, f"Query: {resp.error_as_str()}") - - result = resp.result - tasks = result.get('tasks', []) - - def find_failed_with_logs(task_list): - results = [] - for t in task_list: - if t.get('status') == 'fail': - logs = t.get('logs', []) - desc = t.get('description', 'unknown') - if logs: - results.append(f"{desc}: {logs}") - else: - results.append(f"{desc}: NO LOGS (should have error details)") - - results.extend(find_failed_with_logs(t.get('sub_tasks', []))) - return results - - failed_info = find_failed_with_logs(tasks) - if failed_info: - return (True, f"Failed tasks with logs: {failed_info}") - - return (True, "No failed tasks found (baseline may have recovered)") - - -tr.Processes.Default.Streams.stdout = Testers.CustomJSONRPCResponse(validate_error_logs) -tr.StillRunningAfter = ts - -# ============================================================================ -# Test 8: Force reload to reset state -# ============================================================================ -tr = Test.AddTestRun("Force reload to reset") -tr.DelayStart = 2 - -# Reset ssl_multicert.yaml to valid state -sslcertpath = ts.Disk.ssl_multicert_yaml.AbsPath -tr.Disk.File(sslcertpath, id="ssl_multicert_yaml", typename="ats:config") -tr.Disk.ssl_multicert_yaml.AddLines( - [ - 'ssl_multicert:', - ' - dest_ip: "*"', - ' ssl_cert_name: server.pem', - ' ssl_key_name: server.key', - ]) - -tr.AddJsonRPCClientRequest(ts, Request.admin_config_reload(force=True)) - - -def validate_reset(resp: Response): - '''Reset to clean state''' - if resp.is_error(): - return (True, f"Reset: {resp.error_as_str()}") - - result = resp.result - token = result.get('token', '') - return (True, f"Reset reload started: {token}") - - -tr.Processes.Default.Streams.stdout = Testers.CustomJSONRPCResponse(validate_reset) -tr.StillRunningAfter = ts - -# ============================================================================ -# Test 9: Verify clean state after reset -# ============================================================================ -tr = Test.AddTestRun("Verify clean state after reset") -tr.DelayStart = 3 -tr.AddJsonRPCClientRequest(ts, Request.admin_config_reload()) - - -def validate_clean_state(resp: Response): - '''Verify we're back to clean state''' - if resp.is_error(): - error = resp.error_as_str() - if 'in progress' in error.lower(): - return (True, f"In progress: {error}") - return (True, f"Query: {error}") - - result = resp.result - tasks = result.get('tasks', []) - - # Count failures - def count_failures(task_list): - count = 0 - for t in task_list: - if t.get('status') == 'fail': - count += 1 - count += count_failures(t.get('sub_tasks', [])) - return count - - failures = count_failures(tasks) - if failures > 0: - return (True, f"Still have {failures} failed tasks (may need more time)") - - return (True, "Clean state - no failures") - - -tr.Processes.Default.Streams.stdout = Testers.CustomJSONRPCResponse(validate_clean_state) -tr.StillRunningAfter = ts - -# ============================================================================ -# Test 10: Summary -# ============================================================================ -tr = Test.AddTestRun("Final summary") -tr.DelayStart = 1 -tr.AddJsonRPCClientRequest(ts, Request.admin_config_reload()) - - -def validate_summary(resp: Response): - '''Final summary of failure testing''' - if resp.is_error(): - return (True, f"Final: {resp.error_as_str()}") - - result = resp.result - - summary = """ - Config Reload Failure Testing Summary: - - Failed tasks: Detected when config validation fails - - Failed subtasks: SSL cert failures propagate to parent - - Status propagation: Parent status reflects worst subtask status - - Error logs: Failed tasks should include error details - """ - - return (True, f"Test complete. Token: {result.get('token', 'none')}") - - -tr.Processes.Default.Streams.stdout = Testers.CustomJSONRPCResponse(validate_summary) -tr.StillRunningAfter = ts diff --git a/tests/gold_tests/jsonrpc/config_reload_full_smoke.test.py b/tests/gold_tests/jsonrpc/config_reload_full_smoke.test.py deleted file mode 100644 index e1788b3a15d..00000000000 --- a/tests/gold_tests/jsonrpc/config_reload_full_smoke.test.py +++ /dev/null @@ -1,165 +0,0 @@ -''' -Full reload smoke test. - -Verifies that ALL registered config handlers complete properly by: - Part A: Touching every registered config file, triggering a reload with a - named token, and verifying all deferred handlers reach a terminal - state (no in_progress after a delay). - Part B: Changing one record per module via traffic_ctl config set, waiting, - then verifying no terminal-state conflicts appear in diags.log. - -Registered configs at time of writing: - Files: ip_allow.yaml, parent.config, cache.config, hosting.config, - splitdns.config, logging.yaml, sni.yaml, ssl_multicert.yaml - Record-only: ssl_ticket_key (proxy.config.ssl.server.ticket_key.filename) - -The key assertion is that diags.log does NOT contain: - "ignoring transition from" — means two code paths disagree about task outcome -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -Test.Summary = 'Full reload smoke test: all config files + record triggers' -Test.ContinueOnFail = True - -# --- Setup --- -ts = Test.MakeATSProcess("ts", enable_cache=True) -ts.Disk.records_config.update({ - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'rpc|config|reload', -}) - -# ============================================================================ -# Part A: File-based full reload -# -# ATS starts with valid config content (written via AddLines before start). -# We then touch every file to bump mtime and trigger a reload — this exercises -# the full parse path of each handler. -# ============================================================================ - -# Provide valid content for files whose handlers reject empty input. -ts.Disk.ip_allow_yaml.AddLines([ - 'ip_allow:', - '- apply: in', - ' ip_addrs: 0/0', - ' action: allow', - ' methods: ALL', -]) -ts.Disk.logging_yaml.AddLines([ - 'logging:', - ' formats:', - ' - name: smoke', - ' format: "%"', -]) -ts.Disk.sni_yaml.AddLines([ - 'sni:', - '- fqdn: "*.example.com"', - ' verify_client: NONE', -]) -# parent.config, cache.config, hosting.config, splitdns.config, -# ssl_multicert.yaml are fine empty — handlers accept empty/comment-only files. - -# All registered config files whose mtime we'll bump to trigger reload. -files_to_touch = [ - ts.Disk.ip_allow_yaml, - ts.Disk.parent_config, - ts.Disk.cache_config, - ts.Disk.hosting_config, - ts.Disk.splitdns_config, - ts.Disk.logging_yaml, - ts.Disk.sni_yaml, - ts.Disk.ssl_multicert_yaml, -] -touch_cmd = "touch " + " ".join([f.AbsRunTimePath for f in files_to_touch]) -# Modify records.yaml via traffic_ctl --cold to trigger a real records reload. -records_cmd = 'traffic_ctl config set proxy.config.diags.debug.tags "rpc|config|reload|upd" --cold' - -# Test 1: Start ATS, wait for it to settle, update records.yaml on disk -tr = Test.AddTestRun("Update records.yaml via --cold") -tr.Processes.Default.StartBefore(ts) -tr.Processes.Default.Command = f"sleep 3 && {records_cmd}" -tr.Processes.Default.Env = ts.Env -tr.Processes.Default.ReturnCode = 0 -tr.StillRunningAfter = ts - -# Test 2: Touch all other config files to bump mtime -tr = Test.AddTestRun("Touch all registered config files") -tr.Processes.Default.Command = touch_cmd -tr.Processes.Default.ReturnCode = 0 -tr.StillRunningAfter = ts - -# Test 3: Reload with token — all handlers re-read from disk -tr = Test.AddTestRun("Reload with token - show details") -tr.Processes.Default.Command = "traffic_ctl config reload -t full_reload_smoke" -tr.Processes.Default.Env = ts.Env -tr.Processes.Default.ReturnCode = 0 -tr.StillRunningAfter = ts - -# Test 4: Query status after delay — all deferred handlers (e.g. logging) should have -# reached a terminal state by now. -tr = Test.AddTestRun("Verify no tasks stuck in progress after delay") -tr.DelayStart = 15 -tr.Processes.Default.Command = "traffic_ctl config status -t full_reload_smoke" -tr.Processes.Default.Env = ts.Env -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Streams.stdout += Testers.ExcludesExpression("in_progress", "No task should remain in progress after 15s") -tr.StillRunningAfter = ts - -# ============================================================================ -# Part B: Record-triggered reloads (tracing ENABLED — default) -# -# Change one record per module to exercise the RecordTriggeredReloadContinuation path. -# With proxy.config.admin.reload.trace_record_triggers=1 (default), each -# record-triggered reload creates a "rec-" parent task visible in status/history. -# ============================================================================ - -# One safe record per module: -records_to_change = [ - # (record_name, new_value) — pick values that won't break ATS - ("proxy.config.log.sampling_frequency", "2"), # logging - ("proxy.config.ssl.server.session_ticket.enable", "0"), # ssl_client_coordinator -] - -for record_name, new_value in records_to_change: - tr = Test.AddTestRun(f"Set {record_name}={new_value}") - tr.DelayStart = 2 - tr.Processes.Default.Command = f"traffic_ctl config set {record_name} {new_value}" - tr.Processes.Default.Env = ts.Env - tr.Processes.Default.ReturnCode = 0 - tr.StillRunningAfter = ts - -# Wait for record-triggered reloads to complete -tr = Test.AddTestRun("Wait for record-triggered reloads") -tr.DelayStart = 10 -tr.Processes.Default.Command = "echo 'Waiting for record-triggered reloads to settle'" -tr.Processes.Default.ReturnCode = 0 -tr.StillRunningAfter = ts - -# Final dump of all reload history -tr = Test.AddTestRun("Fetch all reload history") -tr.Processes.Default.Command = "traffic_ctl config status -c all" -tr.Processes.Default.Env = ts.Env -tr.Processes.Default.ReturnCode = 0 -tr.StillRunningAfter = ts - -# ============================================================================ -# Global assertions on diags.log -# ============================================================================ - -# No handler should have conflicting terminal state transitions. -# This catches bugs where e.g. evaluate_config() calls fail() and then -# change_configuration() calls complete() — the guard rejects the second call. -ts.Disk.diags_log.Content = Testers.ExcludesExpression("ignoring transition from", "No handler should fight over terminal states") diff --git a/tests/gold_tests/jsonrpc/config_reload_reserve_subtask.test.py b/tests/gold_tests/jsonrpc/config_reload_reserve_subtask.test.py deleted file mode 100644 index d4b5b17c39f..00000000000 --- a/tests/gold_tests/jsonrpc/config_reload_reserve_subtask.test.py +++ /dev/null @@ -1,134 +0,0 @@ -''' -Test that reserve_subtask correctly pre-registers subtasks during a reload. - -When records.yaml is changed alongside other config files, the "records" -subtask completes first (records.yaml is always processed first in -rereadConfig). This pushes the main task to SUCCESS before other -file-based or record-triggered handlers have a chance to register their -subtasks. reserve_subtask() must accept a SUCCESS parent and pull it back -to IN_PROGRESS by adding a CREATED child. - -This test: - 1. Modifies records.yaml on disk (--cold) to change a trigger record so - that a record-triggered handler fires during the next reload. - 2. Touches additional config files so file-based handlers also fire. - 3. Triggers a reload with a named token. - 4. Verifies that traffic.out contains "Reserved subtask" messages — proving - reserve_subtask() succeeded despite the parent being SUCCESS. - 5. Verifies no "ignoring transition" warnings (no state conflicts). - 6. Verifies the reload reaches a terminal state with all subtasks tracked. -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -Test.Summary = 'Verify reserve_subtask pre-registers subtasks when records.yaml is first' -Test.ContinueOnFail = True - -# --- Setup --- -ts = Test.MakeATSProcess("ts", enable_cache=True) -# Allow extra startup time — cache clearing can take ~10s in some environments. -ts.StartupTimeout = 30 -ts.Disk.records_config.update( - { - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'rpc|config|config.reload|filemanager', - }) - -# Provide valid content for files whose handlers reject empty input. -ts.Disk.ip_allow_yaml.AddLines([ - 'ip_allow:', - '- apply: in', - ' ip_addrs: 0/0', - ' action: allow', - ' methods: ALL', -]) -ts.Disk.logging_yaml.AddLines([ - 'logging:', - ' formats:', - ' - name: reserve_test', - ' format: "%"', -]) -ts.Disk.sni_yaml.AddLines([ - 'sni:', - '- fqdn: "*.example.com"', - ' verify_client: NONE', -]) - -# Files to touch to trigger file-based handlers during reload. -files_to_touch = [ - ts.Disk.ip_allow_yaml, - ts.Disk.logging_yaml, - ts.Disk.sni_yaml, - ts.Disk.cache_config, -] -touch_cmd = "touch " + " ".join([f.AbsRunTimePath for f in files_to_touch]) - -# ============================================================================ -# Test 1: Start ATS, let it settle, then modify records.yaml on disk -# ============================================================================ -tr = Test.AddTestRun("Modify records.yaml via --cold to change a trigger record") -tr.Processes.Default.StartBefore(ts) -# Change debug tags — this touches records.yaml on disk so rereadConfig -# detects it as changed. The --cold flag modifies the file without -# notifying the running process (the reload will pick it up). -tr.Processes.Default.Command = ( - 'sleep 3 && traffic_ctl config set proxy.config.diags.debug.tags ' - '"rpc|config|config.reload|filemanager|upd" --cold') -tr.Processes.Default.Env = ts.Env -tr.Processes.Default.ReturnCode = 0 -tr.StillRunningAfter = ts - -# ============================================================================ -# Test 2: Touch additional config files to bump mtime -# ============================================================================ -tr = Test.AddTestRun("Touch config files to trigger file-based handlers") -tr.Processes.Default.Command = touch_cmd -tr.Processes.Default.ReturnCode = 0 -tr.StillRunningAfter = ts - -# ============================================================================ -# Test 3: Trigger reload with a named token -# ============================================================================ -tr = Test.AddTestRun("Trigger reload with named token") -tr.Processes.Default.Command = "traffic_ctl config reload -t reserve_subtask_test" -tr.Processes.Default.Env = ts.Env -tr.Processes.Default.ReturnCode = 0 -tr.StillRunningAfter = ts - -# ============================================================================ -# Test 4: Wait for all handlers to complete, then query status -# ============================================================================ -tr = Test.AddTestRun("Verify reload completed — no tasks stuck in progress") -tr.DelayStart = 15 -tr.Processes.Default.Command = "traffic_ctl config status -t reserve_subtask_test" -tr.Processes.Default.Env = ts.Env -tr.Processes.Default.ReturnCode = 0 -# No subtask should still be in_progress after 15s. -tr.Processes.Default.Streams.stdout += Testers.ExcludesExpression( - "in_progress", "No task should remain in progress after 15s delay") -tr.Processes.Default.Streams.stdout += Testers.ContainsExpression("success", "Final reload status must be success") -tr.StillRunningAfter = ts - -# ============================================================================ -# Test 5: Verify no state-transition conflicts in traffic.out -# -# "ignoring transition from" means two code paths disagree about a task's -# outcome. This must never happen. -# ============================================================================ -ts.Disk.traffic_out.Content += Testers.ExcludesExpression( - "ignoring transition from", "No state-transition conflicts should appear in traffic.out") -ts.Disk.traffic_out.Content += Testers.ContainsExpression( - "Reserved subtask", "reserve_subtask() must log pre-registration messages") diff --git a/tests/gold_tests/jsonrpc/config_reload_rpc.test.py b/tests/gold_tests/jsonrpc/config_reload_rpc.test.py deleted file mode 100644 index 55d38103452..00000000000 --- a/tests/gold_tests/jsonrpc/config_reload_rpc.test.py +++ /dev/null @@ -1,491 +0,0 @@ -''' -Test inline config reload functionality via unified admin_config_reload RPC method. - -Inline mode is triggered by passing the "configs" parameter. -Tests the following features: -1. Basic inline reload with single config -2. Multiple configs in single request -3. File-based vs inline mode detection -4. Unknown config key error handling -5. Invalid YAML content handling -6. Reload while another is in progress -7. Verify config is actually applied -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -from jsonrpc import Request, Response - -Test.Summary = 'Test inline config reload via RPC' -Test.ContinueOnFail = True - -ts = Test.MakeATSProcess('ts', dump_runroot=True) - -Test.testName = 'config_reload_rpc' - -# Initial configuration -ts.Disk.records_config.update({ - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'rpc|config', -}) - -# ============================================================================ -# Test 1: File-based reload (no configs parameter) -# ============================================================================ -tr = Test.AddTestRun("File-based reload without configs parameter") -tr.Processes.Default.StartBefore(ts) -tr.AddJsonRPCClientRequest(ts, Request.admin_config_reload()) - - -def validate_file_based(resp: Response): - '''Verify file-based reload works when configs is not provided''' - result = resp.result - token = result.get('token', '') - message = result.get('message', []) - - if token: - return (True, f"File-based reload started: token={token}") - - errors = result.get('errors', []) - if errors: - return (True, f"File-based reload response: {errors}") - - return (True, f"Response: {result}") - - -tr.Processes.Default.Streams.stdout = Testers.CustomJSONRPCResponse(validate_file_based) -tr.StillRunningAfter = ts - -# ============================================================================ -# Test 2: Empty configs map (should trigger inline mode but process 0 configs) -# ============================================================================ -tr = Test.AddTestRun("Empty configs map") -tr.DelayStart = 2 -tr.AddJsonRPCClientRequest(ts, Request.admin_config_reload(configs={})) - - -def validate_empty_configs(resp: Response): - '''Verify behavior with empty configs''' - result = resp.result - - # Empty configs should succeed but with 0 changes - success = result.get('success', -1) - failed = result.get('failed', -1) - - if success == 0 and failed == 0: - return (True, f"Empty configs handled: success={success}, failed={failed}") - - # Or it might be an error - errors = result.get('errors', []) - if errors: - return (True, f"Empty configs rejected: {errors}") - - return (True, f"Result: {result}") - - -tr.Processes.Default.Streams.stdout = Testers.CustomJSONRPCResponse(validate_empty_configs) -tr.StillRunningAfter = ts - -# ============================================================================ -# Test 3: Unknown config key (error code 6010) -# ============================================================================ -tr = Test.AddTestRun("Unknown config key should error with code 6010") -tr.DelayStart = 2 -tr.AddJsonRPCClientRequest(ts, Request.admin_config_reload(configs={"unknown_config_key": {"some": "data"}})) - - -def validate_unknown_key(resp: Response): - '''Verify error for unknown config key - should return error code 6010''' - result = resp.result - errors = result.get('errors', []) - - if not errors: - return (False, f"Expected error for unknown key, got: {result}") - - error_str = str(errors) - if '6010' in error_str or 'not registered' in error_str: - return (True, f"Unknown key rejected with code 6010: {errors}") - return (False, f"Expected error 6010, got: {errors}") - - -tr.Processes.Default.Streams.stdout = Testers.CustomJSONRPCResponse(validate_unknown_key) -tr.StillRunningAfter = ts - -# ============================================================================ -# Test 3b: Unregistered config rejected (error code 6010) -# Note: remap.config is not registered in ConfigRegistry -# ============================================================================ -tr = Test.AddTestRun("Unregistered config should error with code 6010") -tr.DelayStart = 1 -tr.AddJsonRPCClientRequest(ts, Request.admin_config_reload(configs={"remap.config": {"some": "data"}})) - - -def validate_legacy_not_supported(resp: Response): - '''Verify unregistered config returns error code 6010''' - result = resp.result - errors = result.get('errors', []) - - if not errors: - return (False, f"Expected rejection for unregistered config, got: {result}") - - error_str = str(errors) - if '6010' in error_str or 'not registered' in error_str: - return (True, f"Unregistered config correctly rejected: {errors}") - return (False, f"Expected error 6010, got: {errors}") - - -tr.Processes.Default.Streams.stdout = Testers.CustomJSONRPCResponse(validate_legacy_not_supported) -tr.StillRunningAfter = ts - -# ============================================================================ -# Test 4: RPC-injected content rejected for FileOnly config (ip_allow) -# ip_allow is registered with ConfigSource::FileOnly — RPC content must be rejected -# ============================================================================ -tr = Test.AddTestRun("RPC-injected content rejected for FileOnly config (ip_allow)") -tr.DelayStart = 2 -tr.AddJsonRPCClientRequest( - ts, - Request.admin_config_reload( - configs={"ip_allow": [{ - "apply": "in", - "ip_addrs": "127.0.0.1", - "action": "allow", - "methods": ["GET", "HEAD"] - }]})) - - -def validate_rpc_inject_rejected(resp: Response): - '''ip_allow is registered as FileOnly — RPC-injected content must be rejected with 6011''' - result = resp.result - errors = result.get('errors', []) - - if not errors: - return (False, f"Expected rejection for FileOnly config, got: {result}") - - error_str = str(errors) - if '6011' in error_str or 'does not support RPC' in error_str: - return (True, f"FileOnly config correctly rejected RPC injection: {errors}") - return (False, f"Expected error 6011, got: {errors}") - - -tr.Processes.Default.Streams.stdout = Testers.CustomJSONRPCResponse(validate_rpc_inject_rejected) -tr.StillRunningAfter = ts - -# ============================================================================ -# Test 5: Multiple configs in single request -# ============================================================================ -tr = Test.AddTestRun("Multiple configs in single request") -tr.DelayStart = 2 -tr.AddJsonRPCClientRequest( - ts, - Request.admin_config_reload( - configs={ - "ip_allow": [{ - "apply": "in", - "ip_addrs": "0.0.0.0/0", - "action": "allow" - }], - "sni": [{ - "fqdn": "*.test.com", - "verify_client": "NONE" - }], - "records": { - "diags": { - "debug": { - "enabled": 1 - } - } - } - })) - - -def validate_multiple_configs(resp: Response): - '''All configs should be rejected — none support RPC content source at this stage''' - result = resp.result - errors = result.get('errors', []) - - if not errors: - return (False, f"Expected rejections for all configs, got: {result}") - - # Each config should produce an error (6010=not registered, 6011=RPC source not supported) - error_str = str(errors) - if '6010' in error_str or '6011' in error_str: - return (True, f"All configs rejected as expected: {len(errors)} errors") - return (False, f"Unexpected errors: {errors}") - - -tr.Processes.Default.Streams.stdout = Testers.CustomJSONRPCResponse(validate_multiple_configs) -tr.StillRunningAfter = ts - -# ============================================================================ -# Test 6: Reload while another is in progress -# ============================================================================ -tr = Test.AddTestRun("First reload request") -tr.DelayStart = 1 -tr.AddJsonRPCClientRequest(ts, Request.admin_config_reload()) - - -def validate_first_reload(resp: Response): - '''Start a regular reload''' - result = resp.result - token = result.get('token', '') - return (True, f"First reload started: token={token}") - - -tr.Processes.Default.Streams.stdout = Testers.CustomJSONRPCResponse(validate_first_reload) -tr.StillRunningAfter = ts - -# Immediately try inline reload -tr = Test.AddTestRun("Inline reload while regular reload in progress") -tr.DelayStart = 0 # No delay - immediately after -tr.AddJsonRPCClientRequest(ts, Request.admin_config_reload(configs={"ip_allow": [{"apply": "in", "ip_addrs": "10.0.0.0/8"}]})) - - -def validate_in_progress_rejection(resp: Response): - '''Should be rejected for RPC source not supported or reload in progress''' - result = resp.result - errors = result.get('errors', []) - - if not errors: - return (False, f"Expected rejection, got: {result}") - - error_str = str(errors) - # Either 6011 (RPC source not supported) or 6004 (reload in progress) - if '6011' in error_str or '6004' in error_str: - return (True, f"Correctly rejected: {errors}") - return (False, f"Unexpected error: {errors}") - - -tr.Processes.Default.Streams.stdout = Testers.CustomJSONRPCResponse(validate_in_progress_rejection) -tr.StillRunningAfter = ts - -# ============================================================================ -# Test 7: Verify token is returned with inline- prefix -# ============================================================================ -tr = Test.AddTestRun("Verify inline token prefix") -tr.DelayStart = 3 # Wait for previous reloads to complete -tr.AddJsonRPCClientRequest(ts, Request.admin_config_reload(configs={"unknown_for_token_test": {"data": "value"}})) - - -def validate_inline_token(resp: Response): - '''Verify token has inline- prefix''' - result = resp.result - token = result.get('token', '') - - if token and token.startswith('inline-'): - return (True, f"Token has correct prefix: {token}") - - if not token: - # Check if there's an error (which is fine) - errors = result.get('errors', []) - if errors: - return (True, f"No token (error case): {errors}") - - return (True, f"Token result: {token}") - - -tr.Processes.Default.Streams.stdout = Testers.CustomJSONRPCResponse(validate_inline_token) -tr.StillRunningAfter = ts - -# ============================================================================ -# Test 8: Nested YAML structure (records.diags.debug) -# ============================================================================ -tr = Test.AddTestRun("Nested YAML structure") -tr.DelayStart = 2 -tr.AddJsonRPCClientRequest( - ts, - Request.admin_config_reload( - configs={"records": { - "diags": { - "debug": { - "enabled": 1, - "tags": "http|rpc|test" - } - }, - "http": { - "cache": { - "http": 1 - } - } - }})) - - -def validate_nested_yaml(resp: Response): - '''Verify nested YAML handling''' - result = resp.result - success = result.get('success', 0) - failed = result.get('failed', 0) - errors = result.get('errors', []) - - return (True, f"Nested YAML: success={success}, failed={failed}, errors={errors}") - - -tr.Processes.Default.Streams.stdout = Testers.CustomJSONRPCResponse(validate_nested_yaml) -tr.StillRunningAfter = ts - -# ============================================================================ -# Test 9: Query status after inline reload -# ============================================================================ -tr = Test.AddTestRun("Query status after inline reload") -tr.DelayStart = 2 -tr.AddJsonRPCClientRequest(ts, Request.get_reload_config_status()) - - -def validate_status_after_inline(resp: Response): - '''Check status includes inline reload info''' - if resp.is_error(): - return (True, f"Status query error (may be expected): {resp.error_as_str()}") - - result = resp.result - tasks = result.get('tasks', []) - - if tasks: - # Check if any task has inline- prefix - for task in tasks: - token = task.get('token', '') - if token.startswith('inline-'): - return (True, f"Found inline reload in status: {token}") - - return (True, f"Status result: {result}") - - -tr.Processes.Default.Streams.stdout = Testers.CustomJSONRPCResponse(validate_status_after_inline) -tr.StillRunningAfter = ts - -# ============================================================================ -# Test 10: Large config content -# ============================================================================ -tr = Test.AddTestRun("Large config content") -tr.DelayStart = 2 - -# Generate a larger config -large_ip_allow = [] -for i in range(50): - large_ip_allow.append({"apply": "in", "ip_addrs": f"10.{i}.0.0/16", "action": "allow"}) - -tr.AddJsonRPCClientRequest(ts, Request.admin_config_reload(configs={"ip_allow": large_ip_allow})) - - -def validate_large_config(resp: Response): - '''Large ip_allow config should also be rejected (FileOnly)''' - result = resp.result - errors = result.get('errors', []) - - if not errors: - return (False, f"Expected rejection for FileOnly config, got: {result}") - - error_str = str(errors) - if '6011' in error_str: - return (True, f"Large config correctly rejected: {errors}") - return (False, f"Expected error 6011, got: {errors}") - - -tr.Processes.Default.Streams.stdout = Testers.CustomJSONRPCResponse(validate_large_config) -tr.StillRunningAfter = ts - -# ============================================================================ -# Test 11: Reload directive for registered FileOnly config (sni) -# Directive-only request — sni is FileOnly, so the RPC handler rejects with 6011. -# Verifies the _reload structure is handled gracefully through the RPC stack. -# ============================================================================ -tr = Test.AddTestRun("Reload directive for FileOnly config (sni)") -tr.DelayStart = 2 -tr.AddJsonRPCClientRequest(ts, Request.admin_config_reload(configs={"sni": {"_reload": {"fqdn": "*.example.com"}}})) - - -def validate_directive_fileonly(resp: Response): - '''sni is FileOnly — directive-only request rejected with 6011''' - result = resp.result - errors = result.get('errors', []) - - if not errors: - return (False, f"Expected rejection for FileOnly config, got: {result}") - - error_str = str(errors) - if '6011' in error_str: - return (True, f"Directive-only correctly rejected for FileOnly config: {errors}") - return (False, f"Expected error 6011, got: {errors}") - - -tr.Processes.Default.Streams.stdout = Testers.CustomJSONRPCResponse(validate_directive_fileonly) -tr.StillRunningAfter = ts - -# ============================================================================ -# Test 12: Reload directive for unregistered config (virtualhost) -# virtualhost is not registered yet — should get 6010. -# This is the intended use case once the virtualhost handler is registered. -# ============================================================================ -tr = Test.AddTestRun("Reload directive for unregistered config (virtualhost)") -tr.DelayStart = 2 -tr.AddJsonRPCClientRequest(ts, Request.admin_config_reload(configs={"virtualhost": {"_reload": {"id": "myhost.example.com"}}})) - - -def validate_directive_unregistered(resp: Response): - '''virtualhost is not registered — rejected with 6010''' - result = resp.result - errors = result.get('errors', []) - - if not errors: - return (False, f"Expected error for unregistered config, got: {result}") - - error_str = str(errors) - if '6010' in error_str or 'not registered' in error_str: - return (True, f"Directive for unregistered config rejected: {errors}") - return (False, f"Expected error 6010, got: {errors}") - - -tr.Processes.Default.Streams.stdout = Testers.CustomJSONRPCResponse(validate_directive_unregistered) -tr.StillRunningAfter = ts - -# ============================================================================ -# Test 13: Directives mixed with content for FileOnly config (ip_allow) -# _reload directives alongside actual config content — still rejected with 6011. -# ============================================================================ -tr = Test.AddTestRun("Directives mixed with content for FileOnly config") -tr.DelayStart = 2 -tr.AddJsonRPCClientRequest( - ts, - Request.admin_config_reload( - configs={ - "ip_allow": { - "_reload": { - "validate_only": "true" - }, - "rules": [{ - "apply": "in", - "ip_addrs": "0/0", - "action": "allow" - }] - } - })) - - -def validate_directive_mixed(resp: Response): - '''ip_allow is FileOnly — mixed directive+content rejected with 6011''' - result = resp.result - errors = result.get('errors', []) - - if not errors: - return (False, f"Expected rejection, got: {result}") - - error_str = str(errors) - if '6011' in error_str: - return (True, f"Mixed directive+content correctly rejected: {errors}") - return (False, f"Expected error 6011, got: {errors}") - - -tr.Processes.Default.Streams.stdout = Testers.CustomJSONRPCResponse(validate_directive_mixed) -tr.StillRunningAfter = ts diff --git a/tests/gold_tests/jsonrpc/config_reload_tracking.test.py b/tests/gold_tests/jsonrpc/config_reload_tracking.test.py deleted file mode 100644 index 272a0e7d599..00000000000 --- a/tests/gold_tests/jsonrpc/config_reload_tracking.test.py +++ /dev/null @@ -1,304 +0,0 @@ -''' -Test config reload tracking functionality. - -Tests the following features: -1. Basic reload with token generation -2. Querying reload status while in progress -3. Reload history tracking -4. Force reload while one is in progress -5. Custom token names -6. Duplicate token prevention -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -from jsonrpc import Request, Response -import time - -Test.Summary = 'Test config reload tracking with tokens and status' -Test.ContinueOnFail = True - -ts = Test.MakeATSProcess('ts', dump_runroot=True) - -Test.testName = 'config_reload_tracking' - -# Initial configuration -ts.Disk.records_config.update({ - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'rpc|config', -}) - -# Store tokens for later tests -stored_tokens = [] - -# ============================================================================ -# Test 1: Basic reload - verify token is returned -# ============================================================================ -tr = Test.AddTestRun("Basic reload with auto-generated token") -tr.Processes.Default.StartBefore(ts) -tr.AddJsonRPCClientRequest(ts, Request.admin_config_reload()) - - -def validate_basic_reload(resp: Response): - '''Verify reload returns a token''' - if resp.is_error(): - return (False, f"Error: {resp.error_as_str()}") - - result = resp.result - token = result.get('token', '') - created_time = result.get('created_time', '') - messages = result.get('message', []) - - if not token: - return (False, "No token returned") - - if not token.startswith('rldtk-'): - return (False, f"Token should start with 'rldtk-', got: {token}") - - # Store for later tests - stored_tokens.append(token) - - return (True, f"Reload started: token={token}, created={created_time}, messages={messages}") - - -tr.Processes.Default.Streams.stdout = Testers.CustomJSONRPCResponse(validate_basic_reload) -tr.StillRunningAfter = ts - -# ============================================================================ -# Test 2: Query status of completed reload -# ============================================================================ -tr = Test.AddTestRun("Query status of completed reload") -tr.DelayStart = 2 # Give time for reload to complete -tr.AddJsonRPCClientRequest(ts, Request.admin_config_reload_status()) - - -def validate_status_query(resp: Response): - '''Check reload status after completion''' - if resp.is_error(): - # If method doesn't exist, that's OK - we're testing the main reload - return (True, f"Status query: {resp.error_as_str()}") - - result = resp.result - return (True, f"Reload status: {result}") - - -tr.Processes.Default.Streams.stdout = Testers.CustomJSONRPCResponse(validate_status_query) -tr.StillRunningAfter = ts - -# ============================================================================ -# Test 3: Reload with custom token -# ============================================================================ -tr = Test.AddTestRun("Reload with custom token") -tr.DelayStart = 1 -custom_token = f"my-custom-token-{int(time.time())}" -tr.AddJsonRPCClientRequest(ts, Request.admin_config_reload(token=custom_token)) - - -def validate_custom_token(resp: Response): - '''Verify custom token is accepted''' - if resp.is_error(): - # Check if it's a "reload in progress" error - error_str = resp.error_as_str() - if 'in progress' in error_str.lower(): - return (True, f"Reload in progress (expected): {error_str}") - return (False, f"Error: {error_str}") - - result = resp.result - token = result.get('token', '') - - if token != custom_token: - return (False, f"Expected custom token '{custom_token}', got '{token}'") - - stored_tokens.append(token) - return (True, f"Custom token accepted: {token}") - - -tr.Processes.Default.Streams.stdout = Testers.CustomJSONRPCResponse(validate_custom_token) -tr.StillRunningAfter = ts - -# ============================================================================ -# Test 4: Force reload while previous might still be processing -# ============================================================================ -tr = Test.AddTestRun("Force reload") -tr.DelayStart = 1 -tr.AddJsonRPCClientRequest(ts, Request.admin_config_reload(force=True)) - - -def validate_force_reload(resp: Response): - '''Verify force reload works''' - if resp.is_error(): - return (False, f"Force reload failed: {resp.error_as_str()}") - - result = resp.result - token = result.get('token', '') - if token: - stored_tokens.append(token) - - return (True, f"Force reload succeeded: token={token}") - - -tr.Processes.Default.Streams.stdout = Testers.CustomJSONRPCResponse(validate_force_reload) -tr.StillRunningAfter = ts - -# ============================================================================ -# Test 5: Try duplicate token (should fail) -# ============================================================================ -tr = Test.AddTestRun("Duplicate token rejection") -tr.DelayStart = 2 # Wait for previous reload to complete - - -def make_duplicate_test(): - # Use the first token we stored - if stored_tokens: - return Request.admin_config_reload(token=stored_tokens[0]) - return Request.admin_config_reload(token="rldtk-duplicate-test") - - -tr.AddJsonRPCClientRequest(ts, make_duplicate_test()) - - -def validate_duplicate_rejection(resp: Response): - '''Verify duplicate tokens are rejected''' - if resp.is_error(): - return (True, f"Duplicate rejected (expected): {resp.error_as_str()}") - - result = resp.result - errors = result.get('error', []) - if errors: - return (True, f"Duplicate token rejected: {errors}") - - # If no error, check if token was actually reused - token = result.get('token', '') - return (True, f"Reload result: token={token}, errors={errors}") - - -tr.Processes.Default.Streams.stdout = Testers.CustomJSONRPCResponse(validate_duplicate_rejection) -tr.StillRunningAfter = ts - -# ============================================================================ -# Test 6: Rapid succession reloads -# ============================================================================ -tr = Test.AddTestRun("Rapid succession reloads - first") -tr.DelayStart = 1 -tr.AddJsonRPCClientRequest(ts, Request.admin_config_reload()) - - -def validate_rapid_first(resp: Response): - '''First rapid reload''' - if resp.is_error(): - return (True, f"First rapid: {resp.error_as_str()}") - - result = resp.result - token = result.get('token', '') - return (True, f"First rapid reload: {token}") - - -tr.Processes.Default.Streams.stdout = Testers.CustomJSONRPCResponse(validate_rapid_first) -tr.StillRunningAfter = ts - -# Second rapid reload (should see in-progress or succeed) -tr = Test.AddTestRun("Rapid succession reloads - second") -tr.DelayStart = 0 # No delay - immediately after first -tr.AddJsonRPCClientRequest(ts, Request.admin_config_reload()) - - -def validate_rapid_second(resp: Response): - '''Second rapid reload - may see in-progress''' - if resp.is_error(): - return (True, f"Second rapid (may be in progress): {resp.error_as_str()}") - - result = resp.result - token = result.get('token', '') - error = result.get('error', []) - - if error: - # In-progress is expected - return (True, f"Second rapid - in progress or error: {error}") - - return (True, f"Second rapid reload: {token}") - - -tr.Processes.Default.Streams.stdout = Testers.CustomJSONRPCResponse(validate_rapid_second) -tr.StillRunningAfter = ts - -# ============================================================================ -# Test 7: Get reload history -# ============================================================================ -tr = Test.AddTestRun("Get reload history") -tr.DelayStart = 2 -tr.AddJsonRPCClientRequest(ts, Request.admin_config_reload_history()) - - -def validate_history(resp: Response): - '''Check reload history''' - if resp.is_error(): - # Method may not exist - return (True, f"History query: {resp.error_as_str()}") - - result = resp.result - history = result.get('history', []) - return (True, f"Reload history: {len(history)} entries") - - -tr.Processes.Default.Streams.stdout = Testers.CustomJSONRPCResponse(validate_history) -tr.StillRunningAfter = ts - -# ============================================================================ -# Test 8: Trigger reload and verify new config is loaded -# ============================================================================ -tr = Test.AddTestRun("Reload after config change") -tr.DelayStart = 1 -tr.AddJsonRPCClientRequest(ts, Request.admin_config_reload(force=True)) - - -def validate_reload_after_change(resp: Response): - '''Verify reload after config change''' - if resp.is_error(): - return (False, f"Reload after change failed: {resp.error_as_str()}") - - result = resp.result - token = result.get('token', '') - return (True, f"Reload after config change: token={token}") - - -tr.Processes.Default.Streams.stdout = Testers.CustomJSONRPCResponse(validate_reload_after_change) -tr.StillRunningAfter = ts - -# ============================================================================ -# Test 10: Final status check -# ============================================================================ -tr = Test.AddTestRun("Final reload status") -tr.DelayStart = 2 -tr.AddJsonRPCClientRequest(ts, Request.admin_config_reload()) - - -def validate_final_status(resp: Response): - '''Final status verification''' - if resp.is_error(): - error_str = resp.error_as_str() - if 'in progress' in error_str.lower(): - return (True, f"Reload still in progress: {error_str}") - return (True, f"Final status: {error_str}") - - result = resp.result - token = result.get('token', '') - created = result.get('created_time', '') - - return (True, f"Final reload: token={token}, created={created}") - - -tr.Processes.Default.Streams.stdout = Testers.CustomJSONRPCResponse(validate_final_status) -tr.StillRunningAfter = ts diff --git a/tests/gold_tests/jsonrpc/jsonrpc_api_schema.test.py b/tests/gold_tests/jsonrpc/jsonrpc_api_schema.test.py deleted file mode 100644 index 0b83fef35a1..00000000000 --- a/tests/gold_tests/jsonrpc/jsonrpc_api_schema.test.py +++ /dev/null @@ -1,220 +0,0 @@ -''' -JSONRPC Schema test. This test will run a basic request/response schema validation. -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import os -import sys -import tempfile -from string import Template - -Test.Summary = 'Test jsonrpc admin API' - -# set the schema folder. -schema_folder = os.path.join(Test.TestDirectory, '..', '..', '..', "src", "mgmt", "rpc", "schema") - - -def substitute_context_in_file(process, file, context): - ''' - Perform substitution based on the passed context dict. This function will return a new path for the substituted file. - ''' - if os.path.isdir(file): - raise ValueError(f"Mapping substitution not supported for directories.") - - with open(os.path.join(process.TestDirectory, file), 'r') as req_file: - req_template = Template(req_file.read()) - req_content = req_template.substitute(context) - tf = tempfile.NamedTemporaryFile(delete=False, dir=process.RunDirectory, suffix=f"_{os.path.basename(file)}") - file = tf.name - with open(file, "w") as new_req_file: - new_req_file.write(req_content) - - return file - - -def add_testrun_for_jsonrpc_request( - test_description, request_file_name, params_schema_file_name=None, result_schema_file_name=None, context=None): - ''' - Simple wrapper around the AddJsonRPCClientRequest method. - - context: - This can be used if a template substitution is needed in the request file. - - stdout_testers: - Testers to be run on the output stream. - - params_schema_file_name: - Schema file to validate the request 'params' field. - - result_schema_file_name: - Schema file to validate the response 'result' field. - ''' - tr = Test.AddTestRun(test_description) - tr.Setup.Copy(request_file_name) - - if context: - request_file_name = substitute_context_in_file(tr, request_file_name, context) - - request_schema_file_name = os.path.join(schema_folder, "jsonrpc_request_schema.json") - tr.AddJsonRPCClientRequest( - ts, - file=os.path.join(ts.RunDirectory, os.path.basename(request_file_name)), - schema_file_name=request_schema_file_name, - params_field_schema_file_name=params_schema_file_name) - - tr.Processes.Default.ReturnCode = 0 - - response_schema_file_name = os.path.join(schema_folder, "jsonrpc_response_schema.json") - tr.Processes.Default.Streams.stdout = Testers.JSONRPCResponseSchemaValidator( - schema_file_name=response_schema_file_name, result_field_schema_file_name=result_schema_file_name) - - tr.StillRunningAfter = ts - return tr - - -ts = Test.MakeATSProcess('ts') - -Test.testName = 'Basic JSONRPC API test' - -ts.Disk.records_config.update( - { - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'rpc|filemanager|http|cache', - 'proxy.config.jsonrpc.filename': "jsonrpc.yaml", # We will be using this record to tests some RPC API. - }) - -# One of the API's will be checking the storage. Need this to get a response with content. -storage_path = os.path.join(Test.RunDirectory, "ts", "storage") -ts.Disk.storage_yaml.AddLine(f''' -cache: - spans: - - name: disk-1 - path: {storage_path} - size: 512M -''') - -# The following tests will only validate the jsonrpc message, it will not run any validation on the content of the 'result' or 'params' -# of the jsonrpc message. This should be added once the schemas are available. - -# jsonrpc 2.0 schema file. This will not check the param fields. - -success_schema_file_name_name = os.path.join(schema_folder, "success_response_schema.json") -# admin_lookup_records - -params_schema_file_name = os.path.join(schema_folder, "admin_lookup_records_params_schema.json") -first = add_testrun_for_jsonrpc_request( - "Test admin_lookup_records", - request_file_name='json/admin_lookup_records_req_1.json', - params_schema_file_name=params_schema_file_name, - context={'record_name': 'proxy.config.jsonrpc.filename'}) -first.Processes.Default.StartBefore(ts) - -add_testrun_for_jsonrpc_request( - "Test admin_lookup_records w/error", request_file_name='json/admin_lookup_records_req_invalid_rec.json') - -add_testrun_for_jsonrpc_request( - "Test admin_lookup_records", - request_file_name='json/admin_lookup_records_req_1.json', - context={'record_name': 'proxy.config.jsonrpc.filename'}) - -add_testrun_for_jsonrpc_request( - "Test admin_lookup_records w/error", request_file_name='json/admin_lookup_records_req_invalid_rec.json') - -add_testrun_for_jsonrpc_request( - "Test admin_lookup_records w/error", - request_file_name='json/admin_lookup_records_req_multiple.json', - context={'record_name': 'proxy.config.jsonrpc.filename'}) - -add_testrun_for_jsonrpc_request( - "Test admin_lookup_records w/error", - request_file_name='json/admin_lookup_records_req_metric.json', - context={'record_name_regex': 'proxy.process.http.total_client_connections_ipv4*'}) - -# admin_config_set_records -add_testrun_for_jsonrpc_request( - "Test admin_lookup_records w/error", - request_file_name='json/admin_config_set_records_req.json', - context={ - 'record_name': 'proxy.config.jsonrpc.filename', - 'record_value': 'test_jsonrpc.yaml' - }) - -# admin_config_reload -# We will wait for this to have a stable schema. I think we may need to adjust the response a bit. -# add_testrun_for_jsonrpc_request( -# "Test admin_config_reload", -# request_file_name='json/admin_config_reload_req.json', -# result_schema_file_name=success_schema_file_name_name) - -# admin_host_set_status -add_testrun_for_jsonrpc_request( - "Test admin_host_set_status", - request_file_name='json/admin_host_set_status_req.json', - context={ - 'operation': 'up', - 'host': 'my.test.host.trafficserver.com' - }) - -# admin_host_set_status -add_testrun_for_jsonrpc_request( - "Test admin_host_set_status", - request_file_name='json/admin_host_set_status_req.json', - context={ - 'operation': 'down', - 'host': 'my.test.host.trafficserver.com' - }) - -# admin_server_start_drain -add_testrun_for_jsonrpc_request( - "Test admin_server_start_drain", - request_file_name='json/method_call_no_params.json', - context={'method': 'admin_server_start_drain'}) - -add_testrun_for_jsonrpc_request( - "Test admin_server_start_drain", - request_file_name='json/method_call_no_params.json', - context={'method': 'admin_server_start_drain'}) - -# admin_server_stop_drain -add_testrun_for_jsonrpc_request( - "Test admin_server_stop_drain", - request_file_name='json/method_call_no_params.json', - context={'method': 'admin_server_stop_drain'}) - -# admin_storage_get_device_status -add_testrun_for_jsonrpc_request( - "Test admin_storage_get_device_status", - request_file_name='json/admin_storage_x_device_status_req.json', - context={ - 'method': 'admin_storage_get_device_status', - 'device': f'{storage_path}/cache.db' - }) - -# admin_storage_set_device_offline -add_testrun_for_jsonrpc_request( - "Test admin_storage_set_device_offline", - request_file_name='json/admin_storage_x_device_status_req.json', - context={ - 'method': 'admin_storage_set_device_offline', - 'device': f'{storage_path}/cache.db' - }) - -# admin_plugin_send_basic_msg -add_testrun_for_jsonrpc_request( - "Test admin_plugin_send_basic_msg", - request_file_name='json/admin_plugin_send_basic_msg_req.json', - result_schema_file_name=success_schema_file_name_name) diff --git a/tests/gold_tests/logging/all_headers.test.py b/tests/gold_tests/logging/all_headers.test.py deleted file mode 100644 index dbb65ba2fd6..00000000000 --- a/tests/gold_tests/logging/all_headers.test.py +++ /dev/null @@ -1,115 +0,0 @@ -''' -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import os -import sys - -Test.Summary = ''' -Test new "all headers" log fields -''' - -# Define ATS. -# -ts = Test.MakeATSProcess("ts") - -# Define MicroServer. -# -server = Test.MakeOriginServer("server") - -request_header = {"headers": "GET / HTTP/1.1\r\nHost: does.not.matter\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -response_header = { - "headers": "HTTP/1.1 200 OK\r\nConnection: close\r\nCache-control: max-age=85000\r\n\r\n", - "timestamp": "1469733493.993", - "body": "xxx" -} -server.addResponse("sessionlog.json", request_header, response_header) - -ts.Disk.records_config.update({ - 'proxy.config.diags.debug.enabled': 0, - 'proxy.config.diags.debug.tags': 'http|dns', -}) - -ts.Disk.remap_config.AddLine('map http://127.0.0.1:{0} http://127.0.0.1:{1}'.format(ts.Variables.port, server.Variables.Port)) - -# Mix in a numeric log field. Hopefull this will detect any binary alignment problems. -# -ts.Disk.logging_yaml.AddLines( - ''' -logging: - formats: - - name: custom - format: "% ___FS___ % ___FS___ % ___FS___ % ___FS___ % ___FS___ % ___FS___ END_TXN" - logs: - - filename: test_all_headers - format: custom -'''.split("\n")) - -# Configure comparison of "sanitized" log file with gold file at end of test. -# -sanitized_log_path = os.path.join(ts.Variables.LOGDIR, 'test_all_headers.log.san') -if Condition.CurlUsingUnixDomainSocket(): - Test.Disk.File(sanitized_log_path, exists=True, content='gold/test_all_headers_uds.gold') -else: - Test.Disk.File(sanitized_log_path, exists=True, content='gold/test_all_headers.gold') - - -def reallyLong(): - value = 'abcdefghijklmnop' - value = value + value - value = value + value - value = value + value - retval = "" - for i in range(3): - retval += ' -H "x-header{}: {}"'.format(i, value) - return retval - - -tr = Test.AddTestRun() -tr.Processes.Default.StartBefore(server) -tr.Processes.Default.StartBefore(Test.Processes.ts) -if Condition.CurlUsingUnixDomainSocket(): - tr.MakeCurlCommand( - '"http://127.0.0.1:{0}" --user-agent "007" -H "Host: 127.0.0.1:{0}" --verbose '.format(ts.Variables.port) + reallyLong(), - ts=ts) -else: - tr.MakeCurlCommand('"http://127.0.0.1:{0}" --user-agent "007" --verbose '.format(ts.Variables.port) + reallyLong(), ts=ts) -tr.Processes.Default.ReturnCode = 0 - -# Repeat same curl, will be answered from the ATS cache. -# -tr = Test.AddTestRun() -if Condition.CurlUsingUnixDomainSocket(): - tr.MakeCurlCommand( - '"http://127.0.0.1:{0}" --user-agent "007" -H "Host: 127.0.0.1:{0}" --verbose '.format(ts.Variables.port) + reallyLong(), - ts=ts) -else: - tr.MakeCurlCommand('"http://127.0.0.1:{0}" --user-agent "007" --verbose '.format(ts.Variables.port) + reallyLong(), ts=ts) -tr.Processes.Default.ReturnCode = 0 - -# Delay to allow TS to flush report to disk, then "sanitize" generated log. -# -tr = Test.AddTestRun() - -log_path = os.path.join(ts.Variables.LOGDIR, 'test_all_headers.log') -sanitizer_python_script = os.path.join(Test.TestDirectory, 'all_headers_sanitizer.py') -sanitizer_shell_script = os.path.join(Test.TestDirectory, 'all_headers_sanitizer.sh') - -tr.Processes.Default.Command = \ - (f'{sys.executable} {sanitizer_python_script} {log_path} {server.Variables.Port} | ' - f'sh {sanitizer_shell_script} > {sanitized_log_path}') -tr.Processes.Default.ReturnCode = 0 diff --git a/tests/gold_tests/logging/binary_log_v3.test.py b/tests/gold_tests/logging/binary_log_v3.test.py deleted file mode 100644 index 9701a389f68..00000000000 --- a/tests/gold_tests/logging/binary_log_v3.test.py +++ /dev/null @@ -1,170 +0,0 @@ -''' -End-to-end test of the self-describing v3 binary log format. -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import os - -Test.Summary = ''' -Write a v3 binary log and read it back with traffic_logcat (ASCII + JSON), -and prove the per-LogObject binary_log_version:2 writer is still readable. -''' - -# chi resolves to the client address; a Unix-domain-socket curl changes it. -Test.SkipIf(Condition.CurlUsingUnixDomainSocket()) - - -class BinaryLogV3Test: - ''' Drive traffic into a v3 binary log, then decode it with traffic_logcat - and compare the ASCII and JSON output against gold files. - ''' - - # A counter to give each ATS process a unique name. - __ts_counter = 1 - - # Number of transactions (and therefore log entries) to generate. - num_requests = 3 - - # One format exercising all four wire types: - # chi = client IP -> IP -> "127.0.0.1" - # cqu = request URL -> STRING -> "http://127.0.0.1:/get" - # pssc = response status -> sINT -> "200" - # sshv = resp HTTP version -> dINT -> "HTTP/1.1" (ASCII) / [1,1] (JSON) - # The dynamic listen port in cqu is masked with `` in the gold files. - log_format = '% % % %' - - def __init__(self): - # Origin returning a plain 200 over HTTP/1.1 (so sshv == HTTP/1.1). - self.httpbin = Test.MakeHttpBinServer("httpbin") - self.ts = self.__configure_traffic_server() - self.__configure_traffic_test_run() - self.__configure_decode_test_runs() - - def __configure_traffic_server(self): - ''' Configure ATS with the ASCII, v2, and v3 binary log objects. - - Return: - The traffic_server process. - ''' - name = f"ts{BinaryLogV3Test.__ts_counter}" - BinaryLogV3Test.__ts_counter += 1 - ts = Test.MakeATSProcess(name) - - ts.Disk.records_config.update( - { - # No caching: every request reaches the origin, so sshv is populated. - 'proxy.config.http.cache.http': 0, - # Flush quickly so the .blog is on disk shortly after the requests. - 'proxy.config.log.max_secs_per_buffer': 1, - 'proxy.config.log.periodic_tasks_interval': 1, - }) - - ts.Disk.remap_config.AddLine(f'map http://127.0.0.1:{ts.Variables.port}/ http://127.0.0.1:{self.httpbin.Variables.Port}/') - - ts.Disk.logging_yaml.AddLines( - f''' - logging: - formats: - - name: custom_fmt - format: "{self.log_format}" - logs: - - filename: v2 - format: custom_fmt - mode: binary - binary_log_version: 2 - - filename: v3 - format: custom_fmt - mode: binary - binary_log_version: 3 - - filename: ascii - format: custom_fmt - mode: ascii - '''.split("\n")) - - logdir = ts.Variables.LOGDIR - self.v2_blog = os.path.join(logdir, 'v2.blog') - self.v3_blog = os.path.join(logdir, 'v3.blog') - self.ascii_log = os.path.join(logdir, 'ascii.log') - return ts - - def __configure_traffic_test_run(self): - ''' Generate num_requests identical origin-backed transactions. ''' - url = f'http://127.0.0.1:{self.ts.Variables.port}/get' - for i in range(BinaryLogV3Test.num_requests): - tr = Test.AddTestRun(f'Generate binary log traffic #{i + 1}') - if i == 0: - tr.Processes.Default.StartBefore(self.httpbin) - tr.Processes.Default.StartBefore(self.ts) - tr.MakeCurlCommand(f'--http1.1 "{url}"', ts=self.ts) - tr.Processes.Default.ReturnCode = 0 - - def __configure_decode_test_runs(self): - ''' Decode the binary logs with traffic_logcat and compare to gold. ''' - logcat = os.path.join(self.ts.Variables.BINDIR, 'traffic_logcat') - - # Wait until the ASCII companion has every entry; the binary logs are - # written in the same periodic flush, so they are complete by then. - Test.AddAwaitFileContainsTestRun('Await the logs to flush.', self.ascii_log, r'/get', BinaryLogV3Test.num_requests) - - # The binary_log_version:2 log decodes to the same ASCII as v3. - tr = Test.AddTestRun('Decode the v2 binary log to ASCII') - tr.Processes.Default.Command = f'{logcat} {self.v2_blog}' - tr.Processes.Default.ReturnCode = 0 - tr.Processes.Default.Streams.stdout = 'gold/binary_log_v3_ascii.gold' - - # traffic_logcat decodes the v3 .blog to the expected ASCII. - tr = Test.AddTestRun('Decode the v3 binary log to ASCII') - tr.Processes.Default.Command = f'{logcat} {self.v3_blog}' - tr.Processes.Default.ReturnCode = 0 - tr.Processes.Default.Streams.stdout = 'gold/binary_log_v3_ascii.gold' - - # traffic_logcat -j decodes the v3 .blog to the expected JSON. - tr = Test.AddTestRun('Decode the v3 binary log to JSON') - tr.Processes.Default.Command = f'{logcat} -j {self.v3_blog}' - tr.Processes.Default.ReturnCode = 0 - tr.Processes.Default.Streams.stdout = 'gold/binary_log_v3_json.gold' - - # traffic_logcat -H prints the v3 segment header, including the - # self-describing field-type schema, instead of decoding entries. - tr = Test.AddTestRun('Print the v3 binary log header') - tr.Processes.Default.Command = f'{logcat} -H {self.v3_blog}' - tr.Processes.Default.ReturnCode = 0 - stdout = tr.Processes.Default.Streams.stdout - stdout += Testers.ContainsExpression(r'version:\s+3', 'The header reports segment version 3.') - stdout += Testers.ContainsExpression(r'format_type:\s+4 \(CUSTOM\)', 'The header reports the custom format type.') - stdout += Testers.ContainsExpression(r'fieldlist:\s+chi,cqu,pssc,sshv', 'The header lists the format field symbols.') - stdout += Testers.ContainsExpression(r'field_type_schema:\s+field_count=4', 'The v3 header has a 4-field type schema.') - stdout += Testers.ContainsExpression(r'chi\s+IP', 'chi is framed as an IP.') - stdout += Testers.ContainsExpression(r'cqu\s+STRING', 'cqu is framed as a STRING.') - stdout += Testers.ContainsExpression(r'pssc\s+sINT', 'pssc is framed as an sINT.') - stdout += Testers.ContainsExpression(r'sshv\s+dINT', 'sshv is framed as a dINT.') - - # traffic_logcat -H also prints the v2 header, but v2 segments carry no - # field-type schema. - tr = Test.AddTestRun('Print the v2 binary log header') - tr.Processes.Default.Command = f'{logcat} -H {self.v2_blog}' - tr.Processes.Default.ReturnCode = 0 - stdout = tr.Processes.Default.Streams.stdout - stdout += Testers.ContainsExpression(r'version:\s+2', 'The header reports segment version 2.') - stdout += Testers.ContainsExpression(r'fieldlist:\s+chi,cqu,pssc,sshv', 'The header lists the format field symbols.') - stdout += Testers.ExcludesExpression(r'field_type_schema', 'A v2 segment has no field-type schema.') - - -# -# Run the test. -# -BinaryLogV3Test() diff --git a/tests/gold_tests/logging/custom-log.test.py b/tests/gold_tests/logging/custom-log.test.py deleted file mode 100644 index 754557f745b..00000000000 --- a/tests/gold_tests/logging/custom-log.test.py +++ /dev/null @@ -1,92 +0,0 @@ -''' -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import os - -Test.Summary = ''' -Test custom log file format -''' - -Test.SkipIf(Condition.CurlUsingUnixDomainSocket()) -# this test depends on Linux specific behavior regarding loopback addresses -Test.SkipUnless(Condition.IsPlatform("linux")) - -# Define default ATS -ts = Test.MakeATSProcess("ts") - -# setup some config file for this server -ts.Disk.remap_config.AddLine('map / http://www.linkedin.com/ @action=deny') - -ts.Disk.logging_yaml.AddLines( - ''' -logging: - formats: - - name: custom - format: "% %" - logs: - - filename: test_log_field - format: custom -'''.split("\n")) - -# ######################################################################### -# at the end of the different test run a custom log file should exist -# Because of this we expect the testruns to pass the real test is if the -# customlog file exists and passes the format check -Test.Disk.File(os.path.join(ts.Variables.LOGDIR, 'test_log_field.log'), exists=True, content='gold/custom.gold') - -# first test is a miss for default -tr = Test.AddTestRun() -tr.MakeCurlCommand('"http://127.0.0.1:{0}" --verbose'.format(ts.Variables.port), ts=ts) -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.StartBefore(Test.Processes.ts) - -tr = Test.AddTestRun() -tr.MakeCurlCommand('"http://127.1.1.1:{0}" --verbose'.format(ts.Variables.port), ts=ts) -tr.Processes.Default.ReturnCode = 0 - -tr = Test.AddTestRun() -tr.MakeCurlCommand('"http://127.2.2.2:{0}" --verbose'.format(ts.Variables.port), ts=ts) -tr.Processes.Default.ReturnCode = 0 - -tr = Test.AddTestRun() -tr.MakeCurlCommand('"http://127.3.3.3:{0}" --verbose'.format(ts.Variables.port), ts=ts) -tr.Processes.Default.ReturnCode = 0 - -tr = Test.AddTestRun() -tr.MakeCurlCommand('"http://127.3.0.1:{0}" --verbose'.format(ts.Variables.port), ts=ts) -tr.Processes.Default.ReturnCode = 0 - -tr = Test.AddTestRun() -tr.MakeCurlCommand('"http://127.43.2.1:{0}" --verbose'.format(ts.Variables.port), ts=ts) -tr.Processes.Default.ReturnCode = 0 - -tr = Test.AddTestRun() -tr.MakeCurlCommand('"http://127.213.213.132:{0}" --verbose'.format(ts.Variables.port), ts=ts) -tr.Processes.Default.ReturnCode = 0 - -tr = Test.AddTestRun() -tr.MakeCurlCommand('"http://127.123.32.243:{0}" --verbose'.format(ts.Variables.port), ts=ts) -tr.Processes.Default.ReturnCode = 0 - -# Wait for all expected log lines to be written. -Test.AddAwaitFileContainsTestRun( - 'Await custom log lines.', - os.path.join(ts.Variables.LOGDIR, 'test_log_field.log'), - r'^127\.', - 8, -) diff --git a/tests/gold_tests/logging/log-field-json.test.py b/tests/gold_tests/logging/log-field-json.test.py deleted file mode 100644 index 9d8f96caa65..00000000000 --- a/tests/gold_tests/logging/log-field-json.test.py +++ /dev/null @@ -1,120 +0,0 @@ -''' -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import os - -Test.Summary = ''' -Test log fields. -''' - -ts = Test.MakeATSProcess("ts", enable_cache=False) -server = Test.MakeOriginServer("server") - -request_header = {'timestamp': 100, "headers": "GET /test-1 HTTP/1.1\r\nHost: test-1\r\n\r\n", "body": ""} -response_header = { - 'timestamp': 100, - "headers": - "HTTP/1.1 200 OK\r\nTest: 1\r\nContent-Type: application/json\r\nConnection: close\r\nContent-Type: application/json\r\n\r\n", - "body": "Test 1" -} -server.addResponse("sessionlog.json", request_header, response_header) -server.addResponse( - "sessionlog.json", { - 'timestamp': 101, - "headers": "GET /test-2 HTTP/1.1\r\nHost: test-2\r\n\r\n", - "body": "" - }, { - 'timestamp': 101, - "headers": - "HTTP/1.1 200 OK\r\nTest: 2\r\nContent-Type: application/jason\r\nConnection: close\r\nContent-Type: application/json\r\n\r\n", - "body": "Test 2" - }) -server.addResponse( - "sessionlog.json", { - 'timestamp': 102, - "headers": "GET /test-3 HTTP/1.1\r\nHost: test-3\r\n\r\n", - "body": "" - }, { - 'timestamp': 102, - "headers": "HTTP/1.1 200 OK\r\nTest: 3\r\nConnection: close\r\nContent-Type: application/json\r\n\r\n", - "body": "Test 3" - }) - -nameserver = Test.MakeDNServer("dns", default='127.0.0.1') - -ts.Disk.records_config.update( - { - 'proxy.config.net.connections_throttle': 100, - 'proxy.config.dns.nameservers': f"127.0.0.1:{nameserver.Variables.Port}", - 'proxy.config.dns.resolv_conf': 'NULL' - }) -# setup some config file for this server -ts.Disk.remap_config.AddLine('map / http://localhost:{}/'.format(server.Variables.Port)) - -ts.Disk.logging_yaml.AddLines( - ''' -logging: - formats: - - name: custom - escape: json - format: '{"foo":"%<{Foo}cqh>","foo-slice":"%<{Foo}cqh[2:-3]>"}' - logs: - - filename: field-json-test - format: custom -'''.split("\n")) - -# ######################################################################### -# at the end of the different test run a custom log file should exist -# Because of this we expect the testruns to pass the real test is if the -# customlog file exists and passes the format check -Test.Disk.File(os.path.join(ts.Variables.LOGDIR, 'field-json-test.log'), exists=True, content='gold/field-json-test.gold') - -# first test is a miss for default -tr = Test.AddTestRun() -# Wait for the micro server -tr.Processes.Default.StartBefore(server) -tr.Processes.Default.StartBefore(nameserver) -# Delay on readiness of our ssl ports -tr.Processes.Default.StartBefore(Test.Processes.ts) - -tr.MakeCurlCommand( - '--verbose --header "Host: test-1" --header "Foo: ab\td/ef" http://localhost:{0}/test-1'.format(ts.Variables.port), ts=ts) -tr.Processes.Default.ReturnCode = 0 - -tr = Test.AddTestRun() -tr.MakeCurlCommand( - '--verbose --header "Host: test-2" --header "Foo: ab\x1fd/ef" http://localhost:{0}/test-2'.format(ts.Variables.port), ts=ts) -tr.Processes.Default.ReturnCode = 0 - -tr = Test.AddTestRun() -tr.MakeCurlCommand( - '--verbose --header "Host: test-3" --header "Foo: abc\x7fde" http://localhost:{0}/test-3'.format(ts.Variables.port), ts=ts) -tr.Processes.Default.ReturnCode = 0 - -tr = Test.AddTestRun() -tr.MakeCurlCommand( - '--verbose --header "Host: test-2" --header "Foo: ab\x80d/ef" http://localhost:{0}/test-4'.format(ts.Variables.port), ts=ts) -tr.Processes.Default.ReturnCode = 0 - -# Wait for all expected log lines to be written. -Test.AddAwaitFileContainsTestRun( - 'Await field-json log lines.', - os.path.join(ts.Variables.LOGDIR, 'field-json-test.log'), - r'^\{"foo":', - 4, -) diff --git a/tests/gold_tests/logging/log-field.test.py b/tests/gold_tests/logging/log-field.test.py deleted file mode 100644 index 4268ec5cc73..00000000000 --- a/tests/gold_tests/logging/log-field.test.py +++ /dev/null @@ -1,121 +0,0 @@ -''' -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import os - -Test.Summary = ''' -Test log fields. -''' - -ts = Test.MakeATSProcess("ts", enable_cache=False) -server = Test.MakeOriginServer("server") - -request_header = {'timestamp': 100, "headers": "GET /test-1 HTTP/1.1\r\nHost: test-1\r\n\r\n", "body": ""} -response_header = { - 'timestamp': 100, - "headers": - "HTTP/1.1 200 OK\r\nTest: 1\r\nContent-Type: application/json\r\nConnection: close\r\nContent-Type: application/json\r\nTransfer-Encoding: chunked\r\n\r\n", - "body": "Test 1" -} -server.addResponse("sessionlog.json", request_header, response_header) -server.addResponse( - "sessionlog.json", { - 'timestamp': 101, - "headers": "GET /test-2 HTTP/1.1\r\nHost: test-2\r\n\r\n", - "body": "" - }, { - 'timestamp': 101, - "headers": - "HTTP/1.1 200 OK\r\nTest: 2\r\nContent-Type: application/jason\r\nConnection: close\r\nContent-Type: application/json\r\n\r\n", - "body": "Test 2" - }) -server.addResponse( - "sessionlog.json", { - 'timestamp': 102, - "headers": "GET /test-3 HTTP/1.1\r\nHost: test-3\r\n\r\n", - "body": "" - }, { - 'timestamp': 102, - "headers": "HTTP/1.1 200 OK\r\nTest: 3\r\nConnection: close\r\nContent-Type: application/json\r\n\r\n", - "body": "Test 3" - }) - -nameserver = Test.MakeDNServer("dns", default='127.0.0.1') - -ts.Disk.records_config.update( - { - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'http|log', - 'proxy.config.net.connections_throttle': 100, - 'proxy.config.dns.nameservers': f"127.0.0.1:{nameserver.Variables.Port}", - 'proxy.config.dns.resolv_conf': 'NULL' - }) -# setup some config file for this server -ts.Disk.remap_config.AddLine('map / http://localhost:{}/'.format(server.Variables.Port)) - -ts.Disk.logging_yaml.AddLines( - ''' -logging: - formats: - - name: custom - format: 'Transfer-Encoding:%<{Transfer-Encoding}ssh> Content-Type:%<{Content-Type}essh> Request-ID:%<{X-Primary-Id}cqh??{X-Secondary-Id}cqh> Request-ID-Default:%<{X-Primary-Id}cqh??{X-Secondary-Id}cqh??"missing-id"> Request-ID-Or-IP:%<{X-Primary-Id}cqh??{X-Secondary-Id}cqh??chi> Remote-IP:%<{X-Remote-Ip}cqh??chi> Path-Fallback:%<{X-Path-Fallback}cqh??cqup[0:7]>' - logs: - - filename: field-test - format: custom -'''.split("\n")) - -# ######################################################################### -# at the end of the different test run a custom log file should exist -# Because of this we expect the testruns to pass the real test is if the -# customlog file exists and passes the format check -# Note that the gold file expects Chunked, not chunked, because microserver -# converts to Chunked. This is not an ATS issue, but a microserver issue. -Test.Disk.File(os.path.join(ts.Variables.LOGDIR, 'field-test.log'), exists=True, content='gold/field-test.gold') - -# first test is a miss for default -tr = Test.AddTestRun() -# Wait for the micro server -tr.Processes.Default.StartBefore(server) -tr.Processes.Default.StartBefore(nameserver) -# Delay on readiness of our ssl ports -tr.Processes.Default.StartBefore(Test.Processes.ts) - -tr.MakeCurlCommand( - '--verbose --ipv4 --header "Host: test-1" --header "X-Primary-Id: primary-1" --header "X-Secondary-Id: secondary-1" --header "X-Remote-Ip: 203.0.113.10" http://127.0.0.1:{0}/test-1' - .format(ts.Variables.port), - ts=ts) -tr.Processes.Default.ReturnCode = 0 - -tr = Test.AddTestRun() -tr.MakeCurlCommand( - '--verbose --ipv4 --header "Host: test-2" --header "X-Secondary-Id: secondary-2" http://127.0.0.1:{0}/test-2'.format( - ts.Variables.port), - ts=ts) -tr.Processes.Default.ReturnCode = 0 - -tr = Test.AddTestRun() -tr.MakeCurlCommand('--verbose --ipv4 --header "Host: test-3" http://127.0.0.1:{0}/test-3'.format(ts.Variables.port), ts=ts) -tr.Processes.Default.ReturnCode = 0 - -# Wait for all expected log lines to be written. -Test.AddAwaitFileContainsTestRun( - 'Await field-test log lines.', - os.path.join(ts.Variables.LOGDIR, 'field-test.log'), - r'^Transfer-Encoding:', - 3, -) diff --git a/tests/gold_tests/logging/log-filenames.test.py b/tests/gold_tests/logging/log-filenames.test.py deleted file mode 100644 index 40781a74100..00000000000 --- a/tests/gold_tests/logging/log-filenames.test.py +++ /dev/null @@ -1,262 +0,0 @@ -''' -Verify log file naming behavior. -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import os -import ports - -Test.Summary = ''' -Verify log file naming behavior. -''' - - -class LogFilenamesTest: - """ Common test configuration logic across the filename tests. - """ - - # A counter for the ATS process to make each of them unique. - __ts_counter = 1 - - # The default log names for the various system logs. - default_log_data = {'diags': 'diags.log', 'error': 'error.log'} - - def __init__(self, description, log_data=default_log_data): - ''' Handle initialization tasks common across the tests. - - Args: - description (str): The description of the test. This is passed to - the TestRun. - - log_data (dict): The log name information passed to the - MakeATSProcess extension. - ''' - self.__description = description - self.ts = self.__configure_traffic_server(log_data) - self.tr = self.__configure_traffic_TestRun(description) - self.__configure_await_TestRun(self.sentinel_log_path) - - def __configure_traffic_server(self, log_data): - ''' Common ATS configuration logic. - - Args: - log_data (dict): The log name information passed to the - MakeATSProcess extension. - - Return: - The traffic_server process. - ''' - self._ts_name = f"ts{LogFilenamesTest.__ts_counter}" - LogFilenamesTest.__ts_counter += 1 - self.ts = Test.MakeATSProcess(self._ts_name, use_traffic_out=False, log_data=log_data) - self.ts.Disk.records_config.update( - { - 'proxy.config.diags.debug.enabled': 0, - 'proxy.config.diags.debug.tags': 'log', - 'proxy.config.log.periodic_tasks_interval': 1, - }) - - # Intentionally retrieve a port that is closed, that is no server is - # listening on it. We will use this to attempt talking with a - # non-existent server, which will result in an error log entry. - ports.get_port(self.ts, 'closed_port') - self.ts.Disk.remap_config.AddLines( - [ - f'map /server/down http://127.0.0.1:{self.ts.Variables.closed_port}', - 'map / https://trafficserver.apache.org @action=deny', - ]) - - # The following log is configured so that we can wait upon it being - # written so we know that ATS is done writing logs. - self.sentinel_log_filename = "sentinel" - self.ts.Disk.logging_yaml.AddLine( - f''' - logging: - formats: - - name: url_and_return_code - format: "%: %" - logs: - - filename: {self.sentinel_log_filename} - format: url_and_return_code - ''') - - self.sentinel_log_path = os.path.join(self.ts.Variables.LOGDIR, f"{self.sentinel_log_filename}.log") - - return self.ts - - def __configure_await_TestRun(self, log_path): - ''' Configure a TestRun that awaits upon the provided log_path to - contain the sentinel log entry. - - Args: - log_path (str): The log file upon which we will wait. - ''' - description = self.__description - Test.AddAwaitFileContainsTestRun( - f'Awaiting log files to be written for: {description}', - log_path, - r'^http://127\.0\.0\.1:\d+/: 502$', - ) - - def __configure_traffic_TestRun(self, description): - ''' Configure a TestRun to run the expected transactions. - - Args: - description (str): The description to use for the TestRun. - ''' - tr = Test.AddTestRun(f'Run traffic for: {description}') - tr.MakeCurlCommand( - f'http://127.0.0.1:{self.ts.Variables.port}/some/path --verbose --next ' - f'http://127.0.0.1:{self.ts.Variables.port}/server/down --verbose', - ts=self.ts) - tr.Processes.Default.ReturnCode = 0 - tr.Processes.Default.StartBefore(self.ts) - - def configure_named_custom_log(self, custom_log_filename): - """ Configure ATS to log to the custom log file via logging.yaml. - - Args: - custom_log_filename (str): The name of the custom log file to - configure. - - Return: - The path to the configured custom log file. - """ - self.custom_log_filename = custom_log_filename - self.ts.Disk.logging_yaml.AddLine( - f''' - - filename: {custom_log_filename} - format: url_and_return_code - ''') - - if custom_log_filename in ('stdout', 'stderr'): - self.custom_log_path = custom_log_filename - if custom_log_filename == 'stdout': - self.ts.Disk.custom_log = self.ts.Streams.stdout - else: - self.ts.Disk.custom_log = self.ts.Streams.stderr - else: - self.custom_log_path = os.path.join(self.ts.Variables.LOGDIR, f"{custom_log_filename}.log") - self.ts.Disk.File(self.custom_log_path, id="custom_log") - return self.custom_log_path - - def set_log_expectations(self): - ''' Configure sanity checks for each of the log types (diags, error, - etc.) to verify they are emitting the expected content. - ''' - - diags_path = self.ts.Disk.diags_log.AbsPath - self.ts.Disk.diags_log.Content += Testers.ContainsExpression( - "logging.yaml finished loading", f"{diags_path} should contain traffic_server diag messages") - - error_log_path = self.ts.Disk.error_log.AbsPath - self.ts.Disk.error_log.Content += Testers.ContainsExpression( - "CONNECT: attempt fail", f"{error_log_path} should contain connection error messages") - - custom_log_path = self.ts.Disk.custom_log.AbsPath - self.ts.Disk.custom_log.Content += Testers.ContainsExpression( - "https://trafficserver.apache.org/some/path: 403", f"{custom_log_path} should contain the custom transaction logs") - - -class DefaultNamedTest(LogFilenamesTest): - ''' Verify that if custom names are not configured, then the default - 'diags.log' and 'error.log' are written to. - ''' - - def __init__(self): - super().__init__('default log filename configuration') - - self.configure_named_custom_log('my_custom_log') - self.set_log_expectations() - - -class CustomNamedTest(LogFilenamesTest): - ''' Verify that the user can assign custom filenames to diags.log, etc. - ''' - - def __init__(self): - log_data = {'diags': 'my_diags.log', 'error': 'my_error.log'} - super().__init__('specify log filename configuration', log_data) - - # Configure custom names for diags.log, etc. - self.ts.Disk.records_config.update( - { - 'proxy.config.diags.logfile.filename': 'my_diags.log', - 'proxy.config.error.logfile.filename': 'my_error.log', - }) - - self.configure_named_custom_log('my_custom_log') - self.set_log_expectations() - - -class stdoutTest(LogFilenamesTest): - ''' Verify that we can configure the logs to go to stdout. - ''' - - def __init__(self): - - log_data = {'diags': 'stdout', 'error': 'stdout'} - super().__init__('specify logs to go to stdout', log_data) - - # Configure custom names for diags.log, etc. - self.ts.Disk.records_config.update( - { - 'proxy.config.diags.logfile.filename': 'stdout', - 'proxy.config.error.logfile.filename': 'stdout', - }) - - self.configure_named_custom_log('stdout') - - # The diags.log file will not be created since we are piping to stdout. - # Therefore, simply wait upon the port being open. - self.ts.Ready = When.PortOpen(self.ts.Variables.port) - self.set_log_expectations() - - -class stderrTest(LogFilenamesTest): - ''' - Verify that we can configure the logs to go to stderr. - ''' - - def __init__(self): - - log_data = {'diags': 'stderr', 'error': 'stderr'} - super().__init__('specify logs to go to stderr', log_data) - - # Configure custom names for diags.log, etc. - self.ts.Disk.records_config.update( - { - 'proxy.config.diags.logfile.filename': 'stderr', - 'proxy.config.error.logfile.filename': 'stderr', - }) - - self.configure_named_custom_log('stderr') - - # The diags.log file will not be created since we are piping to stderr. - # Therefore, simply wait upon the port being open. - self.ts.Ready = When.PortOpen(self.ts.Variables.port) - self.set_log_expectations() - - -# -# Run the tests. -# -DefaultNamedTest() -CustomNamedTest() -stdoutTest() - -stderrTest() diff --git a/tests/gold_tests/logging/log-filter.test.py b/tests/gold_tests/logging/log-filter.test.py deleted file mode 100644 index e9dd9483966..00000000000 --- a/tests/gold_tests/logging/log-filter.test.py +++ /dev/null @@ -1,100 +0,0 @@ -''' -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import os - -Test.Summary = ''' -Test log filter. -''' - -ts = Test.MakeATSProcess("ts", enable_cache=False) -replay_file = "log-filter.replays.yaml" -server = Test.MakeVerifierServerProcess("server", replay_file) -nameserver = Test.MakeDNServer("dns", default='127.0.0.1') - -ts.Disk.records_config.update( - { - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'log', - 'proxy.config.net.connections_throttle': 100, - 'proxy.config.dns.nameservers': f"127.0.0.1:{nameserver.Variables.Port}", - 'proxy.config.dns.resolv_conf': 'NULL' - }) -# setup some config file for this server -ts.Disk.remap_config.AddLine('map / http://localhost:{}/'.format(server.Variables.http_port)) - -ts.Disk.logging_yaml.AddLines( - ''' -logging: - filters: - - name: only_localhost - action: accept - condition: chi MATCH 127.0.0.1 - - - name: not_localhost - action: accept - condition: chi MATCH 3.3.3.3 - - - name: queryparamescaper_cquuc - action: WIPE_FIELD_VALUE - condition: cquuc CASE_INSENSITIVE_CONTAIN password,secret,access_token,session_redirect,cardNumber,code,query,search-query,prefix,keywords,email,handle - - formats: - - name: custom - format: '%' - - logs: - - filename: filter-test - format: custom - filters: - - queryparamescaper_cquuc - - only_localhost - - - filename: should-not-be-written - format: custom - filters: - - queryparamescaper_cquuc - - not_localhost -'''.split("\n")) - -# ######################################################################### -# at the end of the different test run a custom log file should exist -# Because of this we expect the testruns to pass the real test is if the -# customlog file exists and passes the format check -Test.Disk.File(os.path.join(ts.Variables.LOGDIR, 'filter-test.log'), exists=True, content='gold/filter-test.gold') - -tr = Test.AddTestRun() -tr.Processes.Default.StartBefore(server) -tr.Processes.Default.StartBefore(nameserver) -tr.Processes.Default.StartBefore(ts) -tr.AddVerifierClientProcess("client-1", replay_file, http_ports=[ts.Variables.port]) - -# Wait for all expected log lines to be written. -Test.AddAwaitFileContainsTestRun( - 'Await filtered log lines.', - os.path.join(ts.Variables.LOGDIR, 'filter-test.log'), - r'^http://example\.com/test-', - 5, -) - -# We already waited for the above, so we don't have to wait for this one. -test_run = Test.AddTestRun() -test_run.Processes.Default.Command = ( - os.path.join(Test.Variables.AtsTestToolsDir, 'condwait') + ' 1 1 -f ' + - os.path.join(ts.Variables.LOGDIR, 'should-not-be-written.log')) -test_run.Processes.Default.ReturnCode = 1 diff --git a/tests/gold_tests/logging/log-milestone-fields.test.py b/tests/gold_tests/logging/log-milestone-fields.test.py deleted file mode 100644 index 72f5f8617b0..00000000000 --- a/tests/gold_tests/logging/log-milestone-fields.test.py +++ /dev/null @@ -1,176 +0,0 @@ -''' -Verify that msdms milestone difference fields produce valid output for both -cache-miss and cache-hit transactions. This exercises the Phase 1 timing -fields proposed for the squid.log local_disk format. -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import os - -Test.Summary = 'Verify msdms milestone logging fields for cache miss and cache hit paths' -Test.ContinueOnFail = True - - -class MilestoneFieldsTest: - """ - Sends two requests for the same cacheable URL: the first is a cache miss - that populates the cache, the second is a cache hit served from RAM cache. - A custom log format records all Phase 1 milestone timing fields plus the - cache result code. A validation script then parses the log and checks: - - - Every expected key=value pair is present on each line - - All values are integers, with "-" for unset milestones - - Cache miss line: ms > 0, origin-phase fields present - - Cache hit line: hit_proc >= 0 and hit_xfer >= 0 - - No epoch-length garbage values (> 1_000_000_000) - """ - - # All Phase 1 msdms fields plus ms, cache result code, and cache key hash. - LOG_FORMAT = ( - 'crc=% ckh=% ms=%' - ' c_ttfb=%<{TS_MILESTONE_UA_BEGIN_WRITE-TS_MILESTONE_SM_START}msdms>' - ' c_tls=%<{TS_MILESTONE_TLS_HANDSHAKE_END-TS_MILESTONE_TLS_HANDSHAKE_START}msdms>' - ' c_hdr=%<{TS_MILESTONE_UA_READ_HEADER_DONE-TS_MILESTONE_SM_START}msdms>' - ' c_proc=%<{TS_MILESTONE_CACHE_OPEN_READ_BEGIN-TS_MILESTONE_UA_READ_HEADER_DONE}msdms>' - ' cache=%<{TS_MILESTONE_CACHE_OPEN_READ_END-TS_MILESTONE_CACHE_OPEN_READ_BEGIN}msdms>' - ' dns=%<{TS_MILESTONE_SERVER_FIRST_CONNECT-TS_MILESTONE_CACHE_OPEN_READ_END}msdms>' - ' o_tcp=%<{TS_MILESTONE_SERVER_CONNECT_END-TS_MILESTONE_SERVER_FIRST_CONNECT}msdms>' - ' o_wait=%<{TS_MILESTONE_SERVER_FIRST_READ-TS_MILESTONE_SERVER_CONNECT_END}msdms>' - ' o_hdr=%<{TS_MILESTONE_SERVER_READ_HEADER_DONE-TS_MILESTONE_SERVER_FIRST_READ}msdms>' - ' o_proc=%<{TS_MILESTONE_UA_BEGIN_WRITE-TS_MILESTONE_SERVER_READ_HEADER_DONE}msdms>' - ' o_body=%<{TS_MILESTONE_SERVER_CLOSE-TS_MILESTONE_UA_BEGIN_WRITE}msdms>' - ' c_xfer=%<{TS_MILESTONE_SM_FINISH-TS_MILESTONE_SERVER_CLOSE}msdms>' - ' hit_proc=%<{TS_MILESTONE_UA_BEGIN_WRITE-TS_MILESTONE_CACHE_OPEN_READ_END}msdms>' - ' hit_xfer=%<{TS_MILESTONE_SM_FINISH-TS_MILESTONE_UA_BEGIN_WRITE}msdms>') - - def __init__(self): - self._server = Test.MakeOriginServer("server") - self._nameserver = Test.MakeDNServer("dns", default='127.0.0.1') - self._setupOriginServer() - self._setupTS() - - def _setupOriginServer(self): - self._server.addResponse( - "sessionlog.json", - { - 'timestamp': 100, - "headers": "GET /cacheable HTTP/1.1\r\nHost: example.com\r\n\r\n", - "body": "", - }, - { - 'timestamp': 100, - "headers": - ( - "HTTP/1.1 200 OK\r\n" - "Content-Type: text/plain\r\n" - "Cache-Control: max-age=300\r\n" - "Connection: close\r\n" - "\r\n"), - "body": "This is a cacheable response body for milestone testing.", - }, - ) - - def _setupTS(self): - self._ts = Test.MakeATSProcess("ts", enable_cache=True) - - self._ts.Disk.records_config.update( - { - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'http|log', - 'proxy.config.dns.nameservers': f'127.0.0.1:{self._nameserver.Variables.Port}', - 'proxy.config.dns.resolv_conf': 'NULL', - 'proxy.config.http.cache.http': 1, - 'proxy.config.log.max_secs_per_buffer': 1, - }) - - self._ts.Disk.remap_config.AddLine(f'map / http://127.0.0.1:{self._server.Variables.Port}/') - - self._ts.Disk.logging_yaml.AddLines( - f''' -logging: - formats: - - name: milestone_test - format: '{self.LOG_FORMAT}' - logs: - - filename: milestone_fields - format: milestone_test - mode: ascii -'''.split("\n")) - - @property - def _log_path(self) -> str: - return os.path.join(self._ts.Variables.LOGDIR, 'milestone_fields.log') - - @property - def _validate_script(self) -> str: - return os.path.join(Test.TestDirectory, 'verify_milestone_fields.py') - - def run(self): - self._sendCacheMiss() - self._waitForCacheIO() - self._sendCacheHit() - self._waitForLog() - self._validateLog() - - def _sendCacheMiss(self): - tr = Test.AddTestRun('Cache miss request') - tr.Processes.Default.StartBefore(self._server) - tr.Processes.Default.StartBefore(self._nameserver) - tr.Processes.Default.StartBefore(self._ts) - tr.MakeCurlCommand( - f'--verbose --header "Host: example.com" ' - f'http://127.0.0.1:{self._ts.Variables.port}/cacheable', ts=self._ts) - tr.Processes.Default.ReturnCode = 0 - tr.StillRunningAfter = self._server - tr.StillRunningAfter = self._ts - - def _waitForCacheIO(self): - tr = Test.AddTestRun('Wait for cache write to complete') - tr.Processes.Default.Command = 'sleep 1' - tr.Processes.Default.ReturnCode = 0 - tr.StillRunningAfter = self._server - tr.StillRunningAfter = self._ts - - def _sendCacheHit(self): - tr = Test.AddTestRun('Cache hit request') - tr.MakeCurlCommand( - f'--verbose --header "Host: example.com" ' - f'http://127.0.0.1:{self._ts.Variables.port}/cacheable', ts=self._ts) - tr.Processes.Default.ReturnCode = 0 - tr.StillRunningAfter = self._server - tr.StillRunningAfter = self._ts - - def _waitForLog(self): - tr = Test.AddAwaitFileContainsTestRun( - 'Wait for milestone log lines to be written', - self._log_path, - r'^crc=.* hit_xfer=', - desired_count=2, - ) - tr.StillRunningAfter = self._server - tr.StillRunningAfter = self._ts - - def _validateLog(self): - tr = Test.AddTestRun('Validate milestone fields in log') - tr.Processes.Default.Command = f'python3 {self._validate_script} {self._log_path}' - tr.Processes.Default.ReturnCode = 0 - tr.Processes.Default.TimeOut = 10 - tr.Processes.Default.Streams.stdout += Testers.ContainsExpression('PASS', 'Validation script should report PASS') - tr.Processes.Default.Streams.stdout += Testers.ExcludesExpression('FAIL', 'Validation script should not report FAIL') - - -MilestoneFieldsTest().run() diff --git a/tests/gold_tests/logging/log-mstsms.test.py b/tests/gold_tests/logging/log-mstsms.test.py deleted file mode 100644 index cd3e71b515c..00000000000 --- a/tests/gold_tests/logging/log-mstsms.test.py +++ /dev/null @@ -1,119 +0,0 @@ -''' -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import os - -Test.Summary = ''' -Test log fields. -''' - -ts = Test.MakeATSProcess("ts", enable_cache=True) -server = Test.MakeOriginServer("server") - -request_header = {'timestamp': 100, "headers": "GET /test-1 HTTP/1.1\r\nHost: test-1\r\n\r\n", "body": ""} -response_header = { - 'timestamp': 100, - "headers": - "HTTP/1.1 200 OK\r\nTest: 1\r\nContent-Type: application/json\r\nConnection: close\r\nContent-Type: application/json\r\nTransfer-Encoding: chunked\r\n\r\n", - "body": "Test 1" -} -server.addResponse("sessionlog.json", request_header, response_header) -server.addResponse( - "sessionlog.json", { - 'timestamp': 101, - "headers": "GET /test-2 HTTP/1.1\r\nHost: test-2\r\n\r\n", - "body": "" - }, { - 'timestamp': 101, - "headers": - "HTTP/1.1 200 OK\r\nTest: 2\r\nContent-Type: application/jason\r\nConnection: close\r\nContent-Type: application/json\r\n\r\n", - "body": "Test 2" - }) -server.addResponse( - "sessionlog.json", { - 'timestamp': 102, - "headers": "GET /test-3 HTTP/1.1\r\nHost: test-3\r\n\r\n", - "body": "" - }, { - 'timestamp': 102, - "headers": "HTTP/1.1 200 OK\r\nTest: 3\r\nConnection: close\r\nContent-Type: application/json\r\n\r\n", - "body": "Test 3" - }) - -nameserver = Test.MakeDNServer("dns", default='127.0.0.1') - -ts.Disk.records_config.update( - { - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'http|log', - # 'proxy.config.net.connections_throttle': 100, - 'proxy.config.dns.nameservers': f"127.0.0.1:{nameserver.Variables.Port}", - 'proxy.config.dns.resolv_conf': 'NULL', - }) -# setup some config file for this server -ts.Disk.remap_config.AddLine('map / http://localhost:{}/'.format(server.Variables.Port)) - -ts.Disk.logging_yaml.AddLines( - ''' -logging: - formats: - - name: custom - format: 'mstsms:%' - logs: - - filename: field-mstsms - format: custom -'''.split("\n")) - -# first test is a miss for default -tr = Test.AddTestRun() -# Wait for the micro server -tr.Processes.Default.StartBefore(server) -tr.Processes.Default.StartBefore(nameserver) -# Delay on readiness of our ssl ports -tr.Processes.Default.StartBefore(Test.Processes.ts) - -tr.MakeCurlCommand('--verbose --header "Host: test-1" http://localhost:{0}/test-1'.format(ts.Variables.port), ts=ts) -tr.Processes.Default.ReturnCode = 0 - -tr = Test.AddTestRun() -tr.MakeCurlCommand('--verbose --header "Host: test-2" http://localhost:{0}/test-2'.format(ts.Variables.port), ts=ts) -tr.Processes.Default.ReturnCode = 0 - -tr = Test.AddTestRun() -tr.MakeCurlCommand('--verbose --header "Host: test-3" http://localhost:{0}/test-3'.format(ts.Variables.port), ts=ts) -tr.Processes.Default.ReturnCode = 0 - - -# check comma count and ensure last character is a digit -def check_lines(path): - with open(path, 'r') as file: - for line_num, line in enumerate(file, 1): - line = line.rstrip('\n') - comma_count = line.count(',') - if comma_count != 19: - return False, "Check comma count", f"Expected 19 commas, got {comma_count}" - if not line[-1].isdigit(): - return False, "Check last char", f"Expected last character to be a digit got '{line[-1]}'" - return True, "", "" - - -logpath = os.path.join(ts.Variables.LOGDIR, 'field-mstsms.log') - -# Wait for all expected log lines to be written. -tr = Test.AddAwaitFileContainsTestRun('Await mstsms log lines.', logpath, r'^mstsms:', 3) -tr.Streams.All.Content = Testers.Lambda(lambda info, tester: check_lines(logpath)) diff --git a/tests/gold_tests/logging/log_pipe.test.py b/tests/gold_tests/logging/log_pipe.test.py deleted file mode 100644 index fbfcc67b6f5..00000000000 --- a/tests/gold_tests/logging/log_pipe.test.py +++ /dev/null @@ -1,186 +0,0 @@ -''' -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import os -import sys - -Test.Summary = ''' -Test custom log file format -''' -Test.SkipUnless(Condition.HasATSFeature('TS_HAS_PIPE_BUFFER_SIZE_CONFIG')) - -ts_counter = 1 - - -def get_ts(logging_config, disable_log_checks=False): - """ - Create a Traffic Server process. - - :param disable_log_checks: Whether to disable the default diagnostics log - checks on systems where the kernel may reject increasing the pipe - buffer size for an unprivileged Traffic Server process. - """ - global ts_counter - ts = Test.MakeATSProcess("ts{}".format(ts_counter), disable_log_checks=disable_log_checks) - ts_counter += 1 - - ts.Disk.records_config.update( - { - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'log-file', - 'proxy.config.log.max_secs_per_buffer': 1, - }) - - # Since we're only verifying logs and not traffic, we don't need an origin - # server. The following will simply deny the requests and emit a log - # message. - ts.Disk.remap_config.AddLine('map / http://www.linkedin.com/ @action=deny') - - ts.Disk.logging_yaml.AddLines(logging_config) - - return ts - - -# -# Test 1: Default configured log pipe size. -# -tr = Test.AddTestRun() -pipe_name = "default_pipe_size.pipe" -ts = get_ts( - ''' -logging: - formats: - - name: custom - format: "% %" - logs: - - filename: '{}' - mode: ascii_pipe - format: custom -'''.format(pipe_name).split("\n")) - -pipe_path = os.path.join(ts.Variables.LOGDIR, pipe_name) - -ts.Disk.traffic_out.Content += Testers.ContainsExpression( - "Created named pipe .*{}".format(pipe_name), "Verify that the named pipe was created") - -ts.Disk.traffic_out.Content += Testers.ContainsExpression( - "no readers for pipe .*{}".format(pipe_name), "Verify that no readers for the pipe was detected.") - -ts.Disk.traffic_out.Content += Testers.ExcludesExpression( - "New buffer size for pipe".format(pipe_name), "Verify that the default pipe size was used.") - -curl = tr.Processes.Process("client_request", 'curl "http://127.0.0.1:{0}" --verbose'.format(ts.Variables.port)) - -reader_output = os.path.join(ts.Variables.LOGDIR, "reader_output") -pipe_reader = tr.Processes.Process("pipe_reader", 'cat {} | tee {}'.format(pipe_path, reader_output)) - -# Create an arbitrary process that just sleeps so that we can provide a wait -# condition upon the log being emitted. The test won't wait this entire sleep -# period, it will only poll until the FileContains Ready condition is -# fulfilled. -wait_for_log = tr.Processes.Process("wait_for_log", 'sleep 15') -wait_for_log.Ready = When.FileContains(reader_output, '127.0.0.1') - -# This is an arbitrary Default process that will simply provide for -# ordering of the Processes. -tr.Processes.Default.Command = "echo 'Default place holder for process ordering.'" -tr.Processes.Default.Return = 0 - -# Process ordering. -tr.Processes.Default.StartBefore(wait_for_log) -wait_for_log.StartBefore(curl) -curl.StartBefore(pipe_reader) -pipe_reader.StartBefore(ts) - -# -# Test 2: Change the log's buffer size. -# -tr = Test.AddTestRun() -pipe_name = "change_pipe_size.pipe" -# 64 KB is the default, so set the size larger than that to verify we can -# increase the size. -pipe_size = 75000 -ts = get_ts( - ''' -logging: - formats: - - name: custom - format: "% %" - logs: - - filename: '{}' - mode: ascii_pipe - format: custom - pipe_buffer_size: {} - '''.format(pipe_name, pipe_size).split("\n"), - disable_log_checks=True) - -pipe_path = os.path.join(ts.Variables.LOGDIR, pipe_name) - -ts.Disk.diags_log.Content += Testers.ExcludesExpression( - r"ERROR:(?! Set pipe size failed for pipe .*: Operation not permitted)", "The diagnostics should contain no unexpected errors.") -ts.Disk.diags_log.Content += Testers.ExcludesExpression("FATAL:", "The diagnostics should contain no fatal errors.") - -ts.Disk.traffic_out.Content += Testers.ContainsExpression( - "Created named pipe .*{}".format(pipe_name), "Verify that the named pipe was created") - -ts.Disk.traffic_out.Content += Testers.ContainsExpression( - "no readers for pipe .*{}".format(pipe_name), "Verify that no readers for the pipe was detected.") - -ts.Disk.traffic_out.Content += Testers.ContainsExpression( - "Previous buffer size for pipe .*{}".format(pipe_name), "Verify that the named pipe's size was adjusted") - -# See fcntl: -# "Attempts to set the pipe capacity below the page size -# are silently rounded up to the page size." -# -# As a result of this, we cannot check that the pipe size is the exact size we -# requested, but it should be at least that big. We use the -# pipe_buffer_is_larger_than.py helper script to verify that the pipe grew in -# size. -ts.Disk.traffic_out.Content += Testers.ContainsExpression( - "New buffer size for pipe.*{}".format(pipe_name), "Verify that the named pipe's size was adjusted") -buffer_verifier = "pipe_buffer_is_larger_than.py" -tr.Setup.Copy(buffer_verifier) -verify_buffer_size = tr.Processes.Process( - "verify_buffer_size", f"{sys.executable} {buffer_verifier} {pipe_path} {pipe_size} {ts.Disk.diags_log.AbsPath}") -verify_buffer_size.Return = 0 -verify_buffer_size.Streams.All += Testers.ContainsExpression("Success", "The buffer size verifier should report success.") - -curl = tr.Processes.Process("client_request", 'curl "http://127.0.0.1:{0}" --verbose'.format(ts.Variables.port)) - -reader_output = os.path.join(ts.Variables.LOGDIR, "reader_output") -pipe_reader = tr.Processes.Process("pipe_reader", 'cat {} | tee {}'.format(pipe_path, reader_output)) - -# Create an arbitrary process that just sleeps so that we can provide a wait -# condition upon the log being emitted. The test won't wait this entire sleep -# period, it will only poll until the FileContains Ready condition is -# fulfilled. -wait_for_log = tr.Processes.Process("wait_for_log", 'sleep 15') -wait_for_log.Ready = When.FileContains(reader_output, '127.0.0.1') - -# This is an arbitrary Default process that will simply provide for -# ordering of the Processes. -tr.Processes.Default.Command = "echo 'Default place holder for process ordering.'" -tr.Processes.Default.Return = 0 - -# Process ordering. -tr.Processes.Default.StartBefore(verify_buffer_size) -verify_buffer_size.StartBefore(wait_for_log) -wait_for_log.StartBefore(curl) -curl.StartBefore(pipe_reader) -pipe_reader.StartBefore(ts) diff --git a/tests/gold_tests/logging/log_plugin_init.test.py b/tests/gold_tests/logging/log_plugin_init.test.py deleted file mode 100644 index 24fa59ac72e..00000000000 --- a/tests/gold_tests/logging/log_plugin_init.test.py +++ /dev/null @@ -1,38 +0,0 @@ -''' -Verify text logging during plugin initialization. -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import os - -Test.Summary = ''' -Verify plugins can fill a text log buffer before logging threads start. -''' - -ts = Test.MakeATSProcess('ts') -ts.Disk.records_config.update({'proxy.config.log.log_buffer_size': 9216}) -Test.PrepareTestPlugin(os.path.join(Test.Variables.AtsTestPluginsDir, 'test_log_interface.so'), ts, '--write-during-init') - -plugin_log = Test.Disk.File(os.path.join(ts.Variables.LOGDIR, 'test_log_interface.log'), exists=True) -plugin_log.Content = Testers.ContainsExpression( - 'Writing during plugin initialization', 'The pre-initialization log buffer should be flushed') - -tr = Test.AddTestRun('Start ATS with a plugin that fills a text log buffer during initialization') -tr.Processes.Default.Command = 'printf "traffic_server remained running"' -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.StartBefore(ts) -tr.StillRunningAfter = ts diff --git a/tests/gold_tests/logging/log_retention.test.py b/tests/gold_tests/logging/log_retention.test.py deleted file mode 100644 index 0743dd814e0..00000000000 --- a/tests/gold_tests/logging/log_retention.test.py +++ /dev/null @@ -1,488 +0,0 @@ -''' -Verify correct log retention behavior. -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import os -import socket - -Test.Summary = ''' -Test the enforcement of proxy.config.log.max_space_mb_for_logs. -''' - -# This test is sensitive to timing issues, especially in the OS CI for some -# reason. We'll leave the test here because it is helpful for when doing -# development on the log rotate code, but make it generally skipped when the -# suite of AuTests are run so it doesn't generate annoying false negatives. -Test.SkipIf(Condition.true("This test is sensitive to timing issues which makes it flaky.")) - - -class TestLogRetention: - __base_records_config = { - # Do not accept connections from clients until cache subsystem is operational. - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'logspace', - - # Enable log rotation and auto-deletion, the subjects of this test. - 'proxy.config.log.rolling_enabled': 3, - 'proxy.config.log.auto_delete_rolled_files': 1, - - # 10 MB is the minimum rolling size. - 'proxy.config.log.rolling_size_mb': 10, - 'proxy.config.log.periodic_tasks_interval': 1, - } - - __server = None - __ts_counter = 0 - __server_is_started = False - - def __init__(self, records_config, run_description, command="traffic_server"): - """ - Create a TestLogRetention instance. - """ - self.server = TestLogRetention.__create_server() - self.ts = self.__create_ts(records_config, command) - self.__initialize_processes() - self.tr = Test.AddTestRun(run_description) - - def __initialize_processes(self): - """ - Create a run to initialize the server and traffic_server processes so - the caller doesn't have to. - """ - tr = Test.AddTestRun("Initialize processes for ts{}".format(TestLogRetention.__ts_counter - 1)) - tr.MakeCurlCommand(self.get_curl_command(), ts=self.ts) - tr.Processes.Default.ReturnCode = 0 - if not TestLogRetention.__server_is_started: - self.server.StartBefore(self.ts) - tr.Processes.Default.StartBefore(self.server) - TestLogRetention.__server_is_started = True - else: - tr.Processes.Default.StartBefore(self.ts) - - tr.StillRunningAfter = self.ts - tr.StillRunningAfter = self.server - - @classmethod - def __create_server(cls): - """ - Create and return a server process. - - There is only one server process for all the tests. This function is - re-entrant, but subsequent calls to it will return the cached version - of the single server. - """ - if cls.__server: - return cls.__server - - server = Test.MakeOriginServer("server") - request_header = { - "headers": "GET / HTTP/1.1\r\n" - "Host: does.not.matter\r\n\r\n", - "timestamp": "1469733493.993", - "body": "" - } - response_header = { - "headers": "HTTP/1.1 200 OK\r\n" - "Connection: close\r\n" - "Cache-control: max-age=85000\r\n\r\n", - "timestamp": "1469733493.993", - "body": "xxx" - } - server.addResponse("sessionlog.json", request_header, response_header) - cls.__server = server - return cls.__server - - def __create_ts(self, records_config, command="traffic_server"): - """ - Create an ATS process. - - records_config: records_config values for this test. - command: The ATS process to run for the test. - """ - ts_name = "ts{counter}".format(counter=TestLogRetention.__ts_counter) - TestLogRetention.__ts_counter += 1 - self.ts = Test.MakeATSProcess(ts_name, command=command) - - combined_records_config = TestLogRetention.__base_records_config.copy() - combined_records_config.update(records_config) - self.ts.Disk.records_config.update(combined_records_config) - - self.ts.Disk.remap_config.AddLine( - 'map http://127.0.0.1:{0} http://127.0.0.1:{1}'.format(self.ts.Variables.port, self.server.Variables.Port)) - return self.ts - - def get_curl_command(self): - """ - Generate the appropriate single curl command. - """ - return '"http://127.0.0.1:{0}" --verbose'.format(self.ts.Variables.port) - - def get_command_to_rotate_once(self): - """ - Generate the set of curl commands to trigger a log rotate. - """ - return 'for i in {{{{1..2500}}}}; do {{curl}} "http://127.0.0.1:{0}" --verbose; done'.format(self.ts.Variables.port) - - def get_command_to_rotate_thrice(self): - """ - Generate the set of curl commands to trigger a log rotate. - """ - return 'for i in {{{{1..7500}}}}; do {{curl}} "http://127.0.0.1:{0}" --verbose; done'.format(self.ts.Variables.port) - - -# -# Test 0: Verify that log deletion happens when no min_count is specified. -# -specified_hostname = 'my_hostname' -twelve_meg_log_space = { - # The following configures a 12 MB log cap with a required 2 MB head room. - # Thus the rotated log of just over 10 MB should be deleted because it - # will not leave enough head room. - 'proxy.config.log.max_space_mb_headroom': 2, - 'proxy.config.log.max_space_mb_for_logs': 12, - # Verify that setting a hostname changes the hostname used in rolled logs. - 'proxy.config.log.hostname': specified_hostname, -} -test = TestLogRetention(twelve_meg_log_space, "Verify log rotation and deletion of the configured log file with no min_count.") - -# Configure approximately 5 KB entries for a log with no specified min_count. -test.ts.Disk.logging_yaml.AddLines( - ''' -logging: - formats: - - name: long - format: "{prefix}: %" - logs: - - filename: test_deletion - format: long -'''.format(prefix="0123456789" * 500).split("\n")) - -# Verify that each log type was registered for auto-deletion. -test.ts.Disk.traffic_out.Content = Testers.ContainsExpression( - "Registering rotated log deletion for test_deletion.log with min roll count 0", - "Verify test_deletion.log auto-delete configuration") -test.ts.Disk.traffic_out.Content += Testers.ContainsExpression( - "Registering rotated log deletion for error.log with min roll count 0", "Verify error.log auto-delete configuration") -test.ts.Disk.traffic_out.Content += Testers.ContainsExpression( - "Registering rotated log deletion for traffic.out with min roll count 0", "Verify traffic.out auto-delete configuration") -test.ts.Disk.traffic_out.Content += Testers.ContainsExpression( - "Registering rotated log deletion for diags.log with min roll count 0", "Verify diags.log auto-delete configuration") -test.ts.Disk.traffic_out.Content += Testers.ContainsExpression( - "Registering rotated log deletion for manager.log with min roll count 0", "Verify manager.log auto-delete configuration") -# Verify test_deletion was rotated and deleted. -test.ts.Disk.traffic_out.Content += Testers.ContainsExpression( - f"The rolled logfile.*test_deletion.log_{specified_hostname}.*was auto-deleted.*bytes were reclaimed", - "Verify that space was reclaimed") - -test.tr.MakeCurlCommandMulti(test.get_command_to_rotate_once(), ts=test.ts) -test.tr.Processes.Default.ReturnCode = 0 - -test.tr.StillRunningAfter = test.ts -test.tr.StillRunningAfter = test.server - -# -# Test 1: Verify log deletion happens with a min_count of 1. -# -test = TestLogRetention(twelve_meg_log_space, "Verify log rotation and deletion of the configured log file with a min_count of 1.") - -# Configure approximately 5 KB entries for a log with no specified min_count. -test.ts.Disk.logging_yaml.AddLines( - ''' -logging: - formats: - - name: long - format: "{prefix}: %" - logs: - - filename: test_deletion - rolling_min_count: 1 - format: long -'''.format(prefix="0123456789" * 500).split("\n")) - -# Verify that each log type was registered for auto-deletion. -test.ts.Disk.traffic_out.Content = Testers.ContainsExpression( - "Registering rotated log deletion for test_deletion.log with min roll count 1", - "Verify test_deletion.log auto-delete configuration") -# Only the test_deletion should have its min_count overridden. -test.ts.Disk.traffic_out.Content += Testers.ContainsExpression( - "Registering rotated log deletion for error.log with min roll count 0", "Verify error.log auto-delete configuration") -test.ts.Disk.traffic_out.Content += Testers.ContainsExpression( - "Registering rotated log deletion for traffic.out with min roll count 0", "Verify traffic.out auto-delete configuration") -test.ts.Disk.traffic_out.Content += Testers.ContainsExpression( - "Registering rotated log deletion for diags.log with min roll count 0", "Verify diags.log auto-delete configuration") -test.ts.Disk.traffic_out.Content += Testers.ContainsExpression( - "Registering rotated log deletion for manager.log with min roll count 0", "Verify manager.log auto-delete configuration") -# Verify test_deletion was rotated and deleted. -test.ts.Disk.traffic_out.Content += Testers.ContainsExpression( - f"The rolled logfile.*test_deletion.log_{specified_hostname}.*was auto-deleted.*bytes were reclaimed", - "Verify that space was reclaimed") - -test.tr.MakeCurlCommandMulti(test.get_command_to_rotate_once(), ts=test.ts) -test.tr.Processes.Default.ReturnCode = 0 -test.tr.StillRunningAfter = test.ts -test.tr.StillRunningAfter = test.server - -# -# Test 2: Verify log deletion happens for a plugin's logs. -# -test = TestLogRetention(twelve_meg_log_space, "Verify log rotation and deletion of plugin logs.") -Test.PrepareTestPlugin(os.path.join(Test.Variables.AtsTestPluginsDir, 'test_log_interface.so'), test.ts) - -# Verify that the plugin's logs and other core logs were registered for deletion. -test.ts.Disk.traffic_out.Content = Testers.ContainsExpression( - "Registering rotated log deletion for test_log_interface.log with min roll count 0", - "Verify test_log_interface.log auto-delete configuration") -test.ts.Disk.traffic_out.Content += Testers.ContainsExpression( - "Registering rotated log deletion for error.log with min roll count 0", "Verify error.log auto-delete configuration") -test.ts.Disk.traffic_out.Content += Testers.ContainsExpression( - "Registering rotated log deletion for traffic.out with min roll count 0", "Verify traffic.out auto-delete configuration") -test.ts.Disk.traffic_out.Content += Testers.ContainsExpression( - "Registering rotated log deletion for diags.log with min roll count 0", "Verify diags.log auto-delete configuration") -test.ts.Disk.traffic_out.Content += Testers.ContainsExpression( - "Registering rotated log deletion for manager.log with min roll count 0", "Verify manager.log auto-delete configuration") -# Verify test_deletion was rotated and deleted. -test.ts.Disk.traffic_out.Content += Testers.ContainsExpression( - "The rolled logfile.*test_log_interface.log_.*was auto-deleted.*bytes were reclaimed", "Verify that space was reclaimed") - -test.tr.MakeCurlCommandMulti(test.get_command_to_rotate_once(), ts=test.ts) -test.tr.Processes.Default.ReturnCode = 0 -test.tr.StillRunningAfter = test.ts -test.tr.StillRunningAfter = test.server - -# -# Test 3: Verify log deletion priority behavior. -# -twenty_two_meg_log_space = { - # The following configures a 22 MB log cap with a required 2 MB head room. - # This should allow enough room for two logs being rotated. - 'proxy.config.log.max_space_mb_headroom': 2, - 'proxy.config.log.max_space_mb_for_logs': 22, -} -test = TestLogRetention(twenty_two_meg_log_space, "Verify log deletion priority behavior.") - -# Configure approximately 5 KB entries for a log with no specified min_count. -test.ts.Disk.logging_yaml.AddLines( - ''' -logging: - formats: - - name: long - format: "{prefix}: %" - logs: - - filename: test_low_priority_deletion - rolling_min_count: 5 - format: long - - - filename: test_high_priority_deletion - rolling_min_count: 1 - format: long -'''.format(prefix="0123456789" * 500).split("\n")) - -# Verify that each log type was registered for auto-deletion. -test.ts.Disk.traffic_out.Content = Testers.ContainsExpression( - "Registering rotated log deletion for test_low_priority_deletion.log with min roll count 5", - "Verify test_low_priority_deletion.log auto-delete configuration") -test.ts.Disk.traffic_out.Content += Testers.ContainsExpression( - "Registering rotated log deletion for test_high_priority_deletion.log with min roll count 1", - "Verify test_high_priority_deletion.log auto-delete configuration") -# Only the test_deletion should have its min_count overridden. -test.ts.Disk.traffic_out.Content += Testers.ContainsExpression( - "Registering rotated log deletion for error.log with min roll count 0", "Verify error.log auto-delete configuration") -test.ts.Disk.traffic_out.Content += Testers.ContainsExpression( - "Registering rotated log deletion for traffic.out with min roll count 0", "Verify traffic.out auto-delete configuration") -test.ts.Disk.traffic_out.Content += Testers.ContainsExpression( - "Registering rotated log deletion for diags.log with min roll count 0", "Verify diags.log auto-delete configuration") -test.ts.Disk.traffic_out.Content += Testers.ContainsExpression( - "Registering rotated log deletion for manager.log with min roll count 0", "Verify manager.log auto-delete configuration") -# Verify test_deletion was rotated and deleted. -test.ts.Disk.traffic_out.Content += Testers.ExcludesExpression( - "The rolled logfile.*test_low_priority_deletion.log_.*was auto-deleted.*bytes were reclaimed", - "Verify that space was reclaimed from test_high_priority_deletion") - -# Verify that ATS derives the hostname correctly if the user does not specify a -# hostname via 'proxy.config.log.hostname'. -hostname = socket.gethostname() -test.ts.Disk.traffic_out.Content += Testers.ContainsExpression( - f"The rolled logfile.*test_high_priority_deletion.log_{hostname}.*was auto-deleted.*bytes were reclaimed", - "Verify that space was reclaimed from test_high_priority_deletion") - -test.tr.MakeCurlCommandMulti(test.get_command_to_rotate_once(), ts=test.ts) -test.tr.Processes.Default.ReturnCode = 0 -test.tr.StillRunningAfter = test.ts -test.tr.StillRunningAfter = test.server - -# -# Test 4: Verify min_count configuration overrides. -# -various_min_count_overrides = { - 'proxy.config.log.max_space_mb_for_logs': 22, - 'proxy.config.log.rolling_min_count': 3, - 'proxy.config.output.logfile.rolling_min_count': 4, - 'proxy.config.diags.logfile.rolling_min_count': 5, -} -test = TestLogRetention(various_min_count_overrides, "Verify that the various min_count configurations behave as expected") - -# Only the test_deletion should have its min_count overridden. -test.ts.Disk.traffic_out.Content = Testers.ContainsExpression( - "Registering rotated log deletion for error.log with min roll count 3", "Verify error.log auto-delete configuration") -test.ts.Disk.traffic_out.Content += Testers.ContainsExpression( - "Registering rotated log deletion for traffic.out with min roll count 4", "Verify traffic.out auto-delete configuration") -test.ts.Disk.traffic_out.Content += Testers.ContainsExpression( - "Registering rotated log deletion for diags.log with min roll count 5", "Verify diags.log auto-delete configuration") -test.ts.Disk.traffic_out.Content += Testers.ContainsExpression( - "Registering rotated log deletion for manager.log with min roll count 5", "Verify manager.log auto-delete configuration") -# In case a future log is added, make sure the developer doesn't forget to -# set the min count per configuration. -test.ts.Disk.traffic_out.Content += Testers.ExcludesExpression( - "Registering .* with min roll count 0", "Verify nothing has a default min roll count of 0 per configuration") - -# This test doesn't require a log rotation. We just verify that the logs communicate -# the appropriate min_count values above. -test.tr.MakeCurlCommand(test.get_curl_command(), ts=test.ts) -test.tr.Processes.Default.ReturnCode = 0 -test.tr.StillRunningAfter = test.ts -test.tr.StillRunningAfter = test.server - -# -# Test 5: Verify log deletion does not happen when it is disabled. -# -auto_delete_disabled = twelve_meg_log_space.copy() -auto_delete_disabled.update( - { - 'proxy.config.log.auto_delete_rolled_files': 0, - # Verify that setting a hostname changes the hostname used in rolled logs. - 'proxy.config.log.hostname': 'my_hostname', - }) -test = TestLogRetention(auto_delete_disabled, "Verify log deletion does not happen when auto-delet is disabled.") - -# Configure approximately 5 KB entries for a log with no specified min_count. -test.ts.Disk.logging_yaml.AddLines( - ''' -logging: - formats: - - name: long - format: "{prefix}: %" - logs: - - filename: test_deletion - rolling_min_count: 1 - format: long -'''.format(prefix="0123456789" * 500).split("\n")) - -# Verify that each log type was registered for auto-deletion. -test.ts.Disk.traffic_out.Content = Testers.ExcludesExpression( - "Registering rotated log deletion for test_deletion.log with min roll count 1", - "Verify test_deletion.log auto-delete configuration") -# Only the test_deletion should have its min_count overridden. -test.ts.Disk.traffic_out.Content += Testers.ExcludesExpression( - "Registering rotated log deletion for error.log with min roll count 0", "Verify error.log auto-delete configuration") -test.ts.Disk.traffic_out.Content += Testers.ExcludesExpression( - "Registering rotated log deletion for traffic.out with min roll count 0", "Verify traffic.out auto-delete configuration") -test.ts.Disk.traffic_out.Content += Testers.ExcludesExpression( - "Registering rotated log deletion for diags.log with min roll count 0", "Verify diags.log auto-delete configuration") -test.ts.Disk.traffic_out.Content += Testers.ExcludesExpression( - "Registering rotated log deletion for manager.log with min roll count 0", "Verify manager.log auto-delete configuration") -# Verify test_deletion was not deleted. -test.ts.Disk.traffic_out.Content += Testers.ExcludesExpression( - "The rolled logfile.*test_deletion.log_.*was auto-deleted.*bytes were reclaimed", "Verify that space was reclaimed") - -test.tr.MakeCurlCommandMulti(test.get_command_to_rotate_once(), ts=test.ts) -test.tr.Processes.Default.ReturnCode = 0 -test.tr.StillRunningAfter = test.ts -test.tr.StillRunningAfter = test.server - -# -# Test 6: Verify that max_roll_count is respected. -# -max_roll_count_of_2 = { - 'proxy.config.diags.debug.tags': 'log-file', - - # Provide plenty of max_space: we want auto-deletion to happen because of - # rolling_max_count, not max_space_mb_for_logs. - 'proxy.config.log.max_space_mb_headroom': 2, - 'proxy.config.log.max_space_mb_for_logs': 100, - - # This is the configuration under test. - 'proxy.config.log.rolling_max_count': 2, -} -test = TestLogRetention(max_roll_count_of_2, "Verify max_roll_count is respected.") - -# Configure approximately 5 KB entries for a log with no specified min_count. -test.ts.Disk.logging_yaml.AddLines( - ''' -logging: - formats: - - name: long - format: "{prefix}: %" - logs: - - filename: test_deletion - format: long -'''.format(prefix="0123456789" * 500).split("\n")) - -# Verify that trim happened for the rolled file. -test.ts.Disk.traffic_out.Content = Testers.ContainsExpression( - "rolled logfile.*test_deletion.log.*old.* was auto-deleted", "Verify test_deletion.log was trimmed") - -test.tr.MakeCurlCommandMulti(test.get_command_to_rotate_thrice(), ts=test.ts) -test.tr.Processes.Default.ReturnCode = 0 -test.tr.StillRunningAfter = test.ts -test.tr.StillRunningAfter = test.server - -# -# Test 7: Verify log deletion happens after a config reload. -# -test = TestLogRetention(twelve_meg_log_space, "Verify log rotation and deletion after a config reload.") - -test.ts.Disk.logging_yaml.AddLines( - ''' -logging: - formats: - - name: long - format: "{prefix}: %" - logs: - - filename: test_deletion - format: long -'''.format(prefix="0123456789" * 500).split("\n")) - -# Verify that the plugin's logs and other core logs were registered for deletion. -test.ts.Disk.traffic_out.Content = Testers.ContainsExpression( - "Registering rotated log deletion for test_deletion.log with min roll count 0", - "Verify test_deletion.log auto-delete configuration") -test.ts.Disk.traffic_out.Content += Testers.ContainsExpression( - "Registering rotated log deletion for error.log with min roll count 0", "Verify error.log auto-delete configuration") -test.ts.Disk.traffic_out.Content += Testers.ContainsExpression( - "Registering rotated log deletion for traffic.out with min roll count 0", "Verify traffic.out auto-delete configuration") -test.ts.Disk.traffic_out.Content += Testers.ContainsExpression( - "Registering rotated log deletion for diags.log with min roll count 0", "Verify diags.log auto-delete configuration") -test.ts.Disk.traffic_out.Content += Testers.ContainsExpression( - "Registering rotated log deletion for manager.log with min roll count 0", "Verify manager.log auto-delete configuration") -# Verify test_deletion was rotated and deleted. -test.ts.Disk.traffic_out.Content += Testers.ContainsExpression( - "The rolled logfile.*test_deletion.log_.*was auto-deleted.*bytes were reclaimed", "Verify that space was reclaimed") - -# Touch logging.yaml so the config reload applies to logging objects. -test.tr.Processes.Default.Command = "touch " + test.ts.Disk.logging_yaml.AbsRunTimePath -test.tr.Processes.Default.ReturnCode = 0 -test.tr.StillRunningAfter = test.ts -test.tr.StillRunningAfter = test.server - -tr = Test.AddConfigReload(test.ts, description="Perform a config reload") -tr.StillRunningAfter = test.server - -tr = Test.AddTestRun("Get the log to rotate.") -tr.MakeCurlCommandMulti(test.get_command_to_rotate_once(), ts=test.ts) -tr.Processes.Default.ReturnCode = 0 -tr.StillRunningAfter = test.ts -tr.StillRunningAfter = test.server diff --git a/tests/gold_tests/logging/new_log_flds.test.py b/tests/gold_tests/logging/new_log_flds.test.py deleted file mode 100644 index 38be642b3da..00000000000 --- a/tests/gold_tests/logging/new_log_flds.test.py +++ /dev/null @@ -1,119 +0,0 @@ -''' -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import os -import sys - -Test.Summary = ''' -Test new log fields -''' - -Test.SkipUnless(Condition.HasCurlFeature('http2')) - -# ---- -# Setup httpbin Origin Server -# ---- -httpbin = Test.MakeHttpBinServer("httpbin") - -# ---- -# Setup ATS -# ---- -ts = Test.MakeATSProcess("ts", enable_tls=True) - -ts.addDefaultSSLFiles() - -ts.Disk.records_config.update( - { - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'snowflake|http', - 'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir), - 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts.Variables.SSLDir), - }) - -ts.Disk.remap_config.AddLine('map http://127.0.0.1:{0} http://127.0.0.1:{1}/ip'.format(ts.Variables.port, httpbin.Variables.Port)) - -ts.Disk.remap_config.AddLine( - 'map https://127.0.0.1:{0} http://127.0.0.1:{1}/ip'.format(ts.Variables.ssl_port, httpbin.Variables.Port)) - -ts.Disk.remap_config.AddLine( - 'map https://reallyreallyreallyreallylong.com http://127.0.0.1:{1}/ip'.format(ts.Variables.ssl_port, httpbin.Variables.Port)) - -ts.Disk.ssl_multicert_yaml.AddLines( - """ -ssl_multicert: - - dest_ip: "*" - ssl_cert_name: server.pem - ssl_key_name: server.key -""".split("\n")) - -ts.Disk.logging_yaml.AddLines( - ''' -logging: - formats: - - name: custom - format: "% % % %" - logs: - - filename: test_new_log_flds - format: custom -'''.split("\n")) - -tr = Test.AddTestRun() -# Delay on readiness of ssl port -tr.Processes.Default.StartBefore(Test.Processes.ts) -tr.Processes.Default.StartBefore(httpbin) -# -tr.MakeCurlCommand('"http://127.0.0.1:{0}" --verbose'.format(ts.Variables.port), ts=ts) -tr.Processes.Default.ReturnCode = 0 - -tr = Test.AddTestRun() -tr.MakeCurlCommand('"http://127.0.0.1:{0}" --verbose'.format(ts.Variables.port), ts=ts) -tr.Processes.Default.ReturnCode = 0 - -tr = Test.AddTestRun() -tr.MakeCurlCommand('"http://127.0.0.1:{0}" "http://127.0.0.1:{0}" --http1.1 --verbose'.format(ts.Variables.port), ts=ts) -tr.Processes.Default.ReturnCode = 0 - -if not Condition.CurlUsingUnixDomainSocket(): - tr = Test.AddTestRun() - tr.MakeCurlCommand( - '"https://127.0.0.1:{0}" "https://127.0.0.1:{0}" --http2 --insecure --verbose'.format(ts.Variables.ssl_port), ts=ts) - tr.Processes.Default.ReturnCode = 0 - - tr = Test.AddTestRun() - tr.MakeCurlCommand( - ( - '"https://reallyreallyreallyreallylong.com:{0}" --http2 --insecure --verbose' + - ' --resolve reallyreallyreallyreallylong.com:{0}:127.0.0.1').format(ts.Variables.ssl_port), - ts=ts) - tr.Processes.Default.ReturnCode = 0 - -# Wait for the final log line to be written. -# -Test.AddAwaitFileContainsTestRun( - 'Await new log field output.', - os.path.join(ts.Variables.LOGDIR, 'test_new_log_flds.log'), - r'reallyreallyreallyreallylong\.com$', -) - -# Validate generated log. -# -tr = Test.AddTestRun() -observer_script = os.path.join(Test.TestDirectory, 'new_log_flds_observer.py') -log_path = os.path.join(ts.Variables.LOGDIR, 'test_new_log_flds.log') -tr.Processes.Default.Command = f'{sys.executable} {observer_script} < {log_path}' -tr.Processes.Default.ReturnCode = 0 diff --git a/tests/gold_tests/logging/pqsi-pqsp.test.py b/tests/gold_tests/logging/pqsi-pqsp.test.py deleted file mode 100644 index 9ad57855416..00000000000 --- a/tests/gold_tests/logging/pqsi-pqsp.test.py +++ /dev/null @@ -1,89 +0,0 @@ -''' -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import os - -Test.Summary = ''' -Test pqsi and pqsp log fields. -''' - -ts = Test.MakeATSProcess("ts", enable_cache=False) -server = Test.MakeOriginServer("server") - -request_header = { - "headers": "GET /test HTTP/1.1\r\n" - "Host: whatever\r\n" - "\r\n", - "body": "", - 'timestamp': "1469733493.993", -} -response_header = { - "headers": "HTTP/1.1 200 OK\r\n" - "Connection: close\r\n" - "\r\n", - "body": "body\n", - 'timestamp': "1469733493.993", -} -server.addResponse("sessionlog.json", request_header, response_header) - -nameserver = Test.MakeDNServer("dns", default='127.0.0.1') - -ts.Disk.records_config.update( - { - 'proxy.config.dns.nameservers': f"127.0.0.1:{nameserver.Variables.Port}", - 'proxy.config.dns.resolv_conf': 'NULL', - 'proxy.config.http.cache.http': 1, - 'proxy.config.http.cache.required_headers': 0, - }) -ts.Disk.remap_config.AddLine('map / http://localhost:{}/'.format(server.Variables.Port)) - -ts.Disk.logging_yaml.AddLines( - ''' -logging: - formats: - - name: custom - format: '% %' - logs: - - filename: field-test - format: custom -'''.split("\n")) - -tr = Test.AddTestRun() -tr.Processes.Default.StartBefore(server) -tr.Processes.Default.StartBefore(nameserver) -# Delay on readiness of our ssl ports -tr.Processes.Default.StartBefore(Test.Processes.ts) -tr.MakeCurlCommand(f'--verbose http://localhost:{ts.Variables.port}/test', ts=ts) -tr.Processes.Default.ReturnCode = 0 - -# Response for this duplicate request should come from cache. -tr = Test.AddTestRun() -tr.MakeCurlCommand(f'--verbose http://localhost:{ts.Variables.port}/test', ts=ts) -tr.Processes.Default.ReturnCode = 0 - -log_filespec = os.path.join(ts.Variables.LOGDIR, 'field-test.log') - -# Wait for both transaction log lines to be written. Their write order is not -# deterministic. -Test.AddAwaitFileContainsTestRun('Await pqsi/pqsp cache-miss line.', log_filespec, r'^127\.0\.0\.1 [1-6][0-9]*$') -Test.AddAwaitFileContainsTestRun('Await pqsi/pqsp cache-hit line.', log_filespec, r'^0 0$') - -tr = Test.AddTestRun() -tr.Processes.Default.Command = "sed 's/^127\\.0\\.0\\.1 [1-6][0-9]*$$/abc/' < " + log_filespec + " | LC_ALL=C sort" -tr.Processes.Default.Streams.stdout = "gold/pqsi-pqsp.gold" -tr.Processes.Default.ReturnCode = 0 diff --git a/tests/gold_tests/logging/qlog_quiche.test.py b/tests/gold_tests/logging/qlog_quiche.test.py deleted file mode 100644 index 6d5a393407f..00000000000 --- a/tests/gold_tests/logging/qlog_quiche.test.py +++ /dev/null @@ -1,128 +0,0 @@ -''' -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import os - -Test.Summary = ''' -Basic test for qlog using quiche library. -''' - -Test.SkipUnless(Condition.HasATSFeature('TS_HAS_QUICHE')) - -Generate_Qlog = True -No_Qlog = False - - -class quiche_qlog_Test: - replay_file = "replay/basic1.replay.yaml" - client_counter: int = 0 - ts_counter: int = 0 - server_counter: int = 0 - - def __init__(self, name: str): - """Initialize the test. - :param name: The name of the test. - """ - self._name = name - self._generate_qlog = False - - def with_qlogs(self): - self._generate_qlog = True - return self - - def without_qlogs(self): - self._generate_qlog = False - return self - - def _configure_server(self, tr: 'TestRun'): - """Configure the server. - - :param tr: The TestRun object to associate the server process with. - """ - server = tr.AddVerifierServerProcess(f"server_{quiche_qlog_Test.server_counter}", self.replay_file) - quiche_qlog_Test.server_counter += 1 - self._server = server - - def _configure_traffic_server(self, tr: 'TestRun'): - """Configure Traffic Server. - - :param tr: The TestRun object to associate the ts process with. - """ - ts = Test.MakeATSProcess(f"ts-{quiche_qlog_Test.ts_counter}", enable_quic=True, enable_tls=True) - self._ts = ts - quiche_qlog_Test.ts_counter += 1 - self._ts.addDefaultSSLFiles() - self._ts.Disk.records_config.update( - ''' - diags: - debug: - enabled: 1 - tags: vv_quic|v_quic - quic: - no_activity_timeout_in: 3000 - ''') - if self._generate_qlog: - self._ts.Disk.records_config.update( - ''' - quic: - qlog: - file_base: log/test_qlog # we expect to have log/test_qlog-.sqlog - ''') - self._ts.Disk.ssl_multicert_yaml.AddLines( - f""" -ssl_multicert: - - dest_ip: "*" - ssl_cert_name: {ts.Variables.SSLDir}/server.pem - ssl_key_name: {ts.Variables.SSLDir}/server.key -""".split("\n")) - - self._ts.Disk.remap_config.AddLine(f'map / http://127.0.0.1:{self._server.Variables.http_port}') - - def test(self): - """Run the test.""" - tr = Test.AddTestRun(self._name) - self._configure_server(tr) - self._configure_traffic_server(tr) - - tr.Processes.Default.StartBefore(self._server) - tr.Processes.Default.StartBefore(self._ts) - tr.AddVerifierClientProcess( - f"client-{quiche_qlog_Test.client_counter}", self.replay_file, http3_ports=[self._ts.Variables.ssl_port]) - quiche_qlog_Test.client_counter += 1 - tr.Processes.Default.ReturnCode = 0 - - # If requested we will copy the file to a known name as the runtime name is not know - # by the test. So we will rename them all and grab the first one for validation. - test_run = Test.AddTestRun(f"{self._name} : check qlog files") - qlog_base_name = "test_qlog" - rename_script = os.path.join(Test.TestDirectory, 'rename_qlog.sh') - test_run.Processes.Default.Command = f'sleep 5; bash {rename_script} {self._ts.Variables.LOGDIR} {qlog_base_name}' - test_run.Processes.Default.ReturnCode = 0 if self._generate_qlog else 2 # exit 2 is what we want if no qlog was generated. - - # Basic valdation - if self._generate_qlog: - tr = Test.AddTestRun("Check qlog content") - file = os.path.join(self._ts.Variables.LOGDIR, "1.sqlog") - f = tr.Disk.File(file) - tr.Processes.Default.Command = "echo 0" - tr.Processes.Default.ReturnCode = 0 - f.Content = Testers.IncludesExpression('"title":"Apache Traffic Server"', 'Should include this basic text') - - -quiche_qlog_Test("Generate qlog test").with_qlogs().test() -quiche_qlog_Test("Do not generate qlog").without_qlogs().test() diff --git a/tests/gold_tests/logging/sigusr2.test.py b/tests/gold_tests/logging/sigusr2.test.py deleted file mode 100644 index 67f85c5a27f..00000000000 --- a/tests/gold_tests/logging/sigusr2.test.py +++ /dev/null @@ -1,164 +0,0 @@ -''' -Verify support of external log rotation via SIGUSR2. -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import os -import shlex -import sys - -TS_PID_SCRIPT = 'ts_process_handler.py' -ROTATE_DIAGS_SCRIPT = 'sigusr2_rotate_diags.sh' -ROTATE_CUSTOM_LOG_SCRIPT = 'sigusr2_rotate_custom_log.sh' - - -class Sigusr2Test: - """ - Handle this test-specific Traffic Server configuration. - """ - - __ts_counter = 1 - __server_counter = 1 - - @classmethod - def _next_ts_name(cls): - ts_name = f"sigusr2_ts{cls.__ts_counter}" - cls.__ts_counter += 1 - return ts_name - - @classmethod - def _next_server_name(cls): - server_name = f"sigusr2_server{cls.__server_counter}" - cls.__server_counter += 1 - return server_name - - @staticmethod - def _make_script_command(script_name, *args): - quoted_args = ' '.join(shlex.quote(str(arg)) for arg in args) - return f"bash ./{script_name} {quoted_args}" - - def _configure_traffic_server(self, tr): - ts_name = self._next_ts_name() - ts = tr.MakeATSProcess(ts_name) - ts.Disk.records_config.update( - { - 'proxy.config.http.wait_for_cache': 1, - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'log', - 'proxy.config.log.periodic_tasks_interval': 1, - - # All log rotation should be handled externally. - 'proxy.config.log.rolling_enabled': 0, - 'proxy.config.log.auto_delete_rolled_files': 0, - }) - return ts, ts_name - - def _configure_server(self): - server = Test.MakeOriginServer(self._next_server_name()) - - for path in ['/first', '/second', '/third']: - request_header = { - 'headers': f'GET {path} HTTP/1.1\r\nHost: does.not.matter\r\n\r\n', - 'timestamp': '1469733493.993', - 'body': '' - } - response_header = { - 'headers': 'HTTP/1.1 200 OK\r\n' - 'Connection: close\r\n' - 'Cache-control: max-age=85000\r\n\r\n', - 'timestamp': '1469733493.993', - 'body': 'xxx' - } - server.addResponse('sessionlog.json', request_header, response_header) - - return server - - def add_system_log_test(self): - tr = Test.AddTestRun('Verify system logs can be rotated') - ts, ts_name = self._configure_traffic_server(tr) - - rotated_diags_log = f'{ts.Disk.diags_log.AbsPath}_old' - ts.Disk.File(rotated_diags_log, id='diags_log_old') - - tr.Processes.Default.Command = self._make_script_command( - ROTATE_DIAGS_SCRIPT, sys.executable, f'./{TS_PID_SCRIPT}', ts_name, ts.Disk.diags_log.AbsPath, rotated_diags_log) - tr.Processes.Default.ReturnCode = 0 - tr.Processes.Default.StartBefore(ts) - - ts.Disk.diags_log.Content += Testers.ExcludesExpression( - 'traffic server running', 'The new diags.log should not reference the running traffic server') - ts.Disk.diags_log.Content += Testers.ContainsExpression( - 'Reseated diags.log', 'The new diags.log should indicate the newly opened diags.log') - ts.Disk.diags_log_old.Content += Testers.ContainsExpression( - 'traffic server running', 'The rotated diags.log should keep the original startup message') - - def add_configured_log_test(self): - tr = Test.AddTestRun('Verify yaml.log logs can be rotated') - ts, ts_name = self._configure_traffic_server(tr) - server = self._configure_server() - - ts.Disk.remap_config.AddLine(f'map http://127.0.0.1:{ts.Variables.port} http://127.0.0.1:{server.Variables.Port}') - ts.Disk.logging_yaml.AddLine( - ''' - logging: - formats: - - name: has_path - format: "%: %" - logs: - - filename: test_rotation - format: has_path - ''') - - configured_log = os.path.join(ts.Variables.LOGDIR, 'test_rotation.log') - ts.Disk.File(configured_log, id='configured_log') - - rotated_configured_log = f'{configured_log}_old' - ts.Disk.File(rotated_configured_log, id='configured_log_old') - - ts.StartBefore(server) - tr.Processes.Default.Command = self._make_script_command( - ROTATE_CUSTOM_LOG_SCRIPT, sys.executable, f'./{TS_PID_SCRIPT}', ts_name, ts.Variables.port, configured_log, - rotated_configured_log) - tr.Processes.Default.ReturnCode = 0 - tr.Processes.Default.StartBefore(ts) - - ts.Disk.configured_log.Content += Testers.ExcludesExpression( - '/first', 'The new test_rotation.log should not have the first GET retrieval in it.') - ts.Disk.configured_log.Content += Testers.ExcludesExpression( - '/second', 'The new test_rotation.log should not have the second GET retrieval in it.') - ts.Disk.configured_log.Content += Testers.ContainsExpression( - '/third', 'The new test_rotation.log should have the third GET retrieval in it.') - - ts.Disk.configured_log_old.Content += Testers.ContainsExpression( - '/first', 'test_rotation.log_old should have the first GET retrieval in it.') - ts.Disk.configured_log_old.Content += Testers.ContainsExpression( - '/second', 'test_rotation.log_old should have the second GET retrieval in it.') - ts.Disk.configured_log_old.Content += Testers.ExcludesExpression( - '/third', 'test_rotation.log_old should not have the third GET retrieval in it.') - - -Test.Summary = ''' -Verify support of external log rotation via SIGUSR2. -''' - -Test.Setup.Copy(TS_PID_SCRIPT) -Test.Setup.Copy(ROTATE_DIAGS_SCRIPT) -Test.Setup.Copy(ROTATE_CUSTOM_LOG_SCRIPT) - -sigusr2_test = Sigusr2Test() -sigusr2_test.add_system_log_test() -sigusr2_test.add_configured_log_test() diff --git a/tests/gold_tests/next_hop/parent_config/parent_config_dest_ip.test.py b/tests/gold_tests/next_hop/parent_config/parent_config_dest_ip.test.py deleted file mode 100644 index 8359ded6018..00000000000 --- a/tests/gold_tests/next_hop/parent_config/parent_config_dest_ip.test.py +++ /dev/null @@ -1,125 +0,0 @@ -''' -Verify ATS parent.config dest_ip dns bug fix. -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -Test.Summary = ''' -Verify ATS parent_config with dest_ip=... -''' - -# Create origin -origin = Test.MakeOriginServer("origin") - -# default root -request_header_chk = { - "headers": "GET / HTTP/1.1\r\n" + "Host: ats\r\n" + "\r\n", - "timestamp": "1469733493.993", - "body": "", -} - -response_header_chk = { - "headers": "HTTP/1.1 200 OK\r\n" + "Connection: close\r\n" + "\r\n", - "timestamp": "1469733493.993", - "body": "", -} - -origin.addResponse("sessionlog.json", request_header_chk, response_header_chk) - -request_header = { - "headers": - f"GET /foo.txt HTTP/1.1\r\n" - "Host: does.not.matter\r\n" # But cant be omitted - "\r\n", - "timestamp": "1469733493.993", - "body": "" -} - -response_header = { - "headers": "HTTP/1.1 200 OK\r\n" - "Connection: close\r\n" - "Cache-control: max-age=60\r\n" - "\r\n", - "timestamp": "1469733493.993", - "body": f"This is the body for foo.txt\n" -} -origin.addResponse("sessionlog.json", request_header, response_header) - -# Configure DNS for cache layering -dns = Test.MakeDNServer("dns") -dns.addRecords(records={f"origin": ["127.0.0.1"]}) -dns.addRecords(records={f"ts1": ["127.0.0.1"]}) -dns.addRecords(records={f"ts0": ["127.0.0.1"]}) -dns.addRecords(records={f"foo.bar": ["142.250.72.14"]}) # google.com - -# Configure Traffic Server Mid -ts1 = Test.MakeATSProcess("ts1") -ts1.Disk.remap_config.AddLine(f"map / http://origin:{origin.Variables.Port}") - -ts1.Disk.records_config.update( - { - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'http|dns|hostdb|parent', - 'proxy.config.dns.nameservers': f"127.0.0.1:{dns.Variables.Port}", - 'proxy.config.dns.resolv_conf': "NULL", - 'proxy.config.hostdb.lookup_timeout': 2, - 'proxy.config.http.connect_attempts_timeout': 1, - 'proxy.config.http.parent_proxy.self_detect': 0, - 'proxy.config.http.insert_response_via_str': 1, - 'proxy.config.proxy_name': 'ts1', - }) - -# Configure Traffic Server Edge -ts0 = Test.MakeATSProcess("ts0") -ts0.Disk.remap_config.AddLine("map http://foo.bar http://foo.bar") - -ts0.Disk.records_config.update( - { - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'http|dns|hostdb|parent', - 'proxy.config.dns.nameservers': f"127.0.0.1:{dns.Variables.Port}", - 'proxy.config.dns.resolv_conf': "NULL", - 'proxy.config.hostdb.lookup_timeout': 2, - 'proxy.config.http.connect_attempts_timeout': 1, - 'proxy.config.http.parent_proxy.self_detect': 0, - 'proxy.config.http.insert_response_via_str': 1, - 'proxy.config.proxy_name': 'ts0', - }) - -ts0.Disk.parent_config.AddLines( - [ - f"dest_ip=93.184.216.34 port=80 go_direct=true", # example.com - f'dest_host=foo.bar port=80 parent="ts1:{ts1.Variables.port}|1;" go_direct="false" parent_is_proxy="true"', - ]) - -# Start everything up -tr = Test.AddTestRun("init") -tr.Processes.Default.StartBefore(origin) -tr.Processes.Default.StartBefore(dns) -tr.Processes.Default.StartBefore(ts0) -tr.Processes.Default.StartBefore(ts1) -tr.Processes.Default.Command = 'echo start TS, TSH_N, HTTP origin and DNS.' -tr.Processes.Default.ReturnCode = 0 - -curl_and_args = f"-s -D /dev/stdout -o /dev/stderr -x http://127.0.0.1:{ts0.Variables.port}" - -# Request asset that goes through the layers -tr = Test.AddTestRun("request") -ps = tr.Processes.Default -tr.MakeCurlCommand(curl_and_args + ' http://foo.bar/foo.txt', ts=ts0) -ps.ReturnCode = 0 -ps.Streams.stdout.Content = Testers.ContainsExpression("Via:.* ts1 .* ts0 ", "expected via header") -tr.StillRunningAfter = ts0 diff --git a/tests/gold_tests/next_hop/strategies_ch/strategies_ch.test.py b/tests/gold_tests/next_hop/strategies_ch/strategies_ch.test.py deleted file mode 100644 index a20bef02ef2..00000000000 --- a/tests/gold_tests/next_hop/strategies_ch/strategies_ch.test.py +++ /dev/null @@ -1,139 +0,0 @@ -''' -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -Test.Summary = ''' -Test next hop selection using strategies.yaml with consistent hashing. -''' - -# Define and populate MicroServer. -# -server = Test.MakeOriginServer("server") -response_header = { - "headers": "HTTP/1.1 200 OK\r\n" - "Connection: close\r\n" - "Cache-control: max-age=85000\r\n" - "\r\n", - "timestamp": "1469733493.993", - "body": "This is the body.\n" -} -num_objects = 32 -for i in range(num_objects): - request_header = { - "headers": - f"GET /obj{i} HTTP/1.1\r\n" - "Host: does.not.matter\r\n" # But cannot be omitted. - "\r\n", - "timestamp": "1469733493.993", - "body": "" - } - server.addResponse("sessionlog.json", request_header, response_header) - -dns = Test.MakeDNServer("dns") - -# Define next hop trafficserver instances. -# -num_nh = 8 -ts_nh = [] -for i in range(num_nh): - ts = Test.MakeATSProcess(f"ts_nh{i}", use_traffic_out=False, command=f"traffic_server 2>nh_trace{i}.log") - ts.Disk.records_config.update( - { - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'http|dns', - 'proxy.config.dns.nameservers': f"127.0.0.1:{dns.Variables.Port}", - 'proxy.config.dns.resolv_conf': "NULL", - }) - ts.Disk.remap_config.AddLine(f"map / http://127.0.0.1:{server.Variables.Port}") - ts.ReturnCode = Any(0, -2) - ts_nh.append(ts) - -ts = Test.MakeATSProcess("ts") - -ts.Disk.records_config.update( - { - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'http|dns|parent|next_hop|host_statuses|hostdb', - 'proxy.config.dns.nameservers': f"127.0.0.1:{dns.Variables.Port}", # Only nameservers if resolv_conf NULL. - 'proxy.config.dns.resolv_conf': "NULL", # This defaults to /etc/resvolv.conf (OS namesevers) if not NULL. - 'proxy.config.http.cache.http': 0, - 'proxy.config.http.uncacheable_requests_bypass_parent': 0, - 'proxy.config.http.no_dns_just_forward_to_parent': 1, - 'proxy.config.http.parent_proxy.mark_down_hostdb': 0, - 'proxy.config.http.parent_proxy.self_detect': 0, - }) - -ts.Disk.File(ts.Variables.CONFIGDIR + "/strategies.yaml", id="strategies", typename="ats:config") -s = ts.Disk.strategies -s.AddLine("groups:") -s.AddLine(" - &g1") -for i in range(num_nh): - dns.addRecords(records={f"next_hop{i}": ["127.0.0.1"]}) - s.AddLine(f" - host: next_hop{i}") - s.AddLine(f" protocol:") - s.AddLine(f" - scheme: http") - s.AddLine(f" port: {ts_nh[i].Variables.port}") - # The health check URL does not seem to be used currently. - # s.AddLine(f" health_check_url: http://next_hop{i}:{ts_nh[i].Variables.port}") - s.AddLine(f" weight: 1.0") -s.AddLines( - [ - "strategies:", - " - strategy: the-strategy", - " policy: consistent_hash", - " hash_key: path", - " go_direct: false", - " parent_is_proxy: true", - " ignore_self_detect: true", - " groups:", - " - *g1", - " scheme: http", - ]) - -# Fallover not currently tested. -# -# s.AddLines([ -# " fallover:", -# " max_simple_retries: 2", -# " ring_mode: exhaust_ring", -# " response_codes:", -# " - 404", -# " health_check:", -# " - passive",]) - -ts.Disk.remap_config.AddLine("map http://dummy.com http://not_used @strategy=the-strategy") - -tr = Test.AddTestRun() -tr.Processes.Default.StartBefore(server) -tr.Processes.Default.StartBefore(dns) -for i in range(num_nh): - tr.Processes.Default.StartBefore(ts_nh[i]) -tr.Processes.Default.StartBefore(Test.Processes.ts) -tr.Processes.Default.Command = 'echo start TS, HTTP server, DNS server and next hop TSes' -tr.Processes.Default.ReturnCode = 0 - -for i in range(num_objects): - tr = Test.AddTestRun() - tr.MakeCurlCommand(f'--verbose --proxy 127.0.0.1:{ts.Variables.port} http://dummy.com/obj{i}', ts=ts) - tr.Processes.Default.Streams.stdout = "body.gold" - tr.Processes.Default.ReturnCode = 0 - -tr = Test.AddTestRun() -# For some reason, the * won't be expanded when the command is executed, if stdout is not piped through "cat". -tr.Processes.Default.Command = "grep -F '200 OK' nh_trace*.log | cat" -tr.Processes.Default.Streams.stdout = "trace.gold" -tr.Processes.Default.ReturnCode = 0 diff --git a/tests/gold_tests/next_hop/strategies_ch2/strategies_ch2.test.py b/tests/gold_tests/next_hop/strategies_ch2/strategies_ch2.test.py deleted file mode 100644 index 62615a5f411..00000000000 --- a/tests/gold_tests/next_hop/strategies_ch2/strategies_ch2.test.py +++ /dev/null @@ -1,155 +0,0 @@ -''' -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -Test.Summary = ''' -Test next hop selection using strategies.yaml with consistent hashing, with fallover -''' - -# Define and populate MicroServer. -# -server = Test.MakeOriginServer("server") -response_header = { - "headers": "HTTP/1.1 200 OK\r\n" - "Connection: close\r\n" - "Cache-control: max-age=85000\r\n" - "\r\n", - "timestamp": "1469733493.993", - "body": "This is the body.\n" -} -num_objects = 32 -for i in range(num_objects): - request_header = { - "headers": - f"GET /obj{i} HTTP/1.1\r\n" - "Host: does.not.matter\r\n" # But cannot be omitted. - "\r\n", - "timestamp": "1469733493.993", - "body": "" - } - server.addResponse("sessionlog.json", request_header, response_header) - -dns = Test.MakeDNServer("dns") - -# Define next hop trafficserver instances. -# -num_nh = 8 -ts_nh = [] -for i in range(num_nh): - ts = Test.MakeATSProcess(f"ts_nh{i}") - ts.Disk.records_config.update( - { - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'http|dns', - 'proxy.config.dns.nameservers': f"127.0.0.1:{dns.Variables.Port}", - 'proxy.config.dns.resolv_conf': "NULL", - }) - ts.Disk.remap_config.AddLine(f"map / http://127.0.0.1:{server.Variables.Port}") - ts_nh.append(ts) - -ts = Test.MakeATSProcess("ts", use_traffic_out=False, command="traffic_server 2> trace.log") -ts.ReturnCode = Any(0, -2) - -ts.Disk.records_config.update( - { - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'http|dns|parent|next_hop|host_statuses|hostdb', - 'proxy.config.dns.nameservers': f"127.0.0.1:{dns.Variables.Port}", # Only nameservers if resolv_conf NULL. - 'proxy.config.dns.resolv_conf': "NULL", # This defaults to /etc/resvolv.conf (OS namesevers) if not NULL. - 'proxy.config.http.cache.http': 0, - 'proxy.config.http.uncacheable_requests_bypass_parent': 0, - 'proxy.config.http.no_dns_just_forward_to_parent': 1, - 'proxy.config.http.parent_proxy.mark_down_hostdb': 0, - 'proxy.config.http.down_server.cache_time': 1, - 'proxy.config.http.parent_proxy.self_detect': 0, - }) - -ts.Disk.File(ts.Variables.CONFIGDIR + "/strategies.yaml", id="strategies", typename="ats:config") -s = ts.Disk.strategies -s.AddLine("groups:") -s.AddLine(" - &g1") -for i in range(num_nh): - dns.addRecords(records={f"next_hop{i}": ["127.0.0.1"]}) - s.AddLine(f" - host: next_hop{i}") - s.AddLine(f" protocol:") - s.AddLine(f" - scheme: http") - s.AddLine(f" port: {ts_nh[i].Variables.port}") - # The health check URL does not seem to be used currently. - # s.AddLine(f" health_check_url: http://next_hop{i}:{ts_nh[i].Variables.port}") - s.AddLine(f" weight: 1.0") - -s.AddLines( - [ - "strategies:", - " - strategy: the-strategy", - " policy: consistent_hash", - " hash_key: path", - " go_direct: false", - " parent_is_proxy: true", - " ignore_self_detect: true", - " groups:", - " - *g1", - " scheme: http", - ]) - -# Use default fallover config. -# -# s.AddLines([ -# " fallover:", -# " max_simple_retries: 2", -# " ring_mode: exhaust_ring", -# " response_codes:", -# " - 404", -# " health_check:", -# " - passive", -# ]) - -ts.Disk.remap_config.AddLine("map http://dummy.com http://not_used @strategy=the-strategy") - -tr = Test.AddTestRun() -tr.Processes.Default.StartBefore(server) -tr.Processes.Default.StartBefore(dns) -for i in range(num_nh): - if (i != 3) and (i != 6): - tr.Processes.Default.StartBefore(ts_nh[i]) -tr.Processes.Default.StartBefore(Test.Processes.ts) -tr.Processes.Default.Command = 'echo start TS, HTTP server, DNS server and all but 2 next hop TSes' -tr.Processes.Default.ReturnCode = 0 - -for i in range(num_objects): - tr = Test.AddTestRun() - tr.MakeCurlCommand(f'--verbose --proxy 127.0.0.1:{ts.Variables.port} http://dummy.com/obj{i}', ts=ts) - tr.Processes.Default.Streams.stdout = "body.gold" - tr.Processes.Default.ReturnCode = 0 - -tr = Test.AddTestRun() -tr.Processes.Default.StartBefore(ts_nh[3]) -tr.Processes.Default.StartBefore(ts_nh[6]) -tr.Processes.Default.Command = 'echo start the 2 unstarted next hop TSes' -tr.Processes.Default.ReturnCode = 0 - -for i in range(num_objects): - tr = Test.AddTestRun() - tr.MakeCurlCommand(f'--verbose --proxy 127.0.0.1:{ts.Variables.port} http://dummy.com/obj{i}', ts=ts) - tr.Processes.Default.Streams.stdout = "body.gold" - tr.Processes.Default.ReturnCode = 0 - -tr = Test.AddTestRun() -tr.Processes.Default.Command = ( - "grep -F ParentResultType::SPECIFIED trace.log | sed 's/^.*(next_hop) [^ ]* //' | sed 's/[.][0-9]*$$//'") -tr.Processes.Default.Streams.stdout = "trace.gold" -tr.Processes.Default.ReturnCode = 0 diff --git a/tests/gold_tests/next_hop/strategies_ch5/strategies_ch5.test.py b/tests/gold_tests/next_hop/strategies_ch5/strategies_ch5.test.py deleted file mode 100644 index fa4ad52948a..00000000000 --- a/tests/gold_tests/next_hop/strategies_ch5/strategies_ch5.test.py +++ /dev/null @@ -1,153 +0,0 @@ -''' -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -Test.Summary = ''' -Test next hop consistent hashing with 5 rings (MAX_GROUP_RINGS). -Validates mapWrapped and chashIter work at full ring capacity. -''' - -# Define and populate MicroServer. -# -server = Test.MakeOriginServer("server") -response_header = { - "headers": "HTTP/1.1 200 OK\r\n" - "Connection: close\r\n" - "Cache-control: max-age=85000\r\n" - "\r\n", - "timestamp": "1469733493.993", - "body": "This is the body.\n" -} -num_objects = 32 -for i in range(num_objects): - request_header = { - "headers": f"GET /obj{i} HTTP/1.1\r\n" - "Host: does.not.matter\r\n" - "\r\n", - "timestamp": "1469733493.993", - "body": "" - } - server.addResponse("sessionlog.json", request_header, response_header) - -dns = Test.MakeDNServer("dns") - -# 5 rings, 2 hosts each = 10 next hops. -# only the last ring will be started -# -num_rings = 5 -hosts_per_ring = 2 -num_hosts = num_rings * hosts_per_ring -ts_nh = [] -for ii in range(num_hosts): - ts = Test.MakeATSProcess(f"ts_nh{ii}") - ts.Disk.records_config.update( - { - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'http|dns', - 'proxy.config.dns.nameservers': f"127.0.0.1:{dns.Variables.Port}", - 'proxy.config.dns.resolv_conf': "NULL", - }) - line = f"map / http://127.0.0.1:{server.Variables.Port}" - if ii < (num_hosts - hosts_per_ring): - line += " @plugin=header_rewrite.so @pparam=hdr_rw.conf" - ts.Disk.remap_config.AddLine(line) - ts.Disk.MakeConfigFile("hdr_rw.conf").AddLine("set-status 502") - ts_nh.append(ts) - -ts = Test.MakeATSProcess("ts", use_traffic_out=False, command="traffic_server 2> trace.log") -ts.ReturnCode = Any(0, -2) - -ts.Disk.records_config.update( - { - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'http|dns|parent|next_hop|host_statuses|hostdb', - 'proxy.config.dns.nameservers': f"127.0.0.1:{dns.Variables.Port}", - 'proxy.config.dns.resolv_conf': "NULL", - 'proxy.config.http.cache.http': 0, - 'proxy.config.http.parent_proxy.per_parent_connect_attempts': 1, - 'proxy.config.http.uncacheable_requests_bypass_parent': 0, - 'proxy.config.http.no_dns_just_forward_to_parent': 1, - 'proxy.config.http.parent_proxy.mark_down_hostdb': 0, - 'proxy.config.http.down_server.cache_time': 1, - 'proxy.config.http.parent_proxy.self_detect': 0, - }) - -ts.Disk.File(ts.Variables.CONFIGDIR + "/strategies.yaml", id="strategies", typename="ats:config") -s = ts.Disk.strategies -s.AddLine("groups:") - -# Build 5 groups. -idx = 0 -for ring in range(num_rings): - s.AddLine(f" - &g{ring}") - for h in range(hosts_per_ring): - dns.addRecords(records={f"next_hop_{idx}": ["127.0.0.1"]}) - s.AddLine(f" - host: next_hop_{idx}") - s.AddLine(f" protocol:") - s.AddLine(f" - scheme: http") - s.AddLine(f" port: {ts_nh[idx].Variables.port}") - s.AddLine(f" weight: 1.0") - idx = idx + 1 - -strategy_lines = [ - "strategies:", - " - strategy: the-strategy", - " policy: consistent_hash", - " hash_key: path", - " go_direct: false", - " parent_is_proxy: true", - " ignore_self_detect: true", - " scheme: http", - " failover:", - " ring_mode: alternate_ring", - " max_simple_retries: 5", - " response_codes: [404]", - " max_unavailable_retries: 5", - " markdown_codes: [502]", - " groups:", -] -for ring in range(num_rings): - strategy_lines.append(f" - *g{ring}") -s.AddLines(strategy_lines) - -ts.Disk.remap_config.AddLine("map http://dummy.com http://not_used @strategy=the-strategy") - -# Only start ring 5 (last ring, indices 8-9). Forces fallover through all 5 rings. -tr = Test.AddTestRun() -ps = tr.Processes.Default -ps.StartBefore(server) -ps.StartBefore(dns) -#for idx in range((num_rings - 1) * hosts_per_ring, len(ts_nh)): -for idx in range(len(ts_nh)): - ps.StartBefore(ts_nh[idx]) -ps.StartBefore(Test.Processes.ts) -ps.Command = 'echo start TS, origin, DNS, and only last-ring next hops' -ps.ReturnCode = 0 - -# Send requests - must fall through rings 1-4 (down) to ring 5. -for i in range(num_objects): - tr = Test.AddTestRun() - tr.MakeCurlCommand(f'--verbose --proxy 127.0.0.1:{ts.Variables.port} http://dummy.com/obj{i}', ts=ts) - ps = tr.Processes.Default - ps.Streams.stdout.Content = Testers.ContainsExpression("This is the body.", "expected body") - ps.ReturnCode = 0 - -tr = Test.AddTestRun() -ps = tr.Processes.Default -ps.Command = ("grep -F ParentResultType::SPECIFIED trace.log | sed 's/^.*(next_hop) [^ ]* //' | sed 's/[.][0-9]*$$//'") -ps.Streams.stdout = "trace.gold" -ps.ReturnCode = 0 diff --git a/tests/gold_tests/next_hop/strategies_stale/strategies_stale.test.py b/tests/gold_tests/next_hop/strategies_stale/strategies_stale.test.py deleted file mode 100644 index b5d52ea3441..00000000000 --- a/tests/gold_tests/next_hop/strategies_stale/strategies_stale.test.py +++ /dev/null @@ -1,141 +0,0 @@ -''' -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -Test.Summary = ''' -Test next hop selection using strategies.yaml with consistent hashing. -''' - -# Define and populate MicroServer. -# -server = Test.MakeOriginServer("server") -response_header = { - "headers": "HTTP/1.1 200 OK\r\n" - "Connection: close\r\n" - "Cache-control: max-age=2\r\n" - "\r\n", - "timestamp": "1469733493.993", - "body": "This is the body.\n" -} - -request_header = { - "headers": - f"GET /obj0 HTTP/1.1\r\n" - "Host: does.not.matter\r\n" # But cannot be omitted. - "\r\n", - "timestamp": "1469733493.993", - "body": "" -} -server.addResponse("sessionlog.json", request_header, response_header) - -dns = Test.MakeDNServer("dns") - -# Define next hop trafficserver instances. -# -ts_nh = Test.MakeATSProcess(f"ts_nh0", use_traffic_out=False, command=f"traffic_server 2>nh_trace.log") -ts_nh.Disk.records_config.update( - { - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'http|dns', - 'proxy.config.dns.nameservers': f"127.0.0.1:{dns.Variables.Port}", - 'proxy.config.dns.resolv_conf': "NULL", - }) -ts_nh.Disk.remap_config.AddLine(f"map / http://127.0.0.1:{server.Variables.Port}") -ts_nh.ReturnCode = Any(0, -2) - -ts = Test.MakeATSProcess("ts") - -ts.Disk.records_config.update( - { - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'http|dns|parent|next_hop|host_statuses|hostdb', - 'proxy.config.dns.nameservers': f"127.0.0.1:{dns.Variables.Port}", # Only nameservers if resolv_conf NULL. - 'proxy.config.dns.resolv_conf': "NULL", # This defaults to /etc/resvolv.conf (OS namesevers) if not NULL. - 'proxy.config.http.cache.http': 1, - 'proxy.config.http.uncacheable_requests_bypass_parent': 0, - 'proxy.config.http.no_dns_just_forward_to_parent': 1, - 'proxy.config.http.parent_proxy.mark_down_hostdb': 0, - 'proxy.config.http.parent_proxy.self_detect': 0, - }) - -ts.Disk.File(ts.Variables.CONFIGDIR + "/strategies.yaml", id="strategies", typename="ats:config") -s = ts.Disk.strategies -s.AddLine("groups:") -s.AddLine(" - &g1") -dns.addRecords(records={"next_hop0": ["127.0.0.1"]}) -s.AddLine(f" - host: next_hop0") -s.AddLine(f" protocol:") -s.AddLine(f" - scheme: http") -s.AddLine(f" port: {ts_nh.Variables.port}") -# The health check URL does not seem to be used currently. -# s.AddLine(f" health_check_url: http://next_hop0:{ts_nh.Variables.port}") -s.AddLine(f" weight: 1.0") -s.AddLines( - [ - "strategies:", - " - strategy: the-strategy", - " policy: consistent_hash", - " hash_key: path", - " go_direct: false", - " parent_is_proxy: true", - " ignore_self_detect: true", - " groups:", - " - *g1", - " scheme: http", - ]) - -# Fallover not currently tested. -# -# s.AddLines([ -# " fallover:", -# " max_simple_retries: 2", -# " ring_mode: exhaust_ring", -# " response_codes:", -# " - 404", -# " health_check:", -# " - passive",]) - -ts.Disk.remap_config.AddLine("map http://dummy.com http://not_used @strategy=the-strategy") - -tr = Test.AddTestRun() -tr.Processes.Default.StartBefore(server) -tr.Processes.Default.StartBefore(dns) -tr.Processes.Default.StartBefore(ts_nh) -tr.Processes.Default.StartBefore(Test.Processes.ts) -tr.Processes.Default.Command = 'echo start TS, HTTP server, DNS server and next hop TSes' -tr.Processes.Default.ReturnCode = 0 - -tr = Test.AddTestRun() -tr.MakeCurlCommand(f'--verbose --proxy 127.0.0.1:{ts.Variables.port} http://dummy.com/obj0', ts=ts) -tr.Processes.Default.Streams.stdout = "body.gold" -tr.Processes.Default.ReturnCode = 0 - -# Wait enough time that the item should be aged out of the cache. -tr = Test.AddTestRun("Wait for cached object to be stale.") -tr.Processes.Default.Command = "sleep 4" -tr.StillRunningAfter = ts - -# Request should come back as 200 -tr = Test.AddTestRun() -tr.MakeCurlCommand(f'--verbose --proxy 127.0.0.1:{ts.Variables.port} http://dummy.com/obj0', ts=ts) -tr.Processes.Default.Streams.stdout = "body.gold" -tr.Processes.Default.ReturnCode = 0 - -tr = Test.AddTestRun() -tr.Processes.Default.Command = "grep 'Stale in cache' nh_trace*.log | cat" -tr.Processes.Default.Streams.stdout.Content = Testers.ContainsExpression("Stale in cache", "Expected stale content to be evaluated") -tr.Processes.Default.ReturnCode = 0 diff --git a/tests/gold_tests/next_hop/zzz_strategies_peer/zzz_strategies_peer.test.py b/tests/gold_tests/next_hop/zzz_strategies_peer/zzz_strategies_peer.test.py deleted file mode 100644 index 69384861c1b..00000000000 --- a/tests/gold_tests/next_hop/zzz_strategies_peer/zzz_strategies_peer.test.py +++ /dev/null @@ -1,177 +0,0 @@ -''' -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -Test.Summary = ''' -Test next hop selection using strategies.yaml with consistent hashing, with peering. -''' - -# This test must run after tls_conn_timeout and is listed in tests/serial_tests.txt -# to preserve that ordering. - -# Define and populate MicroServer. -# -server = Test.MakeOriginServer("server") -response_header = { - "headers": "HTTP/1.1 200 OK\r\n" - "Connection: close\r\n" - "Cache-control: max-age=85000\r\n" - "\r\n", - "timestamp": "1469733493.993", - "body": "This is the body.\n" -} -num_object = 16 -for i in range(num_object): - request_header = { - "headers": - f"GET /obj{i} HTTP/1.1\r\n" - "Host: does.not.matter\r\n" # But cannot be omitted. - "\r\n", - "timestamp": "1469733493.993", - "body": "" - } - server.addResponse("sessionlog.json", request_header, response_header) - -dns = Test.MakeDNServer("dns") - -# Define upstream trafficserver instances. -# -num_upstream = 6 -ts_upstream = [] -for i in range(num_upstream): - ts = Test.MakeATSProcess(f"ts_upstream{i}") - dns.addRecords(records={f"ts_upstream{i}": ["127.0.0.1"]}) - ts.Disk.records_config.update( - { - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'http|dns', - 'proxy.config.dns.nameservers': f"127.0.0.1:{dns.Variables.Port}", - 'proxy.config.dns.resolv_conf': "NULL", - }) - ts.Disk.remap_config.AddLine(f"map / http://127.0.0.1:{server.Variables.Port}") - ts_upstream.append(ts) - -# Define peer trafficserver instances. -# -num_peer = 8 -ts_peer = [] -for i in range(num_peer): - ts = Test.MakeATSProcess(f"ts_peer{i}", use_traffic_out=False, command=f"traffic_server 2> trace_peer{i}.log") - ts.ReturnCode = Any(0, -2) - ts_peer.append(ts) -for i in range(num_peer): - ts = ts_peer[i] - dns.addRecords(records={f"ts_peer{i}": ["127.0.0.1"]}) - - ts.Disk.records_config.update( - { - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'http|dns|parent|next_hop|host_statuses|hostdb|cachekey', - 'proxy.config.dns.nameservers': f"127.0.0.1:{dns.Variables.Port}", # Only nameservers if resolv_conf NULL. - 'proxy.config.dns.resolv_conf': "NULL", # This defaults to /etc/resvolv.conf (OS namesevers) if not NULL. - 'proxy.config.http.cache.http': 1, - 'proxy.config.http.cache.required_headers': 0, - 'proxy.config.http.uncacheable_requests_bypass_parent': 0, - 'proxy.config.http.no_dns_just_forward_to_parent': 1, - 'proxy.config.http.parent_proxy.mark_down_hostdb': 0, - 'proxy.config.http.parent_proxy.self_detect': 1, - }) - - ts.Disk.File(ts.Variables.CONFIGDIR + "/strategies.yaml", id="strategies", typename="ats:config") - s = ts.Disk.strategies - s.AddLine("groups:") - s.AddLine(" - &peer_group") - for j in range(num_peer): - s.AddLine(f" - host: ts_peer{j}") - s.AddLine(f" protocol:") - s.AddLine(f" - scheme: http") - s.AddLine(f" port: {ts_peer[j].Variables.port}") - # The health check URL does not seem to be used currently. - # s.AddLine(f" health_check_url: http://ts_peer{j}:{ts_peer[j].Variables.port}") - s.AddLine(f" weight: 1.0") - s.AddLine(" - &peer_upstream") - for j in range(num_upstream): - s.AddLine(f" - host: ts_upstream{j}") - s.AddLine(f" protocol:") - s.AddLine(f" - scheme: http") - s.AddLine(f" port: {ts_upstream[j].Variables.port}") - # The health check URL does not seem to be used currently. - # s.AddLine(f" health_check_url: http://ts_upstream{j}:{ts_upstream[j].Variables.port}") - s.AddLine(f" weight: 1.0") - s.AddLines( - [ - "strategies:", - " - strategy: the-strategy", - " policy: consistent_hash", - " hash_key: cache_key", - " go_direct: false", - " parent_is_proxy: true", - " cache_peer_result: false", - " ignore_self_detect: false", - " groups:", - " - *peer_group", - " - *peer_upstream", - " scheme: http", - " failover:", - " ring_mode: peering_ring", - f" self: ts_peer{i}", - #" max_simple_retries: 2", - #" response_codes:", - #" - 404", - #" health_check:", - #" - passive", - ]) - - suffix = " @strategy=the-strategy @plugin=cachekey.so @pparam=--uri-type=remap @pparam=--capture-prefix=/(.*):(.*)/$1/" - ts.Disk.remap_config.AddLines( - [ - "map http://dummy.com http://not_used" + suffix, - "map http://not_used http://also_not_used" + suffix, - ]) - -tr = Test.AddTestRun() -tr.Processes.Default.StartBefore(server) -tr.Processes.Default.StartBefore(dns) -for i in range(num_peer): - tr.Processes.Default.StartBefore(ts_peer[i]) -for i in range(num_upstream): - tr.Processes.Default.StartBefore(ts_upstream[i]) -tr.Processes.Default.Command = 'echo start peer and upstream TSes, HTTP server and DNS server' -tr.Processes.Default.ReturnCode = 0 - -for i in range(num_object): - tr = Test.AddTestRun() - tr.MakeCurlCommand( - f'--verbose --proxy 127.0.0.1:{ts_peer[i % num_peer].Variables.port} http://dummy.com/obj{i}', ts=ts_peer[i % num_peer]) - tr.Processes.Default.Streams.stdout = "body.gold" - tr.Processes.Default.ReturnCode = 0 - -for i in range(num_object): - tr = Test.AddTestRun() - # num_peer must not be a multiple of 3 - tr.MakeCurlCommand( - f'--verbose --proxy 127.0.0.1:{ts_peer[(i * 3) % num_peer].Variables.port} http://dummy.com/obj{i}', - ts=ts_peer[(i * 3) % num_peer]) - tr.Processes.Default.Streams.stdout = "body.gold" - tr.Processes.Default.ReturnCode = 0 - -tr = Test.AddTestRun() -tr.Processes.Default.Command = ( - "grep -e '^+++' -e '^[A-Z].*TTP/' -e '^.alts. --' -e 'ParentResultType::SPECIFIED' trace_peer*.log" - " | sed 's/^.*(next_hop) [^ ]* //' | sed 's/[.][0-9]*$$//'") -tr.Processes.Default.Streams.stdout = "trace.gold" -tr.Processes.Default.ReturnCode = 0 diff --git a/tests/gold_tests/next_hop/zzz_strategies_peer2/zzz_strategies_peer2.test.py b/tests/gold_tests/next_hop/zzz_strategies_peer2/zzz_strategies_peer2.test.py deleted file mode 100644 index 82fa93e9b85..00000000000 --- a/tests/gold_tests/next_hop/zzz_strategies_peer2/zzz_strategies_peer2.test.py +++ /dev/null @@ -1,169 +0,0 @@ -''' -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -Test.Summary = ''' -Test next hop using strategies.yaml with consistent hashing, with peering, and no upstream group" -''' - -# This test must run after tls_conn_timeout and is listed in tests/serial_tests.txt -# to preserve that ordering. - -# Define and populate MicroServer. -# -server = Test.MakeOriginServer("server") -response_header = { - "headers": "HTTP/1.1 200 OK\r\n" - "Connection: close\r\n" - "Cache-control: max-age=85000\r\n" - "\r\n", - "timestamp": "1469733493.993", - "body": "This is the body.\n" -} -num_object = 16 -for i in range(num_object): - request_header = { - "headers": - f"GET /obj{i} HTTP/1.1\r\n" - "Host: does.not.matter\r\n" # But cannot be omitted. - "\r\n", - "timestamp": "1469733493.993", - "body": "" - } - server.addResponse("sessionlog.json", request_header, response_header) - -dns = Test.MakeDNServer("dns") - -# Define upstream trafficserver instances. -# -num_upstream = 6 -ts_upstream = [] -for i in range(num_upstream): - ts = Test.MakeATSProcess(f"ts_upstream{i}") - dns.addRecords(records={f"ts_upstream{i}": ["127.0.0.1"]}) - ts.Disk.records_config.update( - { - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'http|dns', - 'proxy.config.dns.nameservers': f"127.0.0.1:{dns.Variables.Port}", - 'proxy.config.dns.resolv_conf': "NULL", - }) - ts.Disk.remap_config.AddLine(f"map / http://127.0.0.1:{server.Variables.Port}") - ts_upstream.append(ts) - -# Define peer trafficserver instances. -# -num_peer = 8 -ts_peer = [] -for i in range(num_peer): - ts = Test.MakeATSProcess(f"ts_peer{i}", use_traffic_out=False, command=f"traffic_server 2> trace_peer{i}.log") - ts.ReturnCode = Any(0, -2) - ts_peer.append(ts) -for i in range(num_peer): - ts = ts_peer[i] - dns.addRecords(records={f"ts_peer{i}": ["127.0.0.1"]}) - - ts.Disk.records_config.update( - { - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'http|dns|parent|next_hop|host_statuses|hostdb', - 'proxy.config.dns.nameservers': f"127.0.0.1:{dns.Variables.Port}", # Only nameservers if resolv_conf NULL. - 'proxy.config.dns.resolv_conf': "NULL", # This defaults to /etc/resvolv.conf (OS namesevers) if not NULL. - 'proxy.config.http.cache.http': 1, - 'proxy.config.http.cache.required_headers': 0, - 'proxy.config.http.uncacheable_requests_bypass_parent': 0, - 'proxy.config.http.no_dns_just_forward_to_parent': 0, - 'proxy.config.http.parent_proxy.mark_down_hostdb': 0, - 'proxy.config.http.parent_proxy.self_detect': 1, - }) - - ts.Disk.File(ts.Variables.CONFIGDIR + "/strategies.yaml", id="strategies", typename="ats:config") - s = ts.Disk.strategies - s.AddLine("groups:") - s.AddLine(" - &peer_group") - for j in range(num_peer): - s.AddLine(f" - host: ts_peer{j}") - s.AddLine(f" protocol:") - s.AddLine(f" - scheme: http") - s.AddLine(f" port: {ts_peer[j].Variables.port}") - # The health check URL does not seem to be used currently. - # s.AddLine(f" health_check_url: http://ts_peer{j}:{ts_peer[j].Variables.port}") - s.AddLine(f" weight: 1.0") - s.AddLines( - [ - "strategies:", - " - strategy: the-strategy", - " policy: consistent_hash", - " hash_key: path", - " go_direct: true", - " parent_is_proxy: true", - " cache_peer_result: false", - " ignore_self_detect: false", - " groups:", - " - *peer_group", - " scheme: http", - " failover:", - " ring_mode: peering_ring", - f" self: ts_peer{i}", - #" max_simple_retries: 2", - #" response_codes:", - #" - 404", - #" health_check:", - #" - passive", - ]) - - for i in range(num_upstream): - prefix = f"http://ts_upstream{i}:{ts_upstream[i].Variables.port}/" - ts.Disk.remap_config.AddLine(f"map {prefix} {prefix} @strategy=the-strategy") - -tr = Test.AddTestRun() -tr.Processes.Default.StartBefore(server) -tr.Processes.Default.StartBefore(dns) -for i in range(num_peer): - tr.Processes.Default.StartBefore(ts_peer[i]) -for i in range(num_upstream): - tr.Processes.Default.StartBefore(ts_upstream[i]) -tr.Processes.Default.Command = 'echo start peer and upstream TSes, HTTP server and DNS server' -tr.Processes.Default.ReturnCode = 0 - -for i in range(num_object): - tr = Test.AddTestRun() - tr.MakeCurlCommand( - f'--verbose --proxy 127.0.0.1:{ts_peer[i % num_peer].Variables.port} http://ts_upstream0:{ts_upstream[0].Variables.port}/obj{i}', - ts=ts_peer[i % num_peer]) - tr.Processes.Default.Streams.stdout = "body.gold" - tr.Processes.Default.ReturnCode = 0 - -for i in range(num_object): - tr = Test.AddTestRun() - # num_peer must not be a multiple of 3 - tr.MakeCurlCommand( - f'--verbose --proxy 127.0.0.1:{ts_peer[(i * 3) % num_peer].Variables.port} http://ts_upstream0:{ts_upstream[0].Variables.port}/obj{i}', - ts=ts_peer[(i * 3) % num_peer]) - tr.Processes.Default.Streams.stdout = "body.gold" - tr.Processes.Default.ReturnCode = 0 - -normalize_ports = "" -for i in range(num_upstream): - normalize_ports += f" | sed 's/:{ts_upstream[i].Variables.port}/:UP_PORT{i}/'" - -tr = Test.AddTestRun() -tr.Processes.Default.Command = ( - "grep -e '^+++' -e '^[A-Z].*TTP/' -e '^.alts. --' -e 'ParentResultType::SPECIFIED' trace_peer*.log" - " | sed 's/^.*(next_hop) [^ ]* //' | sed 's/[.][0-9]*$$//' " + normalize_ports) -tr.Processes.Default.Streams.stdout = "trace.gold" -tr.Processes.Default.ReturnCode = 0 diff --git a/tests/gold_tests/null_transform/null_transform.test.py b/tests/gold_tests/null_transform/null_transform.test.py deleted file mode 100644 index ea1df92abef..00000000000 --- a/tests/gold_tests/null_transform/null_transform.test.py +++ /dev/null @@ -1,64 +0,0 @@ -''' -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -Test.Summary = ''' -Test a basic null transform plugin -''' - -Test.SkipUnless(Condition.PluginExists('null_transform.so')) - -Test.ContinueOnFail = True - -# Define default ATS -ts = Test.MakeATSProcess("ts") -server = Test.MakeOriginServer("server") - -Test.testName = "" -request_header = {"headers": "GET / HTTP/1.1\r\nHost: www.example.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -# Expected response from origin server -response_header = { - "headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", - "timestamp": "1469733493.993", - "body": "This is expected response." -} - -# Add response the server dictionary -server.addResponse("sessionfile.log", request_header, response_header) -ts.Disk.records_config.update( - { - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.show_location': 0, - 'proxy.config.diags.debug.tags': 'null_transform' - }) -ts.Disk.remap_config.AddLine('map http://www.example.com http://127.0.0.1:{0}'.format(server.Variables.Port)) - -# Load plugin -Test.PrepareInstalledPlugin('null_transform.so', ts) - -# www.example.com Host -tr = Test.AddTestRun() -tr.MakeCurlCommand( - '--proxy 127.0.0.1:{0} "http://www.example.com" --verbose -H "Proxy-Connection: keep-alive"'.format(ts.Variables.port), ts=ts) -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.StartBefore(server) -tr.Processes.Default.StartBefore(Test.Processes.ts) -tr.Processes.Default.Streams.stderr = "gold/null_transform-200.gold" -tr.StillRunningAfter = server - -# Check Plugin Loading Information -ts.Disk.traffic_out.Content = "gold/null_transform-tag.gold" diff --git a/tests/gold_tests/origin_connection/per_server_connection_max.test.py b/tests/gold_tests/origin_connection/per_server_connection_max.test.py deleted file mode 100644 index e7bad788ab3..00000000000 --- a/tests/gold_tests/origin_connection/per_server_connection_max.test.py +++ /dev/null @@ -1,184 +0,0 @@ -''' -Verify the behavior of proxy.config.http.per_server.connection.max. -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -Test.Summary = __doc__ -import os - -Test.SkipIf(Condition.CurlUsingUnixDomainSocket()) - - -class PerServerConnectionMaxTest: - """Define an object to test our max origin connection behavior.""" - - _replay_file: str = 'slow_servers.replay.yaml' - _origin_max_connections: int = 3 - - def __init__(self) -> None: - """Configure the test processes in preparation for the TestRun.""" - self._configure_dns() - self._configure_server() - self._configure_trafficserver() - - def _configure_dns(self) -> None: - """Configure a nameserver for the test.""" - self._dns = Test.MakeDNServer("dns", default='127.0.0.1') - - def _configure_server(self) -> None: - """Configure the server to be used in the test.""" - self._server = Test.MakeVerifierServerProcess('server', self._replay_file) - - def _configure_trafficserver(self) -> None: - """Configure Traffic Server to be used in the test.""" - self._ts = Test.MakeATSProcess("ts1") - self._ts.Disk.remap_config.AddLine(f'map / http://127.0.0.1:{self._server.Variables.http_port}') - self._ts.Disk.records_config.update( - { - 'proxy.config.dns.nameservers': f"127.0.0.1:{self._dns.Variables.Port}", - 'proxy.config.dns.resolv_conf': 'NULL', - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'http|conn_track', - 'proxy.config.http.per_server.connection.max': self._origin_max_connections, - 'proxy.config.http.per_server.connection.metric_enabled': 1, - 'proxy.config.http.per_server.connection.metric_prefix': 'foo', - 'proxy.config.http.per_server.connection.match': 'port', - }) - self._ts.Disk.diags_log.Content += Testers.ContainsExpression( - f'WARNING:.*too many connections:.*limit={self._origin_max_connections}', - 'Verify the user is warned about the connection limit being hit.') - - def _test_metrics(self) -> None: - """Use traffic_ctl to test metrics.""" - tr = Test.AddTestRun("Check connection metrics") - tr.Processes.Default.Command = 'traffic_ctl metric match per_server' - tr.Processes.Default.ReturnCode = 0 - tr.Processes.Default.Env = self._ts.Env - tr.Processes.Default.Streams.All = Testers.ContainsExpression( - f'per_server.total_connection.foo.127.0.0.1:{self._server.Variables.http_port} 4', - 'incorrect statistic return, or possible error.') - tr.Processes.Default.Streams.All = Testers.ContainsExpression( - f'per_server.blocked_connection.foo.127.0.0.1:{self._server.Variables.http_port} 1', - 'incorrect statistic return, or possible error.') - - def run(self) -> None: - """Configure the TestRun.""" - tr = Test.AddTestRun('Verify we enforce proxy.config.http.per_server.connection.max') - tr.Processes.Default.StartBefore(self._dns) - tr.Processes.Default.StartBefore(self._server) - tr.Processes.Default.StartBefore(self._ts) - - tr.AddVerifierClientProcess('client', self._replay_file, http_ports=[self._ts.Variables.port]) - - self._test_metrics() - - -class ConnectMethodTest: - """Test our max origin connection behavior with CONNECT traffic.""" - - _process_counter: int = 0 - _client_counter: int = 0 - - def __init__(self, max_conn) -> None: - """Configure the server processes in preparation for the TestRun.""" - self._configure_dns() - self._configure_origin_server() - self._configure_trafficserver(max_conn) - ConnectMethodTest._process_counter += 1 - - def _configure_dns(self) -> None: - """Configure a nameserver for the test.""" - self._dns = Test.MakeDNServer(f"dns_{ConnectMethodTest._process_counter}", default='127.0.0.1') - - def _configure_origin_server(self) -> None: - """Configure the httpbin origin server.""" - self._server = Test.MakeHttpBinServer(f"server_{ConnectMethodTest._process_counter}") - - def _configure_trafficserver(self, max_conn) -> None: - self._ts = Test.MakeATSProcess("ts2_" + str(max_conn)) - - self._ts.Disk.records_config.update( - { - 'proxy.config.dns.nameservers': f"127.0.0.1:{self._dns.Variables.Port}", - 'proxy.config.dns.resolv_conf': 'NULL', - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'http|dns|hostdb|conn_track', - 'proxy.config.http.server_ports': f"{self._ts.Variables.port} {self._ts.Variables.uds_path}", - 'proxy.config.http.connect_ports': f"{self._server.Variables.Port}", - 'proxy.config.http.per_server.connection.metric_enabled': 1, - 'proxy.config.http.per_server.connection.max': max_conn, - }) - - self._ts.Disk.remap_config.AddLines([ - f"map http://foo.com/ http://www.this.origin.com:{self._server.Variables.Port}/", - ]) - self._ts.addPrivateConnectAllowYaml() - - def _configure_client_with_slow_response(self, tr) -> 'Test.Process': - """Configure a client to perform a CONNECT request with a slow response from the server.""" - p = tr.Processes.Process(f'slow_client_{ConnectMethodTest._client_counter}') - ConnectMethodTest._client_counter += 1 - tr.MakeCurlCommand(f"-v --fail -s -p -x 127.0.0.1:{self._ts.Variables.port} 'http://foo.com/delay/2'", p=p, ts=self._ts) - return p - - def _test_metrics(self, blocked) -> None: - """Use traffic_ctl to test metrics.""" - tr = Test.AddTestRun("Check connection metrics") - tr.Processes.Default.Command = 'traffic_ctl metric match per_server; sleep 2' - tr.Processes.Default.ReturnCode = 0 - tr.Processes.Default.Env = self._ts.Env - tr.Processes.Default.Streams.All = Testers.ContainsExpression( - f'per_server.total_connection.www.this.origin.com.127.0.0.1:{self._server.Variables.Port} 5', - 'incorrect statistic return, or possible error.') - tr.Processes.Default.Streams.All = Testers.ContainsExpression( - f'per_server.blocked_connection.www.this.origin.com.127.0.0.1:{self._server.Variables.Port} {blocked}', - 'incorrect statistic return, or possible error.') - - def run(self, blocked, gold_file) -> None: - """Verify per_server.connection.max with CONNECT traffic.""" - tr = Test.AddTestRun() - tr.Processes.Default.StartBefore(self._dns) - tr.Processes.Default.StartBefore(self._server) - tr.Processes.Default.StartBefore(self._ts) - - slow0 = self._configure_client_with_slow_response(tr) - slow1 = self._configure_client_with_slow_response(tr) - slow2 = self._configure_client_with_slow_response(tr) - - tr.Processes.Default.StartBefore(slow0) - tr.Processes.Default.StartBefore(slow1) - tr.Processes.Default.StartBefore(slow2) - - # With those three slow transactions going on in the background, do a - # couple quick transactions and make sure they both reply with a 503 - # response. - tr.MakeCurlCommandMulti( - f"sleep 1; {{curl}} -v --fail -s -p -x 127.0.0.1:{self._ts.Variables.port} 'http://foo.com/get'" - f"--next -v --fail -s -p -x 127.0.0.1:{self._ts.Variables.port} 'http://foo.com/get'", - ts=self._ts) - # Curl will have a 22 exit code if it receives a 5XX response (and we - # expect a 503). - tr.Processes.Default.ReturnCode = 22 if blocked else 0 - tr.Processes.Default.Streams.stderr = gold_file - tr.Processes.Default.TimeOut = 3 - - self._test_metrics(blocked) - - -PerServerConnectionMaxTest().run() -ConnectMethodTest(3).run(blocked=2, gold_file="gold/two_503_congested.gold") -ConnectMethodTest(0).run(blocked=0, gold_file="gold/two_200_ok.gold") diff --git a/tests/gold_tests/origin_connection/per_server_metric_enabled.test.py b/tests/gold_tests/origin_connection/per_server_metric_enabled.test.py deleted file mode 100644 index 510265c3df9..00000000000 --- a/tests/gold_tests/origin_connection/per_server_metric_enabled.test.py +++ /dev/null @@ -1,113 +0,0 @@ -''' -Verify per_server connection tracking stays accurate when -proxy.config.http.per_server.connection.metric_enabled is set. -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -Test.Summary = __doc__ - -Test.SkipIf(Condition.CurlUsingUnixDomainSocket()) - - -class PerServerMetricEnabledTest: - """Verify that enabling the per_server metrics does not break the group connection count. - - Enabling proxy.config.http.per_server.connection.metric_enabled used to - make the metric the authoritative connection count, leaving the group's - internal counter at zero. That made every pooled origin session look like - it was at or below proxy.config.http.per_server.connection.min, so - keep-alive origin connections were never reaped on inactivity timeout. - """ - - _replay_file: str = 'per_server_metric_enabled.replay.yaml' - _keep_alive_timeout: int = 2 - - def __init__(self) -> None: - """Configure the test processes in preparation for the TestRun.""" - self._configure_server() - self._configure_trafficserver() - - def _configure_server(self) -> None: - """Configure the origin server to be used in the test.""" - self._server = Test.MakeVerifierServerProcess('metric_enabled_server', self._replay_file) - - def _configure_trafficserver(self) -> None: - """Configure Traffic Server to be used in the test.""" - self._ts = Test.MakeATSProcess("ts_metric_enabled") - self._ts.Disk.remap_config.AddLine(f'map / http://127.0.0.1:{self._server.Variables.http_port}') - self._ts.Disk.records_config.update( - { - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'http_ss|conn_track', - 'proxy.config.http.per_server.connection.metric_enabled': 1, - 'proxy.config.http.per_server.connection.metric_prefix': 'bar', - 'proxy.config.http.per_server.connection.match': 'port', - # No minimum number of keep alive origin connections: the pooled - # connection should be closed once it times out. - 'proxy.config.http.per_server.connection.min': 0, - 'proxy.config.http.keep_alive_no_activity_timeout_out': self._keep_alive_timeout, - 'proxy.config.http.server_session_sharing.pool': 'global', - }) - # The connection count should never be decremented below zero. - self._ts.Disk.diags_log.Content += Testers.ExcludesExpression( - 'Number of tracked connections should be greater than or equal to zero', - 'Verify the group connection count is not double decremented.') - - def _test_connection_is_reaped(self) -> None: - """Verify the idle origin connection is closed once it times out.""" - tr = Test.AddTestRun("Verify the idle keep-alive origin connection is reaped") - tr.Processes.Default.Command = ( - f'sleep {self._keep_alive_timeout * 3}; ' - 'traffic_ctl metric get proxy.process.http.current_server_connections; ' - 'traffic_ctl metric match per_server') - tr.Processes.Default.ReturnCode = 0 - tr.Processes.Default.Env = self._ts.Env - tr.Processes.Default.Streams.All += Testers.ContainsExpression( - 'proxy.process.http.current_server_connections 0', - 'The idle origin connection should have been closed by the keep-alive timeout.') - tr.Processes.Default.Streams.All += Testers.ContainsExpression( - f'per_server.current_connection.bar.127.0.0.1:{self._server.Variables.http_port} 0', - 'The per_server connection gauge should have been decremented back to zero.') - tr.Processes.Default.Streams.All += Testers.ContainsExpression( - f'per_server.total_connection.bar.127.0.0.1:{self._server.Variables.http_port} 1', - 'A single origin connection should have been tracked.') - - def _test_tracker_info(self) -> None: - """Verify the JSONRPC connection tracker report agrees with the metrics.""" - tr = Test.AddTestRun("Verify the connection tracker report") - tr.Processes.Default.Command = "traffic_ctl rpc invoke get_connection_tracker_info -p 'table: outbound' -f json" - tr.Processes.Default.ReturnCode = 0 - tr.Processes.Default.Env = self._ts.Env - # Once the connection is released the group count drops to zero and the - # group is removed from the table, so either the table is empty or the - # remaining group reports no current connections. - tr.Processes.Default.Streams.All += Testers.ContainsExpression( - r'"(count|current)":\s*"?0"?', 'The tracker should report no current outbound connections.') - - def run(self) -> None: - """Configure the TestRuns.""" - tr = Test.AddTestRun('Perform a transaction that leaves a pooled origin connection') - tr.Processes.Default.StartBefore(self._server) - tr.Processes.Default.StartBefore(self._ts) - - tr.AddVerifierClientProcess('metric_enabled_client', self._replay_file, http_ports=[self._ts.Variables.port]) - - self._test_connection_is_reaped() - self._test_tracker_info() - - -PerServerMetricEnabledTest().run() diff --git a/tests/gold_tests/parent_config/parent_config_reload.test.py b/tests/gold_tests/parent_config/parent_config_reload.test.py deleted file mode 100644 index 5e71a82bfbd..00000000000 --- a/tests/gold_tests/parent_config/parent_config_reload.test.py +++ /dev/null @@ -1,76 +0,0 @@ -''' -Test parent.config reload via ConfigRegistry. - -Verifies that: -1. parent.config reload works after file touch -2. Record value change (retry_time) triggers parent reload -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import os - -Test.Summary = ''' -Test parent.config reload via ConfigRegistry. -''' - -Test.ContinueOnFail = True - -ts = Test.MakeATSProcess("ts") -ts.Disk.records_config.update({ - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'parent_select|config', -}) - -# Initial parent.config with a simple rule -ts.Disk.parent_config.AddLine('dest_domain=example.com parent="origin.example.com:80"') - -config_dir = ts.Variables.CONFIGDIR - -# ================================================================ -# Test 1: Touch parent.config → reload → handler fires -# ================================================================ - -tr = Test.AddTestRun("Touch parent.config") -tr.Processes.Default.StartBefore(ts) -tr.Processes.Default.Command = f"sleep 3 && touch {os.path.join(config_dir, 'parent.config')} && sleep 1" -tr.Processes.Default.ReturnCode = 0 -tr.StillRunningAfter = ts - -tr = Test.AddConfigReload(ts, expect_tasks=["parent.config"], description="Reload after parent.config touch") -tr.StillRunningAfter = ts - -# ================================================================ -# Test 2: Change retry_time record value → triggers parent reload -# No file touch, no explicit config reload — the -# RecRegisterConfigUpdateCb fires automatically. -# ================================================================ - -tr = Test.AddTestRun("Change parent retry_time record value") -p = tr.Processes.Process("reload-2") -p.Command = ("traffic_ctl config set proxy.config.http.parent_proxy.retry_time 60; " - "sleep 30") -p.Env = ts.Env -p.ReturnCode = Any(0, -2) -# Wait for the 3rd "finished loading" -p.Ready = When.FileContains(ts.Disk.diags_log.Name, "parent.config finished loading", 3) -p.Timeout = 20 -tr.Processes.Default.StartBefore(p) -## TODO: we should have an extension like When.ReloadCompleted(token, success) to validate this instead of parsing -## diags. -tr.Processes.Default.Command = 'echo "waiting for parent.config reload after record change"' -tr.TimeOut = 25 -tr.StillRunningAfter = ts diff --git a/tests/gold_tests/parent_proxy/consistent_hash_ring_walk.test.py b/tests/gold_tests/parent_proxy/consistent_hash_ring_walk.test.py deleted file mode 100644 index a279c2c7c7a..00000000000 --- a/tests/gold_tests/parent_proxy/consistent_hash_ring_walk.test.py +++ /dev/null @@ -1,143 +0,0 @@ -""" -Verify ParentConsistentHash::selectParent() does not waste work walking an -all-down consistent_hash pool. - -A consistent-hash ring has 1024 replica nodes per parent (default -ATSConsistentHash replicas, DEFAULT_PARENT_WEIGHT=1.0), so num_parents*1024 ring -nodes total. When every parent is down, selectParent must walk the ring to -conclude so -- but it must read each parent's HostStatus (the global -host_status_rwlock) at most ONCE, not once per replica node. selectParent tracks -the distinct parents examined and stops as soon as all are rejected, so an -all-down selection over N parents costs exactly N getHostStatus calls -- not the -~2*N*1024 (two ring passes) the naive walk cost, which put ~8-13 ms of inline -ET_NET CPU per request and wedged the vipd health probe in inc-p1s2-260703. - -Proven by the per-selection "getHostStatus calls: " debug line -(ParentConsistentHash.cc): it must equal the parent count, not a five-figure ring -walk. - -Pool size > MAX_PARENTS (64) on purpose: num_parents is NOT capped at -MAX_PARENTS by the config parser, so the per-selection "seen parent" tracking -must be sized to the actual pool (a fixed [MAX_PARENTS] array would overflow). -Running a >64-parent pool to completion (no crash, correct 502, bounded count) -guards that sizing. - -no_dns_just_forward_to_parent=1 lets ATS skip origin resolution and go straight -to parent selection, so this test needs no DNS server and no origin -- every -parent is HostStatus-DOWN => PARENT_FAIL => 502 before any connect. -""" -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import os - -from ports import get_port - -Test.Summary = ''' -An all-down consistent_hash parent pool (marked down via HostStatus) drives -selectParent to read each distinct parent's HostStatus exactly once -(O(num_parents)), not once per ring replica node (~2 * num_parents * 1024). -''' -Test.ContinueOnFail = True - -# > MAX_PARENTS (64): exercises the pool-sized "seen parent" tracking and would -# overflow a fixed [MAX_PARENTS] array. Distinct names matter -- consistent_hash -# keys the ring on hostname, so identical names would collapse to one ring node. -NUM_PARENTS = 100 - - -class ParentDownRingWalkTest: - """All parents HostStatus-DOWN => selectParent reads each parent once => 502.""" - - parent_hostnames = [f'deadparent{i:03d}' for i in range(1, NUM_PARENTS + 1)] - - def __init__(self): - self._setupTS() - - def _setupTS(self): - self.ts = Test.MakeATSProcess('ts', enable_cache=False) - - # A dead (reserved-but-unbound) port per parent. They are never actually - # connected -- every parent is HostStatus-DOWN so selectParent returns - # PARENT_FAIL before any connect -- but parent.config needs a port. - self._parent_ports = [] - for i in range(len(self.parent_hostnames)): - name = f'dead_parent_port_{i}' - get_port(self.ts, name) - self._parent_ports.append(getattr(self.ts.Variables, name)) - - self.ts.Disk.records_config.update( - { - # Enable only the parent_select debug ctl so the per-selection - # "getHostStatus calls: " line is emitted. - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'parent_select', - # Skip origin DNS: forward straight to parent selection. No DNS - # server / origin needed for this isolation. - 'proxy.config.http.no_dns_just_forward_to_parent': 1, - # self_detect off: this test populates the HostStatus map via - # `traffic_ctl host down` below, not via self_detect (the - # deadparents never resolve to this box anyway). - 'proxy.config.http.parent_proxy.fail_threshold': 10, - 'proxy.config.http.parent_proxy.retry_time': 300, - 'proxy.config.http.parent_proxy.self_detect': 0, - 'proxy.config.url_remap.remap_required': 0, - }) - - # Consistent-hash pool of distinct parent hostnames, go_direct=false so an - # all-down pool yields 502 (not a direct-to-origin fallback). - self._parents = list(zip(self.parent_hostnames, self._parent_ports)) - parent_list = ', '.join(f'{host}:{port}|1' for host, port in self._parents) - self.ts.Disk.parent_config.AddLine( - f'dest_domain=. parent="{parent_list}" round_robin=consistent_hash ' - 'go_direct=false parent_is_proxy=true') - - # The all-down selection is a single findParent call. selectParent reads - # each distinct parent's HostStatus at most once and stops once all are - # rejected, so the per-selection "getHostStatus calls" count equals the - # parent count -- not the ~2*N*1024 five-figure ring walk it cost before. - self.ts.Disk.traffic_out.Content += Testers.ContainsExpression( - r'getHostStatus calls: %d\b' % NUM_PARENTS, - 'selectParent read each distinct parent once (O(num_parents)), not the full ring.') - # It must NOT take the HostStatus lock once per ring replica node (the old - # bug reported a five-figure count). - self.ts.Disk.traffic_out.Content += Testers.ExcludesExpression( - r'getHostStatus calls: [0-9]{5}', 'selectParent must not read HostStatus once per ring replica node.') - - def run(self): - traffic_ctl = os.path.join(self.ts.Variables.BINDIR, 'traffic_ctl') - - # 1. Bring the whole parent pool DOWN via HostStatus (one RPC call). - # Populates hosts_statuses (=> every getHostStatus takes the rwlock) - # and forces host_stat==DOWN (=> the full ring walk, no early exit). - down = Test.AddTestRun('Mark the entire parent pool down via HostStatus') - down.Processes.Default.StartBefore(self.ts) - down.Processes.Default.Command = f'{traffic_ctl} host down ' + ' '.join(self.parent_hostnames) - down.Processes.Default.Env = self.ts.Env - down.Processes.Default.ReturnCode = 0 - - # 2. One request through the all-down pool -> bounded walk -> 502. A crash - # here (e.g. seen-parent tracking overflowing on a >64 pool) fails the test. - load = Test.AddTestRun('Request through the all-down pool -> bounded selection -> 502') - load.MakeCurlCommand( - f'-s -o /dev/null -w "%{{http_code}}" --proxy 127.0.0.1:{self.ts.Variables.port} http://example.com/ring-walk-probe', - ts=self.ts) - load.Processes.Default.Streams.stdout = Testers.ContainsExpression('502', 'All parents down => 502.') - load.Processes.Default.ReturnCode = 0 - load.StillRunningAfter = self.ts - - -ParentDownRingWalkTest().run() diff --git a/tests/gold_tests/parent_proxy/parent-retry.test.py b/tests/gold_tests/parent_proxy/parent-retry.test.py deleted file mode 100644 index c5b582b1a21..00000000000 --- a/tests/gold_tests/parent_proxy/parent-retry.test.py +++ /dev/null @@ -1,47 +0,0 @@ -""" -Test parent_retry config settings -""" -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -Test.testName = "Test parent_retry settings" -Test.ContinueOnFail = True - - -class ParentRetryTest: - """ - Test loading parent.config with parent_retry setting enabled - """ - ts_parent_hostname = "localhost:8081" - - def __init__(self): - """Initialize the test.""" - self._configure_ts_child() - - def _configure_ts_child(self): - self.ts_child = Test.MakeATSProcess("ts_child") - self.ts_child.Disk.parent_config.AddLine( - f'dest_domain=. method=get parent="{self.ts_parent_hostname}" parent_retry=unavailable_server_retry unavailable_server_retry_responses="502,503"' - ) - - def run(self): - tr = Test.AddTestRun() - tr.Processes.Default.StartBefore(self.ts_child) - tr.MakeCurlCommand(f'"{self.ts_child.Variables.port}" --verbose', ts=self.ts_child) - tr.StillRunningAfter = self.ts_child - - -ParentRetryTest().run() diff --git a/tests/gold_tests/pipeline/delete_maxforwards_body_desync.test.py b/tests/gold_tests/pipeline/delete_maxforwards_body_desync.test.py deleted file mode 100644 index a7f0b4d221e..00000000000 --- a/tests/gold_tests/pipeline/delete_maxforwards_body_desync.test.py +++ /dev/null @@ -1,89 +0,0 @@ -'''Verify a DELETE self-answered from cache drains its request body.''' - -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -from ports import get_port -import sys - -Test.Summary = ''' -Verify that a DELETE (Max-Forwards: 0) answered from the cache drains its request -body instead of leaving it to be parsed as the next request on the keep-alive -connection. -''' - -Test.ContinueOnFail = False - - -class TestDeleteBodyDrain: - """A DELETE with Max-Forwards: 0 is answered directly from cache. If the - request body is not drained, its bytes are parsed as the next request on the - keep-alive connection (a CL.0 desync). This drives that path and asserts the - smuggled request never reaches the origin nor produces a second response.""" - - _server_script: str = 'desync_server.py' - _client_script: str = 'desync_client.py' - _hostname: str = 'www.example.com' - - def __init__(self) -> None: - tr = Test.AddTestRun('DELETE self-response must drain the request body.') - tr.TimeOut = 60 - self._configure_server(tr) - self._configure_traffic_server(tr) - self._configure_client(tr) - - def _configure_server(self, tr: 'TestRun') -> 'Process': - server = tr.Processes.Process('server') - tr.Setup.Copy(self._server_script) - port = get_port(server, 'http_port') - server.Command = f'{sys.executable} {self._server_script} 127.0.0.1 {port}' - server.Ready = When.PortOpenv4(port) - # The origin must serve the warmed path, and must never see the smuggled one. - server.Streams.All += Testers.ContainsExpression(r'ORIGIN_RECV path=\[/\]', 'origin should serve the warmed GET /') - server.Streams.All += Testers.ExcludesExpression('poisoned', 'the smuggled request must never reach the origin') - self._server = server - return server - - def _configure_traffic_server(self, tr: 'TestRun') -> 'Process': - ts = tr.MakeATSProcess('ts', enable_cache=True) - self._ts = ts - ts.Disk.remap_config.AddLine(f'map http://{self._hostname}/ http://127.0.0.1:{self._server.Variables.http_port}/') - ts.Disk.records_config.update( - { - 'proxy.config.diags.debug.enabled': 0, - 'proxy.config.diags.debug.tags': 'http', - 'proxy.config.http.cache.http': 1, - 'proxy.config.http.insert_age_in_response': 1, - }) - return ts - - def _configure_client(self, tr: 'TestRun') -> 'Process': - client = tr.Processes.Default - tr.Setup.Copy(self._client_script) - client.Command = f'{sys.executable} {self._client_script} 127.0.0.1 {self._ts.Variables.port} {self._hostname}' - client.ReturnCode = 0 - # The DELETE must self-answer 200 from a warm cache, or the hit path was - # never exercised and the test would pass vacuously. - client.Streams.All += Testers.ContainsExpression('DELETE_STATUS=200', 'the DELETE must self-answer 200 from a warm cache') - # The desync signatures: a second (smuggled) response, or smuggled bytes. - client.Streams.All += Testers.ExcludesExpression( - 'SECOND_RESPONSE_RECEIVED=True', 'the client must not receive a smuggled second response') - client.Streams.All += Testers.ExcludesExpression('poisoned', 'no smuggled response bytes should reach the client') - client.StartBefore(self._server) - client.StartBefore(self._ts) - - -TestDeleteBodyDrain() diff --git a/tests/gold_tests/pipeline/delete_maxfwd_noop_drain.test.py b/tests/gold_tests/pipeline/delete_maxfwd_noop_drain.test.py deleted file mode 100644 index 0c19b00aac5..00000000000 --- a/tests/gold_tests/pipeline/delete_maxfwd_noop_drain.test.py +++ /dev/null @@ -1,89 +0,0 @@ -'''Verify the cache-miss (INTERNAL_CACHE_NOOP) DELETE self-response drains its body.''' - -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -from ports import get_port -import sys - -Test.Summary = 'A DELETE to an uncached path self-answers 404 (NOOP) and must drain its body.' -Test.ContinueOnFail = False - - -class TestDeleteMaxfwdNoopDrain: - """A DELETE to an uncached path is self-answered 404 via INTERNAL_CACHE_NOOP. - - Verify the accompanying request body is drained so its bytes are not framed as - the next request on the connection. - """ - - _server_script: str = 'desync_server.py' - _client_script: str = 'desync_client_miss.py' - _hostname: str = 'www.example.com' - - def __init__(self) -> None: - tr = Test.AddTestRun('A NOOP-path DELETE self-response must drain its request body.') - tr.TimeOut = 40 - self._configure_server(tr) - self._configure_traffic_server(tr) - self._configure_client(tr) - - def _configure_server(self, tr: 'TestRun') -> 'Process': - """Configure the origin server. - - :param tr: The test run to associate the origin server with. - :return: The origin server process. - """ - server = tr.Processes.Process('server') - tr.Setup.Copy(self._server_script) - port = get_port(server, 'http_port') - server.Command = f'{sys.executable} {self._server_script} 127.0.0.1 {port}' - server.Ready = When.PortOpenv4(port) - server.Streams.All += Testers.ExcludesExpression('misspoison', 'the smuggled request must not reach the origin') - self._server = server - return server - - def _configure_traffic_server(self, tr: 'TestRun') -> 'Process': - """Configure ATS. - - :param tr: The test run to associate the ATS process with. - :return: The ATS process. - """ - ts = tr.MakeATSProcess('ts', enable_cache=True) - self._ts = ts - ts.Disk.remap_config.AddLine(f'map http://{self._hostname}/ http://127.0.0.1:{self._server.Variables.http_port}/') - ts.Disk.records_config.update({'proxy.config.http.cache.http': 1}) - return ts - - def _configure_client(self, tr: 'TestRun') -> 'Process': - """Configure the client. - - :param tr: The test run to associate the client process with. - :return: The client process. - """ - client = tr.Processes.Default - tr.Setup.Copy(self._client_script) - client.Command = f'{sys.executable} {self._client_script} 127.0.0.1 {self._ts.Variables.port} {self._hostname}' - client.ReturnCode = 0 - client.Streams.All += Testers.ContainsExpression( - 'DELETE_STATUS=404', 'the DELETE must hit the NOOP (miss) path, not a cache hit') - client.Streams.All += Testers.ExcludesExpression('SECOND_RESPONSE_RECEIVED=True', 'no smuggled second response') - client.Streams.All += Testers.ExcludesExpression('misspoison', 'no smuggled bytes should reach the client') - client.StartBefore(self._server) - client.StartBefore(self._ts) - - -TestDeleteMaxfwdNoopDrain() diff --git a/tests/gold_tests/pipeline/noop_keepalive.test.py b/tests/gold_tests/pipeline/noop_keepalive.test.py deleted file mode 100644 index 6d6ea2604de..00000000000 --- a/tests/gold_tests/pipeline/noop_keepalive.test.py +++ /dev/null @@ -1,92 +0,0 @@ -'''Non-idempotency audit: a NOOP self-response drains its body exactly once and keeps the connection alive.''' - -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -from ports import get_port -import sys - -Test.Summary = 'A cache-miss/NOOP DELETE self-response drains its body once and preserves keep-alive for the next request.' -Test.ContinueOnFail = False - - -class TestNoopKeepAlive: - """Audit that the NOOP self-response drain runs exactly once. - - On one keep-alive connection, a NOOP self-response (a DELETE to an uncached - path) with a benign body must drain that body exactly once; a following GET - must then still be served. A double-drain would consume into the next request - and close the connection. - """ - - _server_script: str = 'desync_server.py' - _client_script: str = 'noop_keepalive_client.py' - _hostname: str = 'www.example.com' - - def __init__(self) -> None: - tr = Test.AddTestRun('A NOOP self-response must drain the body exactly once (keep-alive preserved).') - tr.TimeOut = 40 - self._configure_server(tr) - self._configure_traffic_server(tr) - self._configure_client(tr) - - def _configure_server(self, tr: 'TestRun') -> 'Process': - """Configure the origin server. - - :param tr: The test run to associate the origin server with. - :return: The origin server process. - """ - server = tr.Processes.Process('server') - tr.Setup.Copy(self._server_script) - port = get_port(server, 'http_port') - server.Command = f'{sys.executable} {self._server_script} 127.0.0.1 {port}' - server.Ready = When.PortOpenv4(port) - self._server = server - return server - - def _configure_traffic_server(self, tr: 'TestRun') -> 'Process': - """Configure ATS. - - :param tr: The test run to associate the ATS process with. - :return: The ATS process. - """ - ts = tr.MakeATSProcess('ts', enable_cache=True) - self._ts = ts - ts.Disk.remap_config.AddLine(f'map http://{self._hostname}/ http://127.0.0.1:{self._server.Variables.http_port}/') - ts.Disk.records_config.update({'proxy.config.http.cache.http': 1}) - return ts - - def _configure_client(self, tr: 'TestRun') -> 'Process': - """Configure the client. - - :param tr: The test run to associate the client process with. - :return: The client process. - """ - client = tr.Processes.Default - tr.Setup.Copy(self._client_script) - client.Command = f'{sys.executable} {self._client_script} 127.0.0.1 {self._ts.Variables.port} {self._hostname}' - client.ReturnCode = 0 - client.Streams.All += Testers.ContainsExpression( - 'DELETE_STATUS=404', 'the DELETE to an uncached path must self-answer 404 (NOOP path)') - client.Streams.All += Testers.ContainsExpression( - 'SECOND_REQUEST_STATUS=200', 'the following GET / must be served (keep-alive preserved, drained exactly once)') - client.Streams.All += Testers.ContainsExpression( - 'KEEPALIVE_PRESERVED=yes', 'the connection must not be closed by a spurious double-drain') - client.StartBefore(self._server) - client.StartBefore(self._ts) - - -TestNoopKeepAlive() diff --git a/tests/gold_tests/pipeline/pipeline.test.py b/tests/gold_tests/pipeline/pipeline.test.py deleted file mode 100644 index 68217fd4cf9..00000000000 --- a/tests/gold_tests/pipeline/pipeline.test.py +++ /dev/null @@ -1,251 +0,0 @@ -'''Test a pipelined requests.''' - -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -from ports import get_port -import sys - -Test.Summary = '''Test pipelined requests and HTTP/1.1 request framing.''' - -IP_ALLOW_CONTENT = ''' -ip_allow: - - apply: in - ip_addrs: 0/0 - action: deny - methods: - - DELETE -''' - - -class TestPipelining: - """Verify that a set of pipelined requests is handled correctly.""" - - _client_script: str = 'pipeline_client.py' - _server_script: str = 'pipeline_server.py' - - _dns_counter: int = 0 - _server_counter: int = 0 - _ts_counter: int = 0 - - def __init__(self, buffer_requests: bool) -> None: - """Configure the TestRun. - - :param buffer_requests: Whether to configure ATS to buffer client requests. - """ - tr = Test.AddTestRun('Test a pipelined chunked encoded request.') - tr.TimeOut = 10 - self._configure_dns(tr) - self._configure_server(tr) - self._configure_traffic_server(tr, buffer_requests) - self._configure_client(tr) - - def _configure_dns(self, tr: 'TestRun') -> 'Process': - """Configure the DNS. - - :param tr: The test run to associate with the DNS process with. - :return: The DNS process. - """ - name = f'dns_{self._dns_counter}' - TestPipelining._dns_counter += 1 - dns = tr.MakeDNServer(name, default='127.0.0.1') - self._dns = dns - return dns - - def _configure_server(self, tr: 'TestRun') -> 'Process': - """Configure the origin server. - - :param tr: The test run to associate with the origin server with. - :return: The origin server process. - """ - name = f'server_{self._server_counter}' - TestPipelining._server_counter += 1 - server = tr.Processes.Process(name) - tr.Setup.Copy(self._server_script) - port = get_port(server, "http_port") - server.Command = f'{sys.executable} {self._server_script} 127.0.0.1 {port} ' - server.ReturnCode = 0 - server.Ready = When.PortOpenv4(port) - server.Streams.All += Testers.ContainsExpression('/first', 'Should receive the first request') - server.Streams.All += Testers.ContainsExpression('/second', 'Should receive the second request') - - # The third request should be denied due to the ip_allow.yaml rule. - server.Streams.All += Testers.ExcludesExpression('/third', 'Should not receive the third request') - self._server = server - return server - - def _configure_traffic_server(self, tr: 'TestRun', buffer_requests: bool) -> 'Process': - """Configure ATS. - - :param tr: The test run to associate with the ATS process with. - :param buffer_requests: Whether to enable request_buffer_enabled. - :return: The ATS process. - """ - name = f'ts_{self._ts_counter}' - TestPipelining._ts_counter += 1 - ts = tr.MakeATSProcess(name, enable_cache=False) - self._ts = ts - ts.Disk.remap_config.AddLine(f'map / http://backend.server.com:{self._server.Variables.http_port}') - ts.Disk.records_config.update( - { - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'http|ip_allow', - 'proxy.config.dns.nameservers': f'127.0.0.1:{self._dns.Variables.Port}', - 'proxy.config.dns.resolv_conf': 'NULL', - }) - if buffer_requests: - ts.Disk.records_config.update({ - 'proxy.config.http.request_buffer_enabled': 1, - }) - ts.Disk.ip_allow_yaml.AddLines(IP_ALLOW_CONTENT.split("\n")) - return ts - - def _configure_client(self, tr: 'TestRun') -> 'Process': - """Configure the client. - - :param tr: The test run to associate with the client process with. - :return: The client process. - """ - client = tr.Processes.Default - tr.Setup.Copy(self._client_script) - client.Command = (f'{sys.executable} {self._client_script} 127.0.0.1 {self._ts.Variables.port} ' - 'server.com server.com') - client.ReturnCode = 0 - client.Streams.All += Testers.ContainsExpression('X-Response: first', "Should receive the origin's first response.") - client.Streams.All += Testers.ContainsExpression('X-Response: second', "Should receive the origin's second response.") - client.Streams.All += Testers.ExcludesExpression('X-Response: third', "Should not receive the origin's third response.") - client.Streams.All += Testers.ContainsExpression('403', 'Should receive the ATS-generated rejection of the DELETE request.') - client.StartBefore(self._dns) - client.StartBefore(self._server) - client.StartBefore(self._ts) - - -class TestRequestFraming: - """Verify Traffic Server preserves HTTP/1.1 request framing. - - A body-less POST (Content-Length: 0) immediately followed by a second - pipelined request must be delivered to the origin as two independent - requests, so the second request cannot be folded into the first. A request - with conflicting Content-Length header fields must be rejected rather than - forwarded. - """ - - _client_script: str = 'request_framing_client.py' - _server_script: str = 'request_framing_server.py' - _counter: int = 0 - - def __init__(self, mode: str) -> None: - """Configure a test run for the given request mode. - - :param mode: 'pipeline' for a body-less POST followed by a pipelined - request, or 'conflicting_cl' for a request with conflicting - Content-Length header fields. - """ - self._mode = mode - self._name = f'framing_{mode}_{TestRequestFraming._counter}' - TestRequestFraming._counter += 1 - - description = { - 'pipeline': 'Test a body-less POST followed by a pipelined request.', - 'conflicting_cl': 'Test a request with conflicting Content-Length headers.', - }[mode] - tr = Test.AddTestRun(description) - tr.TimeOut = 20 - self._configure_server(tr) - self._configure_traffic_server(tr) - self._configure_client(tr) - - def _configure_server(self, tr: 'TestRun') -> 'Process': - """Configure the recording origin server.""" - server = tr.Processes.Process(f'origin_{self._name}') - tr.Setup.Copy(self._server_script) - http_port = get_port(server, "http_port") - server.Command = f'{sys.executable} {self._server_script} 127.0.0.1 {http_port} ' - server.ReturnCode = 0 - server.Ready = When.PortOpenv4(http_port) - - if self._mode == 'pipeline': - # The origin must see exactly the two requests the client sent, with - # their boundaries intact. If ATS folded them together, the origin - # would see a single request or the second request's bytes inside - # the POST body. - server.Streams.All += Testers.ContainsExpression( - r'REQUEST_LINE: POST / HTTP/1.1', 'Origin should receive the POST as its own request.') - server.Streams.All += Testers.ContainsExpression( - r'REQUEST_LINE: GET /second HTTP/1.1', 'Origin should receive the GET as its own request.') - server.Streams.All += Testers.ContainsExpression( - r'ORIGIN_REQUEST_COUNT: 2', 'Origin should receive the second request as a distinct request.') - server.Streams.All += Testers.ExcludesExpression( - r'ORIGIN_REQUEST_COUNT: 3', 'Origin should receive exactly two requests, no more.') - # The second request's bytes must never appear inside the first - # (POST) request's body. - server.Streams.All += Testers.ExcludesExpression( - r"BODY:.*GET /second", 'The GET request must not appear inside the POST body.') - server.Streams.All += Testers.ExcludesExpression( - r"BODY:.*X-Marker", 'The second request header must not appear in the POST body.') - else: - # An ambiguously-framed request must be rejected by ATS before it - # ever reaches the origin. - server.Streams.All += Testers.ExcludesExpression( - r'REQUEST_LINE:', 'Origin must not receive an ambiguously-framed request.') - self._server = server - return server - - def _configure_traffic_server(self, tr: 'TestRun') -> 'Process': - """Configure ATS as a reverse proxy in front of the origin.""" - ts = tr.MakeATSProcess(f'ts_{self._name}', enable_cache=False) - self._ts = ts - ts.Disk.remap_config.AddLine(f'map / http://127.0.0.1:{self._server.Variables.http_port}/') - ts.Disk.records_config.update({ - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'http', - }) - return ts - - def _configure_client(self, tr: 'TestRun') -> 'Process': - """Configure the client that sends the framed request.""" - client = tr.Processes.Default - tr.Setup.Copy(self._client_script) - client.Command = ( - f'{sys.executable} {self._client_script} 127.0.0.1 {self._ts.Variables.port} ' - f'www.example.com {self._mode}') - client.ReturnCode = 0 - if self._mode == 'pipeline': - # Two independent responses must come back, one per request. - client.Streams.All += Testers.ContainsExpression( - r'STATUS_LINE_COUNT: 2', 'Client should receive two independent responses.') - client.Streams.All += Testers.ContainsExpression( - r'X-Origin-Response: first', 'Client should receive the response to the POST.') - client.Streams.All += Testers.ContainsExpression( - r'X-Origin-Response: second', 'Client should receive the response to the GET.') - else: - # The conflicting Content-Length request must be rejected with a - # 400, exactly one response must come back, and the second request - # must never be answered. - client.Streams.All += Testers.ContainsExpression( - r'HTTP/1.1 400', 'Client should receive a 400 for the ambiguous request.') - client.Streams.All += Testers.ContainsExpression(r'STATUS_LINE_COUNT: 1', 'Client should receive exactly one response.') - client.Streams.All += Testers.ExcludesExpression( - r'X-Origin-Response: second', 'The second request must not be answered.') - client.StartBefore(self._server) - client.StartBefore(self._ts) - - -TestPipelining(buffer_requests=False) -TestPipelining(buffer_requests=True) - -TestRequestFraming(mode='pipeline') -TestRequestFraming(mode='conflicting_cl') diff --git a/tests/gold_tests/pluginTest/TSVConnFd/TSVConnFd.test.py b/tests/gold_tests/pluginTest/TSVConnFd/TSVConnFd.test.py deleted file mode 100644 index 3913d8fa343..00000000000 --- a/tests/gold_tests/pluginTest/TSVConnFd/TSVConnFd.test.py +++ /dev/null @@ -1,72 +0,0 @@ -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import os - -Test.Summary = ''' -Test TSVConnFdCreate() TS API call. -''' - -plugin_name = "TSVConnFd" - -server = Test.MakeOriginServer("server") - -request_header = {"headers": "GET / HTTP/1.1\r\nHost: doesnotmatter\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": "112233"} -server.addResponse("sessionlog.json", request_header, response_header) - -# File to be deleted when tests are fully completed. -# -InProgressFilePathspec = os.path.join(Test.RunDirectory, "in_progress") - -ts = Test.MakeATSProcess("ts", block_for_debug=False) - -ts.Disk.records_config.update( - { - 'proxy.config.proxy_name': 'Poxy_Proxy', # This will be the server name. - 'proxy.config.url_remap.remap_required': 1, - 'proxy.config.diags.debug.enabled': 3, - 'proxy.config.diags.debug.tags': f'{plugin_name}', - }) - -rp = os.path.join(Test.Variables.AtsBuildGoldTestsDir, 'pluginTest', 'TSVConnFd', '.libs', f'{plugin_name}.so') -ts.Setup.Copy(rp, ts.Env['PROXY_CONFIG_PLUGIN_PLUGIN_DIR']) - -Test.GetTcpPort("tcp_port") - -ts.Disk.plugin_config.AddLine(f"{plugin_name}.so {InProgressFilePathspec} {ts.Variables.tcp_port}") - -ts.Disk.remap_config.AddLine("map http://myhost.test http://127.0.0.1:{0}".format(server.Variables.Port)) - -ipv4flag = "" -if not Condition.CurlUsingUnixDomainSocket(): - ipv4flag = "--ipv4" -# Dummy transaction to trigger plugin. -# -tr = Test.AddTestRun() -tr.Processes.Default.StartBefore(server) -tr.Processes.Default.StartBefore(ts) -tr.MakeCurlCommandMulti( - f'touch {InProgressFilePathspec} ; ' + - f'{{curl}} --verbose {ipv4flag} --header "Host:myhost.test" http://localhost:{ts.Variables.port}/', - ts=ts) -tr.Processes.Default.ReturnCode = 0 - -# Give tests up to 10 seconds to complete. -# -tr = Test.AddTestRun() -tr.Processes.Default.Command = (os.path.join(Test.Variables.AtsTestToolsDir, 'condwait') + ' 15 1 -f ' + InProgressFilePathspec) -tr.Processes.Default.ReturnCode = 0 diff --git a/tests/gold_tests/pluginTest/access_control/access_control.test.py b/tests/gold_tests/pluginTest/access_control/access_control.test.py deleted file mode 100644 index 65ed2b3035d..00000000000 --- a/tests/gold_tests/pluginTest/access_control/access_control.test.py +++ /dev/null @@ -1,83 +0,0 @@ -''' -Test access control plugin behaviors -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. -import hashlib -import os -import re - -Test.Summary = ''' -Test access control plugin behaviors -''' - -Test.SkipUnless(Condition.PluginExists('access_control.so')) - - -class AccessControlTest: - replayFile = "replays/access_control.replay.yaml" - - def __init__(self): - self.setupOriginServer() - self.setupTS() - - def setupOriginServer(self): - self.server = Test.MakeVerifierServerProcess("verifier-server", self.replayFile) - - def setupTS(self): - self.ts = Test.MakeATSProcess("ts", enable_tls=True) - self.ts.addDefaultSSLFiles() - self.ts.Disk.ssl_multicert_yaml.AddLines( - """ -ssl_multicert: - - dest_ip: "*" - ssl_cert_name: server.pem - ssl_key_name: server.key -""".split("\n")) - self.ts.Disk.records_config.update( - { - "proxy.config.diags.debug.enabled": 1, - "proxy.config.diags.debug.tags": "http|access_control", - "proxy.config.http.insert_response_via_str": 2, - 'proxy.config.ssl.server.cert.path': f"{self.ts.Variables.SSLDir}", - 'proxy.config.ssl.server.private_key.path': f"{self.ts.Variables.SSLDir}", - 'proxy.config.ssl.client.alpn_protocols': 'http/1.1', - 'proxy.config.ssl.client.verify.server.policy': 'PERMISSIVE', - }) - - self.ts.Setup.Copy("etc/hmac_keys.txt") - self.ts.Disk.remap_config.AddLines( - { - f'''map / https://127.0.0.1:{self.server.Variables.https_port}/ \ - @plugin=access_control.so \ - @pparam=--symmetric-keys-map={Test.RunDirectory}/hmac_keys.txt \ - @pparam=--check-cookie=TokenCookie \ - @pparam=--extract-subject-to-header=@TokenSubject \ - @pparam=--extract-tokenid-to-header=@TokenId \ - @pparam=--extract-status-to-header=@TokenStatus \ - @pparam=--token-response-header=TokenRespHdr''' - }) - - def run(self): - tr = Test.AddTestRun("Session Cookie") - tr.AddVerifierClientProcess( - "verifier-client", self.replayFile, http_ports=[self.ts.Variables.port], https_ports=[self.ts.Variables.ssl_port]) - tr.Processes.Default.StartBefore(self.ts) - tr.Processes.Default.StartBefore(self.server) - tr.StillRunningAfter = self.ts - tr.StillRunningAfter = self.server - - -AccessControlTest().run() diff --git a/tests/gold_tests/pluginTest/block_errors/block_errors.test.py b/tests/gold_tests/pluginTest/block_errors/block_errors.test.py deleted file mode 100644 index c50869356da..00000000000 --- a/tests/gold_tests/pluginTest/block_errors/block_errors.test.py +++ /dev/null @@ -1,176 +0,0 @@ -""" -Verify block_errors plugin message handling. -""" -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -Test.Summary = ''' -Verify block_errors plugin message handling via traffic_ctl. -''' - -Test.SkipUnless(Condition.PluginExists('block_errors.so'),) - -# Define ATS and configure it. -ts = Test.MakeATSProcess("ts") - -ts.Disk.records_config.update({ - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'block_errors', -}) - -# Configure block_errors plugin with initial values. -ts.Disk.plugin_config.AddLine('block_errors.so') - -# Verify the plugin loads. -ts.Disk.diags_log.Content = Testers.ContainsExpression( - "loading plugin.*block_errors.so", "Verify the block_errors plugin got loaded.") - -# -# Test 1: Verify the plugin starts with default values. -# -tr = Test.AddTestRun("Verify plugin starts with default values.") -tr.Processes.Default.Command = "echo verifying plugin starts with default values" -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.StartBefore(ts) -tr.StillRunningAfter = ts - -# Verify the default values are logged at startup. -ts.Disk.traffic_out.Content = Testers.ContainsExpression( - "reset limit: 1000 per minute, timeout limit: 4 minutes, shutdown connection: 0 enabled: 1", - "Verify block_errors starts with default values.") - -# -# Test 2: Verify changing the 'enabled' setting via traffic_ctl. -# -tr = Test.AddTestRun("Verify changing 'enabled' via traffic_ctl.") -tr.Processes.Default.Command = "traffic_ctl plugin msg block_errors.enabled 0" -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Env = ts.Env -tr.StillRunningAfter = ts - -tr = Test.AddTestRun("Await the enabled change.") -tr.Processes.Default.Command = "echo awaiting enabled change" -tr.Processes.Default.ReturnCode = 0 -await_enabled = tr.Processes.Process('await_enabled', 'sleep 30') -await_enabled.Ready = When.FileContains(ts.Disk.traffic_out.Name, "msg_hook: command=enabled data=0") -tr.Processes.Default.StartBefore(await_enabled) - -ts.Disk.traffic_out.Content += Testers.ContainsExpression( - "msg_hook: command=enabled data=0", "Verify block_errors received the enabled command.") -ts.Disk.traffic_out.Content += Testers.ContainsExpression( - "reset limit: 1000 per minute, timeout limit: 4 minutes, shutdown connection: 0 enabled: 0", - "Verify block_errors applied the enabled=0 setting.") - -# -# Test 3: Verify changing the 'limit' setting via traffic_ctl. -# -tr = Test.AddTestRun("Verify changing 'limit' via traffic_ctl.") -tr.Processes.Default.Command = "traffic_ctl plugin msg block_errors.limit 500" -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Env = ts.Env -tr.StillRunningAfter = ts - -tr = Test.AddTestRun("Await the limit change.") -tr.Processes.Default.Command = "echo awaiting limit change" -tr.Processes.Default.ReturnCode = 0 -await_limit = tr.Processes.Process('await_limit', 'sleep 30') -await_limit.Ready = When.FileContains(ts.Disk.traffic_out.Name, "msg_hook: command=limit data=500") -tr.Processes.Default.StartBefore(await_limit) - -ts.Disk.traffic_out.Content += Testers.ContainsExpression( - "msg_hook: command=limit data=500", "Verify block_errors received the limit command.") -ts.Disk.traffic_out.Content += Testers.ContainsExpression( - "reset limit: 500 per minute", "Verify block_errors applied the limit=500 setting.") - -# -# Test 4: Verify changing the 'cycles' setting via traffic_ctl. -# -tr = Test.AddTestRun("Verify changing 'cycles' via traffic_ctl.") -tr.Processes.Default.Command = "traffic_ctl plugin msg block_errors.cycles 8" -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Env = ts.Env -tr.StillRunningAfter = ts - -tr = Test.AddTestRun("Await the cycles change.") -tr.Processes.Default.Command = "echo awaiting cycles change" -tr.Processes.Default.ReturnCode = 0 -await_cycles = tr.Processes.Process('await_cycles', 'sleep 30') -await_cycles.Ready = When.FileContains(ts.Disk.traffic_out.Name, "msg_hook: command=cycles data=8") -tr.Processes.Default.StartBefore(await_cycles) - -ts.Disk.traffic_out.Content += Testers.ContainsExpression( - "msg_hook: command=cycles data=8", "Verify block_errors received the cycles command.") -ts.Disk.traffic_out.Content += Testers.ContainsExpression( - "timeout limit: 8 minutes", "Verify block_errors applied the cycles=8 setting.") - -# -# Test 5: Verify changing the 'shutdown' setting via traffic_ctl. -# -tr = Test.AddTestRun("Verify changing 'shutdown' via traffic_ctl.") -tr.Processes.Default.Command = "traffic_ctl plugin msg block_errors.shutdown 1" -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Env = ts.Env -tr.StillRunningAfter = ts - -tr = Test.AddTestRun("Await the shutdown change.") -tr.Processes.Default.Command = "echo awaiting shutdown change" -tr.Processes.Default.ReturnCode = 0 -await_shutdown = tr.Processes.Process('await_shutdown', 'sleep 30') -await_shutdown.Ready = When.FileContains(ts.Disk.traffic_out.Name, "msg_hook: command=shutdown data=1") -tr.Processes.Default.StartBefore(await_shutdown) - -ts.Disk.traffic_out.Content += Testers.ContainsExpression( - "msg_hook: command=shutdown data=1", "Verify block_errors received the shutdown command.") -ts.Disk.traffic_out.Content += Testers.ContainsExpression( - "shutdown connection: 1", "Verify block_errors applied the shutdown=1 setting.") - -# -# Test 6: Verify an unknown command is handled gracefully. -# -tr = Test.AddTestRun("Verify unknown command is handled gracefully.") -tr.Processes.Default.Command = "traffic_ctl plugin msg block_errors.unknown_command test" -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Env = ts.Env -tr.StillRunningAfter = ts - -tr = Test.AddTestRun("Await the unknown command response.") -tr.Processes.Default.Command = "echo awaiting unknown command response" -tr.Processes.Default.ReturnCode = 0 -await_unknown = tr.Processes.Process('await_unknown', 'sleep 30') -await_unknown.Ready = When.FileContains(ts.Disk.traffic_out.Name, "msg_hook: unknown command 'unknown_command'") -tr.Processes.Default.StartBefore(await_unknown) - -ts.Disk.traffic_out.Content += Testers.ContainsExpression( - "msg_hook: unknown command 'unknown_command'", "Verify block_errors logs unknown commands.") - -# -# Test 7: Verify messages for other plugins are ignored. -# -tr = Test.AddTestRun("Verify messages for other plugins are ignored.") -tr.Processes.Default.Command = "traffic_ctl plugin msg other_plugin.command test" -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Env = ts.Env -tr.StillRunningAfter = ts - -tr = Test.AddTestRun("Await the other plugin message response.") -tr.Processes.Default.Command = "echo awaiting other plugin message response" -tr.Processes.Default.ReturnCode = 0 -await_other = tr.Processes.Process('await_other', 'sleep 30') -await_other.Ready = When.FileContains(ts.Disk.traffic_out.Name, "msg_hook: message for a different plugin: other_plugin") -tr.Processes.Default.StartBefore(await_other) - -ts.Disk.traffic_out.Content += Testers.ContainsExpression( - "msg_hook: message for a different plugin: other_plugin", "Verify block_errors ignores messages for other plugins.") diff --git a/tests/gold_tests/pluginTest/cache_fill/cache_fill.test.py b/tests/gold_tests/pluginTest/cache_fill/cache_fill.test.py deleted file mode 100644 index 5584a602695..00000000000 --- a/tests/gold_tests/pluginTest/cache_fill/cache_fill.test.py +++ /dev/null @@ -1,232 +0,0 @@ -''' -Test cache_fill plugin -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -Test.Summary = ''' -Basic cache_fill plugin test -''' - -Test.SkipUnless( - Condition.PluginExists('cache_fill.so'), - Condition.PluginExists('xdebug.so'), -) -# Skip until cache_fill supports UDS -Test.SkipIf(Condition.CurlUsingUnixDomainSocket()) -Test.ContinueOnFail = True -Test.testName = "cache_fill" - - -class CacheFillTest: - - def __init__(self): - self.setUpOriginServer() - self.setUpTS() - self.curl_and_args = '-s -D /dev/stdout -v -x localhost:{} -H "x-debug: x-cache,x-cache-key"'.format(self.ts.Variables.port) - - def setUpOriginServer(self): - # Define and configure origin server - self.server = Test.MakeOriginServer("server") - - req = { - "headers": "GET /nostore HTTP/1.1\r\n" + "Host: www.example.com\r\n" + "Accept: */*" + "Range: bytes=0-4\r\n" + "\r\n", - "timestamp": "1469733493.993", - "body": "" - } - - res = { - "headers": - "HTTP/1.1 200 OK\r\n" + "Cache-Control: nostore\r\n" + "Connection: close\r\n" + 'Etag: 994324f6-78f6bc3e8d639\r\n', - "timestamp": "1469733493.993", - "body": "hello hello" - } - - self.server.addResponse("sessionlog.json", req, res) - - req = {"headers": "GET /200 HTTP/1.1\r\n" + "Host: www.example.com\r\n" + "\r\n", "timestamp": "1469733493.993", "body": ""} - - res = { - "headers": - "HTTP/1.1 200 OK\r\n" + "Cache-Control: max-age=1\r\n" + "Connection: close\r\n" + - 'Etag: 772102f4-56f4bc1e6d417\r\n', - "timestamp": "1469733493.993", - "body": "hello hello" - } - - self.server.addResponse("sessionlog.json", req, res) - - req = { - "headers": "GET /range HTTP/1.1\r\n" + "Host: www.example.com\r\n" + "Accept: */*" + "Range: bytes=0-4\r\n" + "\r\n", - "timestamp": "1469733493.993", - "body": "" - } - res = { - "headers": - "HTTP/1.1 200 OK\r\n" + "Cache-Control: max-age=1\r\n" + "Connection: close\r\n" + - 'Etag: 883213f5-67f5bc2e7d528\r\n', - "timestamp": "1469733493.993", - "body": "hello hello" - } - - self.server.addResponse("sessionlog.json", req, res) - - def setUpTS(self): - # Define and configure ATS - self.ts = Test.MakeATSProcess("ts") - - self.ts.Disk.remap_config.AddLines( - [ - 'map http://www.example.com/200 http://127.0.0.1:{}/200 @plugin=cache_fill.so'.format(self.server.Variables.Port), - 'map http://www.example.com/range http://127.0.0.1:{}/range @plugin=cache_fill.so'.format( - self.server.Variables.Port), - 'map http://www.example.com/nostore http://127.0.0.1:{}/nostore @plugin=cache_fill.so'.format( - self.server.Variables.Port), - 'map http://www.example.com/304 http://127.0.0.1:{}/range @plugin=cache_fill.so'.format(self.server.Variables.Port), - ]) - - self.ts.Disk.plugin_config.AddLine('xdebug.so --enable=x-cache,x-cache-key') - - self.ts.Disk.records_config.update( - { - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'cache_fill|.*cache.*', - }) - - def test_cacheMiss(self): - # Cache miss; background fetch should fill cache - tr = Test.AddTestRun("Cache miss") - ps = tr.Processes.Default - ps.StartBefore(self.server, ready=When.PortOpen(self.server.Variables.Port)) - ps.StartBefore(Test.Processes.ts) - tr.MakeCurlCommand(self.curl_and_args + ' http://www.example.com/200', ts=self.ts) - ps.ReturnCode = 0 - ps.Streams.stdout.Content = Testers.ContainsExpression("X-Cache: miss", "expected cache miss") - ps.Streams.stdout.Content += Testers.ContainsExpression("200 OK", "Expected 200 OK status") - tr.StillRunningAfter = self.ts - - def test_cacheHit(self): - # Cache hit-fresh from background fill - tr = Test.AddTestRun("Cache hit-fresh") - ps = tr.Processes.Default - tr.MakeCurlCommand(self.curl_and_args + ' http://www.example.com/200', ts=self.ts) - ps.ReturnCode = 0 - ps.Streams.stdout.Content = Testers.ContainsExpression("X-Cache: hit-fresh", "expected cache hit") - ps.Streams.stdout.Content += Testers.ContainsExpression("200 OK", "Expected 200 OK status") - tr.StillRunningAfter = self.ts - - def test_rangeReq_CacheMiss(self): - # Cache miss; background fetch should fill cache - tr = Test.AddTestRun("Range cache miss") - ps = tr.Processes.Default - tr.MakeCurlCommand(self.curl_and_args + ' http://www.example.com/range -r 0-4', ts=self.ts) - ps.ReturnCode = 0 - ps.Streams.stdout.Content = Testers.ContainsExpression("X-Cache: miss", "expected cache miss") - ps.Streams.stdout.Content += Testers.ContainsExpression("200 OK", "Expected 200 status") - tr.StillRunningAfter = self.ts - - def test_rangeReq_CacheHit(self): - # Cache hit-fresh from background fill - tr = Test.AddTestRun("Range cache hit-fresh") - ps = tr.Processes.Default - tr.MakeCurlCommand(self.curl_and_args + ' http://www.example.com/range -r 0-4', ts=self.ts) - ps.ReturnCode = 0 - ps.Streams.stdout.Content = Testers.ContainsExpression("X-Cache: hit-fresh", "expected cache hit") - ps.Streams.stdout.Content += Testers.ContainsExpression("206 Partial Content", "Expected 206 status") - ps.Streams.stdout.Content += Testers.ContainsExpression( - "Content-Range: bytes 0-4/11", "Expected Content-Range: bytes 0-4/11") - tr.StillRunningAfter = self.ts - - def test_noStore_noFill(self): - # Background fetch should NOT fill cache - tr = Test.AddTestRun("nostore cache miss") - ps = tr.Processes.Default - tr.MakeCurlCommand(self.curl_and_args + ' http://www.example.com/nostore -r 0-4', ts=self.ts) - ps.ReturnCode = 0 - ps.Streams.stdout.Content = Testers.ContainsExpression("X-Cache: miss", "expected cache miss") - ps.Streams.stdout.Content += Testers.ContainsExpression("200 OK", "Expected 200 status") - tr.StillRunningAfter = self.ts - - def test_nostore_cacheMiss(self): - # Cache hit miss because background fill was not triggered - tr = Test.AddTestRun("nostore cache hit-fresh") - ps = tr.Processes.Default - tr.MakeCurlCommand(self.curl_and_args + ' http://www.example.com/nostore -r 0-4', ts=self.ts) - ps.ReturnCode = 0 - ps.Streams.stdout.Content = Testers.ContainsExpression("X-Cache: miss", "expected cache miss") - ps.Streams.stdout.Content += Testers.ContainsExpression("200 OK", "Expected 200 status") - tr.StillRunningAfter = self.ts - - def setUpGlobalPlugin(self): - ## Check global plugin ## - self.ts.Disk.plugin_config.AddLine('cache_fill.so') - - self.ts.Disk.remap_config.AddLines( - ['map http://www.example.com/global http://127.0.0.1:{}/global'.format(self.server.Variables.Port)]) - - req = { - "headers": "GET /global HTTP/1.1\r\n" + "Host: www.example.com\r\n" + "\r\n", - "timestamp": "1469733493.993", - "body": "" - } - res = { - "headers": - "HTTP/1.1 200 OK\r\n" + "Cache-Control: max-age=1\r\n" + "Connection: close\r\n" + - 'Etag: 661091f3-45f3bc0e5d306\r\n', - "timestamp": "1469733493.993", - "body": "hello hello" - } - - self.server.addResponse("sessionlog.json", req, res) - - def test_global_cacheMiss(self): - # Global implementation: Cache miss; background fetch should fill cache - tr = Test.AddTestRun("Cache miss - global implementation") - - ps = tr.Processes.Default - tr.MakeCurlCommand(self.curl_and_args + ' http://www.example.com/global', ts=self.ts) - ps.ReturnCode = 0 - ps.Streams.stdout.Content = Testers.ContainsExpression("X-Cache: miss", "expected cache miss") - - ps.Streams.stdout.Content += Testers.ContainsExpression("200 OK", "Expected 200 status") - tr.StillRunningAfter = self.ts - - def test_global_cacheHit(self): - # Global implementation: Cache hit-fresh from background fill - tr = Test.AddTestRun("Cache hit-fresh - global implementation") - ps = tr.Processes.Default - tr.MakeCurlCommand(self.curl_and_args + ' http://www.example.com/global', ts=self.ts) - ps.ReturnCode = 0 - ps.Streams.stdout.Content = Testers.ContainsExpression("X-Cache: hit-fresh", "expected cache hit") - ps.Streams.stdout.Content += Testers.ContainsExpression("200 OK", "Expected 200 status") - tr.StillRunningAfter = self.ts - - def runTraffic(self): - self.test_cacheMiss() - self.test_cacheHit() - self.test_rangeReq_CacheMiss() - self.test_rangeReq_CacheHit() - self.test_noStore_noFill() - self.test_nostore_cacheMiss() - self.setUpGlobalPlugin() - self.test_global_cacheMiss() - self.test_global_cacheHit() - - def run(self): - self.runTraffic() - - -CacheFillTest().run() diff --git a/tests/gold_tests/pluginTest/cache_promote/cache_promote.test.py b/tests/gold_tests/pluginTest/cache_promote/cache_promote.test.py deleted file mode 100644 index 7f4096171d5..00000000000 --- a/tests/gold_tests/pluginTest/cache_promote/cache_promote.test.py +++ /dev/null @@ -1,77 +0,0 @@ -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import os - -Test.Summary = ''' -Test cache_promote plugin -''' - -Test.SkipUnless(Condition.PluginExists('cache_promote.so')) - - -class CachePromotePluginTest: - - def __init__(self): - self._tr = Test.AddTestRun("cache_promote") - self.__setupOriginServer() - self.__setupTS() - self.__setupClient() - - def __setupClient(self): - # Eval template file to set origin server port to redirect - template_file_path = os.path.join(self._tr.TestDirectory, "replay/cache_promote.replay.yaml.tmpl") - with open(template_file_path, 'r') as f: - template = f.read() - - replay_yaml_path = os.path.join(self._tr.RunDirectory, "cache_promote.replay.yaml") - with open(replay_yaml_path, 'w') as f: - f.write(template.format(httpbin_port=self._httpbin.Variables.Port)) - - self._tr.AddVerifierClientProcess("verifier-client", replay_yaml_path, http_ports=[self._ts.Variables.port]) - - def __setupOriginServer(self): - self._httpbin = Test.MakeHttpBinServer("httpbin") - - def __setupTS(self): - self._ts = Test.MakeATSProcess("ts", enable_cache=True) - self._ts.Disk.records_config.update( - { - "proxy.config.diags.debug.enabled": 1, - "proxy.config.diags.debug.tags": "http|cache_promote", - "proxy.config.http.number_of_redirections": 1, # follow redirect - "proxy.config.http.redirect.actions": "self:follow", # redirects to self are not followed by default - }) - self._ts.Disk.plugin_config.AddLine("xdebug.so --enable=x-cache,x-cache-key") - self._ts.Disk.remap_config.AddLines( - { - f""" -map /test_0/ http://127.0.0.1:{self._httpbin.Variables.Port}/ \ - @plugin=cache_promote.so @pparam=--policy=lru @pparam=--hits=2 @pparam=--buckets=15000000 - -map /test_1/ http://127.0.0.1:{self._httpbin.Variables.Port}/ \ - @plugin=cache_promote.so @pparam=--policy=lru @pparam=--hits=2 @pparam=--buckets=15000000 @pparam=--disable-on-redirect \ - @plugin=cachekey.so @pparam=--static-prefix=trafficserver.apache.org/443 -""" - }) - - def run(self): - self._tr.Processes.Default.StartBefore(self._ts) - self._tr.Processes.Default.StartBefore(self._httpbin) - self._tr.StillRunningAfter = self._ts - self._tr.StillRunningAfter = self._httpbin - - -CachePromotePluginTest().run() diff --git a/tests/gold_tests/pluginTest/cache_range_requests/cache_range_requests.test.py b/tests/gold_tests/pluginTest/cache_range_requests/cache_range_requests.test.py deleted file mode 100644 index 5bf59fb4c82..00000000000 --- a/tests/gold_tests/pluginTest/cache_range_requests/cache_range_requests.test.py +++ /dev/null @@ -1,368 +0,0 @@ -''' -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -Test.Summary = ''' -Basic cache_range_requests plugin test -''' - -# Test description: -# Preload the cache with the entire asset to be range requested. -# Reload remap rule with cache_range_requests plugin -# Request content through the cache_range_requests plugin - -Test.SkipUnless( - Condition.PluginExists('cache_range_requests.so'), - Condition.PluginExists('header_rewrite.so'), - Condition.PluginExists('xdebug.so'), -) -Test.ContinueOnFail = False -Test.testName = "cache_range_requests" - -# Define and configure ATS -ts = Test.MakeATSProcess("ts") - -# Define and configure origin server -server = Test.MakeOriginServer("server", lookup_key="{%uuid}") - -# default root -req_chk = { - "headers": "GET / HTTP/1.1\r\n" + "Host: www.example.com\r\n" + "uuid: none\r\n" + "\r\n", - "timestamp": "1469733493.993", - "body": "" -} - -res_chk = {"headers": "HTTP/1.1 200 OK\r\n" + "Connection: close\r\n" + "\r\n", "timestamp": "1469733493.993", "body": ""} - -server.addResponse("sessionlog.json", req_chk, res_chk) - -body = "lets go surfin now" - -req_full = { - "headers": "GET /path HTTP/1.1\r\n" + "Host: www.example.com\r\n" + "Accept: */*\r\n" + "uuid: full\r\n" + "\r\n", - "timestamp": "1469733493.993", - "body": "" -} - -res_full = { - "headers": "HTTP/1.1 200 OK\r\n" + "Cache-Control: max-age=500\r\n" + "Connection: close\r\n" + 'Etag: "path"\r\n' + "\r\n", - "timestamp": "1469733493.993", - "body": body -} - -server.addResponse("sessionlog.json", req_full, res_full) - -block_bytes = 7 -bodylen = len(body) - -inner_str = "7-15" - -req_inner = { - "headers": - "GET /path HTTP/1.1\r\n" + "Host: www.example.com\r\n" + "Accept: */*\r\n" + "Range: bytes={}\r\n".format(inner_str) + - "uuid: inner\r\n" + "\r\n", - "timestamp": "1469733493.993", - "body": "" -} - -res_inner = { - "headers": - "HTTP/1.1 206 Partial Content\r\n" + "Accept-Ranges: bytes\r\n" + "Cache-Control: max-age=500\r\n" + - "Content-Range: bytes {0}/{1}\r\n".format(inner_str, bodylen) + "Connection: close\r\n" + 'Etag: "path"\r\n' + "\r\n", - "timestamp": "1469733493.993", - "body": body[7:15] -} - -server.addResponse("sessionlog.json", req_inner, res_inner) - -frange_str = "0-" - -req_frange = { - "headers": - "GET /path HTTP/1.1\r\n" + "Host: www.example.com\r\n" + "Accept: */*\r\n" + "Range: bytes={}\r\n".format(frange_str) + - "uuid: frange\r\n" + "\r\n", - "timestamp": "1469733493.993", - "body": "" -} - -res_frange = { - "headers": - "HTTP/1.1 206 Partial Content\r\n" + "Accept-Ranges: bytes\r\n" + "Cache-Control: max-age=500\r\n" + - "Content-Range: bytes 0-{0}/{0}\r\n".format(bodylen) + "Connection: close\r\n" + 'Etag: "path"\r\n' + "\r\n", - "timestamp": "1469733493.993", - "body": body -} - -server.addResponse("sessionlog.json", req_frange, res_frange) - -last_str = "-5" - -req_last = { - "headers": - "GET /path HTTP/1.1\r\n" + "Host: www.example.com\r\n" + "Accept: */*\r\n" + "Range: bytes={}\r\n".format(last_str) + - "uuid: last\r\n" + "\r\n", - "timestamp": "1469733493.993", - "body": "" -} - -res_last = { - "headers": - "HTTP/1.1 206 Partial Content\r\n" + "Accept-Ranges: bytes\r\n" + "Cache-Control: max-age=200\r\n" + - "Content-Range: bytes {0}-{1}/{1}\r\n".format(bodylen - 5, bodylen) + "Connection: close\r\n" + 'Etag: "path"\r\n' + "\r\n", - "timestamp": "1469733493.993", - "body": body[-5:] -} - -server.addResponse("sessionlog.json", req_last, res_last) - -pselect_str = "1-10" - -req_pselect = { - "headers": - "GET /path HTTP/1.1\r\n" + "Host: parentselect\r\n" + "Accept: */*\r\n" + "Range: bytes={}\r\n".format(pselect_str) + - "uuid: pselect\r\n" + "\r\n", - "timestamp": "1469733493.993", - "body": "" -} - -res_pselect = { - "headers": - "HTTP/1.1 206 Partial Content\r\n" + "Accept-Ranges: bytes\r\n" + "Cache-Control: max-age=200\r\n" + - "Content-Range: bytes {}/19\r\n".format(pselect_str) + "Connection: close\r\n" + 'Etag: "path"\r\n' + "\r\n", - "timestamp": "1469733493.993", - "body": body[1:10] -} - -server.addResponse("sessionlog.json", req_pselect, res_pselect) - -req_psd = { - "headers": - "GET /path HTTP/1.1\r\n" + "Host: psd\r\n" + "Accept: */*\r\n" + "Range: bytes={}\r\n".format(pselect_str) + - "uuid: pselect\r\n" + "\r\n", - "timestamp": "1469733493.993", - "body": "" -} - -server.addResponse("sessionlog.json", req_psd, res_pselect) - -# long cache key test: URL path long enough to push cache key over 16384 bytes -long_path = 'A' * 16400 -req_long_key = { - "headers": - "GET /{} HTTP/1.1\r\n".format(long_path) + "Host: www.longkey.com\r\n" + "Accept: */*\r\n" + "Range: bytes=0-17\r\n" + - "uuid: long_key\r\n" + "\r\n", - "timestamp": "1469733493.993", - "body": "" -} - -res_long_key = { - "headers": - "HTTP/1.1 206 Partial Content\r\n" + "Accept-Ranges: bytes\r\n" + "Cache-Control: max-age=500\r\n" + - "Content-Range: bytes 0-17/{}\r\n".format(len(body)) + "Connection: close\r\n" + 'Etag: "longkey"\r\n' + "\r\n", - "timestamp": "1469733493.993", - "body": body -} - -server.addResponse("sessionlog.json", req_long_key, res_long_key) - -# cache range requests plugin remap -ts.Setup.CopyAs('reason.conf', Test.RunDirectory) -ts.Disk.remap_config.AddLines( - [ - 'map http://www.example.com http://127.0.0.1:{}'.format(server.Variables.Port) + - ' @plugin=header_rewrite.so @pparam={}/reason.conf @plugin=cache_range_requests.so'.format(Test.RunDirectory), - - # long cache key: URL alone exceeds the 16384-byte stack buffer - 'map http://www.longkey.com http://127.0.0.1:{}'.format(server.Variables.Port) + ' @plugin=cache_range_requests.so', - - # parent select cache key option - 'map http://parentselect http://127.0.0.1:{}'.format(server.Variables.Port) + - ' @plugin=cache_range_requests.so @pparam=--ps-cachekey', - - # deprecated - 'map http://psd http://127.0.0.1:{}'.format(server.Variables.Port) + - ' @plugin=cache_range_requests.so @pparam=ps_mode:cache_key_url', - ]) - -# cache debug -ts.Disk.plugin_config.AddLine('xdebug.so --enable=x-cache,x-parentselection-key') - -# minimal configuration -ts.Disk.records_config.update( - { - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'cache_range_requests|http', - }) - -curl_and_args = '-s -D /dev/stdout -o /dev/stderr -x localhost:{} -H "x-debug: x-cache"'.format(ts.Variables.port) - -# 0 Test - Fetch whole asset into cache -tr = Test.AddTestRun("full asset cache miss bypass") -ps = tr.Processes.Default -ps.StartBefore(server, ready=When.PortOpen(server.Variables.Port)) -ps.StartBefore(Test.Processes.ts) -tr.MakeCurlCommand(curl_and_args + ' http://www.example.com/path -H "uuid: full"', ts=ts) -ps.ReturnCode = 0 -ps.Streams.stderr = "gold/full.stderr.gold" -tr.StillRunningAfter = ts - -# test inner range -# 1 Test - Fetch range into cache -tr = Test.AddTestRun("inner range cache miss") -ps = tr.Processes.Default -tr.MakeCurlCommand(curl_and_args + ' http://www.example.com/path -r {} -H "uuid: inner"'.format(inner_str), ts=ts) -ps.ReturnCode = 0 -ps.Streams.stderr = "gold/inner.stderr.gold" -ps.Streams.stdout.Content = Testers.ContainsExpression("X-Cache: miss", "expected cache miss") -ps.Streams.stdout.Content += Testers.ContainsExpression("Content-Range: bytes 7-15/18", "expected content-range header") -ps.Streams.stdout.Content += Testers.ContainsExpression("206 Foo Bar", "Expected 206 Foo Bar status") -tr.StillRunningAfter = ts - -# 2 Test - Fetch from cache -tr = Test.AddTestRun("inner range cache hit") -ps = tr.Processes.Default -tr.MakeCurlCommand(curl_and_args + ' http://www.example.com/path -r {}'.format(inner_str), ts=ts) -ps.ReturnCode = 0 -ps.Streams.stderr = "gold/inner.stderr.gold" -ps.Streams.stdout.Content = Testers.ContainsExpression("X-Cache: hit", "expected cache hit") -ps.Streams.stdout.Content += Testers.ContainsExpression("Content-Range: bytes 7-15/18", "expected content-range header") -ps.Streams.stdout.Content += Testers.ContainsExpression("206 Foo Bar", "Expected 206 Foo Bar status") -tr.StillRunningAfter = ts - -# full range - -# 3 Test - 0- request -tr = Test.AddTestRun("0- request miss") -ps = tr.Processes.Default -tr.MakeCurlCommand(curl_and_args + ' http://www.example.com/path -r {} -H "uuid: frange"'.format(frange_str), ts=ts) -ps.ReturnCode = 0 -ps.Streams.stderr = "gold/full.stderr.gold" -ps.Streams.stdout.Content = Testers.ContainsExpression("X-Cache: miss", "expected cache miss") -ps.Streams.stdout.Content += Testers.ContainsExpression("Content-Range: bytes 0-18/18", "expected content-range header") -ps.Streams.stdout.Content += Testers.ContainsExpression("206 Foo Bar", "Expected 206 Foo Bar status") -tr.StillRunningAfter = ts - -# 4 Test - 0- request -tr = Test.AddTestRun("0- request hit") -ps = tr.Processes.Default -tr.MakeCurlCommand(curl_and_args + ' http://www.example.com/path -r {}'.format(frange_str), ts=ts) -ps.ReturnCode = 0 -ps.Streams.stderr = "gold/full.stderr.gold" -ps.Streams.stdout.Content = Testers.ContainsExpression("X-Cache: hit", "expected cache hit") -ps.Streams.stdout.Content += Testers.ContainsExpression("Content-Range: bytes 0-18/18", "expected Content-Range header") -ps.Streams.stdout.Content += Testers.ContainsExpression("206 Foo Bar", "Expected 206 Foo Bar header") -tr.StillRunningAfter = ts - -# end range - -# 5 Test - -5 request miss -tr = Test.AddTestRun("-5 request miss") -ps = tr.Processes.Default -tr.MakeCurlCommand(curl_and_args + ' http://www.example.com/path -r {} -H "uuid: last"'.format(last_str), ts=ts) -ps.ReturnCode = 0 -ps.Streams.stderr = "gold/last.stderr.gold" -ps.Streams.stdout.Content = Testers.ContainsExpression("X-Cache: miss", "expected cache miss") -ps.Streams.stdout.Content += Testers.ContainsExpression("Content-Range: bytes 13-18/18", "expected content-range header") -ps.Streams.stdout.Content += Testers.ContainsExpression("206 Foo Bar", "Expected 206 Foo Bar status") -tr.StillRunningAfter = ts - -# 6 Test - -5 request hit -tr = Test.AddTestRun("-5 request hit") -ps = tr.Processes.Default -tr.MakeCurlCommand(curl_and_args + ' http://www.example.com/path -r {}'.format(last_str), ts=ts) -ps.ReturnCode = 0 -ps.Streams.stderr = "gold/last.stderr.gold" -ps.Streams.stdout.Content = Testers.ContainsExpression("X-Cache: hit", "expected cache hit") -ps.Streams.stdout.Content += Testers.ContainsExpression("Content-Range: bytes 13-18/18", "expected content-range header") -ps.Streams.stdout.Content += Testers.ContainsExpression("206 Foo Bar", "Expected 206 Bar Bar status") -tr.StillRunningAfter = ts - -# Ensure 404's aren't getting cached - -# 7 Test - 404 -tr = Test.AddTestRun("404 request 1st") -ps = tr.Processes.Default -tr.MakeCurlCommand(curl_and_args + ' http://www.example.com/404 -r 0-', ts=ts) -ps.Streams.stdout = "gold/404.stdout.gold" -ps.Streams.stdout.Content = Testers.ContainsExpression("X-Cache: miss", "expected cache miss") -tr.StillRunningAfter = ts - -# 8 Test - 404 -tr = Test.AddTestRun("404 request 2nd") -ps = tr.Processes.Default -tr.MakeCurlCommand(curl_and_args + ' http://www.example.com/404 -r 0-', ts=ts) -ps.Streams.stdout.Content = Testers.ContainsExpression("X-Cache: miss", "expected cache miss") -ps.Streams.stdout.Content += Testers.ContainsExpression("404 Not Found", "expected 404 response") -tr.StillRunningAfter = ts - -# 9 Test - origin returns 200 response to range request -tr = Test.AddTestRun("origin returns 200") -ps = tr.Processes.Default -tr.MakeCurlCommand(curl_and_args + ' http://www.example.com/path -r {} -H "uuid: full"', ts=ts) -ps.ReturnCode = 3 # <--- note the return code -ps.Streams.stdout.Content = Testers.ContainsExpression("X-Cache: miss", "expected cache miss") -ps.Streams.stdout.Content += Testers.ContainsExpression("200 OK", "expected full 200 response") -ps.Streams.stdout.Content += Testers.ExcludesExpression("Content-Range:", "didn't expect Content-Range header") -tr.StillRunningAfter = ts - -curl_and_args = '-s -D /dev/stdout -o /dev/stderr -x localhost:{} -H "x-debug: x-parentselection-key"'.format(ts.Variables.port) - -# 10 Test - cache_key_url request -tr = Test.AddTestRun("cache_key_url request") -ps = tr.Processes.Default -tr.MakeCurlCommand(curl_and_args + ' http://parentselect/path -r {} -H "uuid: pselect"'.format(pselect_str), ts=ts) -ps.ReturnCode = 0 -ps.Streams.stdout.Content = Testers.ContainsExpression( - "X-ParentSelection-Key: .*-bytes=", - "expected bytes in parent selection key", -) -tr.StillRunningAfter = ts -tr.StillRunningAfter = server - -# 11 Test - non cache_key_url request ... no X-ParentSelection-Key -tr = Test.AddTestRun("non cache_key_url request") -ps = tr.Processes.Default -tr.MakeCurlCommand(curl_and_args + ' http://www.example.com/path -r {} -H "uuid: inner"'.format(inner_str), ts=ts) -ps.ReturnCode = 0 -ps.Streams.stdout.Content = Testers.ExcludesExpression("X-ParentSelection-Key", "parent select key shouldn't show up") -tr.StillRunningAfter = ts -tr.StillRunningAfter = server - -# 12 Test - cache_key_url request -- deprecated -tr = Test.AddTestRun("cache_key_url request - deprecated") -ps = tr.Processes.Default -tr.MakeCurlCommand(curl_and_args + ' http://psd/path -r {} -H "uuid: pselect"'.format(pselect_str), ts=ts) -ps.ReturnCode = 0 -ps.Streams.stdout.Content = Testers.ContainsExpression( - "X-ParentSelection-Key: .*-bytes=", - "expected bytes in parent selection key", -) -tr.StillRunningAfter = ts -tr.StillRunningAfter = server - -# 13 Test - range request where URL+range_value exceeds 16384 bytes (spill path) -tr = Test.AddTestRun("long cache key spill") -ps = tr.Processes.Default -tr.MakeCurlCommand(curl_and_args + ' http://www.longkey.com/{} -r 0-17 -H "uuid: long_key"'.format(long_path), ts=ts) -ps.ReturnCode = 0 -ps.Streams.stdout.Content = Testers.ContainsExpression("206", "expected 206 for long-key range request") -ts.Disk.diags_log.Content = Testers.ExcludesExpression( - "disabling cache for this transaction", - "spill path must not disable cache", -) -tr.StillRunningAfter = ts -tr.StillRunningAfter = server diff --git a/tests/gold_tests/pluginTest/cache_range_requests/cache_range_requests_cache_complete_responses.test.py b/tests/gold_tests/pluginTest/cache_range_requests/cache_range_requests_cache_complete_responses.test.py deleted file mode 100644 index faa3be859e3..00000000000 --- a/tests/gold_tests/pluginTest/cache_range_requests/cache_range_requests_cache_complete_responses.test.py +++ /dev/null @@ -1,399 +0,0 @@ -''' -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -Test.Summary = ''' -cache_range_requests cache-complete-responses test -''' - -# Test description: -# Three rounds of testing: -# Round 1: -# - seed the cache with an object that is smaller than the slice block size -# - issue requests with various ranges and validate responses are 200s -# Round 2: -# - seed the cache with an object that is larger than the slice block size -# - issue requests with various ranges and validate responses are 206s -# The first two rounds test cache miss, cache hit, and refresh hit scenarios -# - uses the cachekey plugin to add the `Range` request header to the cache key -# - requests content through the slice and cache_range_requests plugin with a 4MB slice block size -# - demonstrates how one might normalize the `Range` header to avoid cache pollution -# The third round tests cache miss, hit, and refresh hit scenarios without any other plugins -# - tests cache misses, then hits, using the same object but two different ranges -# - demonstrates why normalization of the `Range` header is required to prevent pollution - -Test.SkipUnless( - Condition.PluginExists('cachekey.so'), - Condition.PluginExists('cache_range_requests.so'), - Condition.PluginExists('slice.so'), - Condition.PluginExists('xdebug.so'), -) -Test.ContinueOnFail = False -Test.testName = "cache_range_requests_cache_200s" - -# Generate bodies for our responses -small_body_len = 10000 -small_body = 'x' * small_body_len - -slice_body_len = 4 * 1024 * 1024 -slice_body = 'x' * slice_body_len - -# Define and configure ATS -ts = Test.MakeATSProcess("ts") - -# Define and configure origin server -server = Test.MakeOriginServer("server", lookup_key="{%UID}") - -# default root -req_chk = { - "headers": "GET / HTTP/1.1\r\n" + "Host: www.example.com\r\n" + "uuid: none\r\n" + "\r\n", - "timestamp": "1469733493.993", - "body": "" -} - -res_chk = {"headers": "HTTP/1.1 200 OK\r\n" + "Connection: close\r\n" + "\r\n", "timestamp": "1469733493.993", "body": ""} - -server.addResponse("sessionlog.json", req_chk, res_chk) - -small_req = { - "headers": "GET /obj HTTP/1.1\r\n" + "Host: www.example.com\r\n" + "Accept: */*\r\n" + "UID: SMALL\r\n" - "\r\n", - "timestamp": "1469733493.993", - "body": "" -} - -small_resp = { - "headers": - "HTTP/1.1 200 OK\r\n" + "Cache-Control: max-age=1\r\n" + "Connection: close\r\n" + 'Etag: "772102f4-56f4bc1e6d417"\r\n' + - "\r\n", - "timestamp": "1469733493.993", - "body": small_body -} - -small_reval_req = { - "headers": "GET /obj HTTP/1.1\r\n" + "Host: www.example.com\r\n" + "Accept: */*\r\n" + "UID: SMALL-INM\r\n" - "\r\n", - "timestamp": "1469733493.993", - "body": "" -} - -small_reval_resp = { - "headers": - "HTTP/1.1 304 Not Modified\r\n" + "Cache-Control: max-age=10\r\n" + "Connection: close\r\n" + - 'Etag: "772102f4-56f4bc1e6d417"\r\n' + "\r\n", - "timestamp": "1469733493.993" -} - -slice_req = { - "headers": - "GET /slice HTTP/1.1\r\n" + "Host: www.example.com\r\n" + "Range: bytes=0-4194303\r\n" + "Accept: */*\r\n" + - "UID: SLICE\r\n" - "\r\n", - "timestamp": "1469733493.993", -} - -slice_resp = { - "headers": - "HTTP/1.1 206 Partial Content\r\n" + "Cache-Control: max-age=1\r\n" + - "Content-Range: bytes 0-{}/{}\r\n".format(slice_body_len - 1, slice_body_len * 2) + "\r\n" + - "Content-Length: {}\r\n".format(slice_body_len) + "\r\n" + "Connection: close\r\n" + 'Etag: "872104f4-d6bcaa1e6f979"\r\n' + - "\r\n", - "timestamp": "1469733493.993", - "body": slice_body -} - -slice_reval_req = { - "headers": "GET /slice HTTP/1.1\r\n" + "Host: www.example.com\r\n" + "Accept: */*\r\n" + "UID: SLICE-INM\r\n" - "\r\n", - "timestamp": "1469733493.993", - "body": "" -} - -slice_reval_resp = { - "headers": - "HTTP/1.1 304 Not Modified\r\n" + "Cache-Control: max-age=10\r\n" + "Connection: close\r\n" + - 'Etag: "872104f4-d6bcaa1e6f979"\r\n' + "\r\n", - "timestamp": "1469733493.993" -} - -naieve_req = { - "headers": "GET /naieve/obj HTTP/1.1\r\n" + "Host: www.example.com\r\n" + "Accept: */*\r\n" + "UID: NAIEVE\r\n" - "\r\n", - "timestamp": "1469733493.993", - "body": "" -} - -naieve_resp = { - "headers": - "HTTP/1.1 200 OK\r\n" + "Cache-Control: max-age=1\r\n" + "Connection: close\r\n" + 'Etag: "cad04ff4-56f4bc197ceda"\r\n' + - "\r\n", - "timestamp": "1469733493.993", - "body": small_body -} - -naieve_reval_req = { - "headers": "GET /naieve/obj HTTP/1.1\r\n" + "Host: www.example.com\r\n" + "Accept: */*\r\n" + "UID: NAIEVE-INM\r\n" - "\r\n", - "timestamp": "1469733493.993", - "body": "" -} - -naieve_reval_resp = { - "headers": - "HTTP/1.1 304 Not Modified\r\n" + "Cache-Control: max-age=10\r\n" + "Connection: close\r\n" + - 'Etag: "cad04ff4-56f4bc197ceda"\r\n' + "\r\n", - "timestamp": "1469733493.993" -} - -server.addResponse("sessionlog.json", small_req, small_resp) -server.addResponse("sessionlog.json", small_reval_req, small_reval_resp) -server.addResponse("sessionlog.json", slice_req, slice_resp) -server.addResponse("sessionlog.json", slice_reval_req, slice_reval_resp) -server.addResponse("sessionlog.json", naieve_req, naieve_resp) -server.addResponse("sessionlog.json", naieve_reval_req, naieve_reval_resp) - -# remap with the cache range requests plugin only -# this is a "naieve" configuration due to the lack of range normalization performed at remap time by slice -# this config should only be used if ranges have been reliably normalized by the requestor (either the client itself or a cache) -ts.Disk.remap_config.AddLines( - [ - f'map http://example.com/naieve http://127.0.0.1:{server.Variables.Port}/naieve \\' + - ' @plugin=cache_range_requests.so @pparam=--cache-complete-responses', - ]) - -# remap with slice, cachekey, and the cache range requests plugin to ensure range normalization and cache keys are correct -ts.Disk.remap_config.AddLines( - [ - f'map http://example.com http://127.0.0.1:{server.Variables.Port} \\' + ' @plugin=slice.so @pparam=--blockbytes=4m \\', - ' @plugin=cachekey.so @pparam=--key-type=cache_key @pparam=--include-headers=Range @pparam=--remove-all-params=true \\', - ' @plugin=cache_range_requests.so @pparam=--no-modify-cachekey @pparam=--cache-complete-responses', - ]) - -# cache debug -ts.Disk.plugin_config.AddLine('xdebug.so --enable=x-cache,x-cache-key') - -# enable debug -ts.Disk.records_config.update( - { - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'cachekey|cache_range_requests|slice', - }) - -# base cURL command -curl_and_args = '-s -D /dev/stdout -o /dev/stderr -x localhost:{} -H "x-debug: x-cache, x-cache-key"'.format(ts.Variables.port) - -# Test round 1: ensure we fetch and cache objects that are returned with a -# 200 OK and no Content-Range when the object is smaller than the slice -# block size - -# 0 Test - Fetch /obj with a Range header but less than 4MB -tr = Test.AddTestRun("cache miss on /obj") -ps = tr.Processes.Default -ps.StartBefore(server, ready=When.PortOpen(server.Variables.Port)) -ps.StartBefore(Test.Processes.ts) -tr.MakeCurlCommand(curl_and_args + ' -H "UID: SMALL" http://example.com/obj -r 0-5000', ts=ts) -ps.ReturnCode = 0 -ps.Streams.stdout.Content = Testers.ContainsExpression("200 OK", "expected 200 OK") -ps.Streams.stdout.Content = Testers.ExcludesExpression("Content-Range:", "expected no Content-Range header") -ps.Streams.stdout.Content = Testers.ContainsExpression("X-Cache: miss, none", "expected cache miss") -ps.Streams.stdout.Content = Testers.ContainsExpression( - "X-Cache-Key: /.*?/Range:bytes=0-4194303/obj", "expected cache key with bytes 0-4194303") -tr.StillRunningAfter = ts - -# 1 Test - Fetch /obj with a different range but less than 4MB -tr = Test.AddTestRun("cache hit-fresh on /obj") -ps = tr.Processes.Default -tr.MakeCurlCommand(curl_and_args + ' -H "UID: SMALL" http://example.com/obj -r 5001-5999', ts=ts) -ps.ReturnCode = 0 -ps.Streams.stdout.Content = Testers.ContainsExpression("200 OK", "expected 200 OK") -ps.Streams.stdout.Content = Testers.ExcludesExpression("Content-Range:", "expected no Content-Range header") -ps.Streams.stdout.Content = Testers.ContainsExpression("X-Cache: hit-fresh, none", "expected cache hit") -ps.Streams.stdout.Content = Testers.ContainsExpression( - "X-Cache-Key: /.*?/Range:bytes=0-4194303/obj", "expected cache key with bytes 0-4194303") -tr.StillRunningAfter = ts - -# 2 Test - Revalidate /obj with a different range but less than 4MB -tr = Test.AddTestRun("cache hit-stale on /obj") -tr.DelayStart = 2 -ps = tr.Processes.Default -tr.MakeCurlCommand(curl_and_args + ' -H "UID: SMALL-INM" http://example.com/obj -r 0-403', ts=ts) -ps.ReturnCode = 0 -ps.Streams.stdout.Content = Testers.ContainsExpression("200 OK", "expected 200 OK") -ps.Streams.stdout.Content = Testers.ExcludesExpression("Content-Range:", "expected no Content-Range header") -ps.Streams.stdout.Content = Testers.ContainsExpression("X-Cache: hit-stale, none", "expected cache hit stale") -ps.Streams.stdout.Content = Testers.ContainsExpression( - "X-Cache-Key: /.*?/Range:bytes=0-4194303/obj", "expected cache key with bytes 0-4194303") -tr.StillRunningAfter = ts - -# 3 Test - Fetch /obj with a different range but less than 4MB -tr = Test.AddTestRun("cache hit on /obj post revalidation") -ps = tr.Processes.Default -tr.MakeCurlCommand(curl_and_args + ' -H "UID: SMALL" http://example.com/obj -r 0-3999', ts=ts) -ps.ReturnCode = 0 -ps.Streams.stdout.Content = Testers.ContainsExpression("200 OK", "expected 200 OK") -ps.Streams.stdout.Content = Testers.ExcludesExpression("Content-Range:", "expected no Content-Range header") -ps.Streams.stdout.Content = Testers.ContainsExpression("X-Cache: hit-fresh, none", "expected cache hit-fresh") -ps.Streams.stdout.Content = Testers.ContainsExpression( - "X-Cache-Key: /.*?/Range:bytes=0-4194303/obj", "expected cache key with bytes 0-4194303") -tr.StillRunningAfter = ts - -# Test round 2: repeat, but ensure we have 206s and matching Content-Range -# headers due to a base object that exceeds the slice block size - -# 4 Test - Fetch /slice with a Range header but less than 4MB -tr = Test.AddTestRun("cache miss on /slice") -ps = tr.Processes.Default -tr.MakeCurlCommand(curl_and_args + ' -H "UID: SLICE" http://example.com/slice -r 0-5000', ts=ts) -ps.ReturnCode = 0 -ps.Streams.stdout.Content = Testers.ContainsExpression("206 Partial Content", "expected 206 Partial Content") -ps.Streams.stdout.Content = Testers.ContainsExpression( - "Content-Range: bytes 0-5000/8388608", "expected Content-Range: bytes 0-5000/8388608") -ps.Streams.stdout.Content = Testers.ContainsExpression("X-Cache: miss, none", "expected cache miss") -ps.Streams.stdout.Content = Testers.ContainsExpression( - "X-Cache-Key: /.*?/Range:bytes=0-4194303/slice", "expected cache key with bytes 0-4194303") -tr.StillRunningAfter = ts - -# 5 Test - Fetch /slice with a different range but less than 4MB -tr = Test.AddTestRun("cache hit-fresh on /slice") -ps = tr.Processes.Default -tr.MakeCurlCommand(curl_and_args + ' -H "UID: SLICE" http://example.com/slice -r 5001-5999', ts=ts) -ps.ReturnCode = 0 -ps.Streams.stdout.Content = Testers.ContainsExpression("206 Partial Content", "expected 206 Partial Content") -ps.Streams.stdout.Content = Testers.ContainsExpression( - "Content-Range: bytes 5001-5999/8388608", "expected Content-Range: bytes 5001-5999/8388608") -ps.Streams.stdout.Content = Testers.ContainsExpression("X-Cache: hit-fresh, none", "expected cache hit") -ps.Streams.stdout.Content = Testers.ContainsExpression( - "X-Cache-Key: /.*?/Range:bytes=0-4194303/slice", "expected cache key with bytes 0-4194303") -tr.StillRunningAfter = ts - -# 6 Test - Revalidate /slice with a different range but less than 4MB -tr = Test.AddTestRun("cache hit-stale on /slice") -tr.DelayStart = 2 -ps = tr.Processes.Default -tr.MakeCurlCommand(curl_and_args + ' -H "UID: SLICE-INM" http://example.com/slice -r 0-403', ts=ts) -ps.ReturnCode = 0 -ps.Streams.stdout.Content = Testers.ContainsExpression("206 Partial Content", "expected 206 Partial Content") -ps.Streams.stdout.Content = Testers.ContainsExpression( - "Content-Range: bytes 0-403/8388608", "expected Content-Range: bytes 0-403/8388608") -ps.Streams.stdout.Content = Testers.ContainsExpression("X-Cache: hit-stale, none", "expected cache hit stale") -ps.Streams.stdout.Content = Testers.ContainsExpression( - "X-Cache-Key: /.*?/Range:bytes=0-4194303/slice", "expected cache key with bytes 0-4194303") -tr.StillRunningAfter = ts - -# 7 Test - Fetch /slice with a different range but less than 4MB -tr = Test.AddTestRun("cache hit on /slice post revalidation") -ps = tr.Processes.Default -tr.MakeCurlCommand(curl_and_args + ' -H "UID: SLICE" http://example.com/slice -r 0-3999', ts=ts) -ps.ReturnCode = 0 -ps.Streams.stdout.Content = Testers.ContainsExpression("206 Partial Content", "expected 206 Partial Content") -ps.Streams.stdout.Content = Testers.ContainsExpression( - "Content-Range: bytes 0-3999/8388608", "expected Content-Range: bytes 0-3999/8388608") -ps.Streams.stdout.Content = Testers.ContainsExpression("X-Cache: hit-fresh, none", "expected cache hit-fresh") -ps.Streams.stdout.Content = Testers.ContainsExpression( - "X-Cache-Key: /.*?/Range:bytes=0-4194303/slice", "expected cache key with bytes 0-4194303") -tr.StillRunningAfter = ts - -# Test round 3: test behavior of the cache range requests plugin when caching complete ranges *without* the slice and cachekey plugins -# this tests the "naieve" case that requires range normalization to be -# performed by the requestor and demonstrates how the cache can be -# polluted without normalization - -# 8 Test - Fetch /naieve/obj with a Range header -tr = Test.AddTestRun("cache miss on /naieve/obj") -ps = tr.Processes.Default -tr.MakeCurlCommand(curl_and_args + ' -H "UID: NAIEVE" http://example.com/naieve/obj -r 0-5000', ts=ts) -ps.ReturnCode = 0 -ps.Streams.stdout.Content = Testers.ContainsExpression("200 OK", "expected 200 OK") -ps.Streams.stdout.Content = Testers.ExcludesExpression("Content-Range:", "expected no Content-Range header") -ps.Streams.stdout.Content = Testers.ContainsExpression("X-Cache: miss", "expected cache miss") -ps.Streams.stdout.Content = Testers.ContainsExpression( - "X-Cache-Key: http://.*?/naieve/obj-bytes=0-5000", "expected cache key with bytes 0-5000") -tr.StillRunningAfter = ts - -# 9 Test - Fetch /naieve/obj with the same Range header -tr = Test.AddTestRun("cache hit-fresh on /naieve/obj") -ps = tr.Processes.Default -tr.MakeCurlCommand(curl_and_args + ' -H "UID: NAIEVE" http://example.com/naieve/obj -r 0-5000', ts=ts) -ps.ReturnCode = 0 -ps.Streams.stdout.Content = Testers.ContainsExpression("200 OK", "expected 200 OK") -ps.Streams.stdout.Content = Testers.ExcludesExpression("Content-Range:", "expected no Content-Range header") -ps.Streams.stdout.Content = Testers.ContainsExpression("X-Cache: hit-fresh", "expected cache hit") -ps.Streams.stdout.Content = Testers.ContainsExpression( - "X-Cache-Key: http://.*?/naieve/obj-bytes=0-5000", "expected cache key with bytes 0-5000") -tr.StillRunningAfter = ts - -# 10 Test - Revalidate /naieve/obj with the same Range header -tr = Test.AddTestRun("cache hit-stale on /naieve/obj") -tr.DelayStart = 2 -ps = tr.Processes.Default -tr.MakeCurlCommand(curl_and_args + ' -H "UID: NAIEVE-INM" http://example.com/naieve/obj -r 0-5000', ts=ts) -ps.ReturnCode = 0 -ps.Streams.stdout.Content = Testers.ContainsExpression("200 OK", "expected 200 OK") -ps.Streams.stdout.Content = Testers.ExcludesExpression("Content-Range:", "expected no Content-Range header") -ps.Streams.stdout.Content = Testers.ContainsExpression("X-Cache: hit-stale", "expected cache hit stale") -ps.Streams.stdout.Content = Testers.ContainsExpression( - "X-Cache-Key: http://.*?/naieve/obj-bytes=0-5000", "expected cache key with bytes 0-5000") -tr.StillRunningAfter = ts - -# 11 Test - Fetch /naieve/obj with the same Range header -tr = Test.AddTestRun("cache hit on /naieve/obj post revalidation") -ps = tr.Processes.Default -tr.MakeCurlCommand(curl_and_args + ' -H "UID: NAIEVE" http://example.com/naieve/obj -r 0-5000', ts=ts) -ps.ReturnCode = 0 -ps.Streams.stdout.Content = Testers.ContainsExpression("200 OK", "expected 200 OK") -ps.Streams.stdout.Content = Testers.ExcludesExpression("Content-Range:", "expected no Content-Range header") -ps.Streams.stdout.Content = Testers.ContainsExpression("X-Cache: hit-fresh", "expected cache hit-fresh") -ps.Streams.stdout.Content = Testers.ContainsExpression( - "X-Cache-Key: http://.*?/naieve/obj-bytes=0-5000", "expected cache key with bytes 0-5000") -tr.StillRunningAfter = ts - -# 12 Test - Fetch /naieve/obj with a *different* Range header; note the cache key changes and is a miss for the same object -tr = Test.AddTestRun("cache miss on /naieve/obj") -ps = tr.Processes.Default -tr.MakeCurlCommand(curl_and_args + ' -H "UID: NAIEVE" http://example.com/naieve/obj -r 444-777', ts=ts) -ps.ReturnCode = 0 -ps.Streams.stdout.Content = Testers.ContainsExpression("200 OK", "expected 200 OK") -ps.Streams.stdout.Content = Testers.ExcludesExpression("Content-Range:", "expected no Content-Range header") -ps.Streams.stdout.Content = Testers.ContainsExpression("X-Cache: miss", "expected cache miss") -ps.Streams.stdout.Content = Testers.ContainsExpression( - "X-Cache-Key: http://.*?/naieve/obj-bytes=444-777", "expected cache key with bytes 444-777") -tr.StillRunningAfter = ts - -# 13 Test - Fetch /naieve/obj with the prior Range header; now a cache hit but we've effectively cached /naieve/obj twice -# this is why a Range normalization strategy should _always_ be employed when using `--cache-complete-responses` -tr = Test.AddTestRun("cache hit on /naieve/obj") -ps = tr.Processes.Default -tr.MakeCurlCommand(curl_and_args + ' -H "UID: NAIEVE" http://example.com/naieve/obj -r 444-777', ts=ts) -ps.ReturnCode = 0 -ps.Streams.stdout.Content = Testers.ContainsExpression("200 OK", "expected 200 OK") -ps.Streams.stdout.Content = Testers.ExcludesExpression("Content-Range:", "expected no Content-Range header") -ps.Streams.stdout.Content = Testers.ContainsExpression("X-Cache: hit", "expected cache hit-fresh") -ps.Streams.stdout.Content = Testers.ContainsExpression( - "X-Cache-Key: http://.*?/naieve/obj-bytes=444-777", "expected cache key with bytes 444-777") -tr.StillRunningAfter = ts - -# 14 Test - Fetch /naieve/obj with the original Range header (0-5000); still a cache hit -tr = Test.AddTestRun("cache hit on /naieve/obj after requesting with a different Range") -ps = tr.Processes.Default -tr.MakeCurlCommand(curl_and_args + ' -H "UID: NAIEVE" http://example.com/naieve/obj -r 0-5000', ts=ts) -ps.ReturnCode = 0 -ps.Streams.stdout.Content = Testers.ContainsExpression("200 OK", "expected 200 OK") -ps.Streams.stdout.Content = Testers.ExcludesExpression("Content-Range:", "expected no Content-Range header") -ps.Streams.stdout.Content = Testers.ContainsExpression("X-Cache: hit-fresh", "expected cache hit-fresh") -ps.Streams.stdout.Content = Testers.ContainsExpression( - "X-Cache-Key: http://.*?/naieve/obj-bytes=0-5000", "expected cache key with bytes 0-5000") -tr.StillRunningAfter = ts diff --git a/tests/gold_tests/pluginTest/cache_range_requests/cache_range_requests_cachekey.test.py b/tests/gold_tests/pluginTest/cache_range_requests/cache_range_requests_cachekey.test.py deleted file mode 100644 index b67a8a54f05..00000000000 --- a/tests/gold_tests/pluginTest/cache_range_requests/cache_range_requests_cachekey.test.py +++ /dev/null @@ -1,162 +0,0 @@ -''' -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -Test.Summary = ''' -cache_range_requests with cachekey -''' - -# Test description: -# Preload the cache with the entire asset to be range requested. -# Reload remap rule with cache_range_requests plugin -# Request content through the cache_range_requests plugin - -Test.SkipUnless( - Condition.PluginExists('cache_range_requests.so'), - Condition.PluginExists('cachekey.so'), - Condition.PluginExists('xdebug.so'), -) -Test.ContinueOnFail = False -Test.testName = "cache_range_requests_cachekey" - -# Define and configure ATS, enable traffic_ctl config reload -ts = Test.MakeATSProcess("ts") - -# Define and configure origin server -server = Test.MakeOriginServer("server", lookup_key="{%uuid}") - -# default root -req_chk = { - "headers": "GET / HTTP/1.1\r\n" + "Host: www.example.com\r\n" + "uuid: none\r\n" + "\r\n", - "timestamp": "1469733493.993", - "body": "" -} - -res_chk = {"headers": "HTTP/1.1 200 OK\r\n" + "Connection: close\r\n" + "\r\n", "timestamp": "1469733493.993", "body": ""} - -server.addResponse("sessionlog.json", req_chk, res_chk) - -body = "lets go surfin now" -bodylen = len(body) - -# this request should work -req_full = { - "headers": "GET /path HTTP/1.1\r\n" + "Host: www.example.com\r\n" + "Accept: */*\r\n" + "uuid: full\r\n" + "\r\n", - "timestamp": "1469733493.993", - "body": "" -} - -res_full = { - "headers": - "HTTP/1.1 206 Partial Content\r\n" + "Accept-Ranges: bytes\r\n" + 'Etag: "foo"\r\n' + - "Cache-Control: public, max-age=500\r\n" + "Connection: close\r\n" + "\r\n", - "timestamp": "1469733493.993", - "body": body -} - -server.addResponse("sessionlog.json", req_full, res_full) - -# this request should work -req_good = { - "headers": - "GET /path HTTP/1.1\r\n" + "Host: www.example.com\r\n" + "Accept: */*\r\n" + "Range: bytes=0-\r\n" + - "uuid: range_full\r\n" + "\r\n", - "timestamp": "1469733493.993", - "body": "" -} - -res_good = { - "headers": - "HTTP/1.1 206 Partial Content\r\n" + "Accept-Ranges: bytes\r\n" + 'Etag: "foo"\r\n' + - "Cache-Control: public, max-age=500\r\n" + "Content-Range: bytes 0-{0}/{0}\r\n".format(bodylen) + "Connection: close\r\n" + - "\r\n", - "timestamp": "1469733493.993", - "body": body -} - -server.addResponse("sessionlog.json", req_good, res_good) - -# this request should fail with a cache_range_requests asset -req_fail = { - "headers": - "GET /path HTTP/1.1\r\n" + "Host: www.fail.com\r\n" + "Accept: */*\r\n" + "Range: bytes=0-\r\n" + "uuid: range_fail\r\n" + - "\r\n", - "timestamp": "1469733493.993", - "body": "" -} - -res_fail = { - "headers": - "HTTP/1.1 206 Partial Content\r\n" + "Accept-Ranges: bytes\r\n" + 'Etag: "foo"\r\n' + - "Cache-Control: public, max-age=500\r\n" + "Content-Range: bytes 0-{0}/{0}\r\n".format(bodylen) + "Connection: close\r\n" + - "\r\n", - "timestamp": "1469733493.993", - "body": body -} - -server.addResponse("sessionlog.json", req_fail, res_fail) - -# cache range requests plugin remap, working config -ts.Disk.remap_config.AddLine( - 'map http://www.example.com http://127.0.0.1:{}'.format(server.Variables.Port) + - ' @plugin=cachekey.so @pparam=--include-headers=Range' + ' @plugin=cache_range_requests.so @pparam=--no-modify-cachekey',) - -# improperly configured cache_range_requests with cachekey -ts.Disk.remap_config.AddLine( - 'map http://www.fail.com http://127.0.0.1:{}'.format(server.Variables.Port) + ' @plugin=cachekey.so @pparam=--static-prefix=foo' - ' @plugin=cache_range_requests.so',) - -# cache debug -ts.Disk.plugin_config.AddLine('xdebug.so --enable=x-cache') - -# minimal configuration -ts.Disk.records_config.update({ - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'cache_range_requests', -}) - -curl_and_args = '-s -D /dev/stdout -o /dev/stderr -x localhost:{} -H "x-debug: x-cache"'.format(ts.Variables.port) - -# 0 Test - Fetch full asset into cache (ensure cold) -tr = Test.AddTestRun("full asset fetch") -ps = tr.Processes.Default -ps.StartBefore(server, ready=When.PortOpen(server.Variables.Port)) -ps.StartBefore(Test.Processes.ts) -tr.MakeCurlCommand(curl_and_args + ' http://www.example.com/path -H "uuid: full"', ts=ts) -ps.ReturnCode = 0 -ps.Streams.stdout.Content = Testers.ContainsExpression("X-Cache: miss", "expected cache miss for load") -tr.StillRunningAfter = ts - -# 1 Test - Fetch whole asset into cache via range request (ensure cold) -tr = Test.AddTestRun("0- asset fetch") -ps = tr.Processes.Default -tr.MakeCurlCommand(curl_and_args + ' http://www.example.com/path -r 0- -H "uuid: range_full"', ts=ts) -ps.ReturnCode = 0 -ps.Streams.stdout.Content = Testers.ContainsExpression("X-Cache: miss", "expected cache miss for load") -tr.StillRunningAfter = ts - -# 2 Test - Ensure assert happens instead of possible cache poisoning. -tr = Test.AddTestRun("Attempt poisoning") -ps = tr.Processes.Default -tr.MakeCurlCommand(curl_and_args + ' http://www.fail.com/path -r 0- -H "uuid: range_fail"', ts=ts) -ps.ReturnCode = 0 -tr.StillRunningAfter = ts - -ts.Disk.diags_log.Content = Testers.ContainsExpression("ERROR", "error condition hit") -ts.Disk.diags_log.Content = Testers.ContainsExpression( - "Failed to change the cache url, disabling cache for this transaction to avoid cache poisoning.", - "ensure failure for misconfiguration") diff --git a/tests/gold_tests/pluginTest/cache_range_requests/cache_range_requests_cachekey_global.test.py b/tests/gold_tests/pluginTest/cache_range_requests/cache_range_requests_cachekey_global.test.py deleted file mode 100644 index 748b28633b3..00000000000 --- a/tests/gold_tests/pluginTest/cache_range_requests/cache_range_requests_cachekey_global.test.py +++ /dev/null @@ -1,145 +0,0 @@ -''' -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -Test.Summary = ''' -cache_range_requests with cachekey as a global plugin -''' - -# Test description: -# Preload the cache with the entire asset to be range requested. -# Reload remap rule with cache_range_requests plugin -# Request content through the cache_range_requests plugin - -Test.SkipUnless( - Condition.PluginExists('cache_range_requests.so'), - Condition.PluginExists('cachekey.so'), - Condition.PluginExists('xdebug.so'), -) -Test.ContinueOnFail = False -Test.testName = "cache_range_requests_cachekey_global" - -# Define and configure ATS, enable traffic_ctl config reload -ts = Test.MakeATSProcess("ts") - -# Define and configure origin server -server = Test.MakeOriginServer("server", lookup_key="{%uuid}") - -# default root -req_chk = { - "headers": "GET / HTTP/1.1\r\n" + "Host: www.example.com\r\n" + "uuid: none\r\n" + "\r\n", - "timestamp": "1469733493.993", - "body": "" -} - -res_chk = {"headers": "HTTP/1.1 200 OK\r\n" + "Connection: close\r\n" + "\r\n", "timestamp": "1469733493.993", "body": ""} - -server.addResponse("sessionlog.json", req_chk, res_chk) - -body = "lets go surfin now" -bodylen = len(body) - -# this request should work -req_full = { - "headers": "GET /path HTTP/1.1\r\n" + "Host: www.example.com\r\n" + "Accept: */*\r\n" + "uuid: full\r\n" + "\r\n", - "timestamp": "1469733493.993", - "body": "" -} - -res_full = { - "headers": - "HTTP/1.1 206 Partial Content\r\n" + "Accept-Ranges: bytes\r\n" + 'Etag: "foo"\r\n' + - "Cache-Control: public, max-age=500\r\n" + "Connection: close\r\n" + "\r\n", - "timestamp": "1469733493.993", - "body": body -} - -server.addResponse("sessionlog.json", req_full, res_full) - -# this request should work -req_good = { - "headers": - "GET /path HTTP/1.1\r\n" + "Host: www.example.com\r\n" + "Accept: */*\r\n" + "Range: bytes=0-\r\n" + - "uuid: range_full\r\n" + "\r\n", - "timestamp": "1469733493.993", - "body": "" -} - -res_good = { - "headers": - "HTTP/1.1 206 Partial Content\r\n" + "Accept-Ranges: bytes\r\n" + 'Etag: "foo"\r\n' + - "Cache-Control: public, max-age=500\r\n" + "Content-Range: bytes 0-{0}/{0}\r\n".format(bodylen) + "Connection: close\r\n" + - "\r\n", - "timestamp": "1469733493.993", - "body": body -} - -server.addResponse("sessionlog.json", req_good, res_good) - -# this request should fail with a cache_range_requests asset -req_fail = { - "headers": - "GET /path HTTP/1.1\r\n" + "Host: www.fail.com\r\n" + "Accept: */*\r\n" + "Range: bytes=0-\r\n" + "uuid: range_fail\r\n" + - "\r\n", - "timestamp": "1469733493.993", - "body": "" -} - -res_fail = { - "headers": - "HTTP/1.1 206 Partial Content\r\n" + "Accept-Ranges: bytes\r\n" + 'Etag: "foo"\r\n' + - "Cache-Control: public, max-age=500\r\n" + "Content-Range: bytes 0-{0}/{0}\r\n".format(bodylen) + "Connection: close\r\n" + - "\r\n", - "timestamp": "1469733493.993", - "body": body -} - -server.addResponse("sessionlog.json", req_fail, res_fail) - -# cache range requests plugin remap, working config -ts.Disk.remap_config.AddLine('map http://www.example.com http://127.0.0.1:{}'.format(server.Variables.Port)) - -# improperly configured cache_range_requests with cachekey -ts.Disk.remap_config.AddLine('map http://www.fail.com http://127.0.0.1:{}'.format(server.Variables.Port)) - -# cache debug -ts.Disk.plugin_config.AddLines( - [ - 'cachekey.so --include-headers=Range --static-prefix=foo', - 'cache_range_requests.so --no-modify-cachekey', - 'xdebug.so --enable=x-cache,x-cache-key,x-parentselection-key', - ]) - -# minimal configuration -ts.Disk.records_config.update( - { - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'cachekey|cache_range_requests', - }) - -curl_and_args = '-s -D /dev/stdout -o /dev/stderr -x localhost:{} -H "x-debug: x-cache-key"'.format(ts.Variables.port) - -# 0 Test - Fetch full asset via range -tr = Test.AddTestRun("asset fetch via range") -ps = tr.Processes.Default -ps.StartBefore(server, ready=When.PortOpen(server.Variables.Port)) -ps.StartBefore(Test.Processes.ts) -tr.MakeCurlCommand(curl_and_args + ' http://www.example.com/path -r0- -H "uuid: full"', ts=ts) -ps.ReturnCode = 0 -ps.Streams.stdout.Content = Testers.ContainsExpression( - "X-Cache-Key: /foo/Range:bytes=0-/path", "expected cachekey style range request in cachekey") -tr.StillRunningAfter = ts diff --git a/tests/gold_tests/pluginTest/cache_range_requests/cache_range_requests_ident.test.py b/tests/gold_tests/pluginTest/cache_range_requests/cache_range_requests_ident.test.py deleted file mode 100644 index eb3ae9a923a..00000000000 --- a/tests/gold_tests/pluginTest/cache_range_requests/cache_range_requests_ident.test.py +++ /dev/null @@ -1,347 +0,0 @@ -''' -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import datetime -import os -import time - -Test.Summary = ''' -cache_range_requests X-Crr-Ident plugin test -''' - -# Test description: -# Preload the cache with the entire asset to be range requested. -# Ensure asset is stale and request with properly formed header. - -Test.SkipUnless( - Condition.PluginExists('cache_range_requests.so'), - Condition.PluginExists('xdebug.so'), -) -Test.ContinueOnFail = False -Test.testName = "cache_range_requests_ident" - -# Define and configure origin server -server = Test.MakeOriginServer("server") - -# default root -req_chk = { - "headers": "GET / HTTP/1.1\r\n" + "Host: www.example.com\r\n" + "uuid: none\r\n" + "\r\n", - "timestamp": "1469733493.993", - "body": "" -} - -res_chk = {"headers": "HTTP/1.1 200 OK\r\n" + "Connection: close\r\n" + "\r\n", "timestamp": "1469733493.993", "body": ""} - -server.addResponse("sessionlog.json", req_chk, res_chk) - -body = "lets go surfin now" -bodylen = len(body) - -# baseline for testing -last_modified = "Fri, 07 Mar 2025 18:06:58 GMT" -etag = '"772102f4-56f4bc1e6d417"' - -req_both = { - "headers": "GET /both HTTP/1.1\r\n" + "Host: www.example.com\r\n" + "Accept: */*\r\n" + "Range: bytes=0-\r\n" + "\r\n", - "timestamp": "1469733493.993", - "body": "" -} - -res_both = { - "headers": - "HTTP/1.1 206 Partial Content\r\n" + "Accept-Ranges: bytes\r\n" + "Cache-Control: max-age=1\r\n" + - "Content-Range: bytes 0-{0}/{0}\r\n".format(bodylen) + "Connection: close\r\n" + 'Etag: ' + etag + '\r\n' + - 'Last-Modified: ' + last_modified + '\r\n' + "\r\n", - "timestamp": "1469733493.993", - "body": body -} - -server.addResponse("sessionlog.json", req_both, res_both) - -req_etag = { - "headers": "GET /etag HTTP/1.1\r\n" + "Host: www.example.com\r\n" + "Accept: */*\r\n" + "Range: bytes=0-\r\n" + "\r\n", - "timestamp": "1469733493.993", - "body": "" -} - -res_etag = { - "headers": - "HTTP/1.1 206 Partial Content\r\n" + "Accept-Ranges: bytes\r\n" + "Cache-Control: max-age=1\r\n" + - "Content-Range: bytes 0-{0}/{0}\r\n".format(bodylen) + "Connection: close\r\n" + 'Etag: ' + etag + '\r\n' + "\r\n", - "timestamp": "1469733493.993", - "body": body -} - -server.addResponse("sessionlog.json", req_etag, res_etag) - -req_lm = { - "headers": "GET /lm HTTP/1.1\r\n" + "Host: www.example.com\r\n" + "Accept: */*\r\n" + "Range: bytes=0-\r\n" + "\r\n", - "timestamp": "1469733493.993", - "body": "" -} - -res_lm = { - "headers": - "HTTP/1.1 206 Partial Content\r\n" + "Accept-Ranges: bytes\r\n" + "Cache-Control: max-age=1\r\n" + - "Content-Range: bytes 0-{0}/{0}\r\n".format(bodylen) + "Connection: close\r\n" + 'Last-Modified: ' + last_modified + - '\r\n' + "\r\n", - "timestamp": "1469733493.993", - "body": body -} - -server.addResponse("sessionlog.json", req_lm, res_lm) - -# test for custom Ident header -req_custom = { - "headers": "GET /custom HTTP/1.1\r\n" + "Host: www.example.com\r\n" + "Accept: */*\r\n" + "Range: bytes=0-\r\n" + "\r\n", - "timestamp": "1469733493.993", - "body": "" -} - -etag_custom = 'foo' - -res_custom = { - "headers": - "HTTP/1.1 206 Partial Content\r\n" + "Accept-Ranges: bytes\r\n" + "Cache-Control: max-age=1\r\n" + - "Content-Range: bytes 0-{0}/{0}\r\n".format(bodylen) + "Connection: close\r\n" + 'Etag: ' + etag_custom + '\r\n' + '\r\n', - "timestamp": "1469733493.993", - "body": body -} - -server.addResponse("sessionlog.json", req_custom, res_custom) - -# Long lived asset for FRESH to STALE testing -req_fresh = { - "headers": "GET /fresh HTTP/1.1\r\n" + "Host: www.example.com\r\n" + "Accept: */*\r\n" + "Range: bytes=0-\r\n" + "\r\n", - "timestamp": "1469733493.993", - "body": "" -} - -etag_fresh = 'fresh' - -res_fresh = { - "headers": - "HTTP/1.1 206 Partial Content\r\n" + "Accept-Ranges: bytes\r\n" + "Cache-Control: max-age=3600\r\n" + - "Content-Range: bytes 0-{0}/{0}\r\n".format(bodylen) + "Connection: close\r\n" + 'Etag: ' + etag_fresh + '\r\n' + '\r\n' + - 'Last-Modified: ' + last_modified + '\r\n' + "\r\n", - "timestamp": "1469733493.993", - "body": body -} - -server.addResponse("sessionlog.json", req_fresh, res_fresh) - -# Define and configure ATS -ts = Test.MakeATSProcess("ts") - -# cache range requests plugin remap -ts.Disk.remap_config.AddLines( - [ - f'map http://ident http://127.0.0.1:{server.Variables.Port}' + ' @plugin=cache_range_requests.so @pparam=--consider-ident', - f'map http://identheader http://127.0.0.1:{server.Variables.Port}' + - ' @plugin=cache_range_requests.so @pparam=--consider-ident' + ' @pparam=--ident-header=CrrIdent', - ]) - -# cache debug -ts.Disk.plugin_config.AddLine('xdebug.so --enable=x-cache') - -# minimal configuration -ts.Disk.records_config.update({ - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'cache_range_requests', -}) - -curl_and_args = '-s -D /dev/stdout -o /dev/stderr -x localhost:{} -H "x-debug: x-cache"'.format(ts.Variables.port) - -## -## Stale to Fresh testing -## - -## Fetch short lived assets into cache - -# 0 Test - Fetch both asset into cache -tr = Test.AddTestRun("0- range cache load") -ps = tr.Processes.Default -ps.StartBefore(server, ready=When.PortOpen(server.Variables.Port)) -ps.StartBefore(Test.Processes.ts) -tr.MakeCurlCommand(curl_and_args + ' http://ident/both -r 0-', ts=ts) -ps.ReturnCode = 0 -ps.Streams.stdout.Content = Testers.ContainsExpression("X-Cache: miss", "expected cache miss for load") -tr.StillRunningAfter = ts - -# 1 Test - Fetch etag asset into cache -tr = Test.AddTestRun("0- etag cache load") -ps = tr.Processes.Default -tr.MakeCurlCommand(curl_and_args + ' http://ident/etag -r 0-', ts=ts) -ps.ReturnCode = 0 -ps.Streams.stdout.Content = Testers.ContainsExpression("X-Cache: miss", "expected cache miss for load") -tr.StillRunningAfter = ts - -# 2 Test - Fetch lm asset into cache -tr = Test.AddTestRun("0- lm cache load") -ps = tr.Processes.Default -tr.MakeCurlCommand(curl_and_args + ' http://ident/lm -r 0-', ts=ts) -ps.ReturnCode = 0 -ps.Streams.stdout.Content = Testers.ContainsExpression("X-Cache: miss", "expected cache miss for load") -tr.StillRunningAfter = ts - -## both tests - -# 3 Test - Ensure Etag header match results in hit-fresh -tr = Test.AddTestRun("0- both Etag check") -tr.DelayStart = 2 # Time to ensure stale -ps = tr.Processes.Default -tr.MakeCurlCommand(curl_and_args + f" http://ident/both -r 0- -H 'X-Crr-Ident: Etag {etag}'", ts=ts) -ps.ReturnCode = 0 -ps.Streams.stdout.Content = Testers.ContainsExpression("X-Cache: hit-fresh", "expected cache hit-fresh") -tr.StillRunningAfter = ts - -# 4 Test - Plugin expects Etag even if Last-Modified matches - hit-stale -tr = Test.AddTestRun("0- both Last-Modified check") -ps = tr.Processes.Default -tr.MakeCurlCommand(curl_and_args + f' http://ident/both -r 0- -H "X-Crr-Ident: Last-Modified {last_modified}"', ts=ts) -ps.ReturnCode = 0 -ps.Streams.stdout.Content = Testers.ContainsExpression("X-Cache: hit-stale", "expected cache hit-stale") -tr.StillRunningAfter = ts - -# 5 Test - Bad etag stays stale -tr = Test.AddTestRun("0- both bad Etag check") -ps = tr.Processes.Default -tr.MakeCurlCommand(curl_and_args + f" http://ident/both -r 0- -H 'X-Crr-Ident: Etag no_match'", ts=ts) -ps.ReturnCode = 0 -ps.Streams.stdout.Content = Testers.ContainsExpression("X-Cache: hit-stale", "expected cache hit-stale") -tr.StillRunningAfter = ts - -## etag only supplied - -# 6 Test - Ensure Etag header match results in hit-fresh -tr = Test.AddTestRun("0- etag Etag check") -ps = tr.Processes.Default -tr.MakeCurlCommand(curl_and_args + f" http://ident/etag -r 0- -H 'X-Crr-Ident: Etag {etag}'", ts=ts) -ps.ReturnCode = 0 -ps.Streams.stdout.Content = Testers.ContainsExpression("X-Cache: hit-fresh", "expected cache hit-fresh") -tr.StillRunningAfter = ts - -# 7 Test - Last modified will result in stale -tr = Test.AddTestRun("0- etag lm cache stale check") -ps = tr.Processes.Default -tr.MakeCurlCommand(curl_and_args + f' http://ident/etag -r 0- -H "X-Crr-Ident: Last-Modified {last_modified}"', ts=ts) -ps.ReturnCode = 0 -ps.Streams.stdout.Content = Testers.ContainsExpression("X-Cache: hit-stale", "expected cache stale") -tr.StillRunningAfter = ts - -# 8 Test - Bad etag stays stale -tr = Test.AddTestRun("0- etag bad Etag check") -ps = tr.Processes.Default -tr.MakeCurlCommand(curl_and_args + f" http://ident/etag -r 0- -H 'X-Crr-Ident: Etag no_match'", ts=ts) -ps.ReturnCode = 0 -ps.Streams.stdout.Content = Testers.ContainsExpression("X-Cache: hit-stale", "expected cache hit-stale") -tr.StillRunningAfter = ts - -## last modified - -# 9 Test - Last modified will result in fresh -tr = Test.AddTestRun("0- lm lm fresh check") -ps = tr.Processes.Default -tr.MakeCurlCommand(curl_and_args + f' http://ident/lm -r 0- -H "X-Crr-Ident: Last-Modified {last_modified}"', ts=ts) -ps.ReturnCode = 0 -ps.Streams.stdout.Content = Testers.ContainsExpression("X-Cache: hit-fresh", "expected cache fresh") -tr.StillRunningAfter = ts - -# 10 Test - Ensure Etag header match results in hit-fresh -tr = Test.AddTestRun("0- lm Etag check") -ps = tr.Processes.Default -tr.MakeCurlCommand(curl_and_args + f" http://ident/lm -r 0- -H 'X-Crr-Ident: Etag {etag}'", ts=ts) -ps.ReturnCode = 0 -ps.Streams.stdout.Content = Testers.ContainsExpression("X-Cache: hit-stale", "expected cache hit-stale") -tr.StillRunningAfter = ts - -## Fresh to stale testing - -# 11 Test - Fetch "fresh" into cache -tr = Test.AddTestRun("0- fresh range cache load") -ps = tr.Processes.Default -tr.MakeCurlCommand(curl_and_args + ' http://ident/fresh -r 0-', ts=ts) -ps.ReturnCode = 0 -ps.Streams.stdout.Content = Testers.ContainsExpression("X-Cache: miss", "expected cache miss for load") -tr.StillRunningAfter = ts - -# 12 Test - Ensure "fresh" is in cache -tr = Test.AddTestRun("0- fresh range cache check") -ps = tr.Processes.Default -tr.MakeCurlCommand(curl_and_args + ' http://ident/fresh -r 0-', ts=ts) -ps.ReturnCode = 0 -ps.Streams.stdout.Content = Testers.ContainsExpression("X-Cache: hit-fresh", "expected cache fresh") -tr.StillRunningAfter = ts - -# 13 request with different etag and ensure it goes stale -tr = Test.AddTestRun("0- fresh range to stale") -ps = tr.Processes.Default -tr.MakeCurlCommand(curl_and_args + " http://ident/fresh -r 0- -H 'X-Crr-Ident: Etag not_the_same'", ts=ts) -ps.ReturnCode = 0 -ps.Streams.stdout.Content = Testers.ContainsExpression("X-Cache: hit-stale", "expected cache hit-stale") -tr.StillRunningAfter = ts - -# 14 request with Last-Modified ensure it goes stale (expected etag) -tr = Test.AddTestRun("0- etag fresh range to stale") -ps = tr.Processes.Default -tr.MakeCurlCommand(curl_and_args + f' http://ident/fresh -r 0- -H "X-Crr-Ident: Last-Modified {last_modified}"', ts=ts) -ps.ReturnCode = 0 -ps.Streams.stdout.Content = Testers.ContainsExpression("X-Cache: hit-stale", "expected cache hit-stale") -tr.StillRunningAfter = ts - -# 15 Test - Ensure Etag header match results in hit-fresh -tr = Test.AddTestRun("0- fresh ensure Etag check") -ps = tr.Processes.Default -tr.MakeCurlCommand(curl_and_args + f" http://ident/fresh -r 0- -H 'X-Crr-Ident: Etag {etag_fresh}'", ts=ts) -ps.ReturnCode = 0 -ps.Streams.stdout.Content = Testers.ContainsExpression("X-Cache: hit-fresh", "expected cache hit-fresh") -tr.StillRunningAfter = ts - -# 16 hit test asset to ensure its still fresh -tr = Test.AddTestRun("0- plain request path again") -ps = tr.Processes.Default -tr.MakeCurlCommand(curl_and_args + ' http://ident/fresh -r 0-', ts=ts) -ps.ReturnCode = 0 -ps.Streams.stdout.Content = Testers.ContainsExpression("X-Cache: hit-fresh", "expected cache hit-fresh") -tr.StillRunningAfter = ts - -# 17 request with force stale -tr = Test.AddTestRun("0- force stale") -ps = tr.Processes.Default -tr.MakeCurlCommand(curl_and_args + " http://ident/fresh -r 0- -H 'X-Crr-Ident: Stale'", ts=ts) -ps.ReturnCode = 0 -ps.Streams.stdout.Content = Testers.ContainsExpression("X-Cache: hit-stale", "expected cache hit-stale") -tr.StillRunningAfter = ts - -## custom header - -# 18 Test - Fetch custom asset into cache -tr = Test.AddTestRun("0- custom range cache load") -ps = tr.Processes.Default -tr.MakeCurlCommand(curl_and_args + ' http://identheader/custom -r 0-', ts=ts) -ps.ReturnCode = 0 -ps.Streams.stdout.Content = Testers.ContainsExpression("X-Cache: miss", "expected cache miss for load") -tr.StillRunningAfter = ts - -# 19 Test - Ensure CrrIdent Etag header results in hit-fresh, custom header -tr = Test.AddTestRun("0- fresh CrrIdent Etag check") -ps = tr.Processes.Default -tr.MakeCurlCommand(curl_and_args + f" http://identheader/custom -r 0- -H 'CrrIdent: Etag {etag_custom}'", ts=ts) -ps.ReturnCode = 0 -ps.Streams.stdout.Content = Testers.ContainsExpression("X-Cache: hit-fresh", "expected cache hit-fresh") -tr.StillRunningAfter = ts diff --git a/tests/gold_tests/pluginTest/cert_update/cert_update.test.py b/tests/gold_tests/pluginTest/cert_update/cert_update.test.py deleted file mode 100644 index bbbaa31aa02..00000000000 --- a/tests/gold_tests/pluginTest/cert_update/cert_update.test.py +++ /dev/null @@ -1,148 +0,0 @@ -''' -Test the cert_update plugin. -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import ports - -Test.Summary = ''' -Test cert_update plugin. -''' - -Test.SkipIf(Condition.CurlUsingUnixDomainSocket()) -Test.SkipUnless( - Condition.HasProgram("openssl", "Openssl need to be installed on system for this test to work"), - Condition.PluginExists('cert_update.so')) - -# Set up origin server -server = Test.MakeOriginServer("server") -request_header = {"headers": "GET / HTTP/1.1\r\nHost: doesnotmatter\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -server.addResponse("sessionlog.json", request_header, response_header) - -# Set up ATS -ts = Test.MakeATSProcess("ts", enable_tls=True) - -# Set up ssl files -ts.addSSLfile("ssl/server1.pem") -ts.addSSLfile("ssl/server2.pem") -ts.addSSLfile("ssl/client1.pem") -ts.addSSLfile("ssl/client2.pem") - -# reserve port, attach it to 'ts' so it is released later -ports.get_port(ts, 's_server_port') - -ts.Disk.records_config.update( - { - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'cert_update', - 'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir), - 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts.Variables.SSLDir), - 'proxy.config.ssl.client.cert.path': '{0}'.format(ts.Variables.SSLDir), - 'proxy.config.ssl.client.private_key.path': '{0}'.format(ts.Variables.SSLDir), - 'proxy.config.url_remap.pristine_host_hdr': 1 - }) - -ts.Disk.ssl_multicert_yaml.AddLines( - """ -ssl_multicert: - - dest_ip: "*" - ssl_cert_name: server1.pem - ssl_key_name: server1.pem -""".split("\n")) - -ts.Disk.remap_config.AddLines( - [ - 'map https://bar.com http://127.0.0.1:{0}'.format(server.Variables.Port), - 'map https://foo.com https://127.0.0.1:{0}'.format(ts.Variables.s_server_port), - ]) - -ts.Disk.sni_yaml.AddLines([ - 'sni:', - '- fqdn: "*foo.com"', - ' client_cert: "client1.pem"', -]) - -# Set up plugin -Test.PrepareInstalledPlugin('cert_update.so', ts) - -# Server-Cert-Pre -# curl should see that Traffic Server presents bar.com cert from alice -tr = Test.AddTestRun("Server-Cert-Pre") -tr.Processes.Default.StartBefore(server) -tr.Processes.Default.StartBefore(Test.Processes.ts) -tr.MakeCurlCommand( - '--verbose --insecure --ipv4 --resolve bar.com:{0}:127.0.0.1 https://bar.com:{0}'.format(ts.Variables.ssl_port), ts=ts) -tr.Processes.Default.Streams.stderr = "gold/server-cert-pre.gold" -tr.Processes.Default.ReturnCode = 0 -tr.StillRunningAfter = server - -# Server-Cert-Update -tr = Test.AddTestRun("Server-Cert-Update") -tr.Processes.Default.Env = ts.Env -tr.Processes.Default.Command = ( - '{0}/traffic_ctl plugin msg cert_update.server {1}/server2.pem'.format(ts.Variables.BINDIR, ts.Variables.SSLDir)) -ts.Disk.traffic_out.Content = "gold/update.gold" -ts.StillRunningAfter = server - -# Server-Cert-After -# after use traffic_ctl to update server cert, curl should see bar.com cert from bob -tr = Test.AddTestRun("Server-Cert-After") -tr.Processes.Default.Env = ts.Env -tr.MakeCurlCommand( - '--verbose --insecure --ipv4 --resolve bar.com:{0}:127.0.0.1 https://bar.com:{0}'.format(ts.Variables.ssl_port), ts=ts) -tr.Processes.Default.Streams.stderr = "gold/server-cert-after.gold" -tr.Processes.Default.ReturnCode = 0 -ts.StillRunningAfter = server - -# Client-Cert-Pre -# s_server should see client (Traffic Server) as alice.com -tr = Test.AddTestRun("Client-Cert-Pre") -s_server = tr.Processes.Process( - "s_server", "openssl s_server -www -key {0}/server1.pem -cert {0}/server1.pem -accept {1} -Verify 1 -msg".format( - ts.Variables.SSLDir, ts.Variables.s_server_port)) -s_server.Ready = When.PortReady(ts.Variables.s_server_port) -tr.MakeCurlCommand('--verbose --insecure --ipv4 --header "Host: foo.com" https://localhost:{}'.format(ts.Variables.ssl_port), ts=ts) -tr.Processes.Default.StartBefore(s_server) -s_server.Streams.all = "gold/client-cert-pre.gold" -tr.Processes.Default.ReturnCode = 0 -ts.StillRunningAfter = server - -# Client-Cert-Update -tr = Test.AddTestRun("Client-Cert-Update") -tr.Processes.Default.Env = ts.Env -tr.Processes.Default.Command = ( - 'mv {0}/client2.pem {0}/client1.pem && {1}/traffic_ctl plugin msg cert_update.client {0}/client1.pem'.format( - ts.Variables.SSLDir, ts.Variables.BINDIR)) -ts.Disk.traffic_out.Content = "gold/update.gold" -ts.StillRunningAfter = server - -# Client-Cert-After -# after use traffic_ctl to update client cert, s_server should see client (Traffic Server) as bob.com -tr = Test.AddTestRun("Client-Cert-After") -s_server = tr.Processes.Process( - "s_server", "openssl s_server -www -key {0}/server1.pem -cert {0}/server1.pem -accept {1} -Verify 1 -msg".format( - ts.Variables.SSLDir, ts.Variables.s_server_port)) -s_server.Ready = When.PortReady(ts.Variables.s_server_port) -tr.Processes.Default.Env = ts.Env -# Move client2.pem to replace client1.pem since cert path matters in client context mapping -tr.MakeCurlCommand( - '--verbose --insecure --ipv4 --header "Host: foo.com" https://localhost:{0}'.format(ts.Variables.ssl_port), ts=ts) -tr.Processes.Default.StartBefore(s_server) -s_server.Streams.all = "gold/client-cert-after.gold" -tr.Processes.Default.ReturnCode = 0 -ts.StillRunningAfter = server diff --git a/tests/gold_tests/pluginTest/certifier/certifier.test.py b/tests/gold_tests/pluginTest/certifier/certifier.test.py deleted file mode 100644 index 21c8737de49..00000000000 --- a/tests/gold_tests/pluginTest/certifier/certifier.test.py +++ /dev/null @@ -1,294 +0,0 @@ -''' -Test certifier plugin behaviors -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. -import hashlib -import os -import re - -Test.Summary = ''' -Test certifier plugin behaviors -''' - -Test.SkipUnless(Condition.PluginExists('certifier.so')) - - -def prepare_certifier_storage(source_path: str, destination_path: str) -> None: - """Copy the certifier files and make its mutable state writable by ATS.""" - store_path = os.path.join(destination_path, 'store') - serial_path = os.path.join(destination_path, 'ca-serial.txt') - - def set_permissions() -> None: - os.chmod(serial_path, 0o666) - os.chmod(store_path, 0o777) - - Setup.Copy(source_path, destination_path) - Setup.MakeDir(store_path) - Setup.Lambda(func_setup=set_permissions, description="Make certifier state writable by ATS") - - -class DynamicCertTest: - httpsReplayFile = "replays/https.replay.yaml" - certPathSrc = os.path.join(Test.TestDirectory, "certs") - host = "www.tls.com" - certPathDest = "" - - def __init__(self): - self.setupOriginServer() - self.setupTS() - - def setupOriginServer(self): - self.server = Test.MakeVerifierServerProcess("verifier-server1", self.httpsReplayFile) - - def setupTS(self): - self.ts = Test.MakeATSProcess("ts1", enable_tls=True) - self.ts.addDefaultSSLFiles() - # copy over the cert store in which the certs will be generated/stored - self.certPathDest = os.path.join(self.ts.Variables.CONFIGDIR, "certifier-certs") - prepare_certifier_storage(self.certPathSrc, self.certPathDest) - self.ts.Disk.records_config.update( - { - "proxy.config.diags.debug.enabled": 1, - "proxy.config.diags.debug.tags": "http|certifier|ssl", - "proxy.config.ssl.server.cert.path": f'{self.ts.Variables.SSLDir}', - "proxy.config.ssl.server.private_key.path": f'{self.ts.Variables.SSLDir}', - }) - self.ts.Disk.ssl_multicert_yaml.AddLines( - """ -ssl_multicert: - - dest_ip: "*" - ssl_cert_name: server.pem - ssl_key_name: server.key -""".split("\n")) - self.ts.Disk.remap_config.AddLine(f"map / http://127.0.0.1:{self.server.Variables.http_port}/",) - self.ts.Disk.plugin_config.AddLine( - f'certifier.so -s {os.path.join(self.certPathDest, "store")} -m 1000 -c {os.path.join(self.certPathDest, "ca.cert")} -k {os.path.join(self.certPathDest, "ca.key")} -r {os.path.join(self.certPathDest, "ca-serial.txt")}' - ) - # Verify logs for dynamic generation of certs - self.ts.Disk.traffic_out.Content += Testers.ContainsExpression( - "creating shadow certs", "Verify the certifier plugin generates the certificate dynamically.") - - def runHTTPSTraffic(self): - tr = Test.AddTestRun("Test dynamic generation of certs") - tr.AddVerifierClientProcess( - "client1", self.httpsReplayFile, http_ports=[self.ts.Variables.port], https_ports=[self.ts.Variables.ssl_port]) - tr.Processes.Default.StartBefore(self.server) - tr.Processes.Default.StartBefore(self.ts) - tr.StillRunningAfter = self.server - tr.StillRunningAfter = self.ts - - def verifyCert(self, certPath): - tr = Test.AddTestRun("Verify the content of the generated cert") - tr.Processes.Default.Command = f'openssl x509 -in {certPath} -text -noout' - tr.Processes.Default.ReturnCode = 0 - # Verify certificate content - tr.Processes.Default.Streams.All += Testers.ContainsExpression( - "Subject: CN ?= ?www.tls.com", "Subject should match the host in the request") - tr.Processes.Default.Streams.All += Testers.ContainsExpression( - r"X509v3 extensions:\n.*X509v3 Subject Alternative Name:.*\n.*DNS:www.tls.com", - "Should contain the SAN extension", - reflags=re.MULTILINE) - - def verifyCertNotExist(self, certPath): - tr = Test.AddTestRun("Verify the cert doesn't exist in the store") - tr.Processes.Default.Command = "echo verify" - certFile = tr.Disk.File(certPath, exists=False) - - def run(self): - # the certifier plugin generates the cert and store it in a directory - # named with the first three character of the md5 hash of the hostname - genCertPath = os.path.join( - self.certPathDest, 'store', str(hashlib.md5(self.host.encode('utf-8')).hexdigest()[:3]), self.host + ".crt") - self.verifyCertNotExist(genCertPath) - self.runHTTPSTraffic() - self.verifyCert(genCertPath) - - -DynamicCertTest().run() - - -class ReuseExistingCertTest: - httpsReplayFile = "replays/https-two-sessions.replay.yaml" - certPathSrc = os.path.join(Test.TestDirectory, "certs") - host = "www.tls.com" - certPathDest = "" - - def __init__(self): - self.setupOriginServer() - self.setupTS() - - def setupOriginServer(self): - self.server = Test.MakeVerifierServerProcess("verifier-server2", self.httpsReplayFile) - - def setupTS(self): - self.ts = Test.MakeATSProcess("ts2", enable_tls=True) - self.ts.addDefaultSSLFiles() - # copy over the cert store in which the certs will be generated/stored - self.certPathDest = os.path.join(self.ts.Variables.CONFIGDIR, "certifier-certs") - prepare_certifier_storage(self.certPathSrc, self.certPathDest) - self.ts.Disk.records_config.update( - { - "proxy.config.diags.debug.enabled": 1, - "proxy.config.diags.debug.tags": "http|certifier|ssl", - "proxy.config.ssl.server.cert.path": f'{self.ts.Variables.SSLDir}', - "proxy.config.ssl.server.private_key.path": f'{self.ts.Variables.SSLDir}', - }) - self.ts.Disk.ssl_multicert_yaml.AddLines( - """ -ssl_multicert: - - dest_ip: "*" - ssl_cert_name: server.pem - ssl_key_name: server.key -""".split("\n")) - self.ts.Disk.remap_config.AddLine(f"map / http://127.0.0.1:{self.server.Variables.http_port}/",) - self.ts.Disk.plugin_config.AddLine( - f'certifier.so -s {os.path.join(self.certPathDest, "store")} -m 1000 -c {os.path.join(self.certPathDest, "ca.cert")} -k {os.path.join(self.certPathDest, "ca.key")} -r {os.path.join(self.certPathDest, "ca-serial.txt")}' - ) - # Verify logs for reusing existing cert - self.ts.Disk.traffic_out.Content += Testers.ContainsExpression( - "reusing existing cert and context for www.tls.com", "Should reuse the existing certificate") - - def runHTTPSTraffic(self): - tr = Test.AddTestRun("Test dynamic generation of certs") - tr.AddVerifierClientProcess( - "client2", self.httpsReplayFile, http_ports=[self.ts.Variables.port], https_ports=[self.ts.Variables.ssl_port]) - tr.Processes.Default.StartBefore(self.server) - tr.Processes.Default.StartBefore(self.ts) - tr.StillRunningAfter = self.server - tr.StillRunningAfter = self.ts - - def run(self): - self.runHTTPSTraffic() - - -ReuseExistingCertTest().run() - - -class UnsafeSniTest: - httpsReplayFile = "replays/https-path-traversal.replay.yaml" - certPathSrc = os.path.join(Test.TestDirectory, "certs") - escapedCert = "certifier-escaped.crt" - certPathDest = "" - - def __init__(self): - self.setupOriginServer() - self.setupTS() - - def setupOriginServer(self): - self.server = Test.MakeVerifierServerProcess("verifier-server3", self.httpsReplayFile) - - def setupTS(self): - self.ts = Test.MakeATSProcess("ts3", enable_tls=True) - self.ts.addDefaultSSLFiles() - self.certPathDest = os.path.join(self.ts.Variables.CONFIGDIR, "certifier-certs") - prepare_certifier_storage(self.certPathSrc, self.certPathDest) - self.ts.Disk.records_config.update( - { - "proxy.config.diags.debug.enabled": 1, - "proxy.config.diags.debug.tags": "http|certifier|ssl", - "proxy.config.ssl.server.cert.path": f'{self.ts.Variables.SSLDir}', - "proxy.config.ssl.server.private_key.path": f'{self.ts.Variables.SSLDir}', - }) - self.ts.Disk.ssl_multicert_yaml.AddLines( - """ -ssl_multicert: - - dest_ip: "*" - ssl_cert_name: server.pem - ssl_key_name: server.key -""".split("\n")) - self.ts.Disk.remap_config.AddLine(f"map / http://127.0.0.1:{self.server.Variables.http_port}/",) - self.ts.Disk.plugin_config.AddLine( - f'certifier.so -s {os.path.join(self.certPathDest, "store")} -m 1000 -c {os.path.join(self.certPathDest, "ca.cert")} -k {os.path.join(self.certPathDest, "ca.key")} -r {os.path.join(self.certPathDest, "ca-serial.txt")}' - ) - self.ts.Disk.traffic_out.Content += Testers.ContainsExpression( - "rejecting unsafe SNI for certificate storage", "Should reject SNI values that cannot be used safely as file names.") - - def runHTTPSTraffic(self): - tr = Test.AddTestRun("Test unsafe SNI is rejected") - tr.AddVerifierClientProcess( - "client3", self.httpsReplayFile, http_ports=[self.ts.Variables.port], https_ports=[self.ts.Variables.ssl_port]) - tr.Processes.Default.StartBefore(self.server) - tr.Processes.Default.StartBefore(self.ts) - tr.StillRunningAfter = self.server - tr.StillRunningAfter = self.ts - - def verifyCertNotExist(self, certPath): - tr = Test.AddTestRun("Verify unsafe SNI did not escape the store") - tr.Processes.Default.Command = "echo verify" - tr.Disk.File(certPath, exists=False) - - def run(self): - escapedCertPath = os.path.join(self.certPathDest, self.escapedCert) - self.verifyCertNotExist(escapedCertPath) - self.runHTTPSTraffic() - self.verifyCertNotExist(escapedCertPath) - - -UnsafeSniTest().run() - - -class NoSniTest: - httpsReplayFile = "replays/https-no-sni.replay.yaml" - certPathSrc = os.path.join(Test.TestDirectory, "certs") - certPathDest = "" - - def __init__(self): - self.setupOriginServer() - self.setupTS() - - def setupOriginServer(self): - self.server = Test.MakeVerifierServerProcess("verifier-server4", self.httpsReplayFile) - - def setupTS(self): - self.ts = Test.MakeATSProcess("ts4", enable_tls=True) - self.ts.addDefaultSSLFiles() - self.certPathDest = os.path.join(self.ts.Variables.CONFIGDIR, "certifier-certs") - prepare_certifier_storage(self.certPathSrc, self.certPathDest) - self.ts.Disk.records_config.update( - { - "proxy.config.diags.debug.enabled": 1, - "proxy.config.diags.debug.tags": "http|certifier|ssl", - "proxy.config.ssl.server.cert.path": f'{self.ts.Variables.SSLDir}', - "proxy.config.ssl.server.private_key.path": f'{self.ts.Variables.SSLDir}', - }) - self.ts.Disk.ssl_multicert_yaml.AddLines( - """ -ssl_multicert: - - dest_ip: "*" - ssl_cert_name: server.pem - ssl_key_name: server.key -""".split("\n")) - self.ts.Disk.remap_config.AddLine(f"map / http://127.0.0.1:{self.server.Variables.http_port}/",) - self.ts.Disk.plugin_config.AddLine( - f'certifier.so -s {os.path.join(self.certPathDest, "store")} -m 1000 -c {os.path.join(self.certPathDest, "ca.cert")} -k {os.path.join(self.certPathDest, "ca.key")} -r {os.path.join(self.certPathDest, "ca-serial.txt")}' - ) - self.ts.Disk.traffic_out.Content += Testers.ContainsExpression( - "no SNI available; using default certificate", "Should continue the handshake when no SNI is available.") - - def runHTTPSTraffic(self): - tr = Test.AddTestRun("Test missing SNI falls back to default cert") - tr.AddVerifierClientProcess( - "client4", self.httpsReplayFile, http_ports=[self.ts.Variables.port], https_ports=[self.ts.Variables.ssl_port]) - tr.Processes.Default.StartBefore(self.server) - tr.Processes.Default.StartBefore(self.ts) - tr.StillRunningAfter = self.server - tr.StillRunningAfter = self.ts - - def run(self): - self.runHTTPSTraffic() - - -NoSniTest().run() diff --git a/tests/gold_tests/pluginTest/client_context_dump/client_context_dump.test.py b/tests/gold_tests/pluginTest/client_context_dump/client_context_dump.test.py deleted file mode 100644 index 7645684e339..00000000000 --- a/tests/gold_tests/pluginTest/client_context_dump/client_context_dump.test.py +++ /dev/null @@ -1,81 +0,0 @@ -''' -Test the client_context_dump plugin. -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -Test.Summary = ''' -Test client_context_dump plugin -''' - -Test.SkipUnless(Condition.PluginExists('client_context_dump.so')) - -# Set up ATS -ts = Test.MakeATSProcess("ts", enable_tls=True) - -# Set up ssl files -ts.addSSLfile("ssl/one.com.pem") -ts.addSSLfile("ssl/two.com.pem") - -ts.Disk.records_config.update( - { - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'client_context_dump', - 'proxy.config.ssl.server.cert.path': '{}'.format(ts.Variables.SSLDir), - 'proxy.config.ssl.server.private_key.path': '{}'.format(ts.Variables.SSLDir), - 'proxy.config.ssl.client.cert.path': '{}'.format(ts.Variables.SSLDir), - 'proxy.config.ssl.client.private_key.path': '{}'.format(ts.Variables.SSLDir), - }) - -ts.Disk.ssl_multicert_yaml.AddLines( - """ -ssl_multicert: - - dest_ip: "*" - ssl_cert_name: one.com.pem - ssl_key_name: one.com.pem -""".split("\n")) - -ts.Disk.sni_yaml.AddLines( - [ - 'sni:', - '- fqdn: "*one.com"', - ' client_cert: "one.com.pem"', - '- fqdn: "*two.com"', - ' client_cert: "two.com.pem"', - ]) - -# Set up plugin -Test.PrepareInstalledPlugin('client_context_dump.so', ts) - -# custom log comparison. Verify the two certs we have loaded are dumped -log = Test.Disk.File(ts.Variables.LOGDIR + '/client_context_dump.log', exists=True) -log.Content = Testers.ContainsExpression('CN=two.com', 'Info on two.com.pem should dump') -log.Content += Testers.ContainsExpression("CN=one.com", "Info on one.com.pem should dump") - -# traffic server test -t = Test.AddTestRun("Test traffic server started properly") -t.StillRunningAfter = Test.Processes.ts - -p = t.Processes.Default -t.MakeCurlCommand("http://127.0.0.1:{0}".format(ts.Variables.port), ts=ts) -p.ReturnCode = 0 -p.StartBefore(Test.Processes.ts) - -# Client contexts test -tr = Test.AddTestRun() -tr.Processes.Default.Env = ts.Env -tr.Processes.Default.Command = ('{0}/traffic_ctl plugin msg client_context_dump.t 1'.format(ts.Variables.BINDIR)) -tr.Processes.Default.ReturnCode = 0 diff --git a/tests/gold_tests/pluginTest/combo_handler/combo_handler.test.py b/tests/gold_tests/pluginTest/combo_handler/combo_handler.test.py deleted file mode 100644 index 235a9c482bc..00000000000 --- a/tests/gold_tests/pluginTest/combo_handler/combo_handler.test.py +++ /dev/null @@ -1,182 +0,0 @@ -''' -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import os -import sys - -Test.Summary = ''' -Test combo_handler plugin -''' - -# Skip if plugin not present. -# -Test.SkipUnless(Condition.PluginExists('combo_handler.so'),) - -# Function to generate a unique data file path (in the top level of the test's run directory), put data (in string 'data') into -# the file, and return the file name. -# -_data_file__file_count = 0 - - -def data_file(data): - global _data_file__file_count - file_path = Test.RunDirectory + f"/tcp_client_in_{_data_file__file_count}" - _data_file__file_count += 1 - with open(file_path, "x") as f: - f.write(data) - return file_path - - -# Function to return command (string) to run tcp_client.py tool. 'host' 'port', and 'file_path' are the parameters to tcp_client. -# - - -def tcp_client_cmd(host, port, file_path): - return f"{sys.executable} {Test.Variables.AtsTestToolsDir}/tcp_client.py {host} {port} {file_path}" - - -# Function to return command (string) to run tcp_client.py tool. 'host' and 'port' are the first two parameters to tcp_client. -# 'data' is the data to put in the data file input to tcp_client. -# - - -def tcp_client(host, port, data): - return tcp_client_cmd(host, port, data_file(data)) - - -server = Test.MakeOriginServer("server") - - -def add_server_obj(content_type, path, cache_control="public, max-age=31536000"): - request_header = { - "headers": "GET " + path + " HTTP/1.1\r\n" + "Host: just.any.thing\r\n\r\n", - "timestamp": "1469733493.993", - "body": "" - } - response_header = { - "headers": - "HTTP/1.1 200 OK\r\n" + "Connection: close\r\n" + 'Etag: "359670651"\r\n' + "Cache-Control: " + cache_control + "\r\n" + - "Accept-Ranges: bytes\r\n" + "Content-Type: " + content_type + "\r\n" + "\r\n", - "timestamp": "1469733493.993", - "body": "Content for " + path + "\n" - } - server.addResponse("sessionfile.log", request_header, response_header) - - -add_server_obj("text/css ; charset=utf-8", "/obj1") -add_server_obj("text/javascript", "/sub/obj2") -add_server_obj("text/argh", "/obj3") -add_server_obj("application/javascript", "/obj4") -add_server_obj("application/javascript", "/s/assets/module:variant_v1.js") -# Empty Content-Type header: the field is present but carries no value. -# With an allowlist configured this must be rejected; otherwise it would -# silently bypass the type check. -add_server_obj("", "/obj_empty_ct") -# Exercises CacheControlHeader::update via the refactored -# parse_cache_control_value(): private must propagate to the combo -# response and the smaller max-age must win across objects. -add_server_obj("text/javascript", "/obj_priv_short", cache_control="private, max-age=60") -# max-age=0 is a valid directive ("must revalidate"). The combo must -# propagate it as the minimum, not silently fall back to the 10-year -# default. -add_server_obj("text/javascript", "/obj_revalidate", cache_control="public, max-age=0") - -ts = Test.MakeATSProcess("ts") - -ts.Disk.records_config.update({ - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'http|combo_handler', -}) - -ts.Disk.plugin_config.AddLine("combo_handler.so - - - ctwl.txt") - -ts.Disk.remap_config.AddLine('map http://xyz/ http://127.0.0.1/ @plugin=combo_handler.so') -ts.Disk.remap_config.AddLine(f'map http://localhost/127.0.0.1/ http://127.0.0.1:{server.Variables.Port}/') -ts.Disk.remap_config.AddLine(f'map http://localhost/sub/ http://127.0.0.1:{server.Variables.Port}/sub/') -ts.Disk.remap_config.AddLine(f'map http://localhost/s/ http://127.0.0.1:{server.Variables.Port}/s/') - -# Configure the combo_handler's configuration file. -ts.Setup.Copy("ctwl.txt", ts.Variables.CONFIGDIR) - -tr = Test.AddTestRun() -tr.Processes.Default.StartBefore(ts) -tr.Processes.Default.StartBefore(server) -tr.Processes.Default.Command = "echo start stuff" -tr.Processes.Default.ReturnCode = 0 - -tr = Test.AddTestRun() -tr.Processes.Default.Command = tcp_client( - "127.0.0.1", ts.Variables.port, - "GET /admin/v1/combo?obj1&sub:obj2&obj3 HTTP/1.1\n" + "Host: xyz\n" + "Connection: close\n" + "\n") -tr.Processes.Default.ReturnCode = 0 -f = tr.Disk.File("_output/1-tr-Default/stream.all.txt") -f.Content = "combo_handler_files/tr1.gold" - -tr = Test.AddTestRun() -tr.Processes.Default.Command = tcp_client( - "127.0.0.1", ts.Variables.port, - "GET /admin/v1/combo?obj1&sub:obj2&obj4 HTTP/1.1\n" + "Host: xyz\n" + "Connection: close\n" + "\n") -tr.Processes.Default.ReturnCode = 0 -f = tr.Disk.File("_output/2-tr-Default/stream.all.txt") -f.Content = "combo_handler_files/tr2.gold" - -tr = Test.AddTestRun() -tr.Processes.Default.Command = tcp_client( - "127.0.0.1", ts.Variables.port, - "GET /admin/v1/combo?s:assets/module:variant_v1.js HTTP/1.1\n" + "Host: xyz\n" + "Connection: close\n" + "\n") -tr.Processes.Default.ReturnCode = 0 -f = tr.Disk.File("_output/3-tr-Default/stream.all.txt") -f.Content = Testers.ContainsExpression("HTTP/1.1 200 OK", "Should successfully combine an object with a colon in its path") -f.Content += Testers.ContainsExpression( - "Content for /s/assets/module:variant_v1.js", "Should fetch the object path after the first colon") - -# An object whose Content-Type header is present but empty must not slip -# past the allowlist. Pairing it with the allowed obj1 ensures the request -# would have succeeded if the empty value were not enforced -- so a 403 -# here is a regression check for the bypass fix. -tr = Test.AddTestRun() -tr.Processes.Default.Command = tcp_client( - "127.0.0.1", ts.Variables.port, - "GET /admin/v1/combo?obj1&obj_empty_ct HTTP/1.1\n" + "Host: xyz\n" + "Connection: close\n" + "\n") -tr.Processes.Default.ReturnCode = 0 -f = tr.Disk.File("_output/4-tr-Default/stream.all.txt") -f.Content = "combo_handler_files/tr3.gold" - -# Combining a long-TTL public object with a short-TTL private object -# must yield a Cache-Control of "max-age=60, private". This exercises -# both parse paths in the refactored parse_cache_control_value(). -tr = Test.AddTestRun() -tr.Processes.Default.Command = tcp_client( - "127.0.0.1", ts.Variables.port, - "GET /admin/v1/combo?obj1&obj_priv_short HTTP/1.1\n" + "Host: xyz\n" + "Connection: close\n" + "\n") -tr.Processes.Default.ReturnCode = 0 -f = tr.Disk.File("_output/5-tr-Default/stream.all.txt") -f.Content = "combo_handler_files/cache_control_aggregation.gold" - -# An object with max-age=0 must drive the combined response down to -# max-age=0 instead of being filtered out as if it were absent. Pairs -# with the long-TTL obj1 to confirm the min-merge respects zero. -tr = Test.AddTestRun() -tr.Processes.Default.Command = tcp_client( - "127.0.0.1", ts.Variables.port, - "GET /admin/v1/combo?obj1&obj_revalidate HTTP/1.1\n" + "Host: xyz\n" + "Connection: close\n" + "\n") -tr.Processes.Default.ReturnCode = 0 -f = tr.Disk.File("_output/6-tr-Default/stream.all.txt") -f.Content = "combo_handler_files/max_age_zero.gold" - -ts.Disk.diags_log.Content = Testers.ContainsExpression("ERROR", "Some tests are failure tests") diff --git a/tests/gold_tests/pluginTest/compress/compress-cache-untransformed.test.py b/tests/gold_tests/pluginTest/compress/compress-cache-untransformed.test.py deleted file mode 100644 index bf5e0a85ca3..00000000000 --- a/tests/gold_tests/pluginTest/compress/compress-cache-untransformed.test.py +++ /dev/null @@ -1,118 +0,0 @@ -''' -Regression test for https://github.com/apache/trafficserver/issues/12244 - -The crash requires two conditions in the same transaction: -1. An intermediate response (100 Continue) is forwarded to the client, which - sets client_response_hdr_bytes to the intermediate header size via - setup_100_continue_transfer(). -2. The final response goes through a compress transform with untransformed - cache writing (cache=true), which calls setup_server_transfer_to_transform(). - For non-chunked responses, client_response_hdr_bytes is NOT reset to 0. - -perform_cache_write_action() then passes the stale client_response_hdr_bytes -as skip_bytes to the cache-write consumer, but the server-to-transform tunnel -buffer contains only body data (no headers). The assertion in -HttpTunnel::producer_run fires: - - c->skip_bytes <= c->buffer_reader->read_avail() - -This test uses a custom origin that sends "100 Continue" followed by a -compressible, non-chunked 200 OK to trigger the exact crash path. -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import sys - -from ports import get_port - -Test.Summary = ''' -Regression test for compress plugin with cache=true causing assertion failure -when origin sends 100 Continue before a compressible response (#12244) -''' - -Test.SkipUnless(Condition.PluginExists('compress.so')) - - -class CompressCacheUntransformedTest: - - def __init__(self): - self.setupTS() - self.run() - - def setupTS(self): - self.ts = Test.MakeATSProcess("ts", enable_cache=True) - - def run(self): - tr = Test.AddTestRun() - - # Copy scripts into the test run directory. - tr.Setup.CopyAs("compress_100_continue_origin.py") - tr.Setup.Copy("etc/compress-cache-false.config") - - # Create and configure the custom origin server process. - origin = tr.Processes.Process("origin") - origin_port = get_port(origin, 'http_port') - origin.Command = (f'{sys.executable} compress_100_continue_origin.py' - f' --port {origin_port}') - origin.Ready = When.PortOpenv4(origin_port) - origin.ReturnCode = 0 - - # Configure ATS. - self.ts.Disk.records_config.update( - { - "proxy.config.diags.debug.enabled": 1, - "proxy.config.diags.debug.tags": "http|compress|http_tunnel", - # Do NOT send 100 Continue from ATS - let the origin send it. - # This ensures ATS processes the origin's 100 via - # handle_100_continue_response -> setup_100_continue_transfer, - # which sets client_response_hdr_bytes. - "proxy.config.http.send_100_continue_response": 0, - # Enable POST caching so that the 200 OK is cached, triggering - # the cache write path where the stale client_response_hdr_bytes - # causes the crash. - "proxy.config.http.cache.post_method": 1, - }) - - self.ts.Disk.remap_config.AddLine( - f'map / http://127.0.0.1:{origin_port}/' - f' @plugin=compress.so' - f' @pparam={Test.RunDirectory}/compress-cache-false.config') - - # Client sends a POST with Expect: 100-continue but does not wait for - # the 100 response before sending the body (--expect100-timeout 0). - # The crash is triggered by ATS processing the origin's 100 Continue, - # not by the client's behaviour during the handshake. - client = tr.Processes.Default - client.Command = ( - f'curl --http1.1 -s -o /dev/null' - f' -X POST' - f' -H "Accept-Encoding: gzip"' - f' -H "Expect: 100-continue"' - f' --expect100-timeout 0' - f' --data "test body data"' - f' http://127.0.0.1:{self.ts.Variables.port}/test/resource.js') - client.ReturnCode = 0 - client.StartBefore(origin) - client.StartBefore(self.ts) - - # The key assertion: ATS must still be running after the test. - # Without the fix, ATS would have crashed with a failed assertion - # in HttpTunnel::producer_run. - tr.StillRunningAfter = self.ts - - -CompressCacheUntransformedTest() diff --git a/tests/gold_tests/pluginTest/compress/compress-content-type-params.test.py b/tests/gold_tests/pluginTest/compress/compress-content-type-params.test.py deleted file mode 100644 index 0848f754899..00000000000 --- a/tests/gold_tests/pluginTest/compress/compress-content-type-params.test.py +++ /dev/null @@ -1,67 +0,0 @@ -''' -Test compress plugin matching with Content-Type parameters -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -Test.Summary = ''' -Verify content_type_ignore_parameters affects compressible-content-type matching -''' - -Test.SkipUnless(Condition.PluginExists('compress.so')) - - -class CompressPluginContentTypeParamsTest: - replayFile = "replay/compress-content-type-params.replay.yaml" - - def __init__(self): - self.setupOriginServer() - self.setupTS() - - def setupOriginServer(self): - self.server = Test.MakeVerifierServerProcess("verifier-server", self.replayFile) - - def setupTS(self): - self.ts = Test.MakeATSProcess("ts") - self.ts.Disk.records_config.update( - { - "proxy.config.diags.debug.enabled": 1, - "proxy.config.diags.debug.tags": "http|compress", - "proxy.config.http.insert_response_via_str": 2, - }) - - # Copy configs into the run directory - self.ts.Setup.Copy("etc/ignore-params-false.config") - self.ts.Setup.Copy("etc/ignore-params-true.config") - - self.ts.Disk.remap_config.AddLines( - [ - f'map /ignore-params-false/ http://127.0.0.1:{self.server.Variables.http_port}/ @plugin=compress.so' + - f' @pparam={Test.RunDirectory}/ignore-params-false.config', - f'map /ignore-params-true/ http://127.0.0.1:{self.server.Variables.http_port}/ @plugin=compress.so' + - f' @pparam={Test.RunDirectory}/ignore-params-true.config', - ]) - - def run(self): - tr = Test.AddTestRun() - tr.AddVerifierClientProcess("verifier-client", self.replayFile, http_ports=[self.ts.Variables.port]) - tr.Processes.Default.StartBefore(self.ts) - tr.Processes.Default.StartBefore(self.server) - tr.StillRunningAfter = self.ts - tr.StillRunningAfter = self.server - - -CompressPluginContentTypeParamsTest().run() diff --git a/tests/gold_tests/pluginTest/compress/compress-range.test.py b/tests/gold_tests/pluginTest/compress/compress-range.test.py deleted file mode 100644 index 0c15710ef5d..00000000000 --- a/tests/gold_tests/pluginTest/compress/compress-range.test.py +++ /dev/null @@ -1,73 +0,0 @@ -''' -Test compress plugin with range request -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -Test.Summary = ''' -Test compress plugin with range request -''' - -Test.SkipUnless(Condition.PluginExists('compress.so')) - - -class CompressPluginTest: - replayFile = "replay/compress-and-range.replay.yaml" - - def __init__(self): - self.setupOriginServer() - self.setupTS() - - def setupOriginServer(self): - self.server = Test.MakeVerifierServerProcess("verifier-server", self.replayFile) - - def setupTS(self): - self.ts = Test.MakeATSProcess("ts") - self.ts.Disk.records_config.update( - { - "proxy.config.diags.debug.enabled": 1, - "proxy.config.diags.debug.tags": "http|compress", - "proxy.config.http.insert_response_via_str": 2, - }) - - self.ts.Setup.Copy("etc/cache-true-remove-range.config") - self.ts.Setup.Copy("etc/cache-true-remove-accept-encoding.config") - self.ts.Setup.Copy("etc/cache-true-no-compression.config") - - self.ts.Disk.remap_config.AddLines( - { - f""" -map /cache-true-remove-range/ http://127.0.0.1:{self.server.Variables.http_port}/ \ - @plugin=compress.so \ - @pparam={Test.RunDirectory}/cache-true-remove-range.config -map /cache-true-remove-accept-encoding/ http://127.0.0.1:{self.server.Variables.http_port}/ \ - @plugin=compress.so \ - @pparam={Test.RunDirectory}/cache-true-remove-accept-encoding.config -map /cache-true-no-compression/ http://127.0.0.1:{self.server.Variables.http_port}/ \ - @plugin=compress.so \ - @pparam={Test.RunDirectory}/cache-true-no-compression.config -""" - }) - - def run(self): - tr = Test.AddTestRun() - tr.AddVerifierClientProcess("verifier-client", self.replayFile, http_ports=[self.ts.Variables.port]) - tr.Processes.Default.StartBefore(self.ts) - tr.Processes.Default.StartBefore(self.server) - tr.StillRunningAfter = self.ts - tr.StillRunningAfter = self.server - - -CompressPluginTest().run() diff --git a/tests/gold_tests/pluginTest/compress/compress.test.py b/tests/gold_tests/pluginTest/compress/compress.test.py deleted file mode 100644 index 181f3595d99..00000000000 --- a/tests/gold_tests/pluginTest/compress/compress.test.py +++ /dev/null @@ -1,325 +0,0 @@ -''' -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -Test.Summary = ''' -Test compress plugin -''' - -# This test case is very bare-bones. It only covers a few scenarios that have caused problems. - -# Skip if plugins not present. -# -Test.SkipUnless( - Condition.PluginExists('compress.so'), Condition.PluginExists('conf_remap.so'), Condition.HasATSFeature('TS_HAS_BROTLI'), - Condition.HasATSFeature('TS_HAS_ZSTD')) - -server = Test.MakeOriginServer("server", options={'--load': f'{Test.TestDirectory}/compress_observer.py'}) - -# Need a fairly big body, otherwise the plugin will refuse to compress -line = "lets go surfin now everybodys learnin how" -body = f'{line}\n' * 24 + line -orig_path = f'{Test.RunDirectory}/orig.txt' -open(orig_path, 'w').write(body) - -# expected response from the origin server -response_header = { - "headers": - "HTTP/1.1 200 OK\r\nConnection: close\r\n" + 'Etag: "359670651"\r\n' + "Cache-Control: public, max-age=31536000\r\n" + - "Accept-Ranges: bytes\r\n" + "Content-Type: text/javascript\r\n" + "\r\n", - "timestamp": "1469733493.993", - "body": body -} -for i in range(6): - # add request/response to the server dictionary - request_header = {"headers": f"GET /obj{i} HTTP/1.1\r\nHost: just.any.thing\r\n\r\n", "timestamp": "1469733493.993", "body": ""} - server.addResponse("sessionfile.log", request_header, response_header) - -# post for the origin server -post_request_header = { - "headers": - "POST /obj3 HTTP/1.1\r\nHost: just.any.thing\r\nContent-Type: application/x-www-form-urlencoded\r\nContent-Length: 11\r\n\r\n", - "timestamp": "1469733493.993", - "body": "knock knock" -} -server.addResponse("sessionfile.log", post_request_header, response_header) - - -def curl(ts, idx, encodingList, out_path): - return ( - f"-o {out_path} --verbose --proxy http://127.0.0.1:{ts.Variables.port}" - f" --header 'X-Ats-Compress-Test: {idx}/{encodingList}'" - f" --header 'Accept-Encoding: {encodingList}' 'http://ae-{idx}/obj{idx}'" - " 2>> compress_long.log ; printf '\n===\n' >> compress_long.log") - - -def curl_post(ts, idx, encodingList, out_path): - return ( - f"-o {out_path} --verbose -d 'knock knock' --proxy http://127.0.0.1:{ts.Variables.port}" - f" --header 'X-Ats-Compress-Test: {idx}/{encodingList}'" - f" --header 'Accept-Encoding: {encodingList}' 'http://ae-{idx}/obj{idx}'" - " 2>> compress_long.log ; printf '\n===\n' >> compress_long.log") - - -waitForServer = True - -waitForTs = True - -ts = Test.MakeATSProcess("ts", enable_cache=False) - -ts.Disk.records_config.update( - { - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'compress', - 'proxy.config.http.normalize_ae': 0, - }) - -ts.Setup.Copy("compress.config") -ts.Setup.Copy("compress2.config") -ts.Setup.Copy("compress3.config") - -ts.Disk.remap_config.AddLine( - f'map http://ae-0/ http://127.0.0.1:{server.Variables.Port}/' + - f' @plugin=compress.so @pparam={Test.RunDirectory}/compress.config') -ts.Disk.remap_config.AddLine( - f'map http://ae-1/ http://127.0.0.1:{server.Variables.Port}/' + - ' @plugin=conf_remap.so @pparam=proxy.config.http.normalize_ae=1' + - f' @plugin=compress.so @pparam={Test.RunDirectory}/compress.config') -ts.Disk.remap_config.AddLine( - f'map http://ae-2/ http://127.0.0.1:{server.Variables.Port}/' + - ' @plugin=conf_remap.so @pparam=proxy.config.http.normalize_ae=2' + - f' @plugin=compress.so @pparam={Test.RunDirectory}/compress2.config') -ts.Disk.remap_config.AddLine( - f'map http://ae-3/ http://127.0.0.1:{server.Variables.Port}/' + - ' @plugin=conf_remap.so @pparam=proxy.config.http.normalize_ae=3' + - f' @plugin=compress.so @pparam={Test.RunDirectory}/compress2.config') -ts.Disk.remap_config.AddLine( - f'map http://ae-4/ http://127.0.0.1:{server.Variables.Port}/' + - ' @plugin=conf_remap.so @pparam=proxy.config.http.normalize_ae=4' + - f' @plugin=compress.so @pparam={Test.RunDirectory}/compress3.config') -ts.Disk.remap_config.AddLine( - f'map http://ae-5/ http://127.0.0.1:{server.Variables.Port}/' + - ' @plugin=conf_remap.so @pparam=proxy.config.http.normalize_ae=5' + - f' @plugin=compress.so @pparam={Test.RunDirectory}/compress3.config') - -out_path_counter = 0 - - -def get_out_path(): - global out_path_counter - out_path = f'{Test.RunDirectory}/curl_out_{out_path_counter}' - out_path_counter += 1 - return out_path - - -deflate_path = f'{Test.RunDirectory}/deflate.txt' - - -def get_verify_command(out_path, decrompressor): - return f"{decrompressor} {out_path} > {deflate_path} && diff {deflate_path} {orig_path}" - - -for i in range(6): - - tr = Test.AddTestRun(f'gzip, deflate, sdch, br, zstd: {i}') - if (waitForTs): - tr.Processes.Default.StartBefore(ts) - waitForTs = False - if (waitForServer): - tr.Processes.Default.StartBefore(server, ready=When.PortOpen(server.Variables.Port)) - waitForServer = False - tr.Processes.Default.ReturnCode = 0 - out_path = get_out_path() - tr.MakeCurlCommand(curl(ts, i, 'gzip, deflate, sdch, br, zstd', out_path), ts=ts) - tr = Test.AddTestRun(f'verify gzip, deflate, sdch, br, zstd: {i}') - tr.ReturnCode = 0 - if i == 0: - tr.Processes.Default.Command = get_verify_command(out_path, "brotli -d -c") - elif i == 1: - tr.Processes.Default.Command = get_verify_command(out_path, "gunzip -k -c") - elif i == 2: - tr.Processes.Default.Command = get_verify_command(out_path, "brotli -d -c") - elif i == 3: - tr.Processes.Default.Command = get_verify_command(out_path, "brotli -d -c") - elif i == 4: - tr.Processes.Default.Command = get_verify_command(out_path, "zstd -d -c") - elif i == 5: - tr.Processes.Default.Command = get_verify_command(out_path, "zstd -d -c") - - tr = Test.AddTestRun(f'gzip: {i}') - tr.Processes.Default.ReturnCode = 0 - out_path = get_out_path() - tr.MakeCurlCommand(curl(ts, i, "gzip", out_path), ts=ts) - tr = Test.AddTestRun(f'verify gzip: {i}') - tr.ReturnCode = 0 - tr.Processes.Default.Command = get_verify_command(out_path, "gunzip -k -c") - - tr = Test.AddTestRun(f'br: {i}') - tr.Processes.Default.ReturnCode = 0 - out_path = get_out_path() - tr.MakeCurlCommand(curl(ts, i, "br", out_path), ts=ts) - tr = Test.AddTestRun(f'verify br: {i}') - tr.ReturnCode = 0 - if i == 1: - tr.Processes.Default.Command = f"diff {out_path} {orig_path}" - else: - tr.Processes.Default.Command = get_verify_command(out_path, "brotli -d -c") - - tr = Test.AddTestRun(f'deflate: {i}') - tr.Processes.Default.ReturnCode = 0 - out_path = get_out_path() - tr.MakeCurlCommand(curl(ts, i, "deflate", out_path), ts=ts) - tr = Test.AddTestRun(f'verify deflate: {i}') - tr.ReturnCode = 0 - tr.Processes.Default.Command = f"diff {out_path} {orig_path}" - - tr = Test.AddTestRun(f'zstd: {i}') - tr.Processes.Default.ReturnCode = 0 - out_path = get_out_path() - tr.MakeCurlCommand(curl(ts, i, "zstd", out_path)) - tr = Test.AddTestRun(f'verify zstd: {i}') - tr.ReturnCode = 0 - if i == 4 or i == 5: - tr.Processes.Default.Command = get_verify_command(out_path, "zstd -d -c") - else: - tr.Processes.Default.Command = get_verify_command(out_path, "cat") - -# Test Accept-Encoding normalization. - -tr = Test.AddTestRun() -tr.Processes.Default.ReturnCode = 0 -out_path = get_out_path() -tr.MakeCurlCommand(curl(ts, 0, "gzip;q=0.666", out_path), ts=ts) -tr = Test.AddTestRun(f'verify gzip;q=0.666') -tr.ReturnCode = 0 -tr.Processes.Default.Command = get_verify_command(out_path, "gunzip -k -c") - -tr = Test.AddTestRun() -tr.Processes.Default.ReturnCode = 0 -out_path = get_out_path() -tr.MakeCurlCommand(curl(ts, 0, "gzip;q=0.666x", out_path), ts=ts) -tr = Test.AddTestRun(f'verify gzip;q=0.666x') -tr.ReturnCode = 0 -tr.Processes.Default.Command = get_verify_command(out_path, "gunzip -k -c") - -tr = Test.AddTestRun() -tr.Processes.Default.ReturnCode = 0 -out_path = get_out_path() -tr.MakeCurlCommand(curl(ts, 0, "gzip;q=#0.666", out_path), ts=ts) -tr = Test.AddTestRun(f'verify gzip;q=#0.666') -tr.ReturnCode = 0 -tr.Processes.Default.Command = get_verify_command(out_path, "gunzip -k -c") - -tr = Test.AddTestRun() -tr.Processes.Default.ReturnCode = 0 -out_path = get_out_path() -tr.MakeCurlCommand(curl(ts, 0, "gzip; Q = 0.666", out_path), ts=ts) -tr = Test.AddTestRun(f'verify gzip; Q = 0.666') -tr.ReturnCode = 0 -tr.Processes.Default.Command = get_verify_command(out_path, "gunzip -k -c") - -tr = Test.AddTestRun() -tr.Processes.Default.ReturnCode = 0 -out_path = get_out_path() -tr.MakeCurlCommand(curl(ts, 0, "gzip;q=0.0", out_path), ts=ts) -tr = Test.AddTestRun(f'verify gzip;q=0.0') -tr.ReturnCode = 0 -tr.Processes.Default.Command = f"diff {out_path} {orig_path}" - -tr = Test.AddTestRun() -tr.Processes.Default.ReturnCode = 0 -out_path = get_out_path() -tr.MakeCurlCommand(curl(ts, 0, "gzip;q=-0.1", out_path), ts=ts) -tr = Test.AddTestRun(f'verify gzip;q=-0.1') -tr.ReturnCode = 0 -tr.Processes.Default.Command = get_verify_command(out_path, "gunzip -k -c") - -tr = Test.AddTestRun() -tr.Processes.Default.ReturnCode = 0 -out_path = get_out_path() -tr.MakeCurlCommand(curl(ts, 0, "aaa, gzip;q=0.666, bbb", out_path), ts=ts) -tr = Test.AddTestRun(f'verify aaa, gzip;q=0.666, bbb') -tr.ReturnCode = 0 -tr.Processes.Default.Command = get_verify_command(out_path, "gunzip -k -c") - -tr = Test.AddTestRun() -tr.Processes.Default.ReturnCode = 0 -out_path = get_out_path() -tr.MakeCurlCommand(curl(ts, 0, " br ; q=0.666, bbb", out_path), ts=ts) -tr = Test.AddTestRun(f'verify br ; q=0.666, bbb') -tr.ReturnCode = 0 -tr.Processes.Default.Command = get_verify_command(out_path, "brotli -d -c") - -tr = Test.AddTestRun() -tr.Processes.Default.ReturnCode = 0 -out_path = get_out_path() -tr.MakeCurlCommand(curl(ts, 0, "aaa, gzip;q=0.666 , ", out_path), ts=ts) -tr = Test.AddTestRun(f'verify aaa, gzip;q=0.666 , ') -tr.ReturnCode = 0 -tr.Processes.Default.Command = get_verify_command(out_path, "gunzip -k -c") - -# post -tr = Test.AddTestRun() -tr.Processes.Default.ReturnCode = 0 -out_path = get_out_path() -tr.MakeCurlCommand(curl_post(ts, 3, "gzip", out_path), ts=ts) -tr = Test.AddTestRun(f'verify gzip post') -tr.ReturnCode = 0 -tr.Processes.Default.Command = get_verify_command(out_path, "gunzip -k -c") - -# Test Vary header: compressible content without Accept-Encoding should get Vary: Accept-Encoding -tr = Test.AddTestRun('vary header test: no accept-encoding') -tr.Processes.Default.ReturnCode = 0 -out_path = get_out_path() -tr.MakeCurlCommand( - f"-o {out_path} --verbose --proxy http://127.0.0.1:{ts.Variables.port}" - f" --header 'X-Ats-Compress-Test: vary-no-accept-encoding'" - f" 'http://ae-0/obj0'" - " 2>> compress_vary.log", - ts=ts) - -# Test Vary header: compressible content with unsupported Accept-Encoding should get Vary: Accept-Encoding -tr = Test.AddTestRun('vary header test: unsupported accept-encoding') -tr.Processes.Default.ReturnCode = 0 -out_path = get_out_path() -tr.MakeCurlCommand(curl(ts, 0, "compress, identity", out_path).replace("compress_long.log", "compress_vary.log"), ts=ts) - -# Verify Vary header is present in both cases -tr = Test.AddTestRun('verify vary headers') -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Command = ( - r"tr -d '\r' < compress_vary.log | grep -i 'vary:.*accept-encoding' | sort > compress_vary_short.log") -f = tr.Disk.File("compress_vary_short.log") -f.Content = "compress_vary.gold" - -# compress_long.log contains all the output from the curl commands. The tr removes the carriage returns for easier -# readability. Curl seems to have a bug, where it will neglect to output an end of line before outputting an HTTP -# message header line. The sed command is a work-around for this problem. greplog.sh uses the grep command to -# select HTTP request/response line that should be consistent every time the test runs. -# -tr = Test.AddTestRun() -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Command = ( - r"tr -d '\r' < compress_long.log | sed 's/\(..*\)\([<>]\)/\1\n\2/' | {0}/greplog.sh > compress_short.log".format( - Test.TestDirectory)) -f = tr.Disk.File("compress_short.log") -f.Content = "compress.gold" - -tr = Test.AddTestRun() -tr.Processes.Default.Command = "echo" -f = tr.Disk.File("compress_userver.log") -f.Content = "compress_userver.gold" diff --git a/tests/gold_tests/pluginTest/conf_remap/conf_remap.test.py b/tests/gold_tests/pluginTest/conf_remap/conf_remap.test.py deleted file mode 100644 index 9559a91ec63..00000000000 --- a/tests/gold_tests/pluginTest/conf_remap/conf_remap.test.py +++ /dev/null @@ -1,72 +0,0 @@ -''' -Test conf_reamp plugin -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -Test.Summary = ''' -Test conf_remap plugin -''' - -Test.SkipUnless(Condition.PluginExists('conf_remap.so')) - - -class ConfRemapPluginTest: - - def __init__(self): - self._tr = Test.AddTestRun("conf_remap") - self._replay_file = "replay/conf_remap.replay.yaml" - - self.__setupOriginServer() - self.__setupTS() - self.__setupClient() - - def __setupClient(self): - self._tr.AddVerifierClientProcess("verifier-client", self._replay_file, http_ports=[self._ts.Variables.port]) - - def __setupOriginServer(self): - self._server = self._tr.AddVerifierServerProcess("verifier-server", self._replay_file) - - def __setupTS(self): - self._ts = Test.MakeATSProcess("ts", enable_cache=True) - self._ts.Disk.records_config.update( - { - "proxy.config.diags.debug.enabled": 1, - "proxy.config.diags.debug.tags": "http|conf_remap", - "proxy.config.http.insert_response_via_str": 2, - "proxy.config.http.negative_caching_enabled": 0 - }) - - self._ts.Setup.Copy("etc/negative_caching_list.yaml") - - self._ts.Disk.remap_config.AddLines( - { - f""" -map /default_negative_caching_list/ \ - http://127.0.0.1:{self._server.Variables.http_port}/default_negative_caching_list/ \ - @plugin=conf_remap.so @pparam=proxy.config.http.negative_caching_enabled=1 - -map /custom_negative_caching_list/ \ - http://127.0.0.1:{self._server.Variables.http_port}/custom_negative_caching_list/ \ - @plugin=conf_remap.so @pparam={Test.RunDirectory}/negative_caching_list.yaml - """ - }) - - def run(self): - self._tr.Processes.Default.StartBefore(self._ts) - self._tr.StillRunningAfter = self._ts - - -ConfRemapPluginTest().run() diff --git a/tests/gold_tests/pluginTest/cookie_remap/bucketcookie.test.py b/tests/gold_tests/pluginTest/cookie_remap/bucketcookie.test.py deleted file mode 100644 index 98350d61602..00000000000 --- a/tests/gold_tests/pluginTest/cookie_remap/bucketcookie.test.py +++ /dev/null @@ -1,109 +0,0 @@ -''' -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import os - -Test.Summary = ''' - -''' -Test.SkipUnless(Condition.PluginExists('cookie_remap.so')) -Test.ContinueOnFail = True -Test.testName = "cookie_remap: cookie in bucket or not" - -# Define default ATS -ts = Test.MakeATSProcess("ts") - -# First server is run during first test and -# second server is run during second test -server = Test.MakeOriginServer("server", ip='127.0.0.10') - -request_header = { - "headers": "GET /cookiematches HTTP/1.1\r\nHost: www.example.com\r\n\r\n", - "timestamp": "1469733493.993", - "body": "" -} -# expected response from the origin server -response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} - -# add response to the server dictionary -server.addResponse("sessionfile.log", request_header, response_header) - -server2 = Test.MakeOriginServer("server2", ip='127.0.0.11') -request_header2 = { - "headers": "GET /cookiedoesntmatch HTTP/1.1\r\nHost: www.example.com\r\n\r\n", - "timestamp": "1469733493.993", - "body": "" -} -# expected response from the origin server -response_header2 = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} - -# add response to the server dictionary -server2.addResponse("sessionfile.log", request_header2, response_header2) - -# Setup the remap configuration -config_path = os.path.join(Test.TestDirectory, "configs/bucketconfig.txt") -with open(config_path, 'r') as config_file: - config1 = config_file.read() - -ts.Disk.records_config.update( - { - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'cookie_remap.*|http.*|dns.*', - }) - -config1 = config1.replace("$PORT", str(server.Variables.Port)) -config1 = config1.replace("$ALTPORT", str(server2.Variables.Port)) - -ts.Disk.File(ts.Variables.CONFIGDIR + "/bucketconfig.txt", id="config1") -ts.Disk.config1.WriteOn(config1) - -ts.Disk.remap_config.AddLine( - 'map http://www.example.com/magic http://shouldnothit.com @plugin=cookie_remap.so @pparam=config/bucketconfig.txt') - -# Cookie value in bucket -tr = Test.AddTestRun("cookie value in bucket") -tr.MakeCurlCommand( - ''' --proxy 127.0.0.1:{0} \ -"http://www.example.com/magic" \ --H"Cookie: fpbeta=333" \ --H "Proxy-Connection: keep-alive" \ ---verbose \ -'''.format(ts.Variables.port), - ts=ts) -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.StartBefore(server, ready=When.PortOpen(server.Variables.Port)) -tr.Processes.Default.StartBefore(Test.Processes.ts) -tr.StillRunningAfter = ts - -server.Streams.All = "gold/matchcookie.gold" - -# cookie value not in bucket -tr = Test.AddTestRun("cooke value not in bucket") -tr.MakeCurlCommand( - ''' --proxy 127.0.0.1:{0} \ -"http://www.example.com/magic" \ --H"Cookie: fpbeta=etc" \ --H "Proxy-Connection: keep-alive" \ ---verbose \ -'''.format(ts.Variables.port), - ts=ts) -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.StartBefore(server2, ready=When.PortOpen(server2.Variables.Port)) -tr.StillRunningAfter = ts - -server2.Streams.All = "gold/wontmatchcookie.gold" diff --git a/tests/gold_tests/pluginTest/cookie_remap/collapseslashes.test.py b/tests/gold_tests/pluginTest/cookie_remap/collapseslashes.test.py deleted file mode 100644 index 27eab2b8f3c..00000000000 --- a/tests/gold_tests/pluginTest/cookie_remap/collapseslashes.test.py +++ /dev/null @@ -1,78 +0,0 @@ -''' -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import os - -Test.Summary = ''' - -''' -Test.SkipUnless(Condition.PluginExists('cookie_remap.so')) -Test.ContinueOnFail = True -Test.testName = "cookie_remap: plugin collapses consecutive slashes" - -# Define default ATS -ts = Test.MakeATSProcess("ts") - -# We only run a server to capture ATS outbound requests -# and verify it collapsed the double // -server = Test.MakeOriginServer("server", ip='127.0.0.10') - -request_header = { - "headers": "GET /i/like/cheetos?.done=http://finance.yahoo.com HTTP/1.1\r\nHost: www.example.com\r\n\r\n", - "timestamp": "1469733493.993", - "body": "" -} -# expected response from the origin server -response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} - -# add response to the server dictionary -server.addResponse("sessionfile.log", request_header, response_header) - -# Setup the remap configuration -config_path = os.path.join(Test.TestDirectory, "configs/collapseconfig.txt") -with open(config_path, 'r') as config_file: - config1 = config_file.read() - -ts.Disk.records_config.update( - { - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'cookie_remap.*|http.*|dns.*', - }) - -config1 = config1.replace("$PORT", str(server.Variables.Port)) - -ts.Disk.File(ts.Variables.CONFIGDIR + "/collapseconfig.txt", id="config1") -ts.Disk.config1.WriteOn(config1) - -ts.Disk.remap_config.AddLine( - 'map http://www.example.com/magic http://shouldnothit.com/magic @plugin=cookie_remap.so @pparam=config/collapseconfig.txt') - -tr = Test.AddTestRun("collapse consecutive forward slashes") -tr.MakeCurlCommand( - ''' --proxy 127.0.0.1:{0} \ -"http://www.example.com/magic" \ --H "Proxy-Connection: keep-alive" \ ---verbose \ -'''.format(ts.Variables.port), - ts=ts) -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.StartBefore(server, ready=When.PortOpen(server.Variables.Port)) -tr.Processes.Default.StartBefore(Test.Processes.ts) -tr.StillRunningAfter = ts - -server.Streams.All = "gold/collapseslashes.gold" diff --git a/tests/gold_tests/pluginTest/cookie_remap/connector.test.py b/tests/gold_tests/pluginTest/cookie_remap/connector.test.py deleted file mode 100644 index fdd1f163e96..00000000000 --- a/tests/gold_tests/pluginTest/cookie_remap/connector.test.py +++ /dev/null @@ -1,109 +0,0 @@ -''' -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import os - -Test.Summary = ''' - -''' -Test.SkipUnless(Condition.PluginExists('cookie_remap.so')) -Test.ContinueOnFail = True -Test.testName = "cookie_remap: test connector" - -# Define default ATS -ts = Test.MakeATSProcess("ts") - -# First server is run during first test and -# second server is run during second test -server = Test.MakeOriginServer("server", ip='127.0.0.10') - -request_header = { - "headers": "GET /cookiematches HTTP/1.1\r\nHost: www.example.com\r\n\r\n", - "timestamp": "1469733493.993", - "body": "" -} -# expected response from the origin server -response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} - -# add response to the server dictionary -server.addResponse("sessionfile.log", request_header, response_header) - -server2 = Test.MakeOriginServer("server2", ip='127.0.0.11') -request_header2 = { - "headers": "GET /cookiedoesntmatch HTTP/1.1\r\nHost: www.example.com\r\n\r\n", - "timestamp": "1469733493.993", - "body": "" -} -# expected response from the origin server -response_header2 = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} - -# add response to the server dictionary -server2.addResponse("sessionfile.log", request_header2, response_header2) - -# Setup the remap configuration -config_path = os.path.join(Test.TestDirectory, "configs/connectorconfig.txt") -with open(config_path, 'r') as config_file: - config1 = config_file.read() - -ts.Disk.records_config.update( - { - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'cookie_remap.*|http.*|dns.*', - }) - -config1 = config1.replace("$PORT", str(server.Variables.Port)) -config1 = config1.replace("$ALTPORT", str(server2.Variables.Port)) - -ts.Disk.File(ts.Variables.CONFIGDIR + "/connectorconfig.txt", id="config1") -ts.Disk.config1.WriteOn(config1) - -ts.Disk.remap_config.AddLine( - 'map http://www.example.com/magic http://shouldnothit.com/magic @plugin=cookie_remap.so @pparam=config/connectorconfig.txt') - -# Positive test case that remaps because all connected operations pass -tr = Test.AddTestRun("cookie value matches") -tr.MakeCurlCommand( - ''' --proxy 127.0.0.1:{0} \ -"http://www.example.com/magic" \ --H"Cookie: fpbeta=abcd icecream=donteat" \ --H "Proxy-Connection: keep-alive" \ ---verbose \ -'''.format(ts.Variables.port), - ts=ts) -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.StartBefore(server, ready=When.PortOpen(server.Variables.Port)) -tr.Processes.Default.StartBefore(Test.Processes.ts) -tr.StillRunningAfter = ts - -server.Streams.All = "gold/matchcookie.gold" - -# Negative test case that doesn't remap because not all subops pass -tr = Test.AddTestRun("cookie value doesn't match") -tr.MakeCurlCommand( - ''' --proxy 127.0.0.1:{0} \ -"http://www.example.com/magic" \ --H"Cookie: fpbeta=somethingelse" \ --H "Proxy-Connection: keep-alive" \ ---verbose \ -'''.format(ts.Variables.port), - ts=ts) -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.StartBefore(server2, ready=When.PortOpen(server2.Variables.Port)) -tr.StillRunningAfter = ts - -server2.Streams.All = "gold/wontmatchcookie.gold" diff --git a/tests/gold_tests/pluginTest/cookie_remap/disable_pristine_host_hdr.test.py b/tests/gold_tests/pluginTest/cookie_remap/disable_pristine_host_hdr.test.py deleted file mode 100644 index 5174c10db52..00000000000 --- a/tests/gold_tests/pluginTest/cookie_remap/disable_pristine_host_hdr.test.py +++ /dev/null @@ -1,131 +0,0 @@ -''' -Verify cookie_remap plugin's disable_pristine_host_hdr functionality. -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import os - -Test.Summary = ''' -Test cookie_remap plugin's disable_pristine_host_hdr functionality. Verifies that -when disable_pristine_host_hdr is set to true, the pristine host header setting -is disabled for the matched transaction, allowing the Host header to be updated -to match the remapped destination. -''' -Test.SkipUnless(Condition.PluginExists('cookie_remap.so')) -Test.ContinueOnFail = True - - -class TestUpdateHostHeader: - """ - Test the disable_pristine_host_hdr feature of cookie_remap plugin. - - This test verifies that: - 1. Cookie bucket routing works correctly - 2. When disable_pristine_host_hdr is enabled, the Host header is updated - to match the sendto URL destination - 3. When disable_pristine_host_hdr is not set, the Host header remains as - the original client value - """ - - # Counter for unique process names across multiple test instances - test_counter: int = 0 - - def __init__(self, disable_pristine_host_hdr=True): - """Initialize the test by setting up servers and ATS configuration. - :param disable_pristine_host_hdr: Whether to configure disable_pristine_host_hdr - in the cookie_remap configuration. - """ - self.test_id = TestUpdateHostHeader.test_counter - TestUpdateHostHeader.test_counter += 1 - self.disable_pristine_host_hdr = disable_pristine_host_hdr - self.replay_file = f'disable_pristine_host_hdr_{"true" if disable_pristine_host_hdr else "false"}.replay.yaml' - self.canary_replay_file = ( - 'disable_pristine_host_hdr_server_canary_true.replay.yaml' - if disable_pristine_host_hdr else 'disable_pristine_host_hdr_server_canary_false.replay.yaml') - self.stable_replay_file = 'disable_pristine_host_hdr_server_stable.replay.yaml' - self._setupDns() - self._setupServers() - self._setupTS(disable_pristine_host_hdr) - self._setupClient() - - def _setupDns(self): - """Configure the DNS server.""" - self._dns = Test.MakeDNServer(f"dns_{self.test_id}", default='127.0.0.1') - - def _setupServers(self): - """ - Configure the origin servers using proxy-verifier. - - Creates two servers to simulate canary and stable environments. - """ - self._server_canary = Test.MakeVerifierServerProcess(f"server_canary_{self.test_id}", self.canary_replay_file) - expected_host = 'canary.com' if self.disable_pristine_host_hdr else 'example.com' - self._server_canary.Streams.All += Testers.ContainsExpression(expected_host, f'Host header should be {expected_host}') - - self._server_stable = Test.MakeVerifierServerProcess(f"server_stable_{self.test_id}", self.stable_replay_file) - # The else path always preserves pristine host header (example.com). - expected_host = 'example.com' - self._server_stable.Streams.All += Testers.ContainsExpression(expected_host, f'Host header should be {expected_host}') - - def _setupTS(self, disable_pristine_host_hdr): - """Configure Traffic Server with cookie_remap plugin. - :param disable_pristine_host_hdr: Whether to configure disable_pristine_host_hdr in cookie_remap. - """ - ts = Test.MakeATSProcess(f"ts_{self.test_id}", enable_cache=False) - self._ts = ts - - # Enable debug logging for cookie_remap and enable pristine_host_hdr - # (simulating production environment) - ts.Disk.records_config.update( - { - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'cookie_remap|http', - 'proxy.config.dns.nameservers': f"127.0.0.1:{self._dns.Variables.Port}", - 'proxy.config.dns.resolv_conf': 'NULL', - 'proxy.config.url_remap.pristine_host_hdr': 1, - }) - - # Read and configure the cookie_remap configuration file - config_filename = f'disable_pristine_host_hdr_config_{"true" if disable_pristine_host_hdr else "false"}.txt' - config_path = os.path.join(Test.TestDirectory, f"configs/{config_filename}") - with open(config_path, 'r') as config_file: - config_content = config_file.read() - config_content = config_content.replace("$CANARY_PORT", str(self._server_canary.Variables.http_port)) - config_content = config_content.replace("$STABLE_PORT", str(self._server_stable.Variables.http_port)) - ts.Disk.File(ts.Variables.CONFIGDIR + f"/{config_filename}", id="cookie_config") - ts.Disk.cookie_config.WriteOn(config_content) - - # Configure remap rule with cookie_remap plugin - ts.Disk.remap_config.AddLine( - 'map http://example.com http://shouldnothit.com ' - f'@plugin=cookie_remap.so @pparam=config/{config_filename}') - - def _setupClient(self): - """Setup the client for the test.""" - enabled_str = "enabled" if self.disable_pristine_host_hdr else "disabled" - tr = Test.AddTestRun(f'Test cookie bucket routing with disable_pristine_host_hdr {enabled_str}') - - p = tr.AddVerifierClientProcess(f'client_{self.test_id}', self.replay_file, http_ports=[self._ts.Variables.port]) - p.StartBefore(self._dns) - p.StartBefore(self._ts) - p.StartBefore(self._server_canary) - p.StartBefore(self._server_stable) - - -# Execute the test -TestUpdateHostHeader(disable_pristine_host_hdr=True) -TestUpdateHostHeader(disable_pristine_host_hdr=False) diff --git a/tests/gold_tests/pluginTest/cookie_remap/existscookie.test.py b/tests/gold_tests/pluginTest/cookie_remap/existscookie.test.py deleted file mode 100644 index 915960f7574..00000000000 --- a/tests/gold_tests/pluginTest/cookie_remap/existscookie.test.py +++ /dev/null @@ -1,109 +0,0 @@ -''' -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import os - -Test.Summary = ''' - -''' -Test.SkipUnless(Condition.PluginExists('cookie_remap.so')) -Test.ContinueOnFail = True -Test.testName = "cookie_remap: cookie exists" - -# Define default ATS -ts = Test.MakeATSProcess("ts") - -# First server is run during first test and -# second server is run during second test -server = Test.MakeOriginServer("server", ip='127.0.0.10') - -request_header = { - "headers": "GET /cookieexists HTTP/1.1\r\nHost: www.example.com\r\n\r\n", - "timestamp": "1469733493.993", - "body": "" -} -# expected response from the origin server -response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} - -# add response to the server dictionary -server.addResponse("sessionfile.log", request_header, response_header) - -server2 = Test.MakeOriginServer("server2", ip='127.0.0.11') -request_header2 = { - "headers": "GET /cookiedoesntexist HTTP/1.1\r\nHost: www.example.com\r\n\r\n", - "timestamp": "1469733493.993", - "body": "" -} -# expected response from the origin server -response_header2 = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} - -# add response to the server dictionary -server2.addResponse("sessionfile.log", request_header2, response_header2) - -# Setup the remap configuration -config_path = os.path.join(Test.TestDirectory, "configs/existsconfig.txt") -with open(config_path, 'r') as config_file: - config1 = config_file.read() - -ts.Disk.records_config.update( - { - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'cookie_remap.*|http.*|dns.*', - }) - -config1 = config1.replace("$PORT", str(server.Variables.Port)) -config1 = config1.replace("$ALTPORT", str(server2.Variables.Port)) - -ts.Disk.File(ts.Variables.CONFIGDIR + "/existsconfig.txt", id="config1") -ts.Disk.config1.WriteOn(config1) - -ts.Disk.remap_config.AddLine( - 'map http://www.example.com/magic http://shouldnothit.com @plugin=cookie_remap.so @pparam=config/existsconfig.txt') - -# Positive test case that remaps because cookie exists -tr = Test.AddTestRun("cookie fpbeta exists") -tr.MakeCurlCommand( - ''' --proxy 127.0.0.1:{0} \ -"http://www.example.com/magic" \ --H"Cookie: fpbeta=fdkfkdfkdfkdkdfkdfk" \ --H "Proxy-Connection: keep-alive" \ ---verbose \ -'''.format(ts.Variables.port), - ts=ts) -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.StartBefore(server, ready=When.PortOpen(server.Variables.Port)) -tr.Processes.Default.StartBefore(Test.Processes.ts) -tr.StillRunningAfter = ts - -server.Streams.All = "gold/existscookie.gold" - -# Negative test case that doesn't remap because cookie doesn't exist -tr = Test.AddTestRun("cooke fpbeta doesnt exist") -tr.MakeCurlCommand( - ''' --proxy 127.0.0.1:{0} \ -"http://www.example.com/magic" \ --H"Cookie: breadcrumb=etc" \ --H "Proxy-Connection: keep-alive" \ ---verbose \ -'''.format(ts.Variables.port), - ts=ts) -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.StartBefore(server2, ready=When.PortOpen(server2.Variables.Port)) -tr.StillRunningAfter = ts - -server2.Streams.All = "gold/doesntexistcookie.gold" diff --git a/tests/gold_tests/pluginTest/cookie_remap/matchcookie.test.py b/tests/gold_tests/pluginTest/cookie_remap/matchcookie.test.py deleted file mode 100644 index 44d938c8218..00000000000 --- a/tests/gold_tests/pluginTest/cookie_remap/matchcookie.test.py +++ /dev/null @@ -1,109 +0,0 @@ -''' -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import os - -Test.Summary = ''' - -''' -Test.SkipUnless(Condition.PluginExists('cookie_remap.so')) -Test.ContinueOnFail = True -Test.testName = "cookie_remap: match cookie" - -# Define default ATS -ts = Test.MakeATSProcess("ts") - -# First server is run during first test and -# second server is run during second test -server = Test.MakeOriginServer("server", ip='127.0.0.10') - -request_header = { - "headers": "GET /cookiematches?a=1&b=2&c=3 HTTP/1.1\r\nHost: www.example.com\r\n\r\n", - "timestamp": "1469733493.993", - "body": "" -} -# expected response from the origin server -response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} - -# add response to the server dictionary -server.addResponse("sessionfile.log", request_header, response_header) - -server2 = Test.MakeOriginServer("server2", ip='127.0.0.11') -request_header2 = { - "headers": "GET /cookiedoesntmatch?a=1&b=2&c=3 HTTP/1.1\r\nHost: www.example.com\r\n\r\n", - "timestamp": "1469733493.993", - "body": "" -} -# expected response from the origin server -response_header2 = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} - -# add response to the server dictionary -server2.addResponse("sessionfile.log", request_header2, response_header2) - -# Setup the remap configuration -config_path = os.path.join(Test.TestDirectory, "configs/matchconfig.txt") -with open(config_path, 'r') as config_file: - config1 = config_file.read() - -ts.Disk.records_config.update( - { - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'cookie_remap.*|http.*|dns.*', - }) - -config1 = config1.replace("$PORT", str(server.Variables.Port)) -config1 = config1.replace("$ALTPORT", str(server2.Variables.Port)) - -ts.Disk.File(ts.Variables.CONFIGDIR + "/matchconfig.txt", id="config1") -ts.Disk.config1.WriteOn(config1) - -ts.Disk.remap_config.AddLine( - 'map http://www.example.com/magic http://shouldnothit.com @plugin=cookie_remap.so @pparam=config/matchconfig.txt') - -# Positive test case that remaps because cookie matches -tr = Test.AddTestRun("cookie value matches") -tr.MakeCurlCommand( - ''' --proxy 127.0.0.1:{0} \ -"http://www.example.com/magic?a=1&b=2&c=3" \ --H"Cookie: fpbeta=magic" \ --H "Proxy-Connection: keep-alive" \ ---verbose \ -'''.format(ts.Variables.port), - ts=ts) -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.StartBefore(server, ready=When.PortOpen(server.Variables.Port)) -tr.Processes.Default.StartBefore(Test.Processes.ts) -tr.StillRunningAfter = ts - -server.Streams.All = "gold/matchcookie2.gold" - -# Negative test case that doesn't remap because cookie doesn't match -tr = Test.AddTestRun("cookie regex doesn't match") -tr.MakeCurlCommand( - ''' --proxy 127.0.0.1:{0} \ -"http://www.example.com/magic?a=1&b=2&c=3" \ --H"Cookie: fpbeta=magit" \ --H "Proxy-Connection: keep-alive" \ ---verbose \ -'''.format(ts.Variables.port), - ts=ts) -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.StartBefore(server2, ready=When.PortOpen(server2.Variables.Port)) -tr.StillRunningAfter = ts - -server2.Streams.All = "gold/wontmatchcookie2.gold" diff --git a/tests/gold_tests/pluginTest/cookie_remap/matchuri.test.py b/tests/gold_tests/pluginTest/cookie_remap/matchuri.test.py deleted file mode 100644 index d2060862587..00000000000 --- a/tests/gold_tests/pluginTest/cookie_remap/matchuri.test.py +++ /dev/null @@ -1,108 +0,0 @@ -''' -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import os - -Test.Summary = ''' - -''' -Test.SkipUnless(Condition.PluginExists('cookie_remap.so')) -Test.ContinueOnFail = True -Test.testName = "cookie_remap: match cookie" - -# Define default ATS -ts = Test.MakeATSProcess("ts") - -# First server is run during first test and -# second server is run during second test -server = Test.MakeOriginServer("server", ip='127.0.0.10') - -request_header = { - "headers": "GET /cookiematches HTTP/1.1\r\nHost: www.example.com\r\n\r\n", - "timestamp": "1469733493.993", - "body": "" -} -# expected response from the origin server -response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} - -# add response to the server dictionary -server.addResponse("sessionfile.log", request_header, response_header) - -server2 = Test.MakeOriginServer("server2", ip='127.0.0.11') -request_header2 = { - "headers": "GET /cookiedoesntmatch HTTP/1.1\r\nHost: www.example.com\r\n\r\n", - "timestamp": "1469733493.993", - "body": "" -} -# expected response from the origin server -response_header2 = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} - -# add response to the server dictionary -server2.addResponse("sessionfile.log", request_header2, response_header2) - -# Setup the remap configuration -config_path = os.path.join(Test.TestDirectory, "configs/matchuriconfig.txt") -with open(config_path, 'r') as config_file: - config1 = config_file.read() - -ts.Disk.records_config.update( - { - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'cookie_remap.*|http.*|dns.*', - }) - -config1 = config1.replace("$PORT", str(server.Variables.Port)) -config1 = config1.replace("$ALTPORT", str(server2.Variables.Port)) - -ts.Disk.File(ts.Variables.CONFIGDIR + "/matchuriconfig.txt", id="config1") -ts.Disk.config1.WriteOn(config1) - -ts.Disk.remap_config.AddLine( - 'map http://www.example.com/magic http://shouldnothit.com @plugin=cookie_remap.so @pparam=config/matchuriconfig.txt') - -# Positive test case, URI matches rule -tr = Test.AddTestRun("URI value matches") -tr.MakeCurlCommand( - ''' --proxy 127.0.0.1:{0} \ -"http://www.example.com/magic/thisispartofthepath" \ --H "Proxy-Connection: keep-alive" \ ---verbose \ -'''.format(ts.Variables.port), - ts=ts) -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.StartBefore(server, ready=When.PortOpen(server.Variables.Port)) -tr.Processes.Default.StartBefore(Test.Processes.ts) -tr.StillRunningAfter = ts - -server.Streams.All = "gold/matchcookie.gold" - -# Negative test case that doesn't remap because URI doesn't match -tr = Test.AddTestRun("URI value doesn't match") -tr.MakeCurlCommand( - ''' --proxy 127.0.0.1:{0} \ -"http://www.example.com/magic" \ --H"Cookie: fpbeta=etc" \ --H "Proxy-Connection: keep-alive" \ ---verbose \ -'''.format(ts.Variables.port), - ts=ts) -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.StartBefore(server2, ready=When.PortOpen(server2.Variables.Port)) -tr.StillRunningAfter = ts - -server2.Streams.All = "gold/wontmatchcookie.gold" diff --git a/tests/gold_tests/pluginTest/cookie_remap/notexistscookie.test.py b/tests/gold_tests/pluginTest/cookie_remap/notexistscookie.test.py deleted file mode 100644 index e3fb69c7811..00000000000 --- a/tests/gold_tests/pluginTest/cookie_remap/notexistscookie.test.py +++ /dev/null @@ -1,108 +0,0 @@ -''' -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import os - -Test.Summary = ''' - -''' -Test.SkipUnless(Condition.PluginExists('cookie_remap.so')) -Test.ContinueOnFail = True -Test.testName = "cookie_remap: cookie not exists" - -# Define default ATS -ts = Test.MakeATSProcess("ts") - -# First server is run during first test and -# second server is run during second test -server = Test.MakeOriginServer("server", ip='127.0.0.10') - -request_header = { - "headers": "GET /cookiedoesntexist HTTP/1.1\r\nHost: www.example.com\r\n\r\n", - "timestamp": "1469733493.993", - "body": "" -} -# expected response from the origin server -response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} - -# add response to the server dictionary -server.addResponse("sessionfile.log", request_header, response_header) - -server2 = Test.MakeOriginServer("server2", ip='127.0.0.11') -request_header2 = { - "headers": "GET /cookieexists HTTP/1.1\r\nHost: www.example.com\r\n\r\n", - "timestamp": "1469733493.993", - "body": "" -} -# expected response from the origin server -response_header2 = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} - -# add response to the server dictionary -server2.addResponse("sessionfile.log", request_header2, response_header2) - -# Setup the remap configuration -config_path = os.path.join(Test.TestDirectory, "configs/notexistsconfig.txt") -with open(config_path, 'r') as config_file: - config1 = config_file.read() - -ts.Disk.records_config.update( - { - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'cookie_remap.*|http.*|dns.*', - }) - -config1 = config1.replace("$PORT", str(server.Variables.Port)) -config1 = config1.replace("$ALTPORT", str(server2.Variables.Port)) - -ts.Disk.File(ts.Variables.CONFIGDIR + "/notexistsconfig.txt", id="config1") -ts.Disk.config1.WriteOn(config1) - -ts.Disk.remap_config.AddLine( - 'map http://www.example.com/magic http://shouldnothit.com @plugin=cookie_remap.so @pparam=config/notexistsconfig.txt') - -# Positive test case that remaps because cookie doesn't exist -tr = Test.AddTestRun("cookie fpbeta doesn't exist") -tr.MakeCurlCommand( - ''' --proxy 127.0.0.1:{0} \ -"http://www.example.com/magic" \ --H "Proxy-Connection: keep-alive" \ ---verbose \ -'''.format(ts.Variables.port), - ts=ts) -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.StartBefore(server, ready=When.PortOpen(server.Variables.Port)) -tr.Processes.Default.StartBefore(Test.Processes.ts) -tr.StillRunningAfter = ts - -server.Streams.All = "gold/doesntexistcookie.gold" - -# Negative test case that doesn't remap because cookie exists -tr = Test.AddTestRun("cooke fpbeta exists") -tr.MakeCurlCommand( - ''' --proxy 127.0.0.1:{0} \ -"http://www.example.com/magic" \ --H"Cookie: fpbeta=etc" \ --H "Proxy-Connection: keep-alive" \ ---verbose \ -'''.format(ts.Variables.port), - ts=ts) -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.StartBefore(server2, ready=When.PortOpen(server2.Variables.Port)) -tr.StillRunningAfter = ts - -server2.Streams.All = "gold/existscookie.gold" diff --git a/tests/gold_tests/pluginTest/cookie_remap/pcollapseslashes.test.py b/tests/gold_tests/pluginTest/cookie_remap/pcollapseslashes.test.py deleted file mode 100644 index fcf949780b5..00000000000 --- a/tests/gold_tests/pluginTest/cookie_remap/pcollapseslashes.test.py +++ /dev/null @@ -1,78 +0,0 @@ -''' -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import os - -Test.Summary = ''' - -''' -Test.SkipUnless(Condition.PluginExists('cookie_remap.so')) -Test.ContinueOnFail = True -Test.testName = "cookie_remap: plugin collapses consecutive slashes (pre-remap URI)" - -# Define default ATS -ts = Test.MakeATSProcess("ts") - -# We only run a server to capture ATS outbound requests -# and verify it collapsed the double // -server = Test.MakeOriginServer("server", ip='127.0.0.10') - -request_header = { - "headers": "GET /i/like/cheetos?.done=http://finance.yahoo.com HTTP/1.1\r\nHost: www.example.com\r\n\r\n", - "timestamp": "1469733493.993", - "body": "" -} -# expected response from the origin server -response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} - -# add response to the server dictionary -server.addResponse("sessionfile.log", request_header, response_header) - -# Setup the remap configuration -config_path = os.path.join(Test.TestDirectory, "configs/pcollapseconfig.txt") -with open(config_path, 'r') as config_file: - config1 = config_file.read() - -ts.Disk.records_config.update( - { - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'cookie_remap.*|http.*|dns.*', - }) - -config1 = config1.replace("$PORT", str(server.Variables.Port)) - -ts.Disk.File(ts.Variables.CONFIGDIR + "/collapseconfig.txt", id="config1") -ts.Disk.config1.WriteOn(config1) - -ts.Disk.remap_config.AddLine( - 'map http://www.example.com/orig_path http://shouldnothit.com/magic @plugin=cookie_remap.so @pparam=config/collapseconfig.txt') - -tr = Test.AddTestRun("collapse consecutive forward slashes") -tr.MakeCurlCommand( - ''' --proxy 127.0.0.1:{0} \ -"http://www.example.com/orig_path" \ --H "Proxy-Connection: keep-alive" \ ---verbose \ -'''.format(ts.Variables.port), - ts=ts) -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.StartBefore(server, ready=When.PortOpen(server.Variables.Port)) -tr.Processes.Default.StartBefore(Test.Processes.ts) -tr.StillRunningAfter = ts - -server.Streams.All = "gold/collapseslashes.gold" diff --git a/tests/gold_tests/pluginTest/cookie_remap/psubstitute.test.py b/tests/gold_tests/pluginTest/cookie_remap/psubstitute.test.py deleted file mode 100644 index e353d721d99..00000000000 --- a/tests/gold_tests/pluginTest/cookie_remap/psubstitute.test.py +++ /dev/null @@ -1,142 +0,0 @@ -''' -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import os - -Test.Summary = ''' - -''' -Test.SkipUnless(Condition.PluginExists('cookie_remap.so')) -Test.ContinueOnFail = True -Test.testName = "cookie_remap: Substitute variables (pre-remap URL)" - -# Define default ATS -ts = Test.MakeATSProcess("ts") - -server = Test.MakeOriginServer("server", ip='127.0.0.10') - -request_header = { - "headers": "GET /photos/search?query=magic HTTP/1.1\r\nHost: www.example.com\r\n\r\n", - "timestamp": "1469733493.993", - "body": "" -} -response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} - -server.addResponse("sessionfile.log", request_header, response_header) - -request_header = { - "headers": "GET /photos/search?query=/theunmatchedpath HTTP/1.1\r\nHost: www.example.com\r\n\r\n", - "timestamp": "1469733493.993", - "body": "" -} -response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} - -server.addResponse("sessionfile.log", request_header, response_header) - -request_header = { - "headers": "GET /photos/search/magic/foobar HTTP/1.1\r\nHost: www.example.com\r\n\r\n", - "timestamp": "1469733493.993", - "body": "" -} -response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} - -server.addResponse("sessionfile.log", request_header, response_header) - -request_header = { - "headers": - "GET /photos/search/cr_substitutions?query=%28http%3A%2F%2Fwww%2Eexample%2Ecom%2Fmagic HTTP/1.1\r\nHost: www.example.com\r\n\r\n", - "timestamp": "1469733493.993", - "body": "" -} -response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} - -server.addResponse("sessionfile.log", request_header, response_header) - -# Setup the remap configuration -config_path = os.path.join(Test.TestDirectory, "configs/psubstituteconfig.txt") -with open(config_path, 'r') as config_file: - config1 = config_file.read() - -ts.Disk.records_config.update( - { - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'cookie_remap.*|http.*|dns.*', - }) - -config1 = config1.replace("$PORT", str(server.Variables.Port)) - -ts.Disk.File(ts.Variables.CONFIGDIR + "/substituteconfig.txt", id="config1") -ts.Disk.config1.WriteOn(config1) - -ts.Disk.remap_config.AddLine( - 'map http://www.example.com/magic http://shouldnothit.com/not-used @plugin=cookie_remap.so @pparam=config/substituteconfig.txt') - -tr = Test.AddTestRun("Substitute $ppath in the dest query") -tr.MakeCurlCommand( - ''' --proxy 127.0.0.1:{0} \ -"http://www.example.com/magic" \ --H"Cookie: fpbeta=abcd" \ --H "Proxy-Connection: keep-alive" \ ---verbose \ -'''.format(ts.Variables.port), - ts=ts) -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.StartBefore(server, ready=When.PortOpen(server.Variables.Port)) -tr.Processes.Default.StartBefore(Test.Processes.ts) -tr.StillRunningAfter = ts -tr.StillRunningAfter = server - -tr = Test.AddTestRun("Substitute $unmatched_ppath in the dest query") -tr.MakeCurlCommand( - ''' --proxy 127.0.0.1:{0} \ -"http://www.example.com/magic/theunmatchedpath" \ --H"Cookie: oxalpha=3333" \ --H "Proxy-Connection: keep-alive" \ ---verbose \ -'''.format(ts.Variables.port), - ts=ts) -tr.Processes.Default.ReturnCode = 0 -tr.StillRunningAfter = ts -tr.StillRunningAfter = server - -tr = Test.AddTestRun("Substitute $cr_req_purl using $cr_urlencode") -tr.MakeCurlCommand( - ''' --proxy 127.0.0.1:{0} \ -"http://www.example.com/magic" \ --H"Cookie: acgamma=dfndfdfd" \ --H "Proxy-Connection: keep-alive" \ ---verbose \ -'''.format(ts.Variables.port), - ts=ts) -tr.Processes.Default.ReturnCode = 0 -tr.StillRunningAfter = ts -tr.StillRunningAfter = server - -tr = Test.AddTestRun("Substitute $ppath as is in outgoing path") -tr.MakeCurlCommand( - ''' --proxy 127.0.0.1:{0} \ -"http://www.example.com/magic/foobar" \ --H "Proxy-Connection: keep-alive" \ ---verbose \ -'''.format(ts.Variables.port), - ts=ts) -tr.Processes.Default.ReturnCode = 0 -tr.StillRunningAfter = ts -tr.StillRunningAfter = server - -server.Streams.All = "gold/psubstitute.gold" diff --git a/tests/gold_tests/pluginTest/cookie_remap/regexcookie.test.py b/tests/gold_tests/pluginTest/cookie_remap/regexcookie.test.py deleted file mode 100644 index c5a39d7d614..00000000000 --- a/tests/gold_tests/pluginTest/cookie_remap/regexcookie.test.py +++ /dev/null @@ -1,109 +0,0 @@ -''' -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import os - -Test.Summary = ''' - -''' -Test.SkipUnless(Condition.PluginExists('cookie_remap.so')) -Test.ContinueOnFail = True -Test.testName = "cookie_remap: cookie regex match and substitution" - -# Define default ATS -ts = Test.MakeATSProcess("ts") - -# First server is run during first test and -# second server is run during second test -server = Test.MakeOriginServer("server", ip='127.0.0.10') - -request_header = { - "headers": "GET /regexmatches?cookies=oreos-chipsahoy-icecream HTTP/1.1\r\nHost: www.example.com\r\n\r\n", - "timestamp": "1469733493.993", - "body": "" -} -# expected response from the origin server -response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} - -# add response to the server dictionary -server.addResponse("sessionfile.log", request_header, response_header) - -server2 = Test.MakeOriginServer("server2", ip='127.0.0.11') -request_header2 = { - "headers": "GET /regexdoesntmatch HTTP/1.1\r\nHost: www.example.com\r\n\r\n", - "timestamp": "1469733493.993", - "body": "" -} -# expected response from the origin server -response_header2 = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} - -# add response to the server dictionary -server2.addResponse("sessionfile.log", request_header2, response_header2) - -# Setup the remap configuration -config_path = os.path.join(Test.TestDirectory, "configs/regexconfig.txt") -with open(config_path, 'r') as config_file: - config1 = config_file.read() - -ts.Disk.records_config.update( - { - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'cookie_remap.*|http.*|dns.*', - }) - -config1 = config1.replace("$PORT", str(server.Variables.Port)) -config1 = config1.replace("$ALTPORT", str(server2.Variables.Port)) - -ts.Disk.File(ts.Variables.CONFIGDIR + "/regexconfig.txt", id="config1") -ts.Disk.config1.WriteOn(config1) - -ts.Disk.remap_config.AddLine( - 'map http://www.example.com/magic http://shouldnothit.com @plugin=cookie_remap.so @pparam=config/regexconfig.txt') - -# Positive test case that remaps because cookie regex matches -tr = Test.AddTestRun("cookie regex matches") -tr.MakeCurlCommand( - ''' --proxy 127.0.0.1:{0} \ -"http://www.example.com/magic" \ --H"Cookie: fpbeta=ilove-oreos-chipsahoy-icecream" \ --H "Proxy-Connection: keep-alive" \ ---verbose \ -'''.format(ts.Variables.port), - ts=ts) -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.StartBefore(server, ready=When.PortOpen(server.Variables.Port)) -tr.Processes.Default.StartBefore(Test.Processes.ts) -tr.StillRunningAfter = ts - -server.Streams.All = "gold/regexmatches.gold" - -# Negative test case that doesn't remap because cookie regex doesn't match -tr = Test.AddTestRun("cookie regex doesn't match") -tr.MakeCurlCommand( - ''' --proxy 127.0.0.1:{0} \ -"http://www.example.com/magic" \ --H"Cookie: fpbeta=etc" \ --H "Proxy-Connection: keep-alive" \ ---verbose \ -'''.format(ts.Variables.port), - ts=ts) -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.StartBefore(server2, ready=When.PortOpen(server2.Variables.Port)) -tr.StillRunningAfter = ts - -server2.Streams.All = "gold/regexdoesntmatch.gold" diff --git a/tests/gold_tests/pluginTest/cookie_remap/setstatus.test.py b/tests/gold_tests/pluginTest/cookie_remap/setstatus.test.py deleted file mode 100644 index 55b30a26491..00000000000 --- a/tests/gold_tests/pluginTest/cookie_remap/setstatus.test.py +++ /dev/null @@ -1,77 +0,0 @@ -''' -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import os - -Test.Summary = ''' - -''' -Test.SkipUnless(Condition.PluginExists('cookie_remap.so')) -Test.ContinueOnFail = True -Test.testName = "cookie_remap: set HTTP status of the response" - -# Define default ATS -ts = Test.MakeATSProcess("ts") - -# Setup the remap configuration -config_path = os.path.join(Test.TestDirectory, "configs/statusconfig.txt") -with open(config_path, 'r') as config_file: - config1 = config_file.read() - -ts.Disk.records_config.update( - { - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'cookie_remap.*|http.*|dns.*', - }) - -ts.Disk.File(ts.Variables.CONFIGDIR + "/statusconfig.txt", id="config1") -ts.Disk.config1.WriteOn(config1) - -ts.Disk.remap_config.AddLine( - 'map http://www.example.com/magic http://shouldnothit.com @plugin=cookie_remap.so @pparam=config/statusconfig.txt') - -# Plugin sets the HTTP status because first rule matches -tr = Test.AddTestRun("Sets the status to 205") -tr.MakeCurlCommand( - ''' --proxy 127.0.0.1:{0} \ -"http://www.example.com/magic" \ --H"Cookie: fpbeta=magic" \ --H "Proxy-Connection: keep-alive" \ ---verbose \ -'''.format(ts.Variables.port), - ts=ts) -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.StartBefore(Test.Processes.ts) -tr.StillRunningAfter = ts - -tr.Streams.All = "gold/matchstatus.gold" - -# Plugin sets the HTTP status because the else rule matches (i.e. no match) -tr = Test.AddTestRun("Sets the else status to 400") -tr.MakeCurlCommand( - ''' --proxy 127.0.0.1:{0} \ -"http://www.example.com/magic" \ --H "Proxy-Connection: keep-alive" \ --H "Proxy-Connection: keep-alive" \ ---verbose \ -'''.format(ts.Variables.port), - ts=ts) -tr.Processes.Default.ReturnCode = 0 -tr.StillRunningAfter = ts - -tr.Streams.All = "gold/matchelsestatus.gold" diff --git a/tests/gold_tests/pluginTest/cookie_remap/subcookie.test.py b/tests/gold_tests/pluginTest/cookie_remap/subcookie.test.py deleted file mode 100644 index 3ef426f076d..00000000000 --- a/tests/gold_tests/pluginTest/cookie_remap/subcookie.test.py +++ /dev/null @@ -1,111 +0,0 @@ -''' -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import os - -Test.Summary = ''' - -''' -Test.SkipUnless(Condition.PluginExists('cookie_remap.so')) -Test.ContinueOnFail = True -Test.testName = "cookie_remap: test connector" - -# Define default ATS -ts = Test.MakeATSProcess("ts") - -# First server is run during first test and -# second server is run during second test -server = Test.MakeOriginServer("server", ip='127.0.0.10') - -request_header = { - "headers": "GET /cookiematches HTTP/1.1\r\nHost: www.example.com\r\n\r\n", - "timestamp": "1469733493.993", - "body": "" -} -# expected response from the origin server -response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} - -# add response to the server dictionary -server.addResponse("sessionfile.log", request_header, response_header) - -server2 = Test.MakeOriginServer("server2", ip='127.0.0.11') -request_header2 = { - "headers": "GET /cookiedoesntmatch HTTP/1.1\r\nHost: www.example.com\r\n\r\n", - "timestamp": "1469733493.993", - "body": "" -} -# expected response from the origin server -response_header2 = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} - -# add response to the server dictionary -server2.addResponse("sessionfile.log", request_header2, response_header2) - -# Setup the remap configuration -config_path = os.path.join(Test.TestDirectory, "configs/subcookie.txt") -with open(config_path, 'r') as config_file: - config1 = config_file.read() - -ts.Disk.records_config.update( - { - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'cookie_remap.*|http.*|dns.*', - }) - -config1 = config1.replace("$PORT", str(server.Variables.Port)) -config1 = config1.replace("$ALTPORT", str(server2.Variables.Port)) - -ts.Disk.File(ts.Variables.CONFIGDIR + "/subcookie.txt", id="config1") -ts.Disk.config1.WriteOn(config1) - -ts.Disk.remap_config.AddLine( - 'map http://www.example.com http://shouldnothit.com/magic @plugin=cookie_remap.so @pparam=config/subcookie.txt') - -# Positive test case that remaps because all connected operations pass -tr = Test.AddTestRun("cookie value matches") -# Unlike in other places I am using a single line string because the & seems to -# be interpreted by the autest framework or the shell (tried escaping with \) -tr.MakeCurlCommand( - '--proxy 127.0.0.1:{0} "http://www.example.com" -H"Cookie: fpbeta=a=1&b=2&c=3" -H "Proxy-Connection: keep-alive" --verbose ' - .format(ts.Variables.port), - ts=ts) -# tr.Processes.Default.Command = ''' -# curl -# --proxy 127.0.0.1:{0} -# "http://www.example.com" -# -H\"Cookie: fpbeta=a=1&b=2&c=3\" -# -H "Proxy-Connection: keep-alive" -# --verbose -# '''.format(ts.Variables.port) -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.StartBefore(server, ready=When.PortOpen(server.Variables.Port)) -tr.Processes.Default.StartBefore(Test.Processes.ts) -tr.StillRunningAfter = ts - -server.Streams.All = "gold/matchcookie.gold" - -# Negative test case that doesn't remap because not all subops pass -tr = Test.AddTestRun("cookie value doesn't match") -tr.MakeCurlCommand( - '--proxy 127.0.0.1:{0} "http://www.example.com" -H"Cookie: fpbeta=a=1&b=2&c=4" -H "Proxy-Connection: keep-alive" --verbose ' - .format(ts.Variables.port), - ts=ts) -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.StartBefore(server2, ready=When.PortOpen(server2.Variables.Port)) -tr.StillRunningAfter = ts - -server2.Streams.All = "gold/wontmatchcookie.gold" diff --git a/tests/gold_tests/pluginTest/cookie_remap/substitute.test.py b/tests/gold_tests/pluginTest/cookie_remap/substitute.test.py deleted file mode 100644 index 346a7544ed2..00000000000 --- a/tests/gold_tests/pluginTest/cookie_remap/substitute.test.py +++ /dev/null @@ -1,132 +0,0 @@ -''' -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import os - -Test.Summary = ''' - -''' -Test.SkipUnless(Condition.PluginExists('cookie_remap.so')) -Test.ContinueOnFail = True -Test.testName = "cookie_remap: Substitute variables" - -# Define default ATS -ts = Test.MakeATSProcess("ts") - -server = Test.MakeOriginServer("server", ip='127.0.0.10') - -request_header = { - "headers": "GET /photos/search?query=magic HTTP/1.1\r\nHost: www.example.com\r\n\r\n", - "timestamp": "1469733493.993", - "body": "" -} -response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} - -server.addResponse("sessionfile.log", request_header, response_header) - -request_header_2 = { - "headers": "GET /photos/search?query=/theunmatchedpath HTTP/1.1\r\nHost: www.example.com\r\n\r\n", - "timestamp": "1469733493.993", - "body": "" -} -response_header_2 = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} - -server.addResponse("sessionfile.log", request_header_2, response_header_2) - -request_header_3 = { - "headers": "GET /photos/search/magic/foobar HTTP/1.1\r\nHost: www.example.com\r\n\r\n", - "timestamp": "1469733493.993", - "body": "" -} -response_header_3 = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} - -server.addResponse("sessionfile.log", request_header_3, response_header_3) - -# Setup the remap configuration -config_path = os.path.join(Test.TestDirectory, "configs/substituteconfig.txt") -with open(config_path, 'r') as config_file: - config1 = config_file.read() - -ts.Disk.records_config.update( - { - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'cookie_remap.*|http.*|dns.*', - }) - -config1 = config1.replace("$PORT", str(server.Variables.Port)) - -ts.Disk.File(ts.Variables.CONFIGDIR + "/substituteconfig.txt", id="config1") -ts.Disk.config1.WriteOn(config1) - -ts.Disk.remap_config.AddLine( - 'map http://www.example.com/magic http://shouldnothit.com/magic @plugin=cookie_remap.so @pparam=config/substituteconfig.txt') - -tr = Test.AddTestRun("Substitute $path in the dest query") -tr.MakeCurlCommand( - ''' --proxy 127.0.0.1:{0} \ -"http://www.example.com/magic" \ --H"Cookie: fpbeta=abcd" \ --H "Proxy-Connection: keep-alive" \ ---verbose \ -'''.format(ts.Variables.port), - ts=ts) -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.StartBefore(server, ready=When.PortOpen(server.Variables.Port)) -tr.Processes.Default.StartBefore(Test.Processes.ts) -tr.StillRunningAfter = ts -tr.StillRunningAfter = server - -tr = Test.AddTestRun("Substitute $unmatched_path in the dest query") -tr.MakeCurlCommand( - ''' --proxy 127.0.0.1:{0} \ -"http://www.example.com/magic/theunmatchedpath" \ --H"Cookie: oxalpha=3333" \ --H "Proxy-Connection: keep-alive" \ ---verbose \ -'''.format(ts.Variables.port), - ts=ts) -tr.Processes.Default.ReturnCode = 0 -tr.StillRunningAfter = ts -tr.StillRunningAfter = server - -tr = Test.AddTestRun("Substitute $cr_req_url using $cr_urlencode") -tr.MakeCurlCommand( - ''' --proxy 127.0.0.1:{0} \ -"http://www.example.com/magic" \ --H"Cookie: acgamma=dfndfdfd" \ --H "Proxy-Connection: keep-alive" \ ---verbose \ -'''.format(ts.Variables.port), - ts=ts) -tr.Processes.Default.ReturnCode = 0 -tr.StillRunningAfter = ts -tr.StillRunningAfter = server - -tr = Test.AddTestRun("Substitute $path as is in outgoing path") -tr.MakeCurlCommand( - ''' --proxy 127.0.0.1:{0} \ -"http://www.example.com/magic/foobar" \ --H "Proxy-Connection: keep-alive" \ ---verbose \ -'''.format(ts.Variables.port), - ts=ts) -tr.Processes.Default.ReturnCode = 0 -tr.StillRunningAfter = ts -tr.StillRunningAfter = server - -server.Streams.All = "gold/substitute.gold" diff --git a/tests/gold_tests/pluginTest/crash_test/crash_test.test.py b/tests/gold_tests/pluginTest/crash_test/crash_test.test.py deleted file mode 100644 index 90da2057d5b..00000000000 --- a/tests/gold_tests/pluginTest/crash_test/crash_test.test.py +++ /dev/null @@ -1,101 +0,0 @@ -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. -""" -Test that crash logs are generated with backtraces when traffic_server crashes. - -This test intentionally crashes traffic_server using a plugin that dereferences -a null pointer when it receives a specific header. It then verifies that: -1. A crash log file was created -2. The crash log contains thread information -""" - -import os - -Test.Summary = ''' -Test crash log generation with backtrace. -''' - -# Create an origin server for the test. -server = Test.MakeOriginServer("server") - -request_header = {"headers": "GET / HTTP/1.1\r\nHost: example.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": "Hello"} -server.addResponse("sessionlog.json", request_header, response_header) - -ts = Test.MakeATSProcess("ts") - -# We expect ATS to crash with SIGSEGV, allowing us to test crash logging. -ts.ReturnCode = -11 - -ts.Disk.records_config.update( - { - 'proxy.config.proxy_name': 'test_proxy', - 'proxy.config.url_remap.remap_required': 0, - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'crash_test', - # Enable the crash log helper. - 'proxy.config.crash_log_helper': 'traffic_crashlog', - }) - -# Copy the crash_test plugin. -plugin_path = os.path.join(Test.Variables.AtsBuildGoldTestsDir, 'pluginTest', 'crash_test', '.libs', 'crash_test.so') -ts.Setup.Copy(plugin_path, ts.Env['PROXY_CONFIG_PLUGIN_PLUGIN_DIR']) - -ts.Disk.plugin_config.AddLine("crash_test.so") - -ts.Disk.remap_config.AddLine(f"map / http://127.0.0.1:{server.Variables.Port}/") - -ts.Disk.diags_log.Content += Testers.ContainsExpression( - "Received crash trigger header - crashing now!", "Expect the log indicating the intentional crash.") -ts.Disk.diags_log.Content += Testers.ExcludesExpression( - "This should never be reached.", "Expect to not see the log after the crash.") - -# Test 1: Make a normal request to verify the server is running. -tr = Test.AddTestRun("Verify server is running") -tr.Processes.Default.StartBefore(server) -tr.Processes.Default.StartBefore(ts) -tr.MakeCurlCommand(f'-s -o /dev/null -w "%{{http_code}}" http://127.0.0.1:{ts.Variables.port}/', ts=ts) -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Streams.stdout = Testers.ContainsExpression("200", "Expected 200 OK response") -tr.StillRunningAfter = ts -tr.StillRunningAfter = server - -# Test 2: Send the crash trigger header. -tr = Test.AddTestRun("Trigger crash") -# The curl command should fail since ATS will crash. -tr.MakeCurlCommand(f'-s -o /dev/null -H "X-Crash-Test: now" http://127.0.0.1:{ts.Variables.port}/', ts=ts) -tr.Processes.Default.ReturnCode = 52 - -# Test 3: Wait for a crash log to be created. -tr = Test.AddTestRun("Wait for crash log") -crash_log_glob = f'{ts.Variables.LOGDIR}/crash-*.log' -# Wait up to 60 seconds for a crash log file to appear, then 1 extra second for it to be written. -tr.Processes.Default.Command = (f"{os.path.join(Test.Variables.AtsTestToolsDir, 'condwait')} 60 1 -f '{crash_log_glob}'") -tr.Processes.Default.ReturnCode = 0 - -# Test 4: Verify crash log contains expected content. -tr = Test.AddTestRun("Check crash log content") -tr.Processes.Default.Command = (f'cat {ts.Variables.LOGDIR}/crash-*.log 2>&1') -tr.Processes.Default.ReturnCode = 0 -# The crash log should contain signal information (always present). -tr.Processes.Default.Streams.stdout += Testers.ContainsExpression( - "Segmentation fault", "Expected crash log to show segmentation fault signal") -# The crash log should contain the crashing thread information first. -# The crashing thread should be listed first. -tr.Processes.Default.Streams.stdout += Testers.ContainsExpression("Crashing Thread", "Expected crashing thread backtrace first") -# The other threads should be listed after. -tr.Processes.Default.Streams.stdout += Testers.ContainsExpression( - "Other Non-Crashing Threads:", "Expected other non-crashing threads section") diff --git a/tests/gold_tests/pluginTest/escalate/escalate.test.py b/tests/gold_tests/pluginTest/escalate/escalate.test.py deleted file mode 100644 index 46756e5a64a..00000000000 --- a/tests/gold_tests/pluginTest/escalate/escalate.test.py +++ /dev/null @@ -1,294 +0,0 @@ -''' -Verify escalate plugin behavior. -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import os -from ports import get_port - -Test.Summary = ''' -Test the escalate plugin. -''' - -Test.SkipUnless(Condition.PluginExists('escalate.so')) - - -class EscalateTest: - """ - Test the escalate plugin default behavior (GET requests only). - """ - - _replay_original_file: str = 'escalate_original.replay.yaml' - _replay_failover_file: str = 'escalate_failover.replay.yaml' - _server_original_file: str = 'escalate_original_server_default.replay.yaml' - _server_failover_file: str = 'escalate_failover_server_default.replay.yaml' - _process_counter: int = 0 - - def __init__(self, disable_redirect_header: bool = False, enable_cache: bool = False) -> None: - '''Configure the test run. - :param disable_redirect_header: Whether to use --no-redirect-header. - :param enable_cache: Whether to exercise the cached redirect path. - ''' - tr = Test.AddTestRun( - f'Test escalate plugin. disable_redirect_header={disable_redirect_header}, enable_cache={enable_cache}') - self._enable_cache = enable_cache - self._setup_dns(tr) - self._setup_servers(tr, disable_redirect_header) - self._setup_ts(tr, disable_redirect_header) - self._setup_client(tr) - EscalateTest._process_counter += 1 - - def _setup_dns(self, tr: 'TestRun') -> None: - '''Set up the DNS server. - - :param tr: The test run to add the DNS server to. - ''' - process_name = f"dns_{EscalateTest._process_counter}" - self._dns = tr.MakeDNServer(process_name, default='127.0.0.1') - - def _setup_servers(self, tr: 'TestRun', disable_redirect_header: bool) -> None: - '''Set up the origin and failover servers. - - :param tr: The test run to add the servers to. - :param disable_redirect_header: Whether ATS was configured with --no-redirect-header. - ''' - tr.Setup.Copy(self._replay_original_file) - tr.Setup.Copy(self._replay_failover_file) - tr.Setup.Copy(self._server_original_file) - tr.Setup.Copy(self._server_failover_file) - process_name = f"server_origin_{EscalateTest._process_counter}" - self._server_origin = tr.AddVerifierServerProcess(process_name, self._server_original_file) - process_name = f"server_failover_{EscalateTest._process_counter}" - self._server_failover = tr.AddVerifierServerProcess(process_name, self._server_failover_file) - - self._server_origin.Streams.All += Testers.ContainsExpression( - 'uuid: GET', "Verify the origin server received the GET request.") - self._server_origin.Streams.All += Testers.ContainsExpression( - 'uuid: GET_chunked', "Verify the origin server GET request for chunked content.") - self._server_origin.Streams.All += Testers.ContainsExpression( - 'uuid: GET_failed', "Verify the origin server received the GET request that it returns a 502 with.") - self._server_origin.Streams.All += Testers.ContainsExpression( - 'uuid: HEAD_fail_not_escalated', "Verify the origin server received the HEAD request that should not be escalated.") - self._server_origin.Streams.All += Testers.ContainsExpression( - 'uuid: POST_fail_not_escalated', "Verify the origin server received the POST request that should not be escalated.") - self._server_origin.Streams.All += Testers.ExcludesExpression( - 'uuid: GET_down_origin', "Verify the origin server did not receive the down origin request.") - self._server_origin.Streams.All += Testers.ExcludesExpression( - 'x-escalate-redirect', "Verify the origin server should never receive the x-escalate-redirect header.") - - self._server_failover.Streams.All += Testers.ContainsExpression( - 'uuid: GET_failed', "Verify the failover server received the failed GET request.") - self._server_failover.Streams.All += Testers.ContainsExpression( - 'uuid: GET_down_origin', "Verify the failover server received the GET request for the down origin.") - - self._server_failover.Streams.All += Testers.ExcludesExpression( - 'x-request: first', "Verify the failover server did not receive the GET request.") - self._server_failover.Streams.All += Testers.ExcludesExpression( - 'uuid: GET_chunked', "Verify the failover server did not receive the GET request for chunked content.") - # By default, non-GET methods should NOT be escalated to failover - self._server_failover.Streams.All += Testers.ExcludesExpression( - 'uuid: HEAD_fail_not_escalated', - "Verify the failover server did not receive the HEAD request that should not be escalated.") - self._server_failover.Streams.All += Testers.ExcludesExpression( - 'uuid: POST_fail_not_escalated', - "Verify the failover server did not receive the POST request that should not be escalated.") - - if disable_redirect_header: - self._server_failover.Streams.All += Testers.ExcludesExpression( - 'x-escalate-redirect', "Verify the failover server did not receive the x-escalate-redirect header.") - else: - self._server_failover.Streams.All += Testers.ContainsExpression( - 'x-escalate-redirect: 1', "Verify the failover server received the x-escalate-redirect header.") - - def _setup_ts(self, tr: 'Process', disable_redirect_header: bool) -> None: - '''Set up Traffic Server. - - :param tr: The test run to add Traffic Server to. - :param disable_redirect_header: Whether ATS should be configured with --no-redirect-header. - ''' - process_name = f"ts_{EscalateTest._process_counter}" - self._ts = tr.MakeATSProcess(process_name, enable_cache=self._enable_cache) - # Select a port that is guaranteed to not be used at the moment. - dead_port = get_port(self._ts, "dead_port") - self._ts.Disk.records_config.update( - { - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'http|escalate', - 'proxy.config.dns.nameservers': f'127.0.0.1:{self._dns.Variables.Port}', - 'proxy.config.dns.resolv_conf': 'NULL', - 'proxy.config.http.redirect.actions': 'self:follow', - 'proxy.config.http.number_of_redirections': 4, - }) - params = '' - if disable_redirect_header: - params = '@pparam=--no-redirect-header' - self._ts.Disk.remap_config.AddLines( - [ - f'map http://origin.server.com http://backend.origin.server.com:{self._server_origin.Variables.http_port} ' - f'@plugin=escalate.so @pparam=500,502:failover.server.com:{self._server_failover.Variables.http_port} {params}', - - # Now create remap entries for the multiplexed hosts: one that - # verifies HTTP, and another that verifies HTTPS. - f'map http://down_origin.server.com http://backend.down_origin.server.com:{dead_port} ' - f'@plugin=escalate.so @pparam=500,502:failover.server.com:{self._server_failover.Variables.http_port} {params}', - ]) - - def _setup_client(self, tr: 'Process') -> None: - '''Set up the client. - - :param tr: The test run to add the client to. - ''' - process_name = f"client_{EscalateTest._process_counter}" - client = tr.AddVerifierClientProcess(process_name, self._replay_original_file, http_ports=[self._ts.Variables.port]) - client.StartBefore(self._dns) - client.StartBefore(self._server_origin) - client.StartBefore(self._server_failover) - client.StartBefore(self._ts) - - client.Streams.All += Testers.ExcludesExpression(r'\[ERROR\]', 'Verify there were no errors in the replay.') - client.Streams.All += Testers.ExcludesExpression('400 Bad', 'Verify none of the 400 responses make it to the client.') - client.Streams.All += Testers.ExcludesExpression('500 Internal', 'Verify none of the 500 responses make it to the client.') - # GET requests should be escalated and return 200 - client.Streams.All += Testers.ContainsExpression('x-response: first', 'Verify that the first response was received.') - client.Streams.All += Testers.ContainsExpression('x-response: second', 'Verify that the second response was received.') - client.Streams.All += Testers.ContainsExpression('x-response: third', 'Verify that the third response was received.') - client.Streams.All += Testers.ContainsExpression('x-response: fourth', 'Verify that the fourth response was received.') - # Non-GET requests should NOT be escalated and return 502 (default behavior) - client.Streams.All += Testers.ContainsExpression( - 'x-response: head_fail_not_escalated', 'Verify that the HEAD response was received (502).') - client.Streams.All += Testers.ContainsExpression( - 'x-response: post_fail_not_escalated', 'Verify that the POST response was received (502).') - client.Streams.All += Testers.ContainsExpression( - '502 Bad Gateway', 'Verify that non-GET requests return 502 (not escalated by default).') - - -class EscalateNonGetMethodsTest: - """ - Test the escalate plugin with --escalate-non-get-methods option to verify non-GET requests are also escalated. - """ - - _replay_get_method_file: str = 'escalate_non_get_methods.replay.yaml' - _replay_failover_file: str = 'escalate_failover.replay.yaml' - _server_origin_file: str = 'escalate_original_server_non_get.replay.yaml' - _server_failover_file: str = 'escalate_failover_server_non_get.replay.yaml' - - def __init__(self): - '''Configure the test run for escalating non-GET methods testing.''' - tr = Test.AddTestRun('Test escalate plugin with --escalate-non-get-methods option.') - self._setup_dns(tr) - self._setup_servers(tr) - self._setup_ts(tr) - self._setup_client(tr) - - def _setup_dns(self, tr: 'TestRun') -> None: - '''Set up the DNS server.''' - self._dns = tr.MakeDNServer("dns_non_get_methods", default='127.0.0.1') - - def _setup_servers(self, tr: 'TestRun') -> None: - '''Set up the origin and failover servers for non-GET methods testing.''' - tr.Setup.Copy(self._replay_get_method_file) - tr.Setup.Copy(self._replay_failover_file) - tr.Setup.Copy(self._server_origin_file) - tr.Setup.Copy(self._server_failover_file) - self._server_origin = tr.AddVerifierServerProcess("server_origin_non_get_methods", self._server_origin_file) - self._server_failover = tr.AddVerifierServerProcess("server_failover_non_get_methods", self._server_failover_file) - - # Verify the origin server received all requests - self._server_origin.Streams.All += Testers.ContainsExpression( - 'uuid: GET', "Verify the origin server received the first GET request.") - self._server_origin.Streams.All += Testers.ContainsExpression( - 'uuid: GET_chunked', "Verify the origin server received the chunked GET request.") - self._server_origin.Streams.All += Testers.ContainsExpression( - 'uuid: GET_failed', "Verify the origin server received the failed GET request.") - self._server_origin.Streams.All += Testers.ContainsExpression( - 'uuid: POST_success', "Verify the origin server received the successful POST request.") - self._server_origin.Streams.All += Testers.ContainsExpression( - 'uuid: HEAD_fail_escalated', "Verify the origin server received the HEAD request that will be escalated.") - - # The down origin request should NOT be received by this server - self._server_origin.Streams.All += Testers.ExcludesExpression( - 'uuid: GET_down_origin', "Verify the origin server did not receive the down origin request.") - - # Verify failover server receives escalated requests including non-GET methods - self._server_failover.Streams.All += Testers.ContainsExpression( - 'uuid: GET_failed', "Verify the failover server received the failed GET request.") - self._server_failover.Streams.All += Testers.ContainsExpression( - 'uuid: GET_down_origin', "Verify the failover server received the down origin GET request.") - # With --escalate-non-get-methods, the HEAD request should now be escalated - self._server_failover.Streams.All += Testers.ContainsExpression( - 'uuid: HEAD_fail_escalated', "Verify the failover server received the HEAD that is now escalated.") - # The successful POST should also not reach failover (since it succeeds on origin) - self._server_failover.Streams.All += Testers.ExcludesExpression( - 'uuid: POST_success', "Verify the failover server did not receive the successful POST request.") - - def _setup_ts(self, tr: 'TestRun') -> None: - '''Set up Traffic Server with --escalate-non-get-methods option.''' - self._ts = tr.MakeATSProcess("ts_non_get_methods", enable_cache=False) - - self._ts.Disk.records_config.update( - { - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'http|escalate', - 'proxy.config.dns.nameservers': f'127.0.0.1:{self._dns.Variables.Port}', - 'proxy.config.dns.resolv_conf': 'NULL', - 'proxy.config.http.redirect.actions': 'self:follow', - 'proxy.config.http.number_of_redirections': 4, - }) - - # Set up a dead port for the down origin scenario - dead_port = get_port(self._ts, "dead_port") - - # Configure escalate plugin with --escalate-non-get-methods option - self._ts.Disk.remap_config.AddLines( - [ - f'map http://origin.server.com http://backend.origin.server.com:{self._server_origin.Variables.http_port} ' - f'@plugin=escalate.so @pparam=500,502:failover.server.com:{self._server_failover.Variables.http_port} @pparam=--escalate-non-get-methods', - f'map http://down_origin.server.com http://backend.down_origin.server.com:{dead_port} ' - f'@plugin=escalate.so @pparam=500,502:failover.server.com:{self._server_failover.Variables.http_port} @pparam=--escalate-non-get-methods', - ]) - - def _setup_client(self, tr: 'TestRun') -> None: - '''Set up the client for non-GET methods testing.''' - client = tr.AddVerifierClientProcess( - "client_non_get_methods", self._replay_get_method_file, http_ports=[self._ts.Variables.port]) - - client.StartBefore(self._dns) - client.StartBefore(self._server_origin) - client.StartBefore(self._server_failover) - client.StartBefore(self._ts) - - # Verify that successful responses are returned for successful requests and escalated failures - client.Streams.All += Testers.ContainsExpression('x-response: first', 'Verify first GET response received.') - client.Streams.All += Testers.ContainsExpression('x-response: second', 'Verify second GET response received.') - client.Streams.All += Testers.ContainsExpression('x-response: third', 'Verify third GET response received (escalated).') - client.Streams.All += Testers.ContainsExpression('x-response: fourth', 'Verify fourth GET response received (escalated).') - client.Streams.All += Testers.ContainsExpression('x-response: post_success', 'Verify successful POST response received.') - client.Streams.All += Testers.ContainsExpression( - 'x-response: head_fail_escalated', 'Verify escalated HEAD response received.') - - # With --escalate-non-get-methods, POST and HEAD failures should now be escalated and return 200 - client.Streams.All += Testers.ExcludesExpression( - '502 Bad Gateway', 'Verify failed POST and HEAD requests are now escalated') - - # The test should complete without errors - client.Streams.All += Testers.ExcludesExpression(r'\[ERROR\]', 'Verify there were no errors in the replay.') - - -EscalateTest(disable_redirect_header=False) -EscalateTest(disable_redirect_header=True) -EscalateTest(disable_redirect_header=False, enable_cache=True) -EscalateNonGetMethodsTest() diff --git a/tests/gold_tests/pluginTest/esi/esi.test.py b/tests/gold_tests/pluginTest/esi/esi.test.py deleted file mode 100644 index a698ed3bc24..00000000000 --- a/tests/gold_tests/pluginTest/esi/esi.test.py +++ /dev/null @@ -1,380 +0,0 @@ -''' -Test the ESI plugin. -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import os -import re - -Test.Summary = ''' -Test the ESI plugin. -''' - -Test.SkipUnless(Condition.PluginExists('esi.so'),) - - -# An enum of expected plugin behaviors for Cache-Control headers. -class CcBehaviorT(): - REMOVE_CC = 0 - MAKE_PRIVATE = 1 - - -class EsiTest(): - """ - A class that encapsulates the configuration and execution of a set of EPI - test cases. - """ - """ static: The same server Process is used across all tests. """ - _server = None - """ static: A counter to keep the ATS process names unique across tests. """ - _ts_counter = 0 - """ static: A counter to keep any output file names unique across tests. """ - _output_counter = 0 - """ The ATS process for this set of test cases. """ - _ts = None - - def __init__(self, plugin_config, cc_behavior=CcBehaviorT.REMOVE_CC): - """ - Args: - plugin_config (str): The base config line to place in plugin.config for - the ATS process. - cc_behavior (CcBehaviorT): The expected behavior of the ESI plugin - with respect to the Cache-Control header. - """ - if EsiTest._server is None: - EsiTest._server = EsiTest._create_server() - - self._plugin_config = plugin_config - self._cc_behavior = cc_behavior - self._ts = EsiTest._create_ats(self, plugin_config) - - @staticmethod - def _create_server(): - """ - Create and start a server process. - """ - # Configure our server. - server = Test.MakeOriginServer("server") - - # Generate the set of ESI responses derived right from our ESI docs. - # See: - # doc/admin-guide/plugins/esi.en.rst - request_header = { - "headers": ("GET /esi.php HTTP/1.1\r\n" - "Host: www.example.com\r\n" - "Content-Length: 0\r\n\r\n"), - "timestamp": "1469733493.993", - "body": "" - } - esi_body = r''' - - -Hello, - - -''' - response_header = { - "headers": - ( - "HTTP/1.1 200 OK\r\n" - "Content-Type: text/html\r\n" - "X-Esi: 1\r\n" - "Connection: close\r\n" - "Content-Length: {}\r\n" - "Cache-Control: max-age=300\r\n" - "\r\n".format(len(esi_body))), - "timestamp": "1469733493.993", - "body": esi_body - } - server.addResponse("sessionfile.log", request_header, response_header) - request_header = { - "headers": ("GET /date.php HTTP/1.1\r\n" - "Host: www.example.com\r\n" - "Content-Length: 0\r\n\r\n"), - "timestamp": "1469733493.993", - "body": "" - } - date_body = r''' -''' - response_header = { - "headers": - ( - "HTTP/1.1 200 OK\r\n" - "Content-Type: text/html\r\n" - "Connection: close\r\n" - "Content-Length: {}\r\n" - "Cache-Control: max-age=300\r\n" - "\r\n".format(len(date_body))), - "timestamp": "1469733493.993", - "body": date_body - } - server.addResponse("sessionfile.log", request_header, response_header) - # Verify correct functionality with an empty body. - request_header = { - "headers": ("GET /expect_empty_body HTTP/1.1\r\n" - "Host: www.example.com\r\n" - "Content-Length: 0\r\n\r\n"), - "timestamp": "1469733493.993", - "body": "" - } - response_header = { - "headers": - ( - "HTTP/1.1 200 OK\r\n" - "X-ESI: On\r\n" - "Content-Length: 0\r\n" - "Connection: close\r\n" - "Content-Type: text/html; charset=UTF-8\r\n" - "\r\n"), - "timestamp": "1469733493.993", - "body": "" - } - server.addResponse("sessionfile.log", request_header, response_header) - - # Create a run to start the server. - tr = Test.AddTestRun("Start the server.") - tr.Processes.Default.StartBefore(server) - tr.Processes.Default.Command = "echo starting the server" - tr.Processes.Default.ReturnCode = 0 - tr.StillRunningAfter = server - - return server - - @staticmethod - def _create_ats(self, plugin_config): - """ - Create and start an ATS process. - """ - EsiTest._ts_counter += 1 - - # Configure ATS with a vanilla ESI plugin configuration. - ts = Test.MakeATSProcess("ts{}".format(EsiTest._ts_counter)) - ts.Disk.records_config.update({ - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'http|plugin_esi', - }) - ts.Disk.remap_config.AddLine(f'map http://www.example.com/ http://127.0.0.1:{EsiTest._server.Variables.Port}') - ts.Disk.plugin_config.AddLine(plugin_config) - - # Create a run to start the ATS process. - tr = Test.AddTestRun("Start the ATS process.") - tr.Processes.Default.StartBefore(ts) - tr.Processes.Default.Command = "echo starting ATS" - tr.Processes.Default.ReturnCode = 0 - tr.StillRunningAfter = ts - return ts - - def _configure_client_output_expectations(self, client_process): - client_process.Streams.stderr = "gold/esi_headers.gold" - client_process.Streams.stdout = "gold/esi_body.gold" - if self._cc_behavior == CcBehaviorT.REMOVE_CC: - client_process.Streams.stderr += Testers.ExcludesExpression( - 'cache-control:', 'The Cache-Control field should not be present in the response', reflags=re.IGNORECASE) - client_process.Streams.stderr += Testers.ExcludesExpression( - 'expires:', 'The Expires field should not be present in the response', reflags=re.IGNORECASE) - if self._cc_behavior == CcBehaviorT.MAKE_PRIVATE: - client_process.Streams.stderr += Testers.ContainsExpression( - 'cache-control:.*max-age=0, private', - 'The private response directive should be present in the response', - reflags=re.IGNORECASE) - client_process.Streams.stderr += Testers.ContainsExpression( - 'expires: -1', 'The Expires field should be set to -1', reflags=re.IGNORECASE) - - def run_cases_expecting_gzip(self): - # Test 1: Verify basic ESI functionality. - tr = Test.AddTestRun(f"First request for esi.php: not cached: {self._plugin_config}") - tr.MakeCurlCommand( - f'http://127.0.0.1:{self._ts.Variables.port}/esi.php -H"Host: www.example.com" ' - '-H"Accept: */*" --verbose', - ts=self._ts) - tr.Processes.Default.ReturnCode = 0 - self._configure_client_output_expectations(tr.Processes.Default) - tr.StillRunningAfter = self._server - tr.StillRunningAfter = self._ts - - # Test 2: Repeat the above, should now be cached. - tr = Test.AddTestRun(f"Second request for esi.php: will be cached: {self._plugin_config}") - tr.MakeCurlCommand( - f'http://127.0.0.1:{self._ts.Variables.port}/esi.php -H"Host: www.example.com" ' - '-H"Accept: */*" --verbose', - ts=self._ts) - tr.Processes.Default.ReturnCode = 0 - self._configure_client_output_expectations(tr.Processes.Default) - tr.StillRunningAfter = self._server - tr.StillRunningAfter = self._ts - - # Test 3: Verify the ESI plugin can gzip a response when the client accepts it. - tr = Test.AddTestRun(f"Verify the ESI plugin can gzip a response: {self._plugin_config}") - EsiTest._output_counter += 1 - unzipped_body_file = os.path.join(tr.RunDirectory, f"non_empty_curl_output_{EsiTest._output_counter}") - gzipped_body_file = unzipped_body_file + ".gz" - tr.MakeCurlCommand( - f'http://127.0.0.1:{self._ts.Variables.port}/esi.php -H"Host: www.example.com" ' - f'-H "Accept-Encoding: gzip" -H"Accept: */*" --verbose --output {gzipped_body_file}', - ts=self._ts) - tr.Processes.Default.ReturnCode = 0 - tr.Processes.Default.Ready = When.FileExists(gzipped_body_file) - tr.Processes.Default.Streams.stderr = "gold/esi_gzipped.gold" - tr.StillRunningAfter = self._server - tr.StillRunningAfter = self._ts - zipped_body_disk_file = tr.Disk.File(gzipped_body_file) - zipped_body_disk_file.Exists = True - - # Now, unzip the file and make sure its size is the expected body. - tr = Test.AddTestRun(f"Verify the file unzips to the expected body: {self._plugin_config}") - tr.Processes.Default.Command = f"gunzip {gzipped_body_file}" - tr.Processes.Default.Ready = When.FileExists(unzipped_body_file) - tr.Processes.Default.ReturnCode = 0 - unzipped_body_disk_file = tr.Disk.File(unzipped_body_file) - unzipped_body_disk_file.Content = "gold/esi_body.gold" - - # Test 4: Verify correct handling of a gzipped empty response body. - tr = Test.AddTestRun(f"Verify we can handle an empty response: {self._plugin_config}") - EsiTest._output_counter += 1 - empty_body_file = os.path.join(tr.RunDirectory, f"empty_curl_output_{EsiTest._output_counter}") - gzipped_empty_body = empty_body_file + ".gz" - tr.MakeCurlCommand( - f'http://127.0.0.1:{self._ts.Variables.port}/expect_empty_body ' - '-H"Host: www.example.com" -H"Accept-Encoding: gzip" -H"Accept: */*" ' - f'--verbose --output {gzipped_empty_body}', - ts=self._ts) - tr.Processes.Default.ReturnCode = 0 - tr.Processes.Default.Ready = When.FileExists(gzipped_empty_body) - tr.Processes.Default.Streams.stderr = "gold/empty_response_body.gold" - tr.StillRunningAfter = self._server - tr.StillRunningAfter = self._ts - # The gzipped output file should be greater than 0, even though 0 bytes are - # compressed. - gz_disk_file = tr.Disk.File(gzipped_empty_body) - gz_disk_file.Size = Testers.GreaterThan(0) - - # Now, unzip the file and make sure its size is the original 0 size body. - tr = Test.AddTestRun(f"Verify the file unzips to a zero sized file: {self._plugin_config}") - tr.Processes.Default.Command = f"gunzip {gzipped_empty_body}" - tr.Processes.Default.Ready = When.FileExists(empty_body_file) - tr.Processes.Default.ReturnCode = 0 - unzipped_disk_file = tr.Disk.File(empty_body_file) - unzipped_disk_file.Size = 0 - - def run_case_max_doc_size_too_small(self): - tr = Test.AddTestRun(f"Max doc size too small: {self._plugin_config}") - tr.MakeCurlCommand( - f'http://127.0.0.1:{self._ts.Variables.port}/esi.php ' - '-H"Host: www.example.com" -H"Accept: */*" --verbose', - ts=self._ts) - tr.Processes.Default.ReturnCode = 0 - self._ts.Disk.diags_log.Content = Testers.ContainsExpression( - r"ERROR: \[_setup\] Cannot allow attempted doc of size 121; Max allowed size is 100 for URL \[.*esi\.php.*\]", - "max doc size test should have doc size error log") - tr.StillRunningAfter = self._server - tr.StillRunningAfter = self._ts - - def run_cases_expecting_no_gzip(self): - # Test 1: Run an ESI test where the client does not accept gzip. - tr = Test.AddTestRun(f"First request for esi.php: gzip not accepted: {self._plugin_config}") - tr.MakeCurlCommand( - f'http://127.0.0.1:{self._ts.Variables.port}/esi.php ' - '-H"Host: www.example.com" -H"Accept: */*" --verbose', - ts=self._ts) - tr.Processes.Default.ReturnCode = 0 - self._configure_client_output_expectations(tr.Processes.Default) - tr.StillRunningAfter = self._server - tr.StillRunningAfter = self._ts - - # Test 2: Verify the ESI plugin does not gzip the response even if the - # client accepts the gzip encoding. - tr = Test.AddTestRun(f"Verify the ESI plugin refuses to gzip responses with: {self._plugin_config}") - tr.MakeCurlCommand( - f'http://127.0.0.1:{self._ts.Variables.port}/esi.php ' - '-H"Host: www.example.com" -H "Accept-Encoding: gzip" -H"Accept: */*" --verbose', - ts=self._ts) - tr.Processes.Default.ReturnCode = 0 - self._configure_client_output_expectations(tr.Processes.Default) - tr.StillRunningAfter = self._server - tr.StillRunningAfter = self._ts - - def run_cases_expecting_no_transformation(self): - tr = Test.AddTestRun(f"Verify the ESI plugin does not transform responses: {self._plugin_config}") - client = tr.MakeCurlCommand( - f'http://127.0.0.1:{self._ts.Variables.port}/esi.php ' - '-H"Host: www.example.com" -H"Accept: */*" --verbose', - ts=self._ts) - client.ReturnCode = 0 - - # Expect no transformation: the tag should be present without any transformation. - client.Streams.stdout += Testers.ContainsExpression( - 'Hello, ', - 'The response should not be transformed', - reflags=re.IGNORECASE) - tr.StillRunningAfter = self._server - tr.StillRunningAfter = self._ts - - -# -# Configure and run the test cases. -# - -# Run the tests with ESI configured with no parameters. -vanilla_test = EsiTest(plugin_config='esi.so', cc_behavior=CcBehaviorT.REMOVE_CC) -vanilla_test.run_cases_expecting_gzip() - -private_response_test = EsiTest(plugin_config='esi.so --private-response', cc_behavior=CcBehaviorT.MAKE_PRIVATE) -private_response_test.run_cases_expecting_gzip() - -# For these test cases, the behavior should remain the same with -# --first-byte-flush set. -first_byte_flush_test = EsiTest(plugin_config='esi.so --first-byte-flush') -first_byte_flush_test.run_cases_expecting_gzip() - -# For these test cases, the behavior should remain the same with -# --packed-node-support set. -# -# Packed node support is incomplete and the following test does not work. Our -# documentation advises users not to use the --packed-node-support feature. -# This test is left here, commented out, so that it is conveniently available -# for and potential future development on this feature if desired. -# -# packed_node_support_test = EsiTest(plugin_config='esi.so --packed-node-support') -# packed_node_support_test.run_cases_expecting_gzip() - -# Run a set of cases verifying that the plugin does not zip content if -# --disable-gzip-output is set. -gzip_disabled_test = EsiTest(plugin_config='esi.so --disable-gzip-output') -gzip_disabled_test.run_cases_expecting_no_gzip() - -# Run the tests with too small max doc size. -max_doc_100_test = EsiTest(plugin_config='esi.so --max-doc-size 100') -max_doc_100_test.run_case_max_doc_size_too_small() - -# Run the tests with no default, but sufficient, max doc size. -max_doc_2K_test = EsiTest(plugin_config='esi.so --max-doc-size 2K') -max_doc_2K_test.run_cases_expecting_gzip() -max_doc_20M_test = EsiTest(plugin_config='esi.so --max-doc-size 20M') -max_doc_20M_test.run_cases_expecting_gzip() - -# The test doesn't use 304 redirect, so restricting the allowed response codes to 200 -# should not affect the test. -allowed_response_codes_test = EsiTest(plugin_config='esi.so --allowed-response-codes 200') -allowed_response_codes_test.run_cases_expecting_gzip() - -# Do not allow transforming the 200 OK response. Since the test uses a 200 OK response, -# the plugin should not transform it. -response_not_allowed_test = EsiTest(plugin_config='esi.so --allowed-response-codes 304') -response_not_allowed_test.run_cases_expecting_no_transformation() diff --git a/tests/gold_tests/pluginTest/esi/esi_304.test.py b/tests/gold_tests/pluginTest/esi/esi_304.test.py deleted file mode 100644 index 62b60bf79e0..00000000000 --- a/tests/gold_tests/pluginTest/esi/esi_304.test.py +++ /dev/null @@ -1,180 +0,0 @@ -''' -Test the ESI plugin when origin returns 304 response. -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import os - -Test.Summary = ''' -Test the ESI plugin when origin returns 304 response. -''' - -Test.SkipUnless(Condition.PluginExists('esi.so'),) - - -class EsiTest(): - """ - A class that encapsulates the configuration and execution of a set of ESI - test cases. - """ - """ static: The same server Process is used across all tests. """ - _server = None - """ static: A counter to keep the ATS process names unique across tests. """ - _ts_counter = 0 - """ static: A counter to keep any output file names unique across tests. """ - _output_counter = 0 - """ The ATS process for this set of test cases. """ - _ts = None - - def __init__(self, plugin_config): - """ - Args: - plugin_config (str): The config line to place in plugin.config for - the ATS process. - """ - if EsiTest._server is None: - EsiTest._server = EsiTest._create_server() - - self._ts = EsiTest._create_ats(self, plugin_config) - - @staticmethod - def _create_server(): - """ - Create and start a server process. - """ - # Configure our server. - server = Test.MakeOriginServer("server", lookup_key="{%uuid}") - - # Generate the set of ESI responses. - request_header = { - "headers": - "GET /esi_etag.php HTTP/1.1\r\n" + "Host: www.example.com\r\n" + "uuid: first\r\n" + "Content-Length: 0\r\n\r\n", - "timestamp": "1469733493.993", - "body": "" - } - esi_body = r''' - -Hello, ESI 304 test - - -''' - response_header = { - "headers": - "HTTP/1.1 200 OK\r\n" + "X-Esi: 1\r\n" + "Cache-Control: public, max-age=0\r\n" + 'Etag: "esi_304_test"\r\n' + - "Content-Type: text/html\r\n" + "Connection: close\r\n" + "Content-Length: {}\r\n".format(len(esi_body)) + "\r\n", - "timestamp": "1469733493.993", - "body": esi_body - } - server.addResponse("sessionfile.log", request_header, response_header) - - request_header = { - "headers": - "GET /esi_etag.php HTTP/1.1\r\n" + "Host: www.example.com\r\n" + "uuid: second\r\n" + - 'If-None-Match: "esi_304_test"\r\n' + "Content-Length: 0\r\n\r\n", - "timestamp": "1469733493.993", - "body": "" - } - response_header = { - "headers": - "HTTP/1.1 304 Not Modified\r\n" + "Content-Type: text/html\r\n" + "Connection: close\r\n" + - "Content-Length: 0\r\n" + "\r\n", - "timestamp": "1469733493.993", - "body": "" - } - server.addResponse("sessionfile.log", request_header, response_header) - - request_header = { - "headers": "GET /date.php HTTP/1.1\r\n" + "Host: www.example.com\r\n" + "uuid: date\r\n" + "Content-Length: 0\r\n\r\n", - "timestamp": "1469733493.993", - "body": "" - } - date_body = r'''ESI 304 test -No Date -''' - response_header = { - "headers": - "HTTP/1.1 200 OK\r\n" + "Content-Type: text/html\r\n" + "Connection: close\r\n" + - "Content-Length: {}\r\n".format(len(date_body)) + "\r\n", - "timestamp": "1469733493.993", - "body": date_body - } - server.addResponse("sessionfile.log", request_header, response_header) - - # Create a run to start the server. - tr = Test.AddTestRun("Start the server.") - tr.Processes.Default.StartBefore(server) - tr.Processes.Default.Command = "echo starting the server" - tr.Processes.Default.ReturnCode = 0 - tr.StillRunningAfter = server - - return server - - @staticmethod - def _create_ats(self, plugin_config): - """ - Create and start an ATS process. - """ - EsiTest._ts_counter += 1 - - # Configure ATS with a vanilla ESI plugin configuration. - ts = Test.MakeATSProcess("ts{}".format(EsiTest._ts_counter)) - ts.Disk.records_config.update({ - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'http|plugin_esi', - }) - ts.Disk.remap_config.AddLine('map http://www.example.com/ http://127.0.0.1:{0}'.format(EsiTest._server.Variables.Port)) - ts.Disk.plugin_config.AddLine(plugin_config) - - # Create a run to start the ATS process. - tr = Test.AddTestRun("Start the ATS process.") - tr.Processes.Default.StartBefore(ts) - tr.Processes.Default.Command = "echo starting ATS" - tr.Processes.Default.ReturnCode = 0 - tr.StillRunningAfter = ts - return ts - - def run_cases(self): - # Test 1: Verify basic ESI functionality. - tr = Test.AddTestRun("First request for esi_etag.php: not cached") - tr.MakeCurlCommand( - 'http://127.0.0.1:{0}/esi_etag.php -H"Host: www.example.com" ' - '-H"Accept: */*" -H"uuid: first" --verbose -o /dev/stderr'.format(self._ts.Variables.port), - ts=self._ts) - tr.Processes.Default.ReturnCode = 0 - tr.Processes.Default.Streams.stderr = "gold/esi_private_headers.gold" - tr.StillRunningAfter = self._server - tr.StillRunningAfter = self._ts - - # Test 2: Repeat the above, origin should now be returning 304 response. - tr = Test.AddTestRun("Second request for esi_etag.php: will be cached") - tr.MakeCurlCommand( - 'http://127.0.0.1:{0}/esi_etag.php -H"Host: www.example.com" ' - '-H"Accept: */*" -H"uuid: second" --verbose -o /dev/stderr'.format(self._ts.Variables.port), - ts=self._ts) - tr.Processes.Default.ReturnCode = 0 - tr.Processes.Default.Streams.stderr = "gold/esi_private_headers.gold" - tr.StillRunningAfter = self._server - tr.StillRunningAfter = self._ts - - -# -# Configure and run the test cases. -# - -# Run the tests with ESI configured with private response. -private_response_test = EsiTest(plugin_config='esi.so --private-response') -private_response_test.run_cases() diff --git a/tests/gold_tests/pluginTest/esi/esi_nested_html_comment.test.py b/tests/gold_tests/pluginTest/esi/esi_nested_html_comment.test.py deleted file mode 100644 index 02462d1b0f3..00000000000 --- a/tests/gold_tests/pluginTest/esi/esi_nested_html_comment.test.py +++ /dev/null @@ -1,156 +0,0 @@ -''' -Regression test for the ESI comment wrapper handling. - -Covers the sec-035 fix that rejects nested wrappers inside -another . Two scenarios: - - 1. Legitimate single-level wrapper: must still be expanded normally and - the nested-wrapper guard must NOT fire. - - 2. Nested wrapper hidden inside / child_nodes: - must be rejected by the guard and the diags log must record the - "Nested inside " error. This is the - deeper case the prior top-level-only check missed. -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -Test.Summary = ''' -Verify the ESI plugin processes a legitimate single-level wrapper -and rejects a nested hidden inside /. -''' - -Test.SkipUnless(Condition.PluginExists('esi.so'),) - - -class EsiHtmlCommentTest(): - """ - Drive a single request through ATS whose origin response contains an - `` wrapper, and verify the plugin - expands it without tripping the nested-wrapper guard. - """ - - _replay_file: str = "esi_nested_html_comment.replay.yaml" - - def __init__(self, plugin_config: str) -> None: - tr = Test.AddTestRun("ESI single-level wrapper is processed") - self._create_server(tr) - self._create_ats(tr, plugin_config) - self._create_client(tr) - - def _create_server(self, tr: 'TestRun') -> 'Process': - server = tr.AddVerifierServerProcess("server", self._replay_file, other_args='--format "{url}"') - self._server = server - - server.Streams.All += Testers.ContainsExpression( - 'GET /esi-html-comment.php', 'Verify the server received the ESI document request.') - return server - - def _create_ats(self, tr: 'TestRun', plugin_config: str) -> 'Process': - ts = tr.MakeATSProcess("ts") - self._ts = ts - ts.Disk.records_config.update( - { - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'http|plugin_esi|plugin_esi_procesor', - }) - server_port = self._server.Variables.http_port - ts.Disk.remap_config.AddLine(f'map http://www.example.com/ http://127.0.0.1:{server_port}') - ts.Disk.plugin_config.AddLine(plugin_config) - - # The nested-wrapper guard added for sec-035 must NOT fire on a - # legitimate single-level wrapper. - ts.Disk.diags_log.Content = Testers.ExcludesExpression( - r'Nested inside ', 'The nested-wrapper guard must not fire on legitimate input.') - return ts - - def _create_client(self, tr: 'TestRun') -> None: - p = tr.AddVerifierClientProcess( - "client", - self._replay_file, - http_ports=[self._ts.Variables.port], - other_args='--format "{url}" --keys /esi-html-comment.php') - p.ReturnCode = 0 - p.StartBefore(self._server) - p.StartBefore(self._ts) - - # The body must contain the expanded variable from inside the - # wrapper. - p.Streams.stdout += Testers.ContainsExpression('www.example.com', 'Verify the client received the expanded ESI body.') - - -class EsiNestedHtmlCommentRejectTest(): - """ - Drive a request whose origin response contains a nested - hidden inside / child_nodes, and verify the - processor rejects it via the raw-substring guard. The prior fix only - scanned the top level of inner_nodes and would have let this through. - """ - - _replay_file: str = "esi_nested_html_comment_reject.replay.yaml" - - def __init__(self, plugin_config: str) -> None: - tr = Test.AddTestRun("ESI nested wrapper is rejected") - self._create_server(tr) - self._create_ats(tr, plugin_config) - self._create_client(tr) - - def _create_server(self, tr: 'TestRun') -> 'Process': - server = tr.AddVerifierServerProcess("server-reject", self._replay_file, other_args='--format "{url}"') - self._server = server - - server.Streams.All += Testers.ContainsExpression( - 'GET /esi-nested-reject.php', 'Verify the server received the nested-wrapper request.') - return server - - def _create_ats(self, tr: 'TestRun', plugin_config: str) -> 'Process': - ts = tr.MakeATSProcess("ts-reject") - self._ts = ts - ts.Disk.records_config.update( - { - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'http|plugin_esi|plugin_esi_procesor', - }) - server_port = self._server.Variables.http_port - ts.Disk.remap_config.AddLine(f'map http://www.example.com/ http://127.0.0.1:{server_port}') - ts.Disk.plugin_config.AddLine(plugin_config) - - # The guard must fire on a nested wrapper, even when the nested - # is hidden inside / rather - # than appearing at the top level of the outer wrapper's content. - ts.Disk.diags_log.Content = Testers.ContainsExpression( - r'Nested inside is not allowed', - 'The nested-wrapper guard must fire on a nested ESI comment hidden inside /.') - return ts - - def _create_client(self, tr: 'TestRun') -> None: - p = tr.AddVerifierClientProcess( - "client-reject", - self._replay_file, - http_ports=[self._ts.Variables.port], - other_args='--format "{url}" --keys /esi-nested-reject.php') - p.ReturnCode = 0 - p.StartBefore(self._server) - p.StartBefore(self._ts) - # The strong signal that the guard fired is the diags_log - # ContainsExpression in _create_ats. We deliberately do not - # assert on the client body here, since the failure path - # (pass-through vs. error response) is orthogonal to the - # guard and may evolve independently. - - -EsiHtmlCommentTest(plugin_config='esi.so') -EsiNestedHtmlCommentRejectTest(plugin_config='esi.so') diff --git a/tests/gold_tests/pluginTest/esi/esi_nested_include.test.py b/tests/gold_tests/pluginTest/esi/esi_nested_include.test.py deleted file mode 100644 index b69c3814154..00000000000 --- a/tests/gold_tests/pluginTest/esi/esi_nested_include.test.py +++ /dev/null @@ -1,105 +0,0 @@ -''' -Test nested include for the ESI plugin. -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -Test.Summary = ''' -Test nested include for the ESI plugin. -''' - -Test.SkipUnless(Condition.PluginExists('esi.so'),) - - -class EsiTest(): - """ - A class that encapsulates the configuration and execution of a set of ESI - test cases. - """ - - _replay_file: str = "esi_nested_include.replay.yaml" - - def __init__(self, plugin_config) -> None: - """ - :param plugin_config: esi.so configuration for plugin.config. - """ - tr = Test.AddTestRun("Request the ESI generated document") - self._create_server(tr) - self._create_ats(tr, plugin_config) - self._create_client(tr) - - def _create_server(self, tr: 'TestRun') -> 'Process': - """ Create and start a server process. - :param tr: The test run to add the server to. - :return: The server process. - """ - # Configure our server using proxy verifier. - server = tr.AddVerifierServerProcess("server", self._replay_file, other_args='--format "{url}"') - self._server = server - - # Validate server traffic - server.Streams.All += Testers.ContainsExpression('GET /main.php', 'Verify the server received the initial request.') - server.Streams.All += Testers.ContainsExpression( - 'GET /esi-nested-include.html', 'Verify the server received the nested include request.') - - return server - - def _create_ats(self, tr: 'TestRun', plugin_config: str) -> 'Process': - """ Create and start an ATS process. - :param tr: The test run to add the ATS to. - :param plugin_config: The plugin configuration to use. - :return: The ATS process. - """ - # Configure ATS with a vanilla ESI plugin configuration. - ts = tr.MakeATSProcess(f"ts") - self._ts = ts - ts.Disk.records_config.update({ - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'http|plugin_esi', - }) - server_port = self._server.Variables.http_port - ts.Disk.remap_config.AddLine(f'map http://www.example.com/ http://127.0.0.1:{server_port}') - ts.Disk.plugin_config.AddLine(plugin_config) - - ts.Disk.diags_log.Content = Testers.ContainsExpression( - r'The current esi inclusion depth \(3\) is larger than or equal to the max \(3\)', - 'Verify the ESI error concerning the max inclusion depth') - return ts - - def _create_client(self, tr: 'TestRun') -> None: - """ Create and start a client process to generate the request. - :param tr: The test run to add the client to. - """ - # Note, just request the main.php file. Otherwise the client will do the - # ESI requests in the replay file as well. - p = tr.AddVerifierClientProcess( - "client", self._replay_file, http_ports=[self._ts.Variables.port], other_args='--format "{url}" --keys /main.php') - p.ReturnCode = 0 - p.StartBefore(self._server) - p.StartBefore(self._ts) - - # Double check that the client received the response. - p.Streams.stdout += Testers.ContainsExpression( - 'Received an HTTP/1 chunked body', 'Verify the client received the response.') - - p.Streams.stdout += Testers.ContainsExpression( - 'esi:include src="http://www.example.com/esi-nested-include.html"/>', 'Verify the ATS received the esi include.') - - -# -# Configure and run the test cases. -# -EsiTest(plugin_config='esi.so') diff --git a/tests/gold_tests/pluginTest/esi/esi_request_size_cap.test.py b/tests/gold_tests/pluginTest/esi/esi_request_size_cap.test.py deleted file mode 100644 index cf796a39039..00000000000 --- a/tests/gold_tests/pluginTest/esi/esi_request_size_cap.test.py +++ /dev/null @@ -1,91 +0,0 @@ -''' -Test the ESI plugin's HTTP fetch request size cap. -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -Test.Summary = ''' -Verify HttpDataFetcherImpl rejects ESI include fetches whose total HTTP -request size (request line + URL + forwarded headers) would exceed -MAX_REQ_LEN (32 KB), logging an error rather than allocating a buffer -sized by an overflowed size_t. -''' - -Test.SkipUnless(Condition.PluginExists('esi.so'),) - -# Matches MAX_REQ_LEN in plugins/esi/fetcher/HttpDataFetcherImpl.cc. -MAX_REQ_LEN = 32 * 1024 - -# total_len in addFetchRequest is: -# sizeof("GET ") - 1 -> 4 -# + url.length() -# + sizeof(" HTTP/1.0\r\n") - 1 -> 11 -# + _headers_str.length() -# + sizeof("\r\n") - 1 -> 2 -# A path one byte longer than MAX_REQ_LEN guarantees the cap is tripped -# regardless of how many headers end up being forwarded. -oversized_path = 'A' * (MAX_REQ_LEN + 1) -esi_body = ('\n\n' - f'Hello, \n' - '\n\n') - -server = Test.MakeOriginServer("server") -server.addResponse( - "sessionfile.log", { - "headers": ("GET /oversized.php HTTP/1.1\r\n" - "Host: www.example.com\r\n" - "Content-Length: 0\r\n\r\n"), - "timestamp": "1469733493.993", - "body": "" - }, { - "headers": - ( - "HTTP/1.1 200 OK\r\n" - "Content-Type: text/html\r\n" - "X-Esi: 1\r\n" - "Connection: close\r\n" - f"Content-Length: {len(esi_body)}\r\n" - "Cache-Control: max-age=300\r\n\r\n"), - "timestamp": "1469733493.993", - "body": esi_body - }) - -ts = Test.MakeATSProcess("ts") -ts.Disk.records_config.update({ - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'http|plugin_esi', -}) -ts.Disk.remap_config.AddLine(f'map http://www.example.com/ http://127.0.0.1:{server.Variables.Port}') -ts.Disk.plugin_config.AddLine('esi.so') - -tr = Test.AddTestRun("Start the server and ATS.") -tr.Processes.Default.StartBefore(server) -tr.Processes.Default.StartBefore(ts) -tr.Processes.Default.Command = "echo starting" -tr.Processes.Default.ReturnCode = 0 -tr.StillRunningAfter = server -tr.StillRunningAfter = ts - -tr = Test.AddTestRun("Issue a request whose ESI include URL exceeds the 32 KB fetch cap.") -tr.MakeCurlCommand( - f'http://127.0.0.1:{ts.Variables.port}/oversized.php ' - '-H"Host: www.example.com" -H"Accept: */*" --output /dev/null --silent', - ts=ts) -tr.Processes.Default.ReturnCode = 0 -ts.Disk.diags_log.Content = Testers.ContainsExpression( - r"HTTP request size exceeds maximum 32768", "ESI fetcher must log the MAX_REQ_LEN cap error for oversize requests") -tr.StillRunningAfter = server -tr.StillRunningAfter = ts diff --git a/tests/gold_tests/pluginTest/esi/esi_ssrf_validate.test.py b/tests/gold_tests/pluginTest/esi/esi_ssrf_validate.test.py deleted file mode 100644 index ef922920605..00000000000 --- a/tests/gold_tests/pluginTest/esi/esi_ssrf_validate.test.py +++ /dev/null @@ -1,111 +0,0 @@ -''' -Test SSRF validation for ESI include src= URLs. -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -Test.Summary = ''' -Verify the ESI plugin's SSRF guard rejects include URLs with private-IP -hosts, non-http(s) schemes, and attacker-controlled variable expansion, -while still allowing ordinary includes to the configured upstream. -''' - -Test.SkipUnless(Condition.PluginExists('esi.so'),) - - -class EsiSsrfTest: - """Drives the replay file through ATS with the SSRF guard enabled and - confirms each rejection path logs the expected reason while the - allowed include is still fetched and inlined.""" - - _replay_file: str = "esi_ssrf_validate.replay.yaml" - - def __init__(self) -> None: - tr = Test.AddTestRun("ESI include URLs are validated for SSRF") - self._create_server(tr) - self._create_ats(tr) - self._create_client(tr) - - def _create_server(self, tr: 'TestRun') -> None: - server = tr.AddVerifierServerProcess("server", self._replay_file, other_args='--format "{url}"') - self._server = server - - # The snippet for the allowed-include case must reach the origin. - # The rejected cases must never reach any backend other than the - # five top-level documents themselves; we don't assert their - # absence on the server stream because the rejected hosts are - # not mapped to this verifier. - server.Streams.All += Testers.ContainsExpression('GET /allowed.php', 'Verify the allowed top-level request reached origin.') - server.Streams.All += Testers.ContainsExpression( - 'GET /snippet.html', 'Verify the snippet for the allowed include was fetched.') - - def _create_ats(self, tr: 'TestRun') -> None: - ts = tr.MakeATSProcess("ts") - self._ts = ts - ts.Disk.records_config.update({ - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'http|plugin_esi', - }) - - server_port = self._server.Variables.http_port - ts.Disk.remap_config.AddLine(f'map http://www.example.com/ http://127.0.0.1:{server_port}') - - # Default-deny private/loopback hosts; no allow-regex configured, - # so any non-private host is permitted (the upstream maps via - # www.example.com). - ts.Disk.plugin_config.AddLine('esi.so') - - # Each rejection must be logged with its reason. Reason strings - # come from IncludeUrlValidator::reasonString(). - # TSError emits one line per rejection that contains both the - # offending URL substring and the reason token. Match each pair - # loosely so the assertion survives bracket/paren formatting - # differences in the diags log. ``.`` is newline-bounded by - # default, so each regex stays single-line. - ts.Disk.diags_log.Content = Testers.ContainsExpression( - r'Rejecting include URL.*169\.254\.169\.254.*private-host', - 'Cloud-metadata IPv4 literal must be rejected as private-host.') - ts.Disk.diags_log.Content += Testers.ContainsExpression( - r'Rejecting include URL.*127\.0\.0\.1/admin.*private-host', 'Loopback IPv4 literal must be rejected as private-host.') - ts.Disk.diags_log.Content += Testers.ContainsExpression( - r'Rejecting include URL.*gopher://internal\.svc/x.*bad-scheme', 'Non-http(s) scheme must be rejected as bad-scheme.') - ts.Disk.diags_log.Content += Testers.ContainsExpression( - r'Rejecting include URL.*10\.0\.0\.5/secret.*private-host', - 'Attacker-influenced variable expansion must be rejected after \\$\\(...\\) is expanded.') - - # The allowed case must NOT show up as a rejection. - ts.Disk.diags_log.Content += Testers.ExcludesExpression( - r'Rejecting include URL.*www\.example\.com/snippet\.html', - 'The legitimate include to the mapped upstream must not be rejected.') - - def _create_client(self, tr: 'TestRun') -> None: - # Proxy Verifier's default --keys format in this codebase keys - # transactions by URL (see esi_nested_include.test.py), not by the - # `uuid` request header. Match on the top-level document URLs so - # the client drives the four rejection cases and the one allowed - # case; the /snippet.html transaction is omitted because ATS - # fetches it internally via the ESI include. - p = tr.AddVerifierClientProcess( - "client", - self._replay_file, - http_ports=[self._ts.Variables.port], - other_args='--format "{url}" --keys /metadata.php /loopback.php /badscheme.php /varinject.php /allowed.php') - p.ReturnCode = 0 - p.StartBefore(self._server) - p.StartBefore(self._ts) - - -EsiSsrfTest() diff --git a/tests/gold_tests/pluginTest/header_rewrite/header_rewrite_bad_run_plugin.test.py b/tests/gold_tests/pluginTest/header_rewrite/header_rewrite_bad_run_plugin.test.py deleted file mode 100644 index c5371f5c9e9..00000000000 --- a/tests/gold_tests/pluginTest/header_rewrite/header_rewrite_bad_run_plugin.test.py +++ /dev/null @@ -1,156 +0,0 @@ -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. -''' -Verify header_rewrite rejects a run-plugin operator whose target plugin fails to -load. The failure must be caught at config load time, not aborted at request time. -''' - -Test.Summary = ''' -header_rewrite must reject a run-plugin whose target plugin fails to load, at -config load time, rather than aborting the server on the first request. -''' - -# Reproduce the reported crash: run-plugin against a plugin whose instance-init fails -# (conf_remap + a missing file) hands header_rewrite a null instance, which old code aborted on. -Test.SkipUnless( - Condition.PluginExists('header_rewrite.so'), - Condition.PluginExists('conf_remap.so'), -) - - -class TestBadRunPlugin: - '''Verify failed run-plugin initialization is rejected safely.''' - - ERROR_MARKER: str = 'run-plugin unable to load' - BAD_RULE_LINES: list[str] = [ - 'cond %{REMAP_PSEUDO_HOOK}', - ' run-plugin conf_remap.so no_such_conf_remap_file.yaml', - ] - NESTED_BAD_RULE_LINES: list[str] = [ - 'cond %{REMAP_PSEUDO_HOOK}', - ' if', - ' cond %{TRUE}', - ' run-plugin conf_remap.so no_such_conf_remap_file.yaml', - ' endif', - ] - - def __init__(self) -> None: - '''Configure startup and reload rejection scenarios.''' - self._configure_startup_rejection() - self._server = self._configure_origin_server() - self._ts = self._configure_traffic_server() - self._configure_baseline_request() - self._configure_bad_remap_install() - self._configure_failed_reload() - self._configure_post_reload_request() - self._ts.Disk.diags_log.Content = Testers.IncludesExpression( - self.ERROR_MARKER, 'the rejected reload should log the run-plugin failure') - - def _configure_startup_rejection(self) -> None: - '''Verify a bad top-level run-plugin fails startup cleanly.''' - ts = Test.MakeATSProcess("ts-startup", disable_log_checks=True) - ts.Disk.records_config.update({ - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'header_rewrite', - }) - ts.Disk.MakeConfigFile('bad_run_plugin.conf').AddLines(self.BAD_RULE_LINES) - ts.Disk.remap_config.AddLine( - 'map http://startup.example.com/ http://127.0.0.1/ ' - '@plugin=header_rewrite.so @pparam=bad_run_plugin.conf') - - # Invalid remap.config triggers a controlled exit rather than SIGABRT. - ts.ReturnCode = 33 - ts.Ready = 0 - ts.Disk.diags_log.Content = Testers.IncludesExpression( - self.ERROR_MARKER, 'header_rewrite must report the failed run-plugin load') - ts.Disk.traffic_out.Content = Testers.ExcludesExpression( - 'Traffic Server is fully initialized', 'ATS must not initialize with a bad run-plugin config') - - tr = Test.AddTestRun("Bad run-plugin config fails startup instead of crashing") - tr.Processes.Default.Command = 'echo verifying startup rejection' - tr.Processes.Default.ReturnCode = 0 - tr.Processes.Default.StartBefore(ts) - - def _configure_origin_server(self) -> 'Process': - '''Configure the origin used to verify reload behavior.''' - server = Test.MakeOriginServer("server") - request_header = { - "headers": "GET / HTTP/1.1\r\nHost: reload.example.com\r\n\r\n", - "timestamp": "1469733493.993", - "body": "" - } - response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} - server.addResponse("sessionfile.log", request_header, response_header) - return server - - def _configure_traffic_server(self) -> 'Process': - '''Configure ATS with a valid initial remap table.''' - ts = Test.MakeATSProcess("ts-reload", disable_log_checks=True) - ts.Disk.records_config.update({ - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'header_rewrite', - }) - ts.Disk.MakeConfigFile('nested_bad_run_plugin.conf').AddLines(self.NESTED_BAD_RULE_LINES) - ts.Disk.remap_config.AddLine(f'map http://reload.example.com http://127.0.0.1:{self._server.Variables.Port}') - return ts - - def _configure_curl_run(self, name: str, expectation: str) -> 'TestRun': - '''Configure a request that verifies ATS still serves traffic.''' - tr = Test.AddTestRun(name) - tr.MakeCurlCommand( - f'--proxy 127.0.0.1:{self._ts.Variables.port} "http://reload.example.com" ' - '-H "Proxy-Connection: keep-alive" --verbose', - ts=self._ts) - tr.Processes.Default.ReturnCode = 0 - tr.Processes.Default.Streams.stderr = Testers.IncludesExpression('200 OK', expectation) - tr.StillRunningAfter = self._ts - tr.StillRunningAfter = self._server - return tr - - def _configure_baseline_request(self) -> None: - '''Verify the valid initial configuration serves requests.''' - tr = self._configure_curl_run("Baseline request is served before reload", 'baseline request should be served') - tr.Processes.Default.StartBefore(self._server) - tr.Processes.Default.StartBefore(self._ts) - - def _configure_bad_remap_install(self) -> None: - '''Replace remap.config with one containing a bad nested run-plugin.''' - tr = Test.AddTestRun("Install a remap.config with a bad run-plugin") - remap_path = self._ts.Disk.remap_config.AbsPath - tr.Disk.File(remap_path, id="remap_bad", typename="ats:config") - tr.Disk.remap_bad.AddLine( - f'map http://reload.example.com http://127.0.0.1:{self._server.Variables.Port} ' - '@plugin=header_rewrite.so @pparam=nested_bad_run_plugin.conf') - tr.Processes.Default.Command = 'echo installed bad remap.config' - tr.Processes.Default.ReturnCode = 0 - tr.Processes.Default.Env = self._ts.Env - tr.StillRunningAfter = self._ts - tr.StillRunningAfter = self._server - - def _configure_failed_reload(self) -> None: - '''Verify the bad remap table is rejected without stopping ATS.''' - tr = Test.AddConfigReload( - self._ts, expect="fail", delay_start=2, description="Reload with bad run-plugin must be rejected, not fatal") - tr.StillRunningAfter = self._ts - tr.StillRunningAfter = self._server - - def _configure_post_reload_request(self) -> None: - '''Verify the rejected reload leaves the old configuration active.''' - self._configure_curl_run( - "Server still serves the old config after the rejected reload", 'old config should still serve after a rejected reload') - - -TestBadRunPlugin() diff --git a/tests/gold_tests/pluginTest/header_rewrite/header_rewrite_set_body_from.test.py b/tests/gold_tests/pluginTest/header_rewrite/header_rewrite_set_body_from.test.py deleted file mode 100644 index bb03d7c3e60..00000000000 --- a/tests/gold_tests/pluginTest/header_rewrite/header_rewrite_set_body_from.test.py +++ /dev/null @@ -1,204 +0,0 @@ -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -Test.Summary = ''' -Test for successful response manipulation using set-body-from -''' -Test.ContinueOnFail = True - - -class HeaderRewriteSetBodyFromTest: - - def __init__(self): - self.setUpOriginServer() - self.setUpTS() - - def setUpOriginServer(self): - self.server = Test.MakeOriginServer("server") - - # Response for original transaction - response_header = {"headers": "HTTP/1.1 404 Not Found\r\nConnection: close\r\n\r\n", "body": "404 Not Found"} - - # Request/response for original transaction where transaction returns a 200 status code - remap_success_request_header = {"headers": "GET /200 HTTP/1.1\r\nHost: www.example.com\r\n\r\n"} - ooo = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "body": "200 OK"} - - self.server.addResponse("sessionfile.log", remap_success_request_header, ooo) - - # Request/response for original transaction with failed second tranasaction - remap_fail_1_request_header = {"headers": "GET /remap_fail HTTP/1.1\r\nHost: www.example.com\r\n\r\n"} - self.server.addResponse("sessionfile.log", remap_fail_1_request_header, response_header) - - plugin_fail_1_request_header = {"headers": "GET /plugin_fail HTTP/1.1\r\nHost: www.example.com\r\n\r\n"} - self.server.addResponse("sessionfile.log", plugin_fail_1_request_header, response_header) - - # Request/response for original successful transaction with successful second tranasaction - remap_success_1_request_header = {"headers": "GET /remap_success HTTP/1.1\r\nHost: www.example.com\r\n\r\n"} - self.server.addResponse("sessionfile.log", remap_success_1_request_header, response_header) - - plugin_success_1_request_header = {"headers": "GET /plugin_success HTTP/1.1\r\nHost: www.example.com\r\n\r\n"} - self.server.addResponse("sessionfile.log", plugin_success_1_request_header, response_header) - - # Request/response for custom body transaction that successfully retrieves body - success_2_request_header = {"headers": "GET /404.html HTTP/1.1\r\nHost: www.example.com\r\n\r\n"} - success_2_response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "body": "Custom body found\n"} - self.server.addResponse("sessionfile.log", success_2_request_header, success_2_response_header) - - # Request/response for original transaction that triggers binary set-body-from - remap_binary_request_header = {"headers": "GET /remap_binary HTTP/1.1\r\nHost: www.example.com\r\n\r\n"} - self.server.addResponse("sessionfile.log", remap_binary_request_header, response_header) - - # Response for the set-body-from fetch: body has an internal NUL. The strdup - # bug truncated everything after the NUL and emitted heap garbage instead. - binary_body_request_header = {"headers": "GET /binary_body HTTP/1.1\r\nHost: www.example.com\r\n\r\n"} - binary_body_response_header = { - "headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", - "body": "BeforeNUL\x00AfterNUL_AAAAAAAAAAAAAAAAAAAAAAAA" - } - self.server.addResponse("sessionfile.log", binary_body_request_header, binary_body_response_header) - - def setUpTS(self): - self.ts = Test.MakeATSProcess("ts") - - # Set header rewrite rules - self.ts.Setup.CopyAs('rules/rule_set_body_from_remap.conf', Test.RunDirectory) - self.ts.Setup.CopyAs('rules/rule_set_body_from_plugin.conf', Test.RunDirectory) - - self.ts.Disk.remap_config.AddLine( - """\ - map http://www.example.com/remap_success http://127.0.0.1:{0}/remap_success @plugin=header_rewrite.so @pparam={1}/rule_set_body_from_remap.conf - map http://www.example.com/200 http://127.0.0.1:{0}/200 @plugin=header_rewrite.so @pparam={1}/rule_set_body_from_remap.conf - map http://www.example.com/remap_fail http://127.0.0.1:{0}/remap_fail @plugin=header_rewrite.so @pparam={1}/rule_set_body_from_remap.conf - map http://www.example.com/remap_binary http://127.0.0.1:{0}/remap_binary @plugin=header_rewrite.so @pparam={1}/rule_set_body_from_remap.conf - map http://www.example.com/binary_body http://127.0.0.1:{0}/binary_body - map http://www.example.com/plugin_success http://127.0.0.1:{0}/plugin_success - map http://www.example.com/plugin_fail http://127.0.0.1:{0}/plugin_fail - map http://www.example.com/404.html http://127.0.0.1:{0}/404.html - map http://www.example.com/plugin_no_server http://127.0.0.1::{2}/plugin_no_server - """.format(self.server.Variables.Port, Test.RunDirectory, Test.GetTcpPort("bad_port"))) - self.ts.Disk.plugin_config.AddLine('header_rewrite.so {0}/rule_set_body_from_plugin.conf'.format(Test.RunDirectory)) - - def test_setBodyFromFails_remap(self): - ''' - Test where set-body-from request fails - Triggered from remap file - This uses the case where no remap rule is provided - ''' - tr = Test.AddTestRun() - tr.MakeCurlCommand( - '-s -v --proxy 127.0.0.1:{0} "http://www.example.com/remap_fail"'.format(self.ts.Variables.port), ts=self.ts) - tr.Processes.Default.ReturnCode = 0 - tr.Processes.Default.StartBefore(self.server) - tr.Processes.Default.StartBefore(self.ts) - tr.Processes.Default.Streams.stdout = "gold/header_rewrite-set_body_from_remap_fail.gold" - tr.Processes.Default.Streams.stderr.Content = Testers.ContainsExpression("404 Not Found", "Expected 404 response") - tr.StillRunningAfter = self.server - - def test_setBodyFromSucceeds_remap(self): - ''' - Test where set-body-from request succeeds - Triggered from remap file - ''' - tr = Test.AddTestRun() - tr.MakeCurlCommand( - '-s -v --proxy 127.0.0.1:{0} "http://www.example.com/remap_success"'.format(self.ts.Variables.port), ts=self.ts) - tr.Processes.Default.ReturnCode = 0 - tr.Processes.Default.Streams.stdout = "gold/header_rewrite-set_body_from_success.gold" - tr.Processes.Default.Streams.stderr.Content = Testers.ContainsExpression("404 Not Found", "Expected 404 response") - tr.StillRunningAfter = self.server - - def test_setBodyFromSucceeds_plugin(self): - ''' - Test where set-body-from request succeeds - Triggered from plugin file - ''' - tr = Test.AddTestRun() - tr.MakeCurlCommand( - '-s -v --proxy 127.0.0.1:{0} "http://www.example.com/plugin_success"'.format(self.ts.Variables.port), ts=self.ts) - tr.Processes.Default.ReturnCode = 0 - tr.Processes.Default.Streams.stdout = "gold/header_rewrite-set_body_from_success.gold" - tr.Processes.Default.Streams.stderr.Content = Testers.ContainsExpression("404 Not Found", "Expected 404 response") - tr.StillRunningAfter = self.server - - def test_setBodyFromFails_plugin(self): - ''' - Test where set-body-from request fails - This uses the case where the second endpoint cannot connect to the requested server - Triggered from plugin file - ''' - tr = Test.AddTestRun() - tr.MakeCurlCommand( - '-s -v --proxy 127.0.0.1:{0} "http://www.example.com/plugin_fail"'.format(self.ts.Variables.port), ts=self.ts) - tr.Processes.Default.ReturnCode = 0 - tr.Processes.Default.Streams.stdout = "gold/header_rewrite-set_body_from_conn_fail.gold" - tr.Processes.Default.Streams.stderr.Content = Testers.ContainsExpression("404 Not Found", "Expected 404 response") - tr.StillRunningAfter = self.server - - def test_setBodyFromSucceeds_200(self): - ''' - Test where set-body-from request succeeds and returns 200 OK - Triggered from remap file - This is tested because right now, TSHttpTxnErrorBodySet will change OK status codes to 500 INKApi Error - Ideally, this would not occur. - ''' - tr = Test.AddTestRun() - tr.MakeCurlCommand('-s -v --proxy 127.0.0.1:{0} "http://www.example.com/200"'.format(self.ts.Variables.port), ts=self.ts) - tr.Processes.Default.ReturnCode = 0 - tr.Processes.Default.Streams.stdout = "gold/header_rewrite-set_body_from_200.gold" - tr.Processes.Default.Streams.stderr.Content = Testers.ContainsExpression("500 INKApi Error", "Expected 500 response") - tr.StillRunningAfter = self.server - - def test_setBodyFromBinary(self): - ''' - set-body-from must preserve binary bodies that contain internal NUL - bytes. The previous TSstrdup path truncated at the first NUL and - emitted heap bytes for the remainder; check the SHA256 of the body - the client actually receives. - ''' - body_path = f"{Test.RunDirectory}/binary_body.out" - # SHA256 of b"BeforeNUL\x00AfterNUL_AAAAAAAAAAAAAAAAAAAAAAAA" (43 bytes) - expected_sha = "a529bbd61061b739b611ca67a7b76fc433b3d3a9cc3bcc2e385b812f00b0fe63" - - tr = Test.AddTestRun() - tr.MakeCurlCommand( - f'-s --proxy 127.0.0.1:{self.ts.Variables.port} -o {body_path} "http://www.example.com/remap_binary"', ts=self.ts) - tr.Processes.Default.ReturnCode = 0 - tr.StillRunningAfter = self.server - - # Compute the hash via Python instead of sha256sum/shasum so the - # check does not depend on which CLI hash tool the host happens - # to ship. - tr = Test.AddTestRun() - tr.Processes.Default.Command = ( - f'python3 -c \'import hashlib; ' - f'print(hashlib.sha256(open("{body_path}", "rb").read()).hexdigest())\'') - tr.Processes.Default.ReturnCode = 0 - tr.Processes.Default.Streams.stdout.Content = Testers.ContainsExpression( - expected_sha, "Client must receive the exact binary body bytes") - - def runTraffic(self): - self.test_setBodyFromFails_remap() - self.test_setBodyFromSucceeds_remap() - self.test_setBodyFromSucceeds_plugin() - self.test_setBodyFromFails_plugin() - self.test_setBodyFromSucceeds_200() - self.test_setBodyFromBinary() - - def run(self): - self.runTraffic() - - -HeaderRewriteSetBodyFromTest().run() diff --git a/tests/gold_tests/pluginTest/header_rewrite/header_rewrite_url_glob.test.py b/tests/gold_tests/pluginTest/header_rewrite/header_rewrite_url_glob.test.py deleted file mode 100644 index a2f9aa5ab88..00000000000 --- a/tests/gold_tests/pluginTest/header_rewrite/header_rewrite_url_glob.test.py +++ /dev/null @@ -1,64 +0,0 @@ -''' -Test global header_rewrite with set-redirect operator. -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -Test.Summary = ''' -Test global header_rewrite with set-redirect operator. -''' - -Test.ContinueOnFail = True -ts = Test.MakeATSProcess("ts", block_for_debug=False) -server = Test.MakeOriginServer("server") - -# Configure the server to return 200 responses. The rewrite rules below set a -# non-200 status, so if curl gets a 200 response something went wrong. -Test.testName = "" -request_header = {"headers": "GET / HTTP/1.1\r\nHost: no_path.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -server.addResponse("sessionfile.log", request_header, response_header) - -ts.Disk.records_config.update( - { - 'proxy.config.url_remap.remap_required': 0, - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.show_location': 0, - 'proxy.config.diags.debug.tags': 'header', - }) -ts.Setup.CopyAs('rules/glob_set_redirect.conf', Test.RunDirectory) - -ts.Disk.plugin_config.AddLine(f'header_rewrite.so {Test.RunDirectory}/glob_set_redirect.conf') - -# Run operator on Read Response Hdr hook (ID:REQUEST == 0). -tr = Test.AddTestRun() -tr.Processes.Default.StartBefore(ts) -tr.Processes.Default.StartBefore(server) -tr.MakeCurlCommand(f'--head 127.0.0.1:{ts.Variables.port} -H "Host: 127.0.0.1:{server.Variables.Port}" --verbose', ts=ts) -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Streams.stderr = "gold/set-redirect-glob.gold" -tr.StillRunningAfter = server -tr.StillRunningAfter = ts -ts.Disk.traffic_out.Content = "gold/header_rewrite-tag.gold" - -# Run operator on Send Response Hdr hook (ID:REQUEST == 1). -tr = Test.AddTestRun() -tr.MakeCurlCommand(f'--head 127.0.0.1:{ts.Variables.port} -H "Host: 127.0.0.1:{server.Variables.Port}" --verbose', ts=ts) -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Streams.stderr = "gold/set-redirect-glob.gold" -tr.StillRunningAfter = server -tr.StillRunningAfter = ts -ts.Disk.traffic_out.Content = "gold/header_rewrite-tag.gold" diff --git a/tests/gold_tests/pluginTest/healthchecks/healthchecks.test.py b/tests/gold_tests/pluginTest/healthchecks/healthchecks.test.py deleted file mode 100644 index 3392f427910..00000000000 --- a/tests/gold_tests/pluginTest/healthchecks/healthchecks.test.py +++ /dev/null @@ -1,212 +0,0 @@ -""" -Verify healthchecks plugin behavior. -""" -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import os - -Test.Summary = '''Verify healthchecks plugin behavior.''' - -Test.SkipUnless(Condition.PluginExists('healthchecks.so')) - -CONFIG = f''' -/acme {Test.RunDirectory}/acme text/plain 200 404 -/acme-ssl {Test.RunDirectory}/acme-ssl text/plain 200 404 -''' - -CONTENT = 'Some generic content.' - - -class TestFileChangeBehavior: - '''Verify healthchecks plugin file change detection behavior.''' - - def __init__(self) -> None: - '''Initialize the TestRun.''' - self._ts_started = False - self._positive_hc_counter = 0 - self._configure_global_ts() - self._expect_positive_healthchecks() - self._remove_acme_ssl() - self._expect_acme_ssl_404() - self._re_add_acme_ssl() - self._expect_positive_healthchecks() - self._expect_full_buffer_acme_body() - self._rewrite_acme_while_serving() - self._expect_rewritten_acme_body() - - def _configure_global_ts(self) -> None: - '''Configure a global Traffic Server instance for the test runs. - :param tr: The TestRun to associate the ATS instance with. - :return: The Traffic Server Process. - ''' - ts = Test.MakeATSProcess('ts', enable_tls=True) - self._ts = ts - - # healthchecks plugin configuration. - config_name = "healthchecks.config" - config_path = os.path.join(ts.Variables.CONFIGDIR, config_name) - ts.Disk.File(config_path, id='healthchecks_config', typename="ats:config") - config = ts.Disk.healthchecks_config - config.AddLine(f'/acme {Test.RunDirectory}/acme text/plain 200 404') - config.AddLine(f'/acme-ssl {Test.RunDirectory}/acme-ssl text/plain 200 404') - ts.Setup.Copy('acme') - ts.Setup.Copy('acme-ssl') - ts.Disk.plugin_config.AddLine(f'healthchecks.so {ts.Variables.CONFIGDIR}/healthchecks.config') - - # TLS configuration. - ts.addDefaultSSLFiles() - ts.Disk.records_config.update( - { - "proxy.config.ssl.server.cert.path": f'{ts.Variables.SSLDir}', - "proxy.config.ssl.server.private_key.path": f'{ts.Variables.SSLDir}', - "proxy.config.ssl.client.verify.server.policy": 'PERMISSIVE', - }) - ts.Disk.ssl_multicert_yaml.AddLines( - """ -ssl_multicert: - - dest_ip: "*" - ssl_cert_name: server.pem - ssl_key_name: server.key -""".split("\n")) - - # Other configuration. - ts.Disk.records_config.update({ - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'healthchecks', - }) - return ts - - def _expect_positive_healthchecks(self) -> None: - '''Configure a positive healthcheck for the test runs. - :return: None - ''' - self._positive_hc_counter += 1 - counter = self._positive_hc_counter - tr = Test.AddTestRun(f'Positive acme healthchecks: {counter}') - tr.MakeCurlCommand(f'-v http://127.0.0.1:{self._ts.Variables.port}/acme', ts=self._ts) - curl_acme = tr.Processes.Default - if not self._ts_started: - curl_acme.StartBefore(self._ts) - self._ts_started = True - curl_acme.Streams.All += Testers.ContainsExpression('HTTP/1.1 200', 'Verify 200 response for /acme') - - if not Condition.CurlUsingUnixDomainSocket(): - # Repeat for acme-ssl - tr2 = Test.AddTestRun(f'Positive acme-ssl healthchecks: {counter}') - tr2.MakeCurlCommand(f'-kv https://127.0.0.1:{self._ts.Variables.ssl_port}/acme-ssl', ts=self._ts) - curl_acme_ssl = tr2.Processes.Default - if not self._ts_started: - curl_acme_ssl.StartBefore(self._ts) - self._ts_started = True - curl_acme_ssl.Streams.All += Testers.ContainsExpression('HTTP/2 200', 'Verify 200 response for /acme-ssl') - - def _remove_acme_ssl(self) -> None: - '''Remove the acme-ssl file to trigger a file change detection. - :return: None - ''' - if not Condition.CurlUsingUnixDomainSocket(): - tr = Test.AddTestRun('Remove acme-ssl file') - p = tr.Processes.Default - p.Command = f'rm {Test.RunDirectory}/acme-ssl && sleep 1' - tr.Processes.Default.ReturnCode = 0 - - def _expect_acme_ssl_404(self) -> None: - '''Expect a 404 response for the removed acme-ssl file. - :return: None - ''' - tr = Test.AddTestRun('Expect 200 for acme after acme-ssl removal') - tr.MakeCurlCommand(f'-v http://127.0.0.1:{self._ts.Variables.port}/acme', ts=self._ts) - curl_acme = tr.Processes.Default - if not self._ts_started: - curl_acme.StartBefore(self._ts) - self._ts_started = True - curl_acme.Streams.All += Testers.ContainsExpression('HTTP/1.1 200', 'Verify 200 response for /acme after acme-ssl removal') - - if not Condition.CurlUsingUnixDomainSocket(): - tr2 = Test.AddTestRun('Expect 404 for acme-ssl after removal') - tr2.MakeCurlCommand(f'-kv https://127.0.0.1:{self._ts.Variables.ssl_port}/acme-ssl', ts=self._ts) - curl_acme_ssl = tr2.Processes.Default - if not self._ts_started: - curl_acme_ssl.StartBefore(self._ts) - self._ts_started = True - curl_acme_ssl.Streams.All += Testers.ContainsExpression('HTTP/2 404', 'Verify 404 response for /acme-ssl after removal') - - def _re_add_acme_ssl(self) -> None: - '''Re-add the acme-ssl file to restore the healthcheck. - :return: None - ''' - if not Condition.CurlUsingUnixDomainSocket(): - tr = Test.AddTestRun('Re-add acme-ssl file') - tr.Setup.Copy('acme-ssl', Test.RunDirectory) - p = tr.Processes.Default - p.Command = 'sleep 1' - p.ReturnCode = 0 - - def _rewrite_acme_while_serving(self) -> None: - '''Rewrite the acme file repeatedly while healthcheck requests are in flight. - - The plugin replaces the health check file data underneath transactions which may still be - reading the previous data. This drives that replacement so that an ASan enabled build - catches the old data being released while it is still referenced. - :return: None - ''' - tr = Test.AddTestRun('Rewrite acme while healthchecks are being served') - acme_file = os.path.join(Test.RunDirectory, 'acme') - url = f'http://127.0.0.1:{self._ts.Variables.port}/acme' - - # Note that autest runs the command through string.Template, so shell variables cannot be - # used here. The loop is therefore unrolled. - commands = [] - for iteration in range(10): - commands.append(f'echo "{CONTENT} {iteration}" > {acme_file};') - commands.append('{curl} -s -o /dev/null ' + url + ' &') - commands.append('{curl} -s -o /dev/null ' + url + ' &') - commands.append('wait') - - tr.MakeCurlCommandMulti(' '.join(commands), ts=self._ts) - tr.Processes.Default.ReturnCode = 0 - - def _expect_full_buffer_acme_body(self) -> None: - '''Verify that a MAX_BODY_LEN-sized file is not reported as empty. - :return: None - ''' - tr = Test.AddTestRun('Expect a full-sized healthcheck response body') - acme_file = os.path.join(Test.RunDirectory, 'acme') - url = f'http://127.0.0.1:{self._ts.Variables.port}/acme' - command = (f'dd if=/dev/zero of={acme_file} bs=16384 count=1 2>/dev/null && sleep 1 && ' + '{curl} -s ' + url + ' | wc -c') - tr.MakeCurlCommandMulti(command, ts=self._ts) - p = tr.Processes.Default - p.ReturnCode = 0 - p.Streams.All += Testers.ContainsExpression('16384', 'Verify the response contains 16 KiB') - - def _expect_rewritten_acme_body(self) -> None: - '''Verify that the most recently written acme content is what gets served. - :return: None - ''' - tr = Test.AddTestRun('Expect the last written acme content in the response body') - acme_file = os.path.join(Test.RunDirectory, 'acme') - url = f'http://127.0.0.1:{self._ts.Variables.port}/acme' - command = f'echo "{CONTENT} final" > {acme_file} && sleep 1 && ' + '{curl} -v ' + url - tr.MakeCurlCommandMulti(command, ts=self._ts) - p = tr.Processes.Default - p.ReturnCode = 0 - p.Streams.All += Testers.ContainsExpression('HTTP/1.1 200', 'Verify 200 response for /acme') - p.Streams.All += Testers.ContainsExpression(f'{CONTENT} final', 'Verify the reloaded acme content is served') - - -# Instantiate the test -TestFileChangeBehavior() diff --git a/tests/gold_tests/pluginTest/ja3_fingerprint/ja3_fingerprint.test.py b/tests/gold_tests/pluginTest/ja3_fingerprint/ja3_fingerprint.test.py deleted file mode 100644 index 354c30c5aea..00000000000 --- a/tests/gold_tests/pluginTest/ja3_fingerprint/ja3_fingerprint.test.py +++ /dev/null @@ -1,208 +0,0 @@ -''' -Verify the behavior of the JA3 fingerprint plugin. -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import os -import re - -Test.Summary = __doc__ - -# Skip if plugin not present. Plugin gets built only if openssl is used(not BoringSSL). -Test.SkipUnless(Condition.PluginExists('ja3_fingerprint.so')) - - -class JA3FingerprintTest: - """Verify the behavior of the JA3 fingerprint plugin.""" - - _dns_counter: int = 0 - _server_counter: int = 0 - _ts_counter: int = 0 - _client_counter: int = 0 - - def __init__(self, test_remap: bool, modify_incoming: bool) -> None: - """Configure the test processes in preparation for the TestRun. - - :param test_remap: Whether to configure the plugin as a remap plugin - instead of as a global plugin. - :param modify_incoming: Whether ja3_fingerprint should be configured to - modify the client request rather than the proxy request. - """ - if test_remap and modify_incoming: - raise ValueError('modify-incoming is only allowed as a global plugin.') - - self._test_remap = test_remap - if test_remap: - self._replay_file = 'ja3_fingerprint_remap.replay.yaml' - else: - self._replay_file = 'ja3_fingerprint_global.replay.yaml' - - self._modify_incoming = modify_incoming - - tr = Test.AddTestRun(f'test_remap: {test_remap}, modify_incoming: {modify_incoming}') - self._configure_dns(tr) - self._configure_server(tr) - self._configure_trafficserver() - self._configure_client(tr) - self._await_ja3log() - self._verify_internal_headers() - - def _configure_dns(self, tr: 'TestRun') -> None: - """Configure a nameserver for the test. - - :param tr: The TestRun to add the nameserver to. - """ - name = f'dns{self._dns_counter}' - self._dns = tr.MakeDNServer(name, default='127.0.0.1') - JA3FingerprintTest._dns_counter += 1 - - def _configure_server(self, tr: 'TestRun') -> None: - """Configure the server to be used in the test. - - :param tr: The TestRun to add the server to. - """ - name = f'server{self._server_counter}' - self._server = tr.AddVerifierServerProcess(name, self._replay_file) - JA3FingerprintTest._server_counter += 1 - self._server.Streams.All += Testers.ContainsExpression("https-request", "Verify the HTTPS request was received.") - self._server.Streams.All += Testers.ContainsExpression("http2-request", "Verify the HTTP/2 request was received.") - if not self._test_remap: - # The first request has no existing JA3 headers, so headers are added. - self._server.Streams.All += Testers.ContainsExpression( - "x-ja3-raw: .*,", "Verify the new raw header was added.", reflags=re.IGNORECASE) - self._server.Streams.All += Testers.ContainsExpression("x-ja3-via: test.proxy.com", "The x-ja3-via string was added.") - # The second request has existing JA3 headers. With --preserve, - # no new JA3 headers are added (including x-ja3-sig). The replay - # file verifies x-ja3-sig is absent for the http2 transaction. - self._server.Streams.All += Testers.ContainsExpression( - "x-ja3-raw: first-signature", "Verify the already-existing raw header was preserved.", reflags=re.IGNORECASE) - - def _configure_trafficserver(self) -> None: - """Configure Traffic Server to be used in the test.""" - # Associate ATS with the Test so that metrics can be verified. - name = f'ts{self._ts_counter}' - self._ts = Test.MakeATSProcess(name, enable_cache=False, enable_tls=True) - JA3FingerprintTest._ts_counter += 1 - self._ts.addDefaultSSLFiles() - self._ts.Disk.ssl_multicert_yaml.AddLines( - """ -ssl_multicert: - - dest_ip: "*" - ssl_cert_name: server.pem - ssl_key_name: server.key -""".split("\n")) - server_port = self._server.Variables.https_port - self._ts.Disk.remap_config.AddLine(f'map https://https.server.com https://https.backend.com:{server_port}') - - if self._test_remap: - self._ts.Disk.remap_config.AddLine( - f'map https://http2.server.com https://http2.backend.com:{server_port} ' - '@plugin=ja3_fingerprint.so @pparam=--ja3log') - else: - arguments = '--ja3log --ja3raw --preserve' - if self._modify_incoming: - arguments += ' --modify-incoming' - self._ts.Disk.plugin_config.AddLine(f'ja3_fingerprint.so {arguments}') - self._ts.Disk.remap_config.AddLine(f'map https://http2.server.com https://http2.backend.com:{server_port}') - - self._ts.Disk.records_config.update( - { - 'proxy.config.ssl.server.cert.path': self._ts.Variables.SSLDir, - 'proxy.config.ssl.server.private_key.path': self._ts.Variables.SSLDir, - 'proxy.config.ssl.client.verify.server.policy': 'PERMISSIVE', - 'proxy.config.dns.nameservers': f"127.0.0.1:{self._dns.Variables.Port}", - 'proxy.config.dns.resolv_conf': 'NULL', - 'proxy.config.proxy_name': 'test.proxy.com', - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'http|ja3_fingerprint', - }) - - ja3log_path = os.path.join(self._ts.Variables.LOGDIR, "ja3_fingerprint.log") - self._ts.Disk.File(ja3log_path, id='ja3_log') - - if self._test_remap: - # Only the http2 request should be logged because only that request - # had the plugin configured for that remap rule. - regex = r'(.*JA3.*MD5){1}' - else: - regex = r'(.*JA3.*MD5){2}' - - self._ts.Disk.ja3_log.Content += Testers.ContainsExpression( - regex, "Verify the JA3 log contains a JA3 line.", reflags=re.MULTILINE | re.DOTALL) - - def _configure_client(self, tr: 'TestRun') -> None: - """Configure the TestRun. - - :param tr: The TestRun to add the client to. - """ - name = f'client{self._client_counter}' - p = tr.AddVerifierClientProcess( - name, self._replay_file, http_ports=[self._ts.Variables.port], https_ports=[self._ts.Variables.ssl_port]) - JA3FingerprintTest._client_counter += 1 - - p.StartBefore(self._dns) - p.StartBefore(self._server) - p.StartBefore(self._ts) - tr.StillRunningAfter = self._ts - - p.Streams.All += Testers.ContainsExpression("https-response", "Verify the HTTPS response was received.") - p.Streams.All += Testers.ContainsExpression("http2-response", "Verify the HTTP/2 response was received.") - - def _await_ja3log(self) -> None: - """Await the creation of the JA3 log.""" - tr = Test.AddTestRun('Await the contents of the JA3 log.') - - waiter = tr.Processes.Process('waiter', 'sleep 30') - ja3_path = self._ts.Disk.ja3_log.AbsPath - waiter.Ready = When.FileContains(ja3_path, "JA3") - - p = tr.Processes.Default - p.Command = f'echo await {ja3_path} creation' - p.StartBefore(waiter) - - def _verify_internal_headers(self) -> None: - """Verify that the correct headers were modified.""" - - # We use the grep command to get the small snippet of output we want - # from the traffic.out file. Gold file matching against long files, like - # traffic.out, is exceedingly slow. - - tr = Test.AddTestRun('Verify the internal client request headers.') - traffic_out = self._ts.Disk.traffic_out.AbsPath - p = tr.Processes.Default - p.Command = f'grep --after-context=20 "Incoming Request" {traffic_out}' - - if self._modify_incoming: - p.Streams.All += "modify-incoming-client.gold" - else: - p.Streams.All += "modify-sent-client.gold" - - tr = Test.AddTestRun('Verify the internal proxy request headers.') - p = tr.Processes.Default - p.Command = f'grep --after-context=20 "Proxy\'s Request after hooks" {traffic_out}' - - if self._modify_incoming: - p.Streams.All += "modify-incoming-proxy.gold" - elif self._test_remap: - p.Streams.All += "modify-sent-proxy-remap.gold" - else: - p.Streams.All += "modify-sent-proxy-global.gold" - - -JA3FingerprintTest(test_remap=False, modify_incoming=False) -JA3FingerprintTest(test_remap=True, modify_incoming=False) -JA3FingerprintTest(test_remap=False, modify_incoming=True) diff --git a/tests/gold_tests/pluginTest/ja4_fingerprint/ja4_fingerprint.test.py b/tests/gold_tests/pluginTest/ja4_fingerprint/ja4_fingerprint.test.py deleted file mode 100644 index f9de1f2d80c..00000000000 --- a/tests/gold_tests/pluginTest/ja4_fingerprint/ja4_fingerprint.test.py +++ /dev/null @@ -1,207 +0,0 @@ -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# 'License'); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import functools -import os -import re -from typing import Any, Callable, Dict, Optional - -from ports import get_port - -Test.Summary = 'Tests the ja4_fingerprint plugin.' -# The plugin is experimental and therefore may not always be built. It also -# doesn't support QUIC yet. -Test.SkipIf(Condition.CurlUsingUnixDomainSocket()) -Test.SkipUnless(Condition.PluginExists('ja4_fingerprint.so')) - -TestParams = Dict[str, Any] - - -class TestJA4Fingerprint: - '''Configure a test for ja4_fingerprint.''' - - replay_filepath: str = 'ja4_fingerprint.replay.yaml' - basic_server_replay_filepath: str = 'ja4_fingerprint_basic_server.replay.yaml' - client_counter: int = 0 - server_counter: int = 0 - ts_counter: int = 0 - - def __init__(self, name: str, /, autorun: bool, use_preserve: bool = False) -> None: - '''Initialize the test. - - :param name: The name of the test. - :param use_preserve: Whether to use the --preserve flag. - ''' - self.name = name - self.autorun = autorun - self.use_preserve = use_preserve - - def _init_run(self) -> 'TestRun': - '''Initialize processes for the test run.''' - - replay_filepath = self.replay_filepath if self.use_preserve else self.basic_server_replay_filepath - server_one = TestJA4Fingerprint.configure_server('yay.com', replay_filepath=replay_filepath) - self._configure_traffic_server(server_one) - - tr = Test.AddTestRun(self.name) - tr.Processes.Default.StartBefore(server_one) - tr.Processes.Default.StartBefore(self._ts) - - waiting_tr = self._configure_wait_for_log(server_one) - - return { - 'tr': tr, - 'waiting_tr': waiting_tr, - 'ts': self._ts, - 'server_one': server_one, - 'port_one': self._port_one, - } - - @classmethod - def runner(cls, name: str, autorun: bool = True, **kwargs) -> Optional[Callable]: - '''Create a runner for a test case. - - :param autorun: Run the test case once it's set up. Default is True. - :return: Returns a runner that can be used as a decorator. - ''' - test = cls(name, autorun=autorun, **kwargs)._prepare_test_case - return test - - def _prepare_test_case(self, func: Callable) -> Callable: - '''Set up a test case and possibly run it. - - :param func: The test case to set up. - :return: Returns a wrapped function that will have its test params - passed to it on invocation. - ''' - functools.wraps(func) - test_params = self._init_run() - - def wrapper(*args, **kwargs) -> Any: - return func(test_params, *args, **kwargs) - - if self.autorun: - wrapper() - return wrapper - - @staticmethod - def configure_server(domain: str, replay_filepath: str): - server = Test.MakeVerifierServerProcess( - f'server{TestJA4Fingerprint.server_counter + 1}.{domain}', replay_filepath, other_args="--format '{url}'") - TestJA4Fingerprint.server_counter += 1 - - return server - - def _configure_traffic_server(self, server_one: 'Process'): - '''Configure Traffic Server. - - :param server_one: The origin server process. - ''' - ts = Test.MakeATSProcess(f'ts-{TestJA4Fingerprint.ts_counter + 1}', enable_tls=True) - TestJA4Fingerprint.ts_counter += 1 - - ts.addDefaultSSLFiles() - self._port_one = get_port(ts, 'PortOne') - ts.Disk.records_config.update( - { - 'proxy.config.ssl.server.cert.path': f'{ts.Variables.SSLDir}', - 'proxy.config.ssl.server.private_key.path': f'{ts.Variables.SSLDir}', - 'proxy.config.http.server_ports': f'{self._port_one}:ssl', - 'proxy.config.proxy_name': 'test.proxy.com', - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'ja4_fingerprint|http', - }) - - ts.Disk.remap_config.AddLine(f'map / http://localhost:{server_one.Variables.http_port}') - - ts.Disk.ssl_multicert_yaml.AddLines( - """ -ssl_multicert: - - dest_ip: "*" - ssl_cert_name: server.pem - ssl_key_name: server.key -""".split("\n")) - - plugin_args = 'ja4_fingerprint.so' - if self.use_preserve: - plugin_args += ' --preserve' - ts.Disk.plugin_config.AddLine(plugin_args) - - log_path = os.path.join(ts.Variables.LOGDIR, "ja4_fingerprint.log") - ts.Disk.File(log_path, id='log_file') - - self._ts = ts - - def _configure_wait_for_log(self, server_one: 'Process'): - waiting_tr = Test.AddAwaitFileContainsTestRun(self.name, self._ts.Disk.log_file.Name, 'JA4') - return waiting_tr - - -# Tests start. - - -@TestJA4Fingerprint.runner('When we send a request, ' \ - 'then a JA4 header should be attached.') -def test1(params: TestParams) -> None: - client = params['tr'].Processes.Default - params['tr'].MakeCurlCommand( - '-k -v -H "uuid: no-existing-headers" "https://localhost:{0}/resource"'.format(params['port_one']), ts=params['ts']) - - client.ReturnCode = 0 - client.Streams.stdout += Testers.ContainsExpression(r'Yay!', 'We should receive the expected body.') - params['ts'].Disk.traffic_out.Content += Testers.ContainsExpression( - r'JA4 fingerprint:', 'We should receive the expected log message.') - - -@TestJA4Fingerprint.runner('With --preserve, existing JA4 headers should be preserved.', use_preserve=True) -def test_preserve(params: TestParams) -> None: - '''Test that --preserve skips adding headers when JA4 headers exist.''' - tr = params['tr'] - client = tr.Processes.Default - server = params['server_one'] - - # Request 1: No existing JA4 headers - should add them. - tr.MakeCurlCommand( - '-k -v -H "uuid: no-existing-headers" "https://localhost:{0}/resource"'.format(params['port_one']), ts=params['ts']) - client.ReturnCode = 0 - client.Streams.stdout += Testers.ContainsExpression(r'Yay!', 'First request should succeed.') - - # Request 2: With existing JA4 headers - should preserve them. - tr2 = Test.AddTestRun('Verify preserve skips adding JA4 headers when they exist.') - tr2.MakeCurlCommand( - '-k -v -H "uuid: existing-ja4-headers" -H "ja4: upstream-fingerprint" -H "x-ja4-via: upstream.proxy.com" ' - '"https://localhost:{0}/resource-with-headers"'.format(params['port_one']), - ts=params['ts']) - tr2.Processes.Default.ReturnCode = 0 - tr2.Processes.Default.Streams.stdout += Testers.ContainsExpression(r'Preserved!', 'Second request should preserve headers.') - - # Request 3: With only x-ja4-via - should also trigger preserve. - tr3 = Test.AddTestRun('Verify preserve triggers when only x-ja4-via exists.') - tr3.MakeCurlCommand( - '-k -v -H "uuid: existing-via-only" -H "x-ja4-via: upstream.proxy.com" ' - '"https://localhost:{0}/resource-via-only"'.format(params['port_one']), - ts=params['ts']) - tr3.Processes.Default.ReturnCode = 0 - tr3.Processes.Default.Streams.stdout += Testers.ContainsExpression(r'Via only!', 'Third request should succeed.') - - # Verify the replay file's proxy-request validations pass (checked per-request). - # Also verify via Proxy Verifier's validation logs. - server.Streams.All += Testers.ContainsExpression( - r'Equals Success.*"/resource-with-headers".*ja4.*upstream-fingerprint', - 'Preserved ja4 header should match original value.', - reflags=re.DOTALL) - server.Streams.All += Testers.ContainsExpression( - r'Absence Success.*"/resource-via-only".*ja4', 'ja4 should be absent when only x-ja4-via exists.', reflags=re.DOTALL) diff --git a/tests/gold_tests/pluginTest/jax_fingerprint/jax_fingerprint.test.py b/tests/gold_tests/pluginTest/jax_fingerprint/jax_fingerprint.test.py deleted file mode 100644 index 906667f93b4..00000000000 --- a/tests/gold_tests/pluginTest/jax_fingerprint/jax_fingerprint.test.py +++ /dev/null @@ -1,508 +0,0 @@ -''' -Verify the behavior of the jax_fingerprint plugin. - -Covers all supported run modes (overwrite, keep, append), setup types -(global, remap, hybrid), fingerprint methods (JA3, JA4, JA4H), and both -HTTP/1.1 and HTTP/2 client connections. - -Global setup: plugin in plugin.config, applies to every request. -Remap setup: plugin per remap rule, applies only to matched routes. -Hybrid setup: global plugin captures the TLS client hello (creates - context) while a remap plugin reads that context and - sets headers only on matched routes. -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import os -import re - -Test.Summary = __doc__ -Test.SkipUnless(Condition.PluginExists('jax_fingerprint.so')) - - -class JaxFingerprintTest: - '''Verify the behavior of the jax_fingerprint plugin.''' - - _dns_counter: int = 0 - _server_counter: int = 0 - _ts_counter: int = 0 - _client_counter: int = 0 - - def __init__( - self, - name: str, - method: str, - setup: str, - mode: str = 'overwrite', - http2: bool = False, - servernames: str = '', - log_field: str = '') -> None: - '''Configure test processes for the jax_fingerprint plugin. - - :param name: Descriptive name for this test run. - :param method: Fingerprint method: 'JA3', 'JA4', or 'JA4H'. - :param setup: Plugin setup type: 'global', 'remap', or 'hybrid'. - :param mode: Header write mode: 'overwrite', 'keep', or 'append'. - :param http2: If True, the client connects to ATS over HTTP/2 (h2 - over TLS). For JA3/JA4 the h2 ALPN in the ClientHello produces - a fingerprint distinct from the HTTP/1.1 case. For JA4H, - get_version() detects HTTP/2 via the protocol stack. - :param servernames: Comma-separated SNI allowlist passed as - --servernames to the plugin. Connections whose SNI is not in - the list are skipped entirely: handle_client_hello returns early, - no context is created, and handle_read_request_hdr is a no-op. - Only meaningful for CONNECTION_BASED methods (JA3/JA4) in global - setup. - :param log_field: Symbol name for --log-field option. When set, - configures logging.yaml with a custom format using the symbol - and verifies the fingerprint appears in the ATS access log. - Only supported with global setup. - - Method notes: - - JA3 / JA4 are CONNECTION_BASED (triggered on TLS client hello) - and require TLS between the client and ATS. - - JA4H is REQUEST_BASED (triggered on HTTP request read) and - works over plain HTTP, HTTPS, and HTTP/2. - - Setup notes: - - global: plugin.config entry with --standalone so ATS registers - the READ_REQUEST_HDR hook and modifies every request. - - remap: @plugin in remap.config. For CONNECTION_BASED methods - --standalone must also be passed so the remap plugin registers - the SSL_CLIENT_HELLO_HOOK globally and can populate the vconn - context that TSRemapDoRemap reads later. - - hybrid: global plugin (no --standalone) captures the TLS client - hello and stores context on the vconn; a remap plugin (no - --standalone) reads that shared context and sets headers only - on matched routes. Both instances share the same user-arg slot - because TSUserArgIndexReserve is idempotent for identical names. - ''' - self._name = name - self._method = method - self._setup = setup - self._mode = mode - self._http2 = http2 - self._servernames = servernames - self._log_field = log_field - # HTTP/2 always runs over TLS (h2 requires TLS). - self._needs_tls = method in ('JA3', 'JA4') or http2 - self._replay_file = self._choose_replay_file() - self._fingerprint_pattern = rf'{self._method}: [a-z0-9_-]+' - - tr = Test.AddTestRun(name) - self._configure_dns(tr) - self._configure_server(tr) - self._configure_trafficserver() - self._configure_client(tr) - Test.AddAwaitFileContainsTestRun( - f'Await jax_fingerprint.log for: {self._name}', self._ts.Disk.jax_log.AbsPath, self._fingerprint_pattern) - - if self._log_field: - log_field_path = os.path.join(self._ts.Variables.LOGDIR, 'jax_log_field.log') - # Verify the log contains a fingerprint (not just a dash placeholder). - Test.AddAwaitFileContainsTestRun( - f'Await jax_log_field.log for: {self._name}', log_field_path, f'{self._method}: [a-z0-9]') - - # ------------------------------------------------------------------ - # Helpers - # ------------------------------------------------------------------ - - def _choose_replay_file(self) -> str: - '''Return the replay YAML to drive this test. - - Key: (servernames, setup, needs_tls, http2, mode) - ''' - key = (bool(self._servernames), self._setup, self._needs_tls, self._http2, self._mode) - mapping = { - (False, 'global', False, False, 'overwrite'): 'jax_fingerprint_global.replay.yaml', - (False, 'global', False, False, 'keep'): 'jax_fingerprint_keep.replay.yaml', - (False, 'global', False, False, 'append'): 'jax_fingerprint_append.replay.yaml', - (False, 'global', True, False, 'overwrite'): 'jax_fingerprint_tls.replay.yaml', - (False, 'global', True, True, 'overwrite'): 'jax_fingerprint_global_h2.replay.yaml', - (False, 'remap', False, False, 'overwrite'): 'jax_fingerprint_remap.replay.yaml', - (False, 'remap', True, False, 'overwrite'): 'jax_fingerprint_remap_tls.replay.yaml', - (False, 'hybrid', True, False, 'overwrite'): 'jax_fingerprint_remap_tls.replay.yaml', - (True, 'global', True, False, 'overwrite'): 'jax_fingerprint_servernames.replay.yaml', - (True, 'hybrid', True, False, 'overwrite'): 'jax_fingerprint_hybrid_servernames.replay.yaml', - } - return mapping[key] - - def _build_remap_plugin_line(self, add_standalone: bool = False) -> str: - '''Build the @plugin / @pparam fragment for a remap.config line.''' - parts = [ - '@plugin=jax_fingerprint.so', - '@pparam=--method', - f'@pparam={self._method}', - '@pparam=--header', - '@pparam=x-jax', - '@pparam=--via-header', - '@pparam=x-jax-via', - '@pparam=--log-filename', - '@pparam=jax_fingerprint', - ] - if self._mode != 'overwrite': - parts.extend(['@pparam=--mode', f'@pparam={self._mode}']) - if add_standalone: - parts.append('@pparam=--standalone') - return ' '.join(parts) - - # ------------------------------------------------------------------ - # Test-process configuration - # ------------------------------------------------------------------ - - def _configure_dns(self, tr: 'TestRun') -> None: - '''Configure a nameserver for the test.''' - name = f'dns{JaxFingerprintTest._dns_counter}' - self._dns = tr.MakeDNServer(name, default='127.0.0.1') - JaxFingerprintTest._dns_counter += 1 - - def _configure_server(self, tr: 'TestRun') -> None: - '''Configure the origin (verifier) server.''' - name = f'server{JaxFingerprintTest._server_counter}' - self._server = tr.AddVerifierServerProcess(name, self._replay_file) - JaxFingerprintTest._server_counter += 1 - - # For remap / hybrid tests the second session reaches the server - # with the fingerprint header already appended by ATS; verify it. - if self._servernames: - # Only the SNI-matched session carries the fingerprint header. - uuid_key = 'hybrid-servernames-in-filter' if self._setup == 'hybrid' else 'servernames-in-filter' - self._server.Streams.All += Testers.ContainsExpression(uuid_key, 'Verify the allowed-SNI request reached the server.') - self._server.Streams.All += Testers.ContainsExpression( - r'x-jax:', 'Verify the fingerprint header was forwarded for the allowed SNI.', reflags=re.IGNORECASE) - elif self._setup in ('remap', 'hybrid'): - uuid_key = 'remap-tls-plugin-request' if self._needs_tls else 'remap-plugin-request' - self._server.Streams.All += Testers.ContainsExpression(uuid_key, 'Verify the matched-route request reached the server.') - self._server.Streams.All += Testers.ContainsExpression( - r'x-jax:', 'Verify the fingerprint header was forwarded.', reflags=re.IGNORECASE) - else: - # global tests - all requests carry the fingerprint header - self._server.Streams.All += Testers.ContainsExpression( - r'x-jax:', 'Verify the fingerprint header was forwarded.', reflags=re.IGNORECASE) - - def _configure_trafficserver(self) -> None: - '''Configure Traffic Server and its plugin / remap rules.''' - name = f'ts{JaxFingerprintTest._ts_counter}' - self._ts = Test.MakeATSProcess(name, enable_cache=False, enable_tls=True) - JaxFingerprintTest._ts_counter += 1 - - self._ts.addDefaultSSLFiles() - self._ts.Disk.ssl_multicert_yaml.AddLines( - """ -ssl_multicert: - - dest_ip: "*" - ssl_cert_name: server.pem - ssl_key_name: server.key -""".split("\n")) - - if self._needs_tls: - server_port = self._server.Variables.https_port - scheme = 'https' - else: - server_port = self._server.Variables.http_port - scheme = 'http' - - self._ts.Disk.records_config.update( - { - 'proxy.config.ssl.server.cert.path': self._ts.Variables.SSLDir, - 'proxy.config.ssl.server.private_key.path': self._ts.Variables.SSLDir, - 'proxy.config.ssl.client.verify.server.policy': 'PERMISSIVE', - 'proxy.config.dns.nameservers': f"127.0.0.1:{self._dns.Variables.Port}", - 'proxy.config.dns.resolv_conf': 'NULL', - 'proxy.config.proxy_name': 'test.proxy.test', - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'jax_fingerprint|http', - }) - - log_path = os.path.join(self._ts.Variables.LOGDIR, 'jax_fingerprint.log') - self._ts.Disk.File(log_path, id='jax_log') - self._ts.Disk.jax_log.Content += Testers.ContainsExpression( - self._fingerprint_pattern, f'Verify the jax_fingerprint log contains a {self._method} fingerprint.') - - backend = f'{scheme}://jax.backend.test:{server_port}' - backend_no_plugin = f'{scheme}://jax.backend.test:{server_port}' - - if self._setup == 'global': - global_args = ( - f'--method {self._method} ' - f'--header x-jax ' - f'--via-header x-jax-via ' - f'--log-filename jax_fingerprint ' - f'--standalone') - if self._mode != 'overwrite': - global_args += f' --mode {self._mode}' - if self._servernames: - global_args += f' --servernames {self._servernames}' - if self._log_field: - global_args += f' --log-field {self._log_field}' - self._ts.Disk.plugin_config.AddLine(f'jax_fingerprint.so {global_args}') - self._ts.Disk.remap_config.AddLine(f'map {scheme}://jax.server.test {backend}') - if self._servernames: - # Second remap rule for the SNI that is NOT in the allowlist. - self._ts.Disk.remap_config.AddLine(f'map {scheme}://jax-filtered.server.test {backend}') - - elif self._setup == 'remap': - # Route without plugin (session 1 in replay file) - self._ts.Disk.remap_config.AddLine(f'map {scheme}://jax-no-plugin.server.test {backend_no_plugin}') - # Route with plugin (session 2 in replay file) - # CONNECTION_BASED methods need --standalone so the remap plugin - # registers the SSL_CLIENT_HELLO_HOOK to populate the vconn context. - remap_line = self._build_remap_plugin_line(add_standalone=self._needs_tls) - self._ts.Disk.remap_config.AddLine(f'map {scheme}://jax.server.test {backend} {remap_line}') - - elif self._setup == 'hybrid': - # Global plugin: registers SSL_CLIENT_HELLO_HOOK to capture the - # TLS handshake and store context on the vconn. No --standalone - # means no READ_REQUEST_HDR hook, so headers are never set here. - global_plugin_args = f'--method {self._method}' - if self._servernames: - global_plugin_args += f' --servernames {self._servernames}' - self._ts.Disk.plugin_config.AddLine(f'jax_fingerprint.so {global_plugin_args}') - remap_line = self._build_remap_plugin_line(add_standalone=False) - if self._servernames: - # Both routes have the remap plugin. Only the SNI-allowed - # connection has a context on the vconn, so only that request - # gets fingerprint headers even though both routes are mapped. - self._ts.Disk.remap_config.AddLine( - f'map https://jax.server.test https://jax.backend.test:{server_port} {remap_line}') - self._ts.Disk.remap_config.AddLine( - f'map https://jax-filtered.server.test https://jax.backend.test:{server_port} {remap_line}') - else: - # Route without remap plugin: context is captured but no headers set. - self._ts.Disk.remap_config.AddLine(f'map https://jax-no-plugin.server.test https://jax.backend.test:{server_port}') - # Route with remap plugin: reads shared vconn context, sets headers. - self._ts.Disk.remap_config.AddLine( - f'map https://jax.server.test https://jax.backend.test:{server_port} {remap_line}') - - if self._log_field: - self._ts.Disk.logging_yaml.AddLines( - f''' -logging: - formats: - - name: jax_custom - format: '{self._method}: %<{self._log_field}>' - logs: - - filename: jax_log_field - format: jax_custom -'''.split("\n")) - - def _configure_client(self, tr: 'TestRun') -> None: - '''Configure the verifier client.''' - name = f'client{JaxFingerprintTest._client_counter}' - p = tr.AddVerifierClientProcess( - name, self._replay_file, http_ports=[self._ts.Variables.port], https_ports=[self._ts.Variables.ssl_port]) - JaxFingerprintTest._client_counter += 1 - - p.StartBefore(self._dns) - p.StartBefore(self._server) - p.StartBefore(self._ts) - tr.StillRunningAfter = self._ts - - -# ====================================================================== -# Test instances -# ====================================================================== - -# --- Global setup ------------------------------------------------------- - -# All HTTP/1.1 requests receive a JA4H fingerprint header (overwrite mode). -JaxFingerprintTest('Global JA4H overwrite', 'JA4H', 'global') - -# All TLS requests receive a JA3 fingerprint header (overwrite mode). -JaxFingerprintTest('Global JA3 overwrite', 'JA3', 'global') - -# --- Remap setup -------------------------------------------------------- - -# Only requests matching the remap rule receive a JA4H header. -JaxFingerprintTest('Remap JA4H', 'JA4H', 'remap') - -# Remap plugin with --standalone captures TLS client hellos globally and -# sets JA3 headers only on the matched route. -JaxFingerprintTest('Remap JA3 standalone', 'JA3', 'remap') - -# --- Hybrid setup ------------------------------------------------------- - -# Global plugin captures TLS client hellos (creates vconn context). -# Remap plugin reads that shared context and sets headers on matched routes. -JaxFingerprintTest('Hybrid JA4', 'JA4', 'hybrid') - -# --- HTTP/2 (h2 over TLS) ----------------------------------------------- - -# JA4: the h2 ALPN in the ClientHello produces a different fingerprint than -# the HTTP/1.1 case – exercises the full TLS extension path with h2 ALPN. -JaxFingerprintTest('Global JA4 HTTP/2', 'JA4', 'global', http2=True) - -# JA4H: exercises the HTTP/2 branch of get_version() which detects h2 via -# TSHttpTxnClientProtocolStackContains. -JaxFingerprintTest('Global JA4H HTTP/2', 'JA4H', 'global', http2=True) - -# keep mode: existing x-jax / x-jax-via headers are not overwritten. -JaxFingerprintTest('Global JA4H keep mode', 'JA4H', 'global', mode='keep') - -# append mode: fingerprint / proxy name are appended to existing header values. -JaxFingerprintTest('Global JA4H append mode', 'JA4H', 'global', mode='append') - -# --- SNI allowlist (--servernames) -------------------------------------- - -# --servernames restricts fingerprinting to connections whose TLS SNI is in -# the list. Connections with a non-matching SNI are skipped at the client- -# hello hook: no context is created and handle_read_request_hdr is a no-op, -# so neither the fingerprint header nor the via header are set. -JaxFingerprintTest('Global JA4 servernames', 'JA4', 'global', servernames='jax.server.test') - -# --- Hybrid + SNI allowlist (most common production pattern) --------------- - -# Global plugin captures TLS client hellos for allowed SNIs only. -# Remap plugin sets headers on both routes, but only the SNI-allowed -# connection has a vconn context, so only that request gets headers. -JaxFingerprintTest('Hybrid JA4 servernames', 'JA4', 'hybrid', servernames='jax.server.test') - -# --- Custom log field (--log-field) ----------------------------------------- - -# Register a custom log field via --log-field and verify the fingerprint -# appears in the ATS access log configured in logging.yaml. -JaxFingerprintTest('Global JA4H log-field', 'JA4H', 'global', log_field='jaxja4h') -JaxFingerprintTest('Global JA4 log-field', 'JA4', 'global', log_field='jaxja4') - -# ====================================================================== -# All Methods Test - Verify shared context map works with multiple methods -# ====================================================================== - - -class AllMethodsTest: - '''Test multiple fingerprint methods loaded simultaneously. - - When multiple jax_fingerprint instances are loaded, they share user arg - slots via a ContextMap. This test verifies that JA3/JA4 and JA4H can - coexist and produce fingerprints while sharing that context machinery - across their respective vconn and txn storage. - ''' - - _dns_counter: int = 0 - _server_counter: int = 0 - _ts_counter: int = 0 - _client_counter: int = 0 - - def __init__(self, name: str) -> None: - '''Configure test with multiple methods loaded.''' - self._name = name - self._replay_file = 'jax_fingerprint_all_methods.replay.yaml' - - tr = Test.AddTestRun(name) - self._configure_dns(tr) - self._configure_server(tr) - self._configure_trafficserver() - self._configure_client(tr) - - def _configure_dns(self, tr: 'TestRun') -> None: - '''Configure a nameserver for the test.''' - name = f'dns_all{AllMethodsTest._dns_counter}' - self._dns = tr.MakeDNServer(name, default='127.0.0.1') - AllMethodsTest._dns_counter += 1 - - def _configure_server(self, tr: 'TestRun') -> None: - '''Configure the origin (verifier) server.''' - name = f'server_all{AllMethodsTest._server_counter}' - self._server = tr.AddVerifierServerProcess(name, self._replay_file) - AllMethodsTest._server_counter += 1 - - # Verify all headers were forwarded to the origin. - for header in ['x-ja3', 'x-ja4', 'x-ja4h']: - self._server.Streams.All += Testers.ContainsExpression( - rf'{header}:', f'Verify {header} header was forwarded.', reflags=re.IGNORECASE) - - def _configure_trafficserver(self) -> None: - '''Configure Traffic Server with multiple methods.''' - name = f'ts_all{AllMethodsTest._ts_counter}' - self._ts = Test.MakeATSProcess(name, enable_cache=False, enable_tls=True) - AllMethodsTest._ts_counter += 1 - - self._ts.addDefaultSSLFiles() - self._ts.Disk.ssl_multicert_yaml.AddLines( - """ -ssl_multicert: - - dest_ip: "*" - ssl_cert_name: server.pem - ssl_key_name: server.key -""".split("\n")) - - server_port = self._server.Variables.https_port - - self._ts.Disk.records_config.update( - { - 'proxy.config.ssl.server.cert.path': self._ts.Variables.SSLDir, - 'proxy.config.ssl.server.private_key.path': self._ts.Variables.SSLDir, - 'proxy.config.ssl.client.verify.server.policy': 'PERMISSIVE', - 'proxy.config.dns.nameservers': f"127.0.0.1:{self._dns.Variables.Port}", - 'proxy.config.dns.resolv_conf': 'NULL', - 'proxy.config.proxy_name': 'test.proxy.test', - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'jax_fingerprint', - }) - - # Each of the following pairs makes sure that the expression exists - # exactly once. - self._ts.Disk.traffic_out.Content += Testers.ContainsExpression( - r'Reserved shared user_arg slot: type=vconn, index=\d+', 'Verify a shared vconn user arg slot was reserved.') - self._ts.Disk.traffic_out.Content += Testers.ExcludesExpression( - r'Reserved shared user_arg slot: type=vconn, index=\d+.*Reserved shared user_arg slot: type=vconn, index=\d+', - 'Verify the shared vconn user arg slot was reserved only once.', - reflags=re.MULTILINE | re.DOTALL) - - self._ts.Disk.traffic_out.Content += Testers.ContainsExpression( - r'Reserved shared user_arg slot: type=txn, index=\d+', 'Verify a shared txn user arg slot was reserved.') - self._ts.Disk.traffic_out.Content += Testers.ExcludesExpression( - r'Reserved shared user_arg slot: type=txn, index=\d+.*Reserved shared user_arg slot: type=txn, index=\d+', - 'Verify the shared txn user arg slot was reserved only once.', - reflags=re.MULTILINE | re.DOTALL) - - # Ensure that JA3 and JA4 share the same vconn user arg slot. - self._ts.Disk.traffic_out.Content += Testers.ContainsExpression( - r'Using shared user_arg: type=vconn, method=JA3, index=(\d+).*Using shared user_arg: type=vconn, method=JA4, index=\1', - 'Verify JA3 and JA4 share the same vconn user arg slot.', - reflags=re.MULTILINE | re.DOTALL) - # Note that JA4H is on txn not vconn as JA3 and JA4 above. - self._ts.Disk.traffic_out.Content += Testers.ContainsExpression( - r'Using shared user_arg: type=txn, method=JA4H, index=\d+', 'Verify JA4H uses the shared txn user arg slot.') - - # Load multiple methods - all share the same user arg slot via ContextMap. - self._ts.Disk.plugin_config.AddLines( - [ - 'jax_fingerprint.so --method JA3 --header x-ja3 --standalone', - 'jax_fingerprint.so --method JA4 --header x-ja4 --standalone', - 'jax_fingerprint.so --method JA4H --header x-ja4h --standalone', - ]) - - self._ts.Disk.remap_config.AddLine(f'map https://jax.server.test https://jax.backend.test:{server_port}') - - def _configure_client(self, tr: 'TestRun') -> None: - '''Configure the verifier client.''' - name = f'client_all{AllMethodsTest._client_counter}' - p = tr.AddVerifierClientProcess( - name, self._replay_file, http_ports=[self._ts.Variables.port], https_ports=[self._ts.Variables.ssl_port]) - AllMethodsTest._client_counter += 1 - - p.StartBefore(self._dns) - p.StartBefore(self._server) - p.StartBefore(self._ts) - tr.StillRunningAfter = self._ts - - -# --- All Methods Test -------------------------------------------------------- - -# Multiple methods loaded simultaneously, verifying shared context map works. -AllMethodsTest('Multiple methods loaded simultaneously') diff --git a/tests/gold_tests/pluginTest/lua/lua_client_hook.test.py b/tests/gold_tests/pluginTest/lua/lua_client_hook.test.py deleted file mode 100644 index 74b8678979f..00000000000 --- a/tests/gold_tests/pluginTest/lua/lua_client_hook.test.py +++ /dev/null @@ -1,57 +0,0 @@ -''' -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import os - -Test.Summary = ''' -Test lua functionality -''' - -Test.SkipUnless(Condition.PluginExists('tslua.so'),) - -Test.ContinueOnFail = True -# Define default ATS -ts = Test.MakeATSProcess("ts") -server = Test.MakeOriginServer("server") - -Test.testName = "" -request_header = {"headers": "GET / HTTP/1.1\r\nHost: www.example.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -# expected response from the origin server -response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": "AAAA"} - -# add response to the server dictionary -server.addResponse("sessionfile.log", request_header, response_header) - -ts.Disk.remap_config.AddLine( - 'map / http://127.0.0.1:{}/'.format(server.Variables.Port) + ' @plugin=tslua.so @pparam=client_hook.lua') - -# Configure the tslua's configuration file. -ts.Setup.Copy("client_hook.lua", ts.Variables.CONFIGDIR) - -ts.Disk.records_config.update({'proxy.config.diags.debug.enabled': 1, 'proxy.config.diags.debug.tags': 'ts_lua'}) - -# Test for watermark debug output -ts.Disk.traffic_out.Content = Testers.ContainsExpression(r"AAAA", "Response is properly captured") - -# Test if watermark upstream is set -tr = Test.AddTestRun("Lua Response Client Hook") -tr.MakeCurlCommand("-v http://127.0.0.1:{0}".format(ts.Variables.port), ts=ts) -tr.Processes.Default.StartBefore(server, ready=When.PortOpen(server.Variables.Port)) - -tr.Processes.Default.StartBefore(ts) -tr.StillRunningAfter = server diff --git a/tests/gold_tests/pluginTest/lua/lua_debug_tags.test.py b/tests/gold_tests/pluginTest/lua/lua_debug_tags.test.py deleted file mode 100644 index 1d4bd3812d7..00000000000 --- a/tests/gold_tests/pluginTest/lua/lua_debug_tags.test.py +++ /dev/null @@ -1,51 +0,0 @@ -''' -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import os - -Test.Summary = ''' -Test lua is_debug_tag_set functionality -''' - -Test.SkipUnless(Condition.PluginExists('tslua.so'),) - -Test.ContinueOnFail = False -# Define default ATS -ts = Test.MakeATSProcess("ts") - -ts.Disk.remap_config.AddLine('map http://test http://127.0.0.1/ @plugin=tslua.so @pparam=tags.lua') - -# Configure the tslua's configuration file. -ts.Setup.Copy("tags.lua", ts.Variables.CONFIGDIR) -ts.Setup.Copy("tags.sh") - -ts.Disk.records_config.update({ - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'foo|ts_lua', -}) - -curl_and_args = f'{{curl}} -s -D /dev/stderr -o /dev/stdout -x localhost:{ts.Variables.port} http://test/test.html' - -# 0 Ensure no debug tag set -tr = Test.AddTestRun("check tags") -ps = tr.Processes.Default -tr.StillRunningAfter = ts -ps.StartBefore(Test.Processes.ts) -tr.MakeCurlCommandMulti(f"bash ./tags.sh {curl_and_args}", ts=ts) -ps.Env = ts.Env -ps.ReturnCode = 0 diff --git a/tests/gold_tests/pluginTest/lua/lua_fetch_ipv6_cliaddr.test.py b/tests/gold_tests/pluginTest/lua/lua_fetch_ipv6_cliaddr.test.py deleted file mode 100644 index 35c53755ed0..00000000000 --- a/tests/gold_tests/pluginTest/lua/lua_fetch_ipv6_cliaddr.test.py +++ /dev/null @@ -1,59 +0,0 @@ -''' -Verify ts.fetch handles an IPv6 cliaddr option. -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -Test.Summary = ''' -Verify ts.fetch parses an IPv6 cliaddr and the inner fetch completes. -''' - -Test.SkipUnless(Condition.PluginExists('tslua.so'),) - -Test.ContinueOnFail = True - -ts = Test.MakeATSProcess("ts") -server = Test.MakeOriginServer("server") - -# Inner sub-request fetched by the Lua post_remap hook. -inner_req = {"headers": "GET /inner.txt HTTP/1.1\r\nHost: www.example.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -inner_resp = { - "headers": "HTTP/1.1 200 OK\r\nConnection: close\r\nContent-Length: 5\r\n\r\n", - "timestamp": "1469733493.993", - "body": "AAAAA" -} -server.addResponse("sessionfile.log", inner_req, inner_resp) - -# Outer request driven by curl below. -outer_req = {"headers": "GET / HTTP/1.1\r\nHost: www.example.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -outer_resp = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": "outer"} -server.addResponse("sessionfile.log", outer_req, outer_resp) - -ts.Disk.remap_config.AddLine( - 'map / http://127.0.0.1:{}/'.format(server.Variables.Port) + ' @plugin=tslua.so @pparam=fetch_ipv6_cliaddr.lua') - -ts.Setup.Copy("fetch_ipv6_cliaddr.lua", ts.Variables.CONFIGDIR) - -ts.Disk.records_config.update({'proxy.config.diags.debug.enabled': 1, 'proxy.config.diags.debug.tags': 'ts_lua'}) - -tr = Test.AddTestRun("ts.fetch with IPv6 cliaddr") -tr.MakeCurlCommand("-s -D - http://127.0.0.1:{0}/".format(ts.Variables.port), ts=ts) -tr.Processes.Default.StartBefore(server, ready=When.PortOpen(server.Variables.Port)) -tr.Processes.Default.StartBefore(ts) -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Streams.stdout.Content = Testers.ContainsExpression( - "Sub-Body-Len: 5", "Inner fetch using IPv6 cliaddr should return the full origin body") -tr.StillRunningAfter = server diff --git a/tests/gold_tests/pluginTest/lua/lua_global_shutdown.test.py b/tests/gold_tests/pluginTest/lua/lua_global_shutdown.test.py deleted file mode 100644 index ca2bb95046f..00000000000 --- a/tests/gold_tests/pluginTest/lua/lua_global_shutdown.test.py +++ /dev/null @@ -1,64 +0,0 @@ -''' -Test __shutdown__ lua global plugin hook. -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -Test.Summary = ''' -Test __shutdown__ lua global plugin hook -''' - -Test.SkipUnless(Condition.PluginExists('tslua.so'),) - -Test.ContinueOnFail = True - -server = Test.MakeOriginServer("server") -ts = Test.MakeATSProcess("ts") - -Test.Setup.Copy("global_shutdown.lua") - -request_header = {"headers": "GET / HTTP/1.1\r\nHost: www.example.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -server.addResponse("sessionfile.log", request_header, response_header) - -ts.Disk.remap_config.AddLine('map / http://127.0.0.1:{}/'.format(server.Variables.Port)) - -# Use 2 states so the shutdown handler is called a predictable number of times. -ts.Disk.plugin_config.AddLine('tslua.so --states=2 {}/global_shutdown.lua'.format(Test.RunDirectory)) - -ts.Disk.records_config.update({ - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'ts_lua', -}) - -curl_and_args = '-s -D /dev/stdout -o /dev/stderr -x localhost:{} '.format(ts.Variables.port) - -# 0 Test - Send a request to confirm the global plugin is active. -tr = Test.AddTestRun("Lua global read request hook fires for HTTP requests") -ps = tr.Processes.Default -ps.StartBefore(server, ready=When.PortOpen(server.Variables.Port)) -ps.StartBefore(Test.Processes.ts) -tr.MakeCurlCommand(curl_and_args + 'http://www.example.com/', ts=ts) -ps.ReturnCode = 0 -tr.StillRunningAfter = ts - -# Verify do_global_read_request was invoked for the HTTP request above. -ts.Disk.traffic_out.Content = Testers.ContainsExpression( - r'do_global_read_request called', 'do_global_read_request should be called for HTTP requests') - -# After all test runs complete AuTest stops ATS, which fires TS_LIFECYCLE_SHUTDOWN_HOOK. -# The shutdown handler calls __shutdown__ once per Lua state (2 states configured). -ts.Disk.traffic_out.Content += Testers.ContainsExpression(r'__shutdown__ called', '__shutdown__ should be called on ATS shutdown') diff --git a/tests/gold_tests/pluginTest/lua/lua_header_table.test.py b/tests/gold_tests/pluginTest/lua/lua_header_table.test.py deleted file mode 100644 index 50a8c355e1e..00000000000 --- a/tests/gold_tests/pluginTest/lua/lua_header_table.test.py +++ /dev/null @@ -1,41 +0,0 @@ -''' -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -Test.Summary = ''' -Test lua header table functionality -''' - -Test.SkipUnless(Condition.PluginExists('tslua.so'),) - -Test.ContinueOnFail = True -# Define default ATS -ts = Test.MakeATSProcess("ts") - -ts.Disk.remap_config.AddLine(f"map / http://127.0.0.1 @plugin=tslua.so @pparam=header_table.lua") -# Configure the tslua's configuration file. -ts.Setup.Copy("header_table.lua", ts.Variables.CONFIGDIR) - -# Test - Check for header table -tr = Test.AddTestRun("Lua Header Table") -ps = tr.Processes.Default # alias -ps.StartBefore(Test.Processes.ts) -tr.MakeCurlCommand(f"-s -D /dev/stderr -H 'X-Test: test1' -H 'X-Test: test2' http://127.0.0.1:{ts.Variables.port}", ts=ts) -ps.Env = ts.Env -ps.ReturnCode = 0 -ps.Streams.stdout.Content = Testers.ContainsExpression("test1test2", "expected header table results") -tr.StillRunningAfter = ts diff --git a/tests/gold_tests/pluginTest/lua/lua_proxy_protocol.test.py b/tests/gold_tests/pluginTest/lua/lua_proxy_protocol.test.py deleted file mode 100644 index 16e45207240..00000000000 --- a/tests/gold_tests/pluginTest/lua/lua_proxy_protocol.test.py +++ /dev/null @@ -1,82 +0,0 @@ -''' -Test Lua plugin PROXY protocol support -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import os - -Test.Summary = ''' -Test Lua plugin PROXY protocol API -''' - -Test.SkipUnless( - Condition.PluginExists('tslua.so'), - Condition.HasCurlOption("--haproxy-clientip"), -) - -Test.ContinueOnFail = True - -# ---- Setup Origin Server ---- -server = Test.MakeOriginServer("server") - -request_header = {"headers": "GET / HTTP/1.1\r\nHost: www.example.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -response_header = { - "headers": "HTTP/1.1 200 OK\r\nConnection: close\r\nContent-Length: 4\r\n\r\n", - "timestamp": "1469733493.993", - "body": "TEST" -} -server.addResponse("sessionfile.log", request_header, response_header) - -# ---- Setup ATS ---- -ts = Test.MakeATSProcess("ts", enable_proxy_protocol=True) - -ts.Disk.remap_config.AddLine( - 'map / http://127.0.0.1:{0}/'.format(server.Variables.Port) + ' @plugin=tslua.so @pparam=proxy_protocol.lua') - -ts.Setup.Copy("proxy_protocol.lua", ts.Variables.CONFIGDIR) - -ts.Disk.records_config.update({'proxy.config.diags.debug.enabled': 1, 'proxy.config.diags.debug.tags': 'ts_lua'}) - -# ---- Test with PROXY protocol ---- -tr = Test.AddTestRun("Test with PROXY protocol v1") -tr.Processes.Default.Command = ( - f"curl --haproxy-protocol --haproxy-clientip 192.168.1.100 " - f"http://127.0.0.1:{ts.Variables.proxy_protocol_port}/") -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.StartBefore(server, ready=When.PortOpen(server.Variables.Port)) -tr.Processes.Default.StartBefore(ts) -tr.Processes.Default.Streams.stdout = Testers.ContainsExpression("TEST", "Response body should be received") -tr.StillRunningAfter = server -tr.StillRunningAfter = ts - -# Check debug log for PROXY protocol info -ts.Disk.traffic_out.Content += Testers.ContainsExpression(r"PP-Version: 1", "PROXY protocol version should be logged") -ts.Disk.traffic_out.Content += Testers.ContainsExpression( - r"PP-Source: 192\.168\.1\.100", "PROXY protocol source address should be logged") -ts.Disk.traffic_out.Content += Testers.ContainsExpression(r"PP-Protocol: 2", "PROXY protocol IP family should be logged") -ts.Disk.traffic_out.Content += Testers.ContainsExpression(r"PP-SocketType: 1", "PROXY protocol socket type should be logged") - -# ---- Test without PROXY protocol ---- -tr2 = Test.AddTestRun("Test without PROXY protocol") -tr2.Processes.Default.Command = f"curl http://127.0.0.1:{ts.Variables.port}/" -tr2.Processes.Default.ReturnCode = 0 -tr2.Processes.Default.Streams.stdout = Testers.ContainsExpression("TEST", "Response body should be received") -tr2.StillRunningAfter = server -tr2.StillRunningAfter = ts - -# Check that PP-Not-Present is logged when PROXY protocol is not used -ts.Disk.traffic_out.Content += Testers.ContainsExpression(r"PP-Version: 0", "Should log when PROXY protocol is not present") diff --git a/tests/gold_tests/pluginTest/lua/lua_remap_after_hook.test.py b/tests/gold_tests/pluginTest/lua/lua_remap_after_hook.test.py deleted file mode 100644 index eeba08880a6..00000000000 --- a/tests/gold_tests/pluginTest/lua/lua_remap_after_hook.test.py +++ /dev/null @@ -1,55 +0,0 @@ -''' -Verify ts.remap.* APIs respect their documented do_remap-only context. -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -Test.Summary = ''' -The ts.remap.* family is documented as context: do_remap. Verify that -calling one (ts.remap.get_from_url_host) from a transaction hook -registered during do_remap returns nil and does not crash, while the -same call inside do_remap returns the from-URL host. -''' - -Test.SkipUnless(Condition.PluginExists('tslua.so'),) - -Test.ContinueOnFail = True - -ts = Test.MakeATSProcess("ts") -server = Test.MakeOriginServer("server") - -req = {"headers": "GET / HTTP/1.1\r\nHost: www.example.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -resp = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": "ok"} -server.addResponse("sessionfile.log", req, resp) - -ts.Disk.remap_config.AddLine( - 'map http://www.example.com/ http://127.0.0.1:{}/'.format(server.Variables.Port) + - ' @plugin=tslua.so @pparam=remap_after_hook.lua') - -ts.Setup.Copy("remap_after_hook.lua", ts.Variables.CONFIGDIR) - -ts.Disk.records_config.update({'proxy.config.diags.debug.enabled': 1, 'proxy.config.diags.debug.tags': 'ts_lua'}) - -tr = Test.AddTestRun("ts.remap.* in do_remap and from a txn hook") -tr.MakeCurlCommand("-s -D - -H 'Host: www.example.com' http://127.0.0.1:{0}/".format(ts.Variables.port), ts=ts) -tr.Processes.Default.StartBefore(server, ready=When.PortOpen(server.Variables.Port)) -tr.Processes.Default.StartBefore(ts) -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Streams.stdout.Content = Testers.ContainsExpression( - "Remap-From-Host: www.example.com", "ts.remap.* should return the from-URL host inside do_remap") -tr.Processes.Default.Streams.stdout.Content += Testers.ContainsExpression( - "Hook-From-Host: ", "ts.remap.* should return nil when called outside do_remap") -tr.StillRunningAfter = server diff --git a/tests/gold_tests/pluginTest/lua/lua_set_error_response.test.py b/tests/gold_tests/pluginTest/lua/lua_set_error_response.test.py deleted file mode 100644 index 7408566dedf..00000000000 --- a/tests/gold_tests/pluginTest/lua/lua_set_error_response.test.py +++ /dev/null @@ -1,98 +0,0 @@ -'''Verify ts.client_response.set_error_resp functionality.''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import os - -Test.Summary = ''' -Test lua functionality -''' - -Test.SkipUnless(Condition.PluginExists('tslua.so'),) - - -class TestLuaSetErrorResponse: - '''Verify ts.client_response.set_error_resp functionality.''' - - replay_file: str = 'set_error_response.replay.yaml' - lua_script: str = 'set_error_response.lua' - - def __init__(self): - tr = Test.AddTestRun("Lua ts.client_response.set_error_resp") - self._configure_dns(tr) - self._configure_server(tr) - self._configure_ts(tr) - self._configure_client(tr) - - def _configure_dns(self, tr: 'TestRun') -> 'Process': - '''Configure the DNS server. - - :param tr: The test run to configure the DNS server in. - :return: The newly created DNS process. - ''' - dns = tr.MakeDNServer("dns", default='127.0.0.1') - self._dns = dns - return dns - - def _configure_server(self, tr: 'TestRun') -> 'Process': - '''Configure the origin server. - - :param tr: The test run to configure the server in. - :return: The newly created server process. - ''' - server = tr.AddVerifierServerProcess("server", self.replay_file) - self._server = server - return server - - def _configure_ts(self, tr: 'TestRun') -> 'Process': - '''Configure the ATS process. - - :param tr: The test run to configure the ATS process in. - :return: The newly created ATS process. - ''' - ts = tr.MakeATSProcess("ts", enable_cache=False) - self._ts = ts - ts.Setup.Copy(self.lua_script, ts.Variables.CONFIGDIR) - port = self._server.Variables.http_port - ts.Disk.remap_config.AddLine(f'map / http://backend.example.com:{port}/ @plugin=tslua.so @pparam={self.lua_script}') - ts.Disk.records_config.update( - { - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'ts_lua|http', - 'proxy.config.dns.nameservers': f"127.0.0.1:{self._dns.Variables.Port}", - 'proxy.config.dns.resolv_conf': 'NULL' - }) - return ts - - def _configure_client(self, tr: 'TestRun') -> 'Process': - '''Configure the client process. - - :param tr: The test run to configure the client process in. - :return: The newly created client process. - ''' - port = self._ts.Variables.port - client = tr.AddVerifierClientProcess('client', self.replay_file, http_ports=[port]) - self._client = client - client.StartBefore(self._server) - client.StartBefore(self._dns) - client.StartBefore(self._ts) - client.Streams.All += Testers.ContainsExpression( - 'HTTP/1.1 418', 'The modified HTTP 418 response should be received by the client.') - client.Streams.All += Testers.ContainsExpression('bad luck', 'The modified HTTP response body should contain "bad luck".') - return client - - -TestLuaSetErrorResponse() diff --git a/tests/gold_tests/pluginTest/lua/lua_states_stats.test.py b/tests/gold_tests/pluginTest/lua/lua_states_stats.test.py deleted file mode 100644 index f4a1713e077..00000000000 --- a/tests/gold_tests/pluginTest/lua/lua_states_stats.test.py +++ /dev/null @@ -1,96 +0,0 @@ -''' -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -Test.Summary = ''' -Test lua states and stats functionality -''' - -Test.SkipUnless(Condition.PluginExists('tslua.so'),) - -Test.ContinueOnFail = True -# Define default ATS -server = Test.MakeOriginServer("server") - -ts = Test.MakeATSProcess("ts") - -Test.testName = "Lua states and stats" - -Test.Setup.Copy("hello.lua") -Test.Setup.Copy("global.lua") -Test.Setup.Copy("metrics.sh") -Test.Setup.Copy("lifecycle_stats.sh") - -# test to ensure origin server works -request_header = {"headers": "GET / HTTP/1.1\r\nHost: www.example.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} - -# add response to the server dictionary -server.addResponse("sessionfile.log", request_header, response_header) - -ts.Disk.remap_config.AddLines( - { - 'map / http://127.0.0.1:{}/'.format(server.Variables.Port), - 'map http://hello http://127.0.0.1:{}/'.format(server.Variables.Port) + - ' @plugin=tslua.so @pparam={}/hello.lua'.format(Test.RunDirectory) - }) - -ts.Disk.plugin_config.AddLine('tslua.so {}/global.lua'.format(Test.RunDirectory)) - -ts.Disk.records_config.update( - { - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'ts_lua', - 'proxy.config.plugin.lua.max_states': 4, - }) - -curl_and_args = '-s -D /dev/stdout -o /dev/stderr -x localhost:{} '.format(ts.Variables.port) - -# 0 Test - Check for configured lua states -tr = Test.AddTestRun("Lua states") -ps = tr.Processes.Default # alias -ps.StartBefore(server, ready=When.PortOpen(server.Variables.Port)) -ps.StartBefore(Test.Processes.ts) -ps.Command = "traffic_ctl config match lua" -ps.Env = ts.Env -ps.ReturnCode = 0 -ps.Streams.stdout.Content = Testers.ContainsExpression("proxy.config.plugin.lua.max_states: 4", "expected 4 states") -tr.StillRunningAfter = ts - -# 1 Test - Exercise lua script -tr = Test.AddTestRun("Lua hello") -ps = tr.Processes.Default # alias -tr.MakeCurlCommand(curl_and_args + ' http://hello/hello', ts=ts) -ps.ReturnCode = 0 -ps.Streams.stderr.Content = Testers.ContainsExpression("Hello, World", "hello world content") -tr.StillRunningAfter = ts - -# 2 Test - Check for metrics -tr = Test.AddTestRun("Check for metrics") -ps = tr.Processes.Default # alias -ps.Command = "bash -c ./metrics.sh" -ps.Env = ts.Env -ps.ReturnCode = 0 -tr.StillRunningAfter = ts - -# 3 Test - Check for developer lifecycle stats -tr = Test.AddTestRun("Check for lifecycle stats") -ps = tr.Processes.Default # alias -ps.Command = "bash -c ./lifecycle_stats.sh" -ps.Env = ts.Env -ps.ReturnCode = 0 -tr.StillRunningAfter = ts diff --git a/tests/gold_tests/pluginTest/lua/lua_verified_addr.test.py b/tests/gold_tests/pluginTest/lua/lua_verified_addr.test.py deleted file mode 100644 index adfd0f66569..00000000000 --- a/tests/gold_tests/pluginTest/lua/lua_verified_addr.test.py +++ /dev/null @@ -1,79 +0,0 @@ -''' -Test lua verified address functionality -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -Test.Summary = ''' -Test lua verified address get/set functionality -''' - -Test.SkipUnless(Condition.PluginExists('tslua.so'),) - -Test.ContinueOnFail = True - -# Define ATS process -ts = Test.MakeATSProcess("ts") - -# Configure remap -ts.Disk.remap_config.AddLine(f"map / http://127.0.0.1 @plugin=tslua.so @pparam=verified_addr.lua") - -# Copy the Lua script -ts.Setup.Copy("verified_addr.lua", ts.Variables.CONFIGDIR) - -# Enable debug logging -ts.Disk.records_config.update({'proxy.config.diags.debug.enabled': 1, 'proxy.config.diags.debug.tags': 'ts_lua'}) - -# Test 1: IPv4 verified address -tr = Test.AddTestRun("Lua Verified Address - IPv4") -ps = tr.Processes.Default -ps.StartBefore(Test.Processes.ts) -tr.MakeCurlCommand(f"-s -H 'X-Real-IP: 192.0.2.100' http://127.0.0.1:{ts.Variables.port}", ts=ts) -ps.Env = ts.Env -ps.ReturnCode = 0 -ps.Streams.stdout.Content = Testers.ContainsExpression( - "initial:nil;set:success;get:192.0.2.100:2;", "IPv4 verified address should be set and retrieved correctly") -tr.StillRunningAfter = ts - -# Test 2: IPv6 verified address -tr = Test.AddTestRun("Lua Verified Address - IPv6") -ps = tr.Processes.Default -tr.MakeCurlCommand(f"-s -H 'X-Real-IP-V6: 2001:db8::1' http://127.0.0.1:{ts.Variables.port}", ts=ts) -ps.Env = ts.Env -ps.ReturnCode = 0 -ps.Streams.stdout.Content = Testers.ContainsExpression( - "initial:nil;setv6:success;getv6:2001:db8::1:10;", "IPv6 verified address should be set and retrieved correctly") -tr.StillRunningAfter = ts - -# Test 3: Invalid IP address (should be rejected) -tr = Test.AddTestRun("Lua Verified Address - Invalid IP") -ps = tr.Processes.Default -tr.MakeCurlCommand(f"-s -H 'X-Invalid-IP: not.a.valid.ip' http://127.0.0.1:{ts.Variables.port}", ts=ts) -ps.Env = ts.Env -ps.ReturnCode = 0 -ps.Streams.stdout.Content = Testers.ContainsExpression("invalid:rejected;", "Invalid IP address should be rejected") -tr.StillRunningAfter = ts - -# Test 4: Both IPv4 and IPv6 in sequence -tr = Test.AddTestRun("Lua Verified Address - IPv4 then IPv6") -ps = tr.Processes.Default -tr.MakeCurlCommand(f"-s -H 'X-Real-IP: 203.0.113.42' -H 'X-Real-IP-V6: 2001:db8::42' http://127.0.0.1:{ts.Variables.port}", ts=ts) -ps.Env = ts.Env -ps.ReturnCode = 0 -ps.Streams.stdout.Content = Testers.ContainsExpression( - "initial:nil;set:success;get:203.0.113.42:2;setv6:success;getv6:2001:db8::42:10;", - "Both IPv4 and IPv6 verified addresses should work in sequence") -tr.StillRunningAfter = ts diff --git a/tests/gold_tests/pluginTest/lua/lua_watermark.test.py b/tests/gold_tests/pluginTest/lua/lua_watermark.test.py deleted file mode 100644 index d2c1bddc242..00000000000 --- a/tests/gold_tests/pluginTest/lua/lua_watermark.test.py +++ /dev/null @@ -1,56 +0,0 @@ -''' -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import os - -Test.Summary = ''' -Test lua functionality -''' - -Test.SkipUnless(Condition.PluginExists('tslua.so'),) - -Test.ContinueOnFail = True -# Define default ATS -ts = Test.MakeATSProcess("ts") -server = Test.MakeOriginServer("server") - -Test.testName = "" -request_header = {"headers": "GET / HTTP/1.1\r\nHost: www.example.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -# expected response from the origin server -response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} - -# add response to the server dictionary -server.addResponse("sessionfile.log", request_header, response_header) - -ts.Disk.remap_config.AddLine('map / http://127.0.0.1:{}/'.format(server.Variables.Port) + ' @plugin=tslua.so @pparam=watermark.lua') - -# Configure the tslua's configuration file. -ts.Setup.Copy("watermark.lua", ts.Variables.CONFIGDIR) - -ts.Disk.records_config.update({'proxy.config.diags.debug.enabled': 1, 'proxy.config.diags.debug.tags': 'ts_lua'}) - -# Test for watermark debug output -ts.Disk.traffic_out.Content = Testers.ContainsExpression(r"WMbytes\(31337\)", "Upstream watermark should be properly set") - -# Test if watermark upstream is set -tr = Test.AddTestRun("Lua Watermark") -tr.MakeCurlCommand("-v http://127.0.0.1:{0}".format(ts.Variables.port), ts=ts) -tr.Processes.Default.StartBefore(server, ready=When.PortOpen(server.Variables.Port)) - -tr.Processes.Default.StartBefore(ts) -tr.StillRunningAfter = server diff --git a/tests/gold_tests/pluginTest/money_trace/money_trace.test.py b/tests/gold_tests/pluginTest/money_trace/money_trace.test.py deleted file mode 100644 index 1783db061f6..00000000000 --- a/tests/gold_tests/pluginTest/money_trace/money_trace.test.py +++ /dev/null @@ -1,206 +0,0 @@ -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. -import os - -Test.Summary = ''' -Test money_trace remap -''' - -# Test description: - -Test.SkipUnless(Condition.PluginExists('money_trace.so'),) -Test.ContinueOnFail = False -Test.testName = "money_trace remap" - -# Define ATS and configure -ts = Test.MakeATSProcess("ts", enable_cache=False) - -# configure origin server -server = Test.MakeOriginServer("server") - -req_chk = {"headers": "GET / HTTP/1.1\r\n" + "Host: origin\r\n" + "\r\n", "timestamp": "1469733493.993", "body": ""} -res_chk = {"headers": "HTTP/1.1 200 OK\r\n" + "Connection: close\r\n" + "\r\n", "timestamp": "1469733493.993", "body": ""} -server.addResponse("sessionlog.json", req_chk, res_chk) - -req_hdr = {"headers": "GET /path HTTP/1.1\r\n" + "Host: www.example.com\r\n" + "\r\n", "timestamp": "1469733493.993", "body": ""} -res_hdr = {"headers": "HTTP/1.1 200 OK\r\n" + "Connection: close\r\n" + "\r\n", "timestamp": "1469733493.993", "body": ""} -server.addResponse("sessionlog.json", req_hdr, res_hdr) - -ts.Disk.remap_config.AddLines( - [ - f"map http://none/ http://127.0.0.1:{server.Variables.Port}", - f"map http://basic/ http://127.0.0.1:{server.Variables.Port} @plugin=money_trace.so", - f"map http://header/ http://127.0.0.1:{server.Variables.Port} @plugin=money_trace.so @pparam=--header=mt", - f"map http://pregen/ http://127.0.0.1:{server.Variables.Port} @plugin=money_trace.so @pparam=--pregen-header=@pregen", - f"map http://pgh/ http://127.0.0.1:{server.Variables.Port} @plugin=money_trace.so @pparam=--header=mt @pparam=--pregen-header=@pregen", - f"map http://create/ http://127.0.0.1:{server.Variables.Port} @plugin=money_trace.so @pparam=--create-if-none=true", - f"map http://cheader/ http://127.0.0.1:{server.Variables.Port} @plugin=money_trace.so @pparam=--create-if-none=true @pparam=--header=mt", - f"map http://cpregen/ http://127.0.0.1:{server.Variables.Port} @plugin=money_trace.so @pparam=--create-if-none=true @pparam=--pregen-header=@pregen", - f"map http://passthru/ http://127.0.0.1:{server.Variables.Port} @plugin=money_trace.so @pparam=--passthru=true", - ]) - -# minimal configuration -ts.Disk.records_config.update({ - 'proxy.config.diags.debug.enabled': 0, - 'proxy.config.diags.debug.tags': 'money_trace', -}) - -ts.Disk.logging_yaml.AddLines( - ''' -logging: - formats: - - name: custom - format: 'cqh: %<{X-MoneyTrace}cqh> %<{mt}cqh> %<{@pregen}cqh> pqh: %<{X-MoneyTrace}pqh> %<{mt}pqh> psh: %<{X-MoneyTrace}psh> %<{mt}psh>' - logs: - - filename: remap - format: custom -'''.split("\n")) - -Test.Disk.File(os.path.join(ts.Variables.LOGDIR, 'remap.log'), exists=True, content='gold/remap-log.gold') - -curl_and_args = f"-s -D /dev/stdout -o /dev/stderr -x 127.0.0.1:{ts.Variables.port}" - -# 0 Test -tr = Test.AddTestRun("no plugin test") -ps = tr.Processes.Default -ps.StartBefore(server) -ps.StartBefore(Test.Processes.ts) -tr.MakeCurlCommand(curl_and_args + " http://none/path", ts=ts) -ps.ReturnCode = 0 -tr.StillRunningAfter = ts -tr.StillRunningAfter = server - -# 1 Test -tr = Test.AddTestRun("basic config, no money trace client header") -ps = tr.Processes.Default -tr.MakeCurlCommand(curl_and_args + " http://basic/path", ts=ts) -ps.ReturnCode = 0 -tr.StillRunningAfter = ts -tr.StillRunningAfter = server - - -def maketrace(name): - return f'trace-id={name};parent-id=foo;span-id=bar' - - -# 2 Test -tr = Test.AddTestRun("basic config, money trace client header") -ps = tr.Processes.Default -tr.MakeCurlCommand(curl_and_args + ' http://basic/path -H "X-MoneyTrace: ' + maketrace("basic") + '"', ts=ts) -ps.ReturnCode = 0 -tr.StillRunningAfter = ts -tr.StillRunningAfter = server - -# 3 Test -tr = Test.AddTestRun("header config, mt client header") -ps = tr.Processes.Default -tr.MakeCurlCommand(curl_and_args + ' http://header/path -H "mt: ' + maketrace("header") + '"', ts=ts) -ps.ReturnCode = 0 -tr.StillRunningAfter = ts -tr.StillRunningAfter = server - -# 4 Test -tr = Test.AddTestRun("pregen config, but no header passed in") -ps = tr.Processes.Default -tr.MakeCurlCommand(curl_and_args + ' http://pregen/path', ts=ts) -ps.ReturnCode = 0 -tr.StillRunningAfter = ts -tr.StillRunningAfter = server - -# 5 Test -tr = Test.AddTestRun("pregen config, money trace client header") -ps = tr.Processes.Default -tr.MakeCurlCommand(curl_and_args + ' http://pregen/path -H "X-MoneyTrace: ' + maketrace("pregen") + '"', ts=ts) -ps.ReturnCode = 0 -tr.StillRunningAfter = ts -tr.StillRunningAfter = server - -# 6 Test -tr = Test.AddTestRun("pregen config, mt client header") -ps = tr.Processes.Default -tr.MakeCurlCommand(curl_and_args + ' http://pgh/path -H "mt: ' + maketrace("pgh") + '"', ts=ts) -ps.ReturnCode = 0 -tr.StillRunningAfter = ts -tr.StillRunningAfter = server - -# 7 Test -tr = Test.AddTestRun("create config, money trace client header") -ps = tr.Processes.Default -tr.MakeCurlCommand(curl_and_args + ' http://create/path', ts=ts) -ps.ReturnCode = 0 -tr.StillRunningAfter = ts -tr.StillRunningAfter = server - -# 8 Test -tr = Test.AddTestRun("create config, mt client header") -ps = tr.Processes.Default -tr.MakeCurlCommand(curl_and_args + ' http://cheader/path', ts=ts) -ps.ReturnCode = 0 -tr.StillRunningAfter = ts -tr.StillRunningAfter = server - -# 9 Test -tr = Test.AddTestRun("create config, pregen client header") -ps = tr.Processes.Default -tr.MakeCurlCommand(curl_and_args + ' http://cpregen/path', ts=ts) -ps.ReturnCode = 0 -tr.StillRunningAfter = ts -tr.StillRunningAfter = server - -# 10 Test -tr = Test.AddTestRun("passthru mode") -ps = tr.Processes.Default -tr.MakeCurlCommand(curl_and_args + ' http://passthru/path -H "X-MoneyTrace: ' + maketrace("passthru") + '"', ts=ts) -ps.ReturnCode = 0 -tr.StillRunningAfter = ts -tr.StillRunningAfter = server - -# Parsing robustness - -trace_strings = [ - "trace-id=spaces; parent-id=foo; span-id=bar", - "trace-id=noparent;span-id=8215111;span-name=this is some description", - "trace-id=badspan;span-id=", - "trace-id=traceonly", - "trace-id=traceonlysemi;", - "not a trace header", -] - -# 11 Test -for trace in trace_strings: - tr = Test.AddTestRun(trace) - ps = tr.Processes.Default - tr.MakeCurlCommand(curl_and_args + ' http://pregen/path -H "X-MoneyTrace: ' + trace + '"', ts=ts) - ps.ReturnCode = 0 - tr.StillRunningAfter = ts - tr.StillRunningAfter = server - -# 11 Test -for trace in trace_strings: - tr = Test.AddTestRun(trace) - ps = tr.Processes.Default - tr.MakeCurlCommand(curl_and_args + ' http://cpregen/path -H "X-MoneyTrace: ' + trace + '"', ts=ts) - ps.ReturnCode = 0 - tr.StillRunningAfter = ts - tr.StillRunningAfter = server - -# Wait for the final log line to be written. -# 11 Test -Test.AddAwaitFileContainsTestRun( - 'Await final money_trace remap log line.', - os.path.join(ts.Variables.LOGDIR, 'remap.log'), - r'^cqh: not a trace header - trace-id=.* pqh: trace-id=.* - psh: not a trace header -$', -) diff --git a/tests/gold_tests/pluginTest/money_trace/money_trace_global.test.py b/tests/gold_tests/pluginTest/money_trace/money_trace_global.test.py deleted file mode 100644 index a8de1876cde..00000000000 --- a/tests/gold_tests/pluginTest/money_trace/money_trace_global.test.py +++ /dev/null @@ -1,106 +0,0 @@ -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import os - -Test.Summary = ''' -Test money_trace global -''' - -# Test description: - -Test.SkipUnless( - # Condition.PluginExists('xdebug.so'), - Condition.PluginExists('money_trace.so'),) -Test.ContinueOnFail = False -Test.testName = "money_trace global" - -# Define ATS and configure -ts = Test.MakeATSProcess("ts", enable_cache=False) - -# configure origin server -server = Test.MakeOriginServer("server") - -req_chk = {"headers": "GET / HTTP/1.1\r\n" + "Host: origin\r\n" + "\r\n", "timestamp": "1469733493.993", "body": ""} -res_chk = {"headers": "HTTP/1.1 200 OK\r\n" + "Connection: close\r\n" + "\r\n", "timestamp": "1469733493.993", "body": ""} -server.addResponse("sessionlog.json", req_chk, res_chk) - -req_hdr = {"headers": "GET /path HTTP/1.1\r\n" + "Host: www.example.com\r\n" + "\r\n", "timestamp": "1469733493.993", "body": ""} -res_hdr = {"headers": "HTTP/1.1 200 OK\r\n" + "Connection: close\r\n" + "\r\n", "timestamp": "1469733493.993", "body": ""} -server.addResponse("sessionlog.json", req_hdr, res_hdr) - -ts.Disk.remap_config.AddLines([ - f"map http://ats http://127.0.0.1:{server.Variables.Port}", -]) - -ts.Disk.plugin_config.AddLine( - "money_trace.so --pregen-header=@pregen --header=MoneyTrace --create-if-none=true --global-skip-header=Skip-Global-MoneyTrace") - -# minimal configuration -ts.Disk.records_config.update({ - 'proxy.config.diags.debug.enabled': 0, - 'proxy.config.diags.debug.tags': 'money_trace', -}) - -ts.Disk.logging_yaml.AddLines( - ''' -logging: - formats: - - name: custom - format: 'cqh: %<{MoneyTrace}cqh> %<{@pregen}cqh> pqh: %<{MoneyTrace}pqh> psh: %<{MoneyTrace}psh>' - logs: - - filename: global - format: custom -'''.split("\n")) - -Test.Disk.File(os.path.join(ts.Variables.LOGDIR, 'global.log'), exists=True, content='gold/global-log.gold') - -curl_and_args = f"-s -D /dev/stdout -o /dev/stderr -x 127.0.0.1:{ts.Variables.port}" # -H 'X-Debug: Probe' " - -clientvalue = "trace-id=foo;parent-id=bar;span-id=baz" - -# 0 Test -tr = Test.AddTestRun("normal header test") -ps = tr.Processes.Default -ps.StartBefore(server, ready=When.PortOpen(server.Variables.Port)) -ps.StartBefore(Test.Processes.ts) -tr.MakeCurlCommand(curl_and_args + ' http://ats/path -H "MoneyTrace: ' + clientvalue + '"', ts=ts) -ps.ReturnCode = 0 -tr.StillRunningAfter = ts -tr.StillRunningAfter = server - -# 1 Test -tr = Test.AddTestRun("skip plugin test - pregen will still be set") -ps = tr.Processes.Default -tr.MakeCurlCommand(curl_and_args + ' http://ats/path -H "Skip-Global-MoneyTrace: true"', ts=ts) -ps.ReturnCode = 0 -tr.StillRunningAfter = ts -tr.StillRunningAfter = server - -# 2 Test -tr = Test.AddTestRun("create header test") -ps = tr.Processes.Default -tr.MakeCurlCommand(curl_and_args + ' http://ats/path', ts=ts) -ps.ReturnCode = 0 -tr.StillRunningAfter = ts -tr.StillRunningAfter = server - -# Wait for the final log line to be written. -Test.AddAwaitFileContainsTestRun( - 'Await final money_trace global log line.', - os.path.join(ts.Variables.LOGDIR, 'global.log'), - r'^cqh: - trace-id=.* pqh: trace-id=.* psh: -$', -) diff --git a/tests/gold_tests/pluginTest/multiplexer/multiplexer.test.py b/tests/gold_tests/pluginTest/multiplexer/multiplexer.test.py deleted file mode 100644 index 890712afdea..00000000000 --- a/tests/gold_tests/pluginTest/multiplexer/multiplexer.test.py +++ /dev/null @@ -1,324 +0,0 @@ -''' -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import os - -Test.Summary = ''' -Test the Multiplexer plugin. -''' - -Test.SkipUnless(Condition.PluginExists('multiplexer.so')) - - -class MultiplexerTestBase: - """ - Encapsulates the base configuration used by each test. - """ - - client_counter = 0 - dns_counter = 0 - server_counter = 0 - ts_counter = 0 - - def __init__(self, replay_file, multiplexed_host_replay_file, skip_post): - self.replay_file = replay_file - self.multiplexed_host_replay_file = multiplexed_host_replay_file - - self.setupServers() - self.setupDns() - self.setupTS(skip_post) - - def setupDns(self): - counter = MultiplexerTestBase.dns_counter - MultiplexerTestBase.dns_counter += 1 - self.dns = Test.MakeDNServer(f"dns_{counter}", default='127.0.0.1') - - def setupServers(self): - counter = MultiplexerTestBase.server_counter - MultiplexerTestBase.server_counter += 1 - self.server_origin = Test.MakeVerifierServerProcess(f"server_origin_{counter}", self.replay_file) - self.server_http = Test.MakeVerifierServerProcess(f"server_http_{counter}", self.multiplexed_host_replay_file) - self.server_https = Test.MakeVerifierServerProcess(f"server_https_{counter}", self.multiplexed_host_replay_file) - - # The origin should never receive "X-Multiplexer: copy" - self.server_origin.Streams.All += Testers.ExcludesExpression( - 'X-Multiplexer: copy', 'Verify the original server target never receives a "copy".') - - # Nor should the multiplexed hosts receive an "original" X-Multiplexer value. - self.server_http.Streams.All += Testers.ExcludesExpression( - 'X-Multiplexer: original', 'Verify the HTTP multiplexed host does not receive an "original".') - self.server_https.Streams.All += Testers.ExcludesExpression( - 'X-Multiplexer: original', 'Verify the HTTPS multiplexed host does not receive an "original".') - self.server_https.Streams.All += Testers.ExcludesExpression(r'\[ERROR\]', 'Verify there were no errors in the replay.') - - # In addition, the original server should always receive the POST and - # PUT requests. - self.server_origin.Streams.All += Testers.ContainsExpression( - 'uuid: POST', "Verify the client's original target received the POST transaction.") - self.server_origin.Streams.All += Testers.ContainsExpression( - 'uuid: PUT', "Verify the client's original target received the PUT transaction.") - self.server_origin.Streams.All += Testers.ExcludesExpression(r'\[ERROR\]', 'Verify there were no errors in the replay.') - # The chunked POST should go to the origin. - self.server_origin.Streams.All += Testers.ContainsExpression( - 'uuid: CHUNKED_POST', "Verify the client's original target received the chunked POST transaction.") - - # Under all configurations, the GET request should be multiplexed. - self.server_origin.Streams.All += Testers.ContainsExpression( - 'X-Multiplexer: original', 'Verify the client\'s original target received the "original" request.') - self.server_origin.Streams.All += Testers.ContainsExpression( - 'uuid: GET', "Verify the client's original target received the GET request.") - - self.server_http.Streams.All += Testers.ContainsExpression( - 'X-Multiplexer: copy', 'Verify the HTTP server received a "copy" of the request.') - self.server_http.Streams.All += Testers.ContainsExpression('uuid: GET', "Verify the HTTP server received the GET request.") - self.server_http.Streams.All += Testers.ExcludesExpression(r'\[ERROR\]', 'Verify there were no errors in the replay.') - # Chunked POST requests are not supported for multiplexing. - self.server_http.Streams.All += Testers.ExcludesExpression( - 'uuid: CHUNKED_POST', 'We do not expect a multiplexed chunked POST.') - - self.server_https.Streams.All += Testers.ContainsExpression( - 'X-Multiplexer: copy', 'Verify the HTTPS server received a "copy" of the request.') - self.server_https.Streams.All += Testers.ContainsExpression( - 'uuid: GET', "Verify the HTTPS server received the GET request.") - # Chunked POST requests are not supported for multiplexing. - self.server_https.Streams.All += Testers.ExcludesExpression( - 'uuid: CHUNKED_POST', 'We do not expect a multiplexed chunked POST.') - - # Verify that the CUSTOM_METHOD is sent to all servers. - self.server_origin.Streams.All += Testers.ContainsExpression( - 'uuid: MYCUSTOMMETHOD', "Verify the client's original target received the MYCUSTOMMETHOD transaction.") - self.server_http.Streams.All += Testers.ContainsExpression( - 'uuid: MYCUSTOMMETHOD', "Verify the HTTP server received the MYCUSTOMMETHOD request.") - self.server_http.Streams.All += Testers.ContainsExpression( - 'x-response: fifth', "Verify the HTTP server sent the MYCUSTOMMETHOD response.") - self.server_https.Streams.All += Testers.ContainsExpression( - 'uuid: MYCUSTOMMETHOD', "Verify the HTTPS server received the MYCUSTOMMETHOD request.") - self.server_https.Streams.All += Testers.ContainsExpression( - 'x-response: fifth', "Verify the HTTPS server sent the MYCUSTOMMETHOD response.") - - # Verify that the HTTPS server receives a TLS connection. - self.server_https.Streams.All += Testers.ContainsExpression( - 'Finished accept using TLSSession', "Verify the HTTPS was indeed used by the HTTPS server.") - - def setupTS(self, skip_post): - counter = MultiplexerTestBase.ts_counter - MultiplexerTestBase.ts_counter += 1 - self.ts = Test.MakeATSProcess(f"ts_{counter}", enable_tls=True, enable_cache=False) - self.ts.addDefaultSSLFiles() - self.ts.Disk.records_config.update( - { - "proxy.config.ssl.server.cert.path": f'{self.ts.Variables.SSLDir}', - "proxy.config.ssl.server.private_key.path": f'{self.ts.Variables.SSLDir}', - "proxy.config.ssl.client.verify.server.policy": 'PERMISSIVE', - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'http|multiplexer', - 'proxy.config.dns.nameservers': f'127.0.0.1:{self.dns.Variables.Port}', - 'proxy.config.dns.resolv_conf': 'NULL', - }) - self.ts.Disk.ssl_multicert_yaml.AddLines( - """ -ssl_multicert: - - dest_ip: "*" - ssl_cert_name: server.pem - ssl_key_name: server.key -""".split("\n")) - skip_remap_param = '' - if skip_post: - skip_remap_param = ' @pparam=proxy.config.multiplexer.skip_post_put=1' - self.ts.Disk.remap_config.AddLines( - [ - f'map https://origin.server.com https://backend.origin.server.com:{self.server_origin.Variables.https_port} ' - f'@plugin=multiplexer.so @pparam=nontls.server.com @pparam=tls.server.com' - f'{skip_remap_param}', - - # Now create remap entries for the multiplexed hosts: one that - # verifies HTTP, and another that verifies HTTPS. - f'map http://nontls.server.com http://backend.nontls.server.com:{self.server_http.Variables.http_port}', - f'map http://tls.server.com https://backend.tls.server.com:{self.server_https.Variables.https_port}', - ]) - - def run(self): - tr = Test.AddTestRun() - self.ts.StartBefore(self.dns) - tr.Processes.Default.StartBefore(self.server_origin) - tr.Processes.Default.StartBefore(self.server_http) - tr.Processes.Default.StartBefore(self.server_https) - tr.Processes.Default.StartBefore(self.ts) - - counter = MultiplexerTestBase.client_counter - MultiplexerTestBase.client_counter += 1 - client = tr.AddVerifierClientProcess(f"client_{counter}", self.replay_file, https_ports=[self.ts.Variables.ssl_port]) - client.Streams.All += Testers.ExcludesExpression(r'\[ERROR\]', 'Verify there were no errors in the replay.') - - -class MultiplexerTest(MultiplexerTestBase): - """ - Exercise multiplexing without skip_post configuration. - """ - - replay_file = os.path.join("replays", "multiplexer_original.replay.yaml") - multiplexed_host_replay_file = os.path.join("replays", "multiplexer_copy.replay.yaml") - - def __init__(self): - super().__init__(MultiplexerTest.replay_file, MultiplexerTest.multiplexed_host_replay_file, skip_post=False) - - def setupServers(self): - super().setupServers() - - # Both of the multiplexed hosts should receive the POST because skip_post - # is disabled. - self.server_http.Streams.All += Testers.ContainsExpression( - 'uuid: POST', "Verify the HTTP server received the POST request.") - self.server_https.Streams.All += Testers.ContainsExpression( - 'uuid: POST', "Verify the HTTPS server received the POST request.") - self.server_http.Streams.All += Testers.ContainsExpression( - 'x-response: second', "Verify the HTTP server sent the POST response.") - self.server_https.Streams.All += Testers.ContainsExpression( - 'x-response: second', "Verify the HTTPS server sent the POST response.") - self.server_https.Streams.All += Testers.ContainsExpression( - 'x-response: fifth', "Verify the HTTPS server sent the custom method response.") - - # Same with PUT - self.server_http.Streams.All += Testers.ContainsExpression('uuid: PUT', "Verify the HTTP server received the PUT request.") - self.server_https.Streams.All += Testers.ContainsExpression( - 'uuid: PUT', "Verify the HTTPS server received the PUT request.") - - # Same with MYCUSTOMMETHOD - self.server_http.Streams.All += Testers.ContainsExpression( - 'uuid: MYCUSTOMMETHOD', "Verify the HTTP server received the custom method request.") - self.server_https.Streams.All += Testers.ContainsExpression( - 'uuid: MYCUSTOMMETHOD', "Verify the HTTPS server received the custom method request.") - - -class MultiplexerSkipPostTest(MultiplexerTestBase): - """ - Exercise multiplexing with skip_post configuration. - """ - - replay_file = os.path.join("replays", "multiplexer_original_skip_post.replay.yaml") - multiplexed_host_replay_file = os.path.join("replays", "multiplexer_copy_skip_post.replay.yaml") - - def __init__(self): - super().__init__(MultiplexerSkipPostTest.replay_file, MultiplexerSkipPostTest.multiplexed_host_replay_file, skip_post=True) - - def setupServers(self): - super().setupServers() - - # Neither of the multiplexed hosts should receive the POST because skip_post - # is enabled. - self.server_http.Streams.All += Testers.ExcludesExpression( - 'uuid: POST', "Verify the HTTP server did not receive the POST request.") - self.server_https.Streams.All += Testers.ExcludesExpression( - 'uuid: POST', "Verify the HTTPS server did not receive the POST request.") - - # Same with PUT. - self.server_http.Streams.All += Testers.ExcludesExpression( - 'uuid: PUT', "Verify the HTTP server did not receive the PUT request.") - self.server_https.Streams.All += Testers.ExcludesExpression( - 'uuid: PUT', "Verify the HTTPS server did not receive the PUT request.") - - -class MultiplexerInvalidChunkedResponseTest: - """ - Verify a copied upstream response with an oversized chunk-size does not - disrupt the original client transaction. - """ - - replay_file = os.path.join("replays", "multiplexer_invalid_chunk_original.replay.yaml") - multiplexed_host_replay_file = os.path.join("replays", "multiplexer_invalid_chunk_copy.replay.yaml") - - def __init__(self): - self.setupServers() - self.setupDns() - self.setupTS() - - def setupDns(self): - counter = MultiplexerTestBase.dns_counter - MultiplexerTestBase.dns_counter += 1 - self.dns = Test.MakeDNServer(f"dns_{counter}", default='127.0.0.1') - - def setupServers(self): - counter = MultiplexerTestBase.server_counter - MultiplexerTestBase.server_counter += 1 - self.server_origin = Test.MakeVerifierServerProcess(f"server_origin_{counter}", self.replay_file) - self.server_http = Test.MakeVerifierServerProcess(f"server_http_{counter}", self.multiplexed_host_replay_file) - self.server_https = Test.MakeVerifierServerProcess(f"server_https_{counter}", self.multiplexed_host_replay_file) - - self.server_origin.Streams.All += Testers.ContainsExpression( - 'uuid: INVALID_CHUNK', "Verify the original server received the invalid chunk test request.") - self.server_origin.Streams.All += Testers.ContainsExpression( - 'X-Multiplexer: original', 'Verify the original target received the "original" request.') - self.server_origin.Streams.All += Testers.ExcludesExpression(r'\[ERROR\]', 'Verify there were no errors in the replay.') - - for server in [self.server_http, self.server_https]: - server.Streams.All += Testers.ContainsExpression( - 'uuid: INVALID_CHUNK', "Verify the multiplexed server received the invalid chunk test request.") - server.Streams.All += Testers.ContainsExpression( - 'X-Multiplexer: copy', 'Verify the multiplexed server received a "copy" of the request.') - server.Streams.All += Testers.ExcludesExpression(r'\[ERROR\]', 'Verify there were no errors in the replay.') - - self.server_https.Streams.All += Testers.ContainsExpression( - 'Finished accept using TLSSession', "Verify the HTTPS was indeed used by the HTTPS server.") - - def setupTS(self): - counter = MultiplexerTestBase.ts_counter - MultiplexerTestBase.ts_counter += 1 - self.ts = Test.MakeATSProcess(f"ts_{counter}", enable_tls=True, enable_cache=False) - self.ts.addDefaultSSLFiles() - self.ts.Disk.records_config.update( - { - "proxy.config.ssl.server.cert.path": f'{self.ts.Variables.SSLDir}', - "proxy.config.ssl.server.private_key.path": f'{self.ts.Variables.SSLDir}', - "proxy.config.ssl.client.verify.server.policy": 'PERMISSIVE', - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'http|multiplexer', - 'proxy.config.dns.nameservers': f'127.0.0.1:{self.dns.Variables.Port}', - 'proxy.config.dns.resolv_conf': 'NULL', - }) - self.ts.Disk.ssl_multicert_yaml.AddLines( - """ -ssl_multicert: - - dest_ip: "*" - ssl_cert_name: server.pem - ssl_key_name: server.key -""".split("\n")) - self.ts.Disk.remap_config.AddLines( - [ - f'map https://origin.server.com https://backend.origin.server.com:{self.server_origin.Variables.https_port} ' - f'@plugin=multiplexer.so @pparam=nontls.server.com @pparam=tls.server.com', - f'map http://nontls.server.com http://backend.nontls.server.com:{self.server_http.Variables.http_port}', - f'map http://tls.server.com https://backend.tls.server.com:{self.server_https.Variables.https_port}', - ]) - - def run(self): - tr = Test.AddTestRun("Multiplexed response with oversized chunk-size") - self.ts.StartBefore(self.dns) - tr.Processes.Default.StartBefore(self.server_origin) - tr.Processes.Default.StartBefore(self.server_http) - tr.Processes.Default.StartBefore(self.server_https) - tr.Processes.Default.StartBefore(self.ts) - - counter = MultiplexerTestBase.client_counter - MultiplexerTestBase.client_counter += 1 - client = tr.AddVerifierClientProcess(f"client_{counter}", self.replay_file, https_ports=[self.ts.Variables.ssl_port]) - client.Streams.All += Testers.ExcludesExpression(r'\[ERROR\]', 'Verify there were no errors in the replay.') - - -MultiplexerTest().run() -MultiplexerSkipPostTest().run() -MultiplexerInvalidChunkedResponseTest().run() diff --git a/tests/gold_tests/pluginTest/origin_server_auth/origin_server_auth.test.py b/tests/gold_tests/pluginTest/origin_server_auth/origin_server_auth.test.py deleted file mode 100644 index dcb9f658c33..00000000000 --- a/tests/gold_tests/pluginTest/origin_server_auth/origin_server_auth.test.py +++ /dev/null @@ -1,135 +0,0 @@ -''' -Test origin_server_auth config parsing -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -Test.testName = "origin_server_auth: config parsing" -Test.Summary = ''' -Test for successful authentication to origin server -''' -Test.ContinueOnFail = True - - -class OriginServerAuthTest: - - def __init__(self): - self.setUpOriginServer() - self.setUpTS() - - def setUpOriginServer(self): - self.server = Test.MakeOriginServer("server") - # define the s3 request header and the desired response header - request_header = { - "headers": "GET /s3-bucket HTTP/1.1\r\nHost: www.example.com\r\n\r\n", - "timestamp": "1469733493.993", - "x-amz-security-token": - "hkMsi6/bfHyBKrSeM/H0hoXeyx8z1yZ/mJ0c+B/TqYx=tTJDjnQWtul38Z9iVJjeH1HB4VT2c=2o3yE3o=I9kmFs/lJDR85qWjB8e5asY/WbjyRpbAzmDipQpboIcYnUYg55bxrQFidV/q8gZa5A9MpR3n=op1C0lWjeBqcEJxpevNZxteSQTQfeGsi98Cdf+On=/SINVlKrNhMnmMsDOLMGx1YYt9d4UsRg1jtVrwxL4Vd/F7aHCZySAXKv+1rkhACR023wpa3dhp+xirGJxSO9LWwvcrTdM4xJo4RS8B40tGENOJ1NKixUJxwN/6og58Oft/u==uleR89Ja=7zszK2H7tX3DqmEYNvNDYQh/7VBRe5otghQtPwJzWpXAGk+Vme4hPPM5K6axH2LxipXzRiIV=oxNs0upKNu1FvuzbCQmkQdKQVmXl0344vngngrgN7wkEfrYtmKwICmpAS0cbW9jdSClgziVo4NaFc/hsIfok=4UA3hVtxIdw74lFNXD0RR7HKXkFPLIn85M7peOZsqMUCfO4gxr7KCfabszQQf0YcP/mt79XK50=WrSJG7oUyn+clUySPhlegqHAfT9a50uSK5WiQmOnGNGLF4wDO10sqKN1xRgQbYHPtwL+Ye0EMisvmYA3==kScorTSGaQWyibSWXAvxq9+IVGBYShVJ6S7DmTT=u/2d/fGEge+Xmbxlftza=cxJ=Md=k1Q71Lp6Boa56d7wtYRpK6tXHJ9I/2r7rN1E4OtwkFqb7SfWV3UXwyUrXyaaNPTIbqnAHnbgUGtuU6pgICpfREiIxVqvKBf6ErbxHRmMmAuYKxk5E9Mn6nnbxR4WTniweKYeDv2w39zge/tss+36Moeuio9d2eoyRFqXhq=rUGtDwX3fzXV0wV+dUojxOYQ57GQDl7+68PwHPcX794OIXuGOxBk83lNIYIcYz3Vc7qnGy6tFTz7f6S9+EZuSGN7TY5VKkT2eWye46DebrDF9Nwzs/FVpTzbPD/KGDIBtFIbazglhKoWe9txqb1QW8vFNNVOEhYa+cViO3g8ZmY1wG960US2zsnX5Eg8Q5a4h3+sxaJSJ4ONiXZWJuAgKRQzcrszu+M5C0ZVoCOv1goEgfNJeSm/yFc/3rx8wmeWLIJFtq65B7zF72HRKq1nthHAguaxXr20nguHpKkDpNBDVa=WwuJsbeGI", - "body": "" - } - - # desired response from the origin server - response_header = { - "headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", - "timestamp": "1469733493.993", - "body": "success!" - } - self.server.addResponse("sessionlog.log", request_header, response_header) - - # define the GCP request header and the desired response header - request_header = { - "headers": "GET /gcp HTTP/1.1\r\nHost: www.example.com\r\n\r\n", - "timestamp": "1469733493.993", - "Authorization": - "Bearer hkMsi6/bfHyBKrSeM/H0hoXeyx8z1yZ/mJ0c+B/TqYx=tTJDjnQWtul38Z9iVJjeH1HB4VT2c=2o3yE3o=I9kmFs/lJDR85qWjB8e5asY/WbjyRpbAzmDipQpboIcYnUYg55bxrQFidV/q8gZa5A9MpR3n=op1C0lWjeBqcEJxpevNZxteSQTQfeGsi98Cdf+On=/SINVlKrNhMnmMsDOLMGx1YYt9d4UsRg1jtVrwxL4Vd/F7aHCZySAXKv+1rkhACR023wpa3dhp+xirGJxSO9LWwvcrTdM4xJo4RS8B40tGENOJ1NKixUJxwN/6og58Oft/u==uleR89Ja=7zszK2H7tX3DqmEYNvNDYQh/7VBRe5otghQtPwJzWpXAGk+Vme4hPPM5K6axH2LxipXzRiIV=oxNs0upKNu1FvuzbCQmkQdKQVmXl0344vngngrgN7wkEfrYtmKwICmpAS0cbW9jdSClgziVo4NaFc/hsIfok=4UA3hVtxIdw74lFNXD0RR7HKXkFPLIn85M7peOZsqMUCfO4gxr7KCfabszQQf0YcP/mt79XK50=WrSJG7oUyn+clUySPhlegqHAfT9a50uSK5WiQmOnGNGLF4wDO10sqKN1xRgQbYHPtwL+Ye0EMisvmYA3==kScorTSGaQWyibSWXAvxq9+IVGBYShVJ6S7DmTT=u/2d/fGEge+Xmbxlftza=cxJ=Md=k1Q71Lp6Boa56d7wtYRpK6tXHJ9I/2r7rN1E4OtwkFqb7SfWV3UXwyUrXyaaNPTIbqnAHnbgUGtuU6pgICpfREiIxVqvKBf6ErbxHRmMmAuYKxk5E9Mn6nnbxR4WTniweKYeDv2w39zge/tss+36Moeuio9d2eoyRFqXhq=rUGtDwX3fzXV0wV+dUojxOYQ57GQDl7+68PwHPcX794OIXuGOxBk83lNIYIcYz3Vc7qnGy6tFTz7f6S9+EZuSGN7TY5VKkT2eWye46DebrDF9Nwzs/FVpTzbPD/KGDIBtFIbazglhKoWe9txqb1QW8vFNNVOEhYa+cViO3g8ZmY1wG960US2zsnX5Eg8Q5a4h3+sxaJSJ4ONiXZWJuAgKRQzcrszu+M5C0ZVoCOv1goEgfNJeSm/yFc/3rx8wmeWLIJFtq65B7zF72HRKq1nthHAguaxXr20nguHpKkDpNBDVa=WwuJsbeGI", - "body": "" - } - - # desired response from the origin server - response_header = { - "headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", - "timestamp": "1469733493.993", - "body": "success!" - } - - # add request/response - self.server.addResponse("sessionlog.log", request_header, response_header) - - def setUpTS(self): - self.ts = Test.MakeATSProcess("ts") - - self.ts.Disk.records_config.update( - { - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.show_location': 0, - 'proxy.config.diags.debug.tags': 'origin_server_auth', - }) - - self.ts.Setup.CopyAs('rules/v4-parse-test.test_input', Test.RunDirectory) - - self.ts.Disk.remap_config.AddLine( - f'map http://www.example.com/s3-bucket http://127.0.0.1:{self.server.Variables.Port}/s3-bucket \ - @plugin=origin_server_auth.so \ - @pparam=--config @pparam={Test.RunDirectory}/v4-parse-test.test_input') - - self.ts.Disk.remap_config.AddLine( - f'map http://www.example.com/gcp http://127.0.0.1:{self.server.Variables.Port}/gcp \ - @plugin=origin_server_auth.so \ - @pparam=--access_key @pparam=1234567 \ - @pparam=--session_token @pparam=hkMsi6/bfHyBKrSeM/H0hoXeyx8z1yZ/mJ0c+B/TqYx=tTJDjnQWtul38Z9iVJjeH1HB4VT2c=2o3yE3o=I9kmFs/lJDR85qWjB8e5asY/WbjyRpbAzmDipQpboIcYnUYg55bxrQFidV/q8gZa5A9MpR3n=op1C0lWjeBqcEJxpevNZxteSQTQfeGsi98Cdf+On=/SINVlKrNhMnmMsDOLMGx1YYt9d4UsRg1jtVrwxL4Vd/F7aHCZySAXKv+1rkhACR023wpa3dhp+xirGJxSO9LWwvcrTdM4xJo4RS8B40tGENOJ1NKixUJxwN/6og58Oft/u==uleR89Ja=7zszK2H7tX3DqmEYNvNDYQh/7VBRe5otghQtPwJzWpXAGk+Vme4hPPM5K6axH2LxipXzRiIV=oxNs0upKNu1FvuzbCQmkQdKQVmXl0344vngngrgN7wkEfrYtmKwICmpAS0cbW9jdSClgziVo4NaFc/hsIfok=4UA3hVtxIdw74lFNXD0RR7HKXkFPLIn85M7peOZsqMUCfO4gxr7KCfabszQQf0YcP/mt79XK50=WrSJG7oUyn+clUySPhlegqHAfT9a50uSK5WiQmOnGNGLF4wDO10sqKN1xRgQbYHPtwL+Ye0EMisvmYA3==kScorTSGaQWyibSWXAvxq9+IVGBYShVJ6S7DmTT=u/2d/fGEge+Xmbxlftza=cxJ=Md=k1Q71Lp6Boa56d7wtYRpK6tXHJ9I/2r7rN1E4OtwkFqb7SfWV3UXwyUrXyaaNPTIbqnAHnbgUGtuU6pgICpfREiIxVqvKBf6ErbxHRmMmAuYKxk5E9Mn6nnbxR4WTniweKYeDv2w39zge/tss+36Moeuio9d2eoyRFqXhq=rUGtDwX3fzXV0wV+dUojxOYQ57GQDl7+68PwHPcX794OIXuGOxBk83lNIYIcYz3Vc7qnGy6tFTz7f6S9+EZuSGN7TY5VKkT2eWye46DebrDF9Nwzs/FVpTzbPD/KGDIBtFIbazglhKoWe9txqb1QW8vFNNVOEhYa+cViO3g8ZmY1wG960US2zsnX5Eg8Q5a4h3+sxaJSJ4ONiXZWJuAgKRQzcrszu+M5C0ZVoCOv1goEgfNJeSm/yFc/3rx8wmeWLIJFtq65B7zF72HRKq1nthHAguaxXr20nguHpKkDpNBDVa=WwuJsbeGI \ - @pparam=--version @pparam=gcpv1') - - def test_s3_origin_remap(self): - ''' - Test s3 v4 origin server - ''' - tr = Test.AddTestRun() - tr.MakeCurlCommand(f'-s -v -H "Host: www.example.com" http://127.0.0.1:{self.ts.Variables.port}/s3-bucket;', ts=self.ts) - tr.Processes.Default.ReturnCode = 0 - tr.Processes.Default.StartBefore(self.server) - tr.Processes.Default.StartBefore(self.ts) - tr.Processes.Default.Streams.stderr.Content = Testers.ContainsExpression("200 OK", "expected 200 response") - tr.Processes.Default.Streams.stderr.Content += Testers.ContainsExpression("Content-Length: 8", "expected content-length 8") - tr.StillRunningAfter = self.server - - def test_gcp_origin_remap(self): - ''' - Test GCP origin server - ''' - tr = Test.AddTestRun() - tr.MakeCurlCommand(f'-s -v -H "Host: www.example.com" http://127.0.0.1:{self.ts.Variables.port}/gcp;', ts=self.ts) - tr.Processes.Default.ReturnCode = 0 - tr.Processes.Default.Streams.stderr.Content = Testers.ContainsExpression("200 OK", "expected 200 response") - tr.Processes.Default.Streams.stderr.Content += Testers.ContainsExpression("Content-Length: 8", "expected content-length 8") - tr.StillRunningAfter = self.server - - def checkLogOutput(self): - if Condition.CurlUsingUnixDomainSocket(): - self.ts.Disk.traffic_out.Content = "gold/origin_server_auth_parsing_ts_uds.gold" - else: - self.ts.Disk.traffic_out.Content = "gold/origin_server_auth_parsing_ts.gold" - self.ts.ReturnCode = 0 - - def runTraffic(self): - self.test_s3_origin_remap() - self.test_gcp_origin_remap() - self.checkLogOutput() - - def run(self): - self.runTraffic() - - -OriginServerAuthTest().run() diff --git a/tests/gold_tests/pluginTest/origin_server_auth/s3_url_encoding.test.py b/tests/gold_tests/pluginTest/origin_server_auth/s3_url_encoding.test.py deleted file mode 100644 index 7c2e4086e72..00000000000 --- a/tests/gold_tests/pluginTest/origin_server_auth/s3_url_encoding.test.py +++ /dev/null @@ -1,198 +0,0 @@ -''' -Test origin_server_auth URL encoding fix (verifies fix for mixed-encoding bug) - -This test verifies the fix for a bug where mixed URL encoding in request paths -caused signature mismatch with S3. - -Fixed bug: When a URL had SOME characters encoded (e.g., %5B) but others NOT -encoded (e.g., parentheses), the old isUriEncoded() function incorrectly -returned true, causing canonicalEncode() to skip re-encoding. This resulted -in a signature calculated for a partially-encoded path, which didn't match -what S3 expected. - -With the fix, isUriEncoded() now correctly returns false for mixed-encoding -URLs, triggering the decode/re-encode path to normalize the URL. - -Example (now handled correctly): - Client sends: /app/(channel)/%5B%5Bparts%5D%5D/page.js - isUriEncoded(): Returns false (detects unencoded parentheses) - canonicalEncode(): Decodes then re-encodes properly - Signature for: /app/%28channel%29/%5B%5Bparts%5D%5D/page.js <- CORRECT - -This test sends requests with various URL encodings and verifies that -the origin receives correctly normalized Authorization headers. -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -Test.testName = "origin_server_auth: URL encoding bug" -Test.Summary = ''' -Test for S3 signature calculation with mixed URL encoding -''' -Test.ContinueOnFail = True - - -class S3UrlEncodingTest: - """ - Test class for verifying URL encoding behavior in S3 auth signature calculation. - """ - - def __init__(self): - self.setupOriginServer() - self.setupTS() - - def setupOriginServer(self): - """ - Create an origin server that captures and logs the request path and headers. - ATS preserves URL encoding from the client, so we need to match exactly. - """ - self.server = Test.MakeOriginServer("s3_mock") - - # Test case 1: Fully unencoded path with parentheses - # Client sends: /bucket/app/(channel)/test.js (unencoded) - # ATS forwards: /bucket/app/(channel)/test.js (as-is) - request1 = { - "headers": "GET /bucket/app/(channel)/test.js HTTP/1.1\r\nHost: s3.amazonaws.com\r\n\r\n", - "timestamp": "1469733493.993", - "body": "" - } - response1 = { - "headers": "HTTP/1.1 200 OK\r\nConnection: close\r\nContent-Length: 7\r\n\r\n", - "timestamp": "1469733493.993", - "body": "test1ok" - } - self.server.addResponse("sessionlog.log", request1, response1) - - # Test case 2: Fully encoded parentheses - # Client sends: /bucket/app/%28channel%29/test.js (encoded) - # ATS forwards: /bucket/app/%28channel%29/test.js (as-is, preserves encoding) - request2 = { - "headers": "GET /bucket/app/%28channel%29/test.js HTTP/1.1\r\nHost: s3.amazonaws.com\r\n\r\n", - "timestamp": "1469733493.993", - "body": "" - } - response2 = { - "headers": "HTTP/1.1 200 OK\r\nConnection: close\r\nContent-Length: 7\r\n\r\n", - "timestamp": "1469733493.993", - "body": "test2ok" - } - self.server.addResponse("sessionlog.log", request2, response2) - - # Test case 3: Mixed encoding - BUG CASE - # Client sends: /bucket/app/(channel)/%5B%5Bparts%5D%5D/page.js - # - Parentheses () NOT encoded - # - Brackets [] ARE encoded as %5B%5D - # ATS forwards: /bucket/app/(channel)/%5B%5Bparts%5D%5D/page.js (as-is) - # - # BUG: isUriEncoded() sees %5B -> returns true -> canonicalEncode() skips encoding - # Result: Signature calculated for /bucket/app/(channel)/%5B%5Bparts%5D%5D/page.js - # But S3 expects signature for /bucket/app/%28channel%29/%5B%5Bparts%5D%5D/page.js - request3 = { - "headers": "GET /bucket/app/(channel)/%5B%5Bparts%5D%5D/page.js HTTP/1.1\r\nHost: s3.amazonaws.com\r\n\r\n", - "timestamp": "1469733493.993", - "body": "" - } - response3 = { - "headers": "HTTP/1.1 200 OK\r\nConnection: close\r\nContent-Length: 7\r\n\r\n", - "timestamp": "1469733493.993", - "body": "test3ok" - } - self.server.addResponse("sessionlog.log", request3, response3) - - def setupTS(self): - """Configure ATS with the origin_server_auth plugin.""" - self.ts = Test.MakeATSProcess("ts") - - self.ts.Disk.records_config.update( - { - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'origin_server_auth', - 'proxy.config.url_remap.pristine_host_hdr': 1, - }) - - # Copy the config file to the test directory and use it - self.ts.Setup.CopyAs('rules/s3_url_encoding.test_input', Test.RunDirectory) - - # Remap rule with S3 auth - self.ts.Disk.remap_config.AddLine( - f'map http://s3.amazonaws.com/ http://127.0.0.1:{self.server.Variables.Port}/ ' - f'@plugin=origin_server_auth.so ' - f'@pparam=--config @pparam={Test.RunDirectory}/s3_url_encoding.test_input') - - def test_unencoded_parentheses(self): - """ - Test 1: Request with unencoded parentheses. - The plugin should encode () to %28%29 for the signature calculation. - """ - tr = Test.AddTestRun("Unencoded parentheses in path") - tr.Processes.Default.Command = ( - f'curl -s -v -o /dev/null ' - f'-H "Host: s3.amazonaws.com" ' - f'"http://127.0.0.1:{self.ts.Variables.port}/bucket/app/(channel)/test.js"') - tr.Processes.Default.ReturnCode = 0 - tr.Processes.Default.StartBefore(self.server) - tr.Processes.Default.StartBefore(self.ts) - tr.Processes.Default.Streams.stderr.Content = Testers.ContainsExpression("200 OK", "Expected 200 OK response") - tr.StillRunningAfter = self.ts - - def test_encoded_parentheses(self): - """ - Test 2: Request with URL-encoded parentheses. - ATS preserves the encoding when forwarding to origin. - """ - tr = Test.AddTestRun("Encoded parentheses in path") - tr.Processes.Default.Command = ( - f'curl -s -v -o /dev/null ' - f'-H "Host: s3.amazonaws.com" ' - f'"http://127.0.0.1:{self.ts.Variables.port}/bucket/app/%28channel%29/test.js"') - tr.Processes.Default.ReturnCode = 0 - tr.Processes.Default.Streams.stderr.Content = Testers.ContainsExpression("200 OK", "Expected 200 OK response") - tr.StillRunningAfter = self.ts - - def test_mixed_encoding_bug(self): - """ - Test 3: BUG CASE - Mixed encoding with unencoded parentheses and encoded brackets. - - Client URL: /bucket/app/(channel)/%5B%5Bparts%5D%5D/page.js - - Parentheses () are NOT encoded - - Brackets [] ARE encoded as %5B%5D - - BUG: isUriEncoded() finds %5B and returns true, so canonicalEncode() - returns the path as-is without encoding the parentheses. - - This causes signature mismatch because: - - Plugin calculates signature for: /bucket/app/(channel)/%5B%5Bparts%5D%5D/page.js - - S3 expects signature for: /bucket/app/%28channel%29/%5B%5Bparts%5D%5D/page.js - """ - tr = Test.AddTestRun("Mixed encoding - BUG CASE") - tr.Processes.Default.Command = ( - f'curl -s -v -o /dev/null ' - f'-H "Host: s3.amazonaws.com" ' - f'"http://127.0.0.1:{self.ts.Variables.port}/bucket/app/(channel)/%5B%5Bparts%5D%5D/page.js"') - tr.Processes.Default.ReturnCode = 0 - # This should succeed with our mock server, but would fail with real S3 - # The test documents the bug - when fixed, the signature would be correct - tr.Processes.Default.Streams.stderr.Content = Testers.ContainsExpression( - "200 OK", "Expected 200 OK response (mock server accepts any signature)") - tr.StillRunningAfter = self.ts - - def run(self): - self.test_unencoded_parentheses() - self.test_encoded_parentheses() - self.test_mixed_encoding_bug() - - -S3UrlEncodingTest().run() diff --git a/tests/gold_tests/pluginTest/packet_mark/packet_mark.test.py b/tests/gold_tests/pluginTest/packet_mark/packet_mark.test.py deleted file mode 100644 index 1af23f2cbda..00000000000 --- a/tests/gold_tests/pluginTest/packet_mark/packet_mark.test.py +++ /dev/null @@ -1,181 +0,0 @@ -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import os -import socket - -Test.Summary = ''' -Verify TSHttpTxnClientPacketMarkSet and TSHttpTxnServerPacketMarkSet set the -firewall mark on the client- and server-side connections respectively. Each is -driven by a test plugin that applies the mark and reads it back off the relevant -socket with getsockopt(SO_MARK), echoing the observed value into a response -header this test asserts on. -''' - - -def _can_set_so_mark() -> bool: - """Probe whether SO_MARK can actually be set on this host. - - Setting SO_MARK is Linux-only and requires CAP_NET_ADMIN or CAP_NET_RAW. - On any host that lacks the capability (or the platform), setsockopt raises, - and the applied value would be unobservable -- so the test is skipped - rather than failed. - """ - if not hasattr(socket, "SO_MARK"): - return False - try: - with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as probe: - probe.setsockopt(socket.SOL_SOCKET, socket.SO_MARK, 0x1) - return True - except (OSError, PermissionError): - return False - - -Test.SkipUnless( - Condition.IsPlatform("linux"), - # pass_value defaults to True: run only when the probe reports SO_MARK is settable. - Condition(_can_set_so_mark, "Setting SO_MARK requires Linux with CAP_NET_ADMIN or CAP_NET_RAW"), -) - -# SOCK_OPT_PACKET_MARK (0x10) | SOCK_OPT_NO_DELAY (0x1). The mark is only pushed -# to the socket when the PACKET_MARK bit is set in the sock option flag. -SOCK_OPT_FLAG_PACKET_MARK = 0x11 - - -class PacketMarkTest: - """Drive a TSHttpTxn*PacketMarkSet API through a test plugin and assert on the - firewall mark read back off the relevant socket. - - This base holds the shared skeleton -- process setup, the common records, the - curl-and-assert case runner. Each subclass supplies its plugin and echo - header and extends _configure() (via super()) with the side-specific mark and - flag records. - """ - - # Value the plugin sets; the mark is expected to become exactly this. - SET_MARK = 0x0000000A - # Seeded starting mark, distinct from SET_MARK so a no-op would be visible. - SEED_MARK = 0x0000FF00 - - # Bumped per instance so each side gets uniquely-numbered processes. - _counter = 0 - - def __init__(self): - self._num = PacketMarkTest._counter - PacketMarkTest._counter += 1 - self._server = self._make_server() - self._ts = self._make_ats() - self._configure(self._ts) - self._started = False - - def _make_server(self): - server = Test.MakeOriginServer(f"server{self._num}") - request_header = {"headers": "GET / HTTP/1.1\r\nHost: example.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} - response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} - for _ in range(2): - server.addResponse("sessionlog.json", request_header, response_header) - return server - - def _make_ats(self): - return Test.MakeATSProcess(f"ts{self._num}", enable_cache=False) - - def _configure(self, ts): - # Records and remap shared by both sides. Subclasses override to add the - # side-specific mark/flag records and load their plugin, calling super() - # for these. - ts.Disk.records_config.update( - { - 'proxy.config.url_remap.remap_required': 0, - # Keep ATS running as the invoking user inside sudo (no privilege drop). - 'proxy.config.admin.user_id': '#-1', - }) - ts.Disk.remap_config.AddLine(f"map / http://127.0.0.1:{self._server.Variables.Port}") - - def _add_case(self, echo_header: str, description: str, set_header: str): - # The mark is set to the supplied value, regardless of the seeded - # starting mark. The set is driven by whichever request header the plugin - # keys on; the observed mark is echoed into echo_header. The origin server - # and ATS are started before the first case, independent of the order in - # which cases are added. - tr = Test.AddTestRun(description) - tr.Processes.Default.StartBefore(self._server) - tr.Processes.Default.StartBefore(self._ts) - if not self._started: - tr.StillRunningAfter = self._server - tr.StillRunningAfter = self._ts - self._started = True - tr.MakeCurlCommand( - f'--verbose --ipv4 --header "{set_header}: 0x{self.SET_MARK:08x}" http://localhost:{self._ts.Variables.port}/', - ts=self._ts) - tr.Processes.Default.ReturnCode = 0 - tr.Processes.Default.Streams.All += Testers.ContainsExpression( - f"{echo_header}: 0x{self.SET_MARK:08x}", f"Observed packet mark should be 0x{self.SET_MARK:08x}") - - -class ClientPacketMarkTest(PacketMarkTest): - """Exercise TSHttpTxnClientPacketMarkSet. The client mark is seeded on the - inbound socket. - """ - - ECHO_HEADER = "X-Client-Packet-Mark" - - def _configure(self, ts): - super()._configure(ts) - ts.Disk.records_config.update( - { - 'proxy.config.net.sock_packet_mark_in': self.SEED_MARK, - 'proxy.config.net.sock_option_flag_in': SOCK_OPT_FLAG_PACKET_MARK, - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'http|client_packet_mark', - }) - Test.PrepareTestPlugin(os.path.join(Test.Variables.AtsTestPluginsDir, f'client_packet_mark.so'), ts) - - def run(self): - self._add_case(self.ECHO_HEADER, "client_packet_mark sets the client-side mark on the live connection", "X-Set-Mark") - - -class ServerPacketMarkTest(PacketMarkTest): - """Exercise TSHttpTxnServerPacketMarkSet. The server mark is seeded on the - outbound socket. - - The server API additionally records the mark for a *future* origin - connection (TSHttpTxnConfigIntSet on TS_CONFIG_NET_SOCK_PACKET_MARK_OUT), - which the client API has no equivalent of. The server plugin exposes this by - honoring X-Set-Mark-Preconnect at READ_REQUEST_HDR, before any origin - connection exists -- so the mark can only reach the socket via that seed. - """ - - ECHO_HEADER = "X-Server-Packet-Mark" - - def _configure(self, ts): - super()._configure(ts) - ts.Disk.records_config.update( - { - 'proxy.config.net.sock_packet_mark_out': self.SEED_MARK, - 'proxy.config.net.sock_option_flag_out': SOCK_OPT_FLAG_PACKET_MARK, - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'http|server_packet_mark', - }) - Test.PrepareTestPlugin(os.path.join(Test.Variables.AtsTestPluginsDir, f'server_packet_mark.so'), ts) - - def run(self): - self._add_case(self.ECHO_HEADER, "server_packet_mark sets the server-side mark on the live connection", "X-Set-Mark") - self._add_case( - self.ECHO_HEADER, "server_packet_mark seeds the mark for a future origin connection", "X-Set-Mark-Preconnect") - - -ClientPacketMarkTest().run() -ServerPacketMarkTest().run() diff --git a/tests/gold_tests/pluginTest/per_plugin_metrics/per_plugin_metrics.test.py b/tests/gold_tests/pluginTest/per_plugin_metrics/per_plugin_metrics.test.py deleted file mode 100644 index 202412e6219..00000000000 --- a/tests/gold_tests/pluginTest/per_plugin_metrics/per_plugin_metrics.test.py +++ /dev/null @@ -1,111 +0,0 @@ -''' -Verify the per-plugin workload counters proxy.process.plugin..{invocations,bytes,transfers}. -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import re - -Test.Summary = ''' -Verify the per-plugin workload counters proxy.process.plugin..invocations (global and remap -dispatch), .bytes and .transfers (PluginVC intercept transport). -''' - -Test.SkipUnless( - Condition.PluginExists('header_rewrite.so'), Condition.PluginExists('conf_remap.so'), Condition.PluginExists('generator.so')) - -Test.ContinueOnFail = True - - -class TestPerPluginWorkloadCounters: - - def __init__(self): - self.setUpOriginServer() - self.setUpTS() - - def setUpOriginServer(self): - self.server = Test.MakeOriginServer("server") - request_header = {"headers": "GET / HTTP/1.1\r\nHost: test.example\r\n\r\n", "timestamp": "1469733493.993", "body": ""} - response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} - self.server.addResponse("sessionfile.log", request_header, response_header) - - def setUpTS(self): - self.ts = Test.MakeATSProcess("ts") - - # header_rewrite (global) and conf_remap (remap) exercise the two invocation-counter sites; - # generator serves its body through a PluginVC intercept, exercising the bytes and transfers counters. - self.ts.Setup.CopyAs('rules/global.conf', Test.RunDirectory) - self.ts.Disk.plugin_config.AddLine(f'header_rewrite.so {Test.RunDirectory}/global.conf') - self.ts.Disk.remap_config.AddLine( - f'map http://test.example http://127.0.0.1:{self.server.Variables.Port} ' - f'@plugin=conf_remap.so @pparam=proxy.config.url_remap.pristine_host_hdr=1') - self.ts.Disk.remap_config.AddLine('map http://gen.example http://127.0.0.1/ @plugin=generator.so') - - self.ts.Disk.records_config.update({ - 'proxy.config.diags.debug.enabled': 0, - 'proxy.config.diags.debug.tags': 'plugin', - }) - - def driveTraffic(self): - # curl as a forward proxy sends an absolute-URI request so the named remap rule (conf_remap) - # matches; the 200 confirms it was served rather than 404'd. - tr = Test.AddTestRun("Drive traffic through global + remap plugins") - tr.Processes.Default.StartBefore(self.server) - tr.Processes.Default.StartBefore(self.ts) - tr.MakeCurlCommand(f'-s -D - -o /dev/null --proxy 127.0.0.1:{self.ts.Variables.port} "http://test.example/"', ts=self.ts) - tr.Processes.Default.ReturnCode = 0 - tr.Processes.Default.Streams.stdout = Testers.ContainsExpression( - '200 OK', 'request should match the remap rule and be served') - tr.StillRunningAfter = self.server - tr.StillRunningAfter = self.ts - - # Drive a generator request: it serves a 4096-byte body through a PluginVC intercept. - tr = Test.AddTestRun("Drive traffic through a PluginVC intercept plugin") - tr.MakeCurlCommand( - f'-s -o /dev/null --proxy 127.0.0.1:{self.ts.Variables.port} "http://gen.example/nocache/4096"', ts=self.ts) - tr.Processes.Default.ReturnCode = 0 - tr.StillRunningAfter = self.ts - tr.StillRunningAfter = self.server - - def checkMetric(self, description, metric, message): - tr = Test.AddTestRun(description) - tr.Processes.Default.Env = self.ts.Env - tr.Processes.Default.Command = f'traffic_ctl metric get {metric}' - tr.Processes.Default.ReturnCode = 0 - tr.Processes.Default.Streams.stdout = Testers.ContainsExpression(f'{re.escape(metric)} [1-9]', message) - tr.StillRunningAfter = self.ts - tr.StillRunningAfter = self.server - - def checkMetrics(self): - self.checkMetric( - "Global plugin invocation counter is non-zero", 'proxy.process.plugin.header_rewrite.invocations', - 'global header_rewrite invocations should be counted') - self.checkMetric( - "Remap plugin invocation counter is non-zero", 'proxy.process.plugin.conf_remap.invocations', - 'remap conf_remap invocations should be counted') - self.checkMetric( - "PluginVC intercept bytes counter is non-zero", 'proxy.process.plugin.generator.bytes', - 'generator PluginVC transport bytes should be counted') - self.checkMetric( - "PluginVC intercept transfers counter is non-zero", 'proxy.process.plugin.generator.transfers', - 'generator PluginVC transfer events should be counted') - - def run(self): - self.driveTraffic() - self.checkMetrics() - - -TestPerPluginWorkloadCounters().run() diff --git a/tests/gold_tests/pluginTest/plugin_yaml/plugin_yaml.test.py b/tests/gold_tests/pluginTest/plugin_yaml/plugin_yaml.test.py deleted file mode 100644 index 34623850ff3..00000000000 --- a/tests/gold_tests/pluginTest/plugin_yaml/plugin_yaml.test.py +++ /dev/null @@ -1,90 +0,0 @@ -''' -Test plugin.yaml loading with inline config and enabled/disabled plugins. -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -Test.Summary = ''' -Test that plugin.yaml is loaded instead of plugin.config, that inline config -works with header_rewrite.so, and that enabled: false skips a plugin. -''' - -Test.ContinueOnFail = True - -server = Test.MakeOriginServer("server") - -request_header = {"headers": "GET /test HTTP/1.1\r\nHost: example.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -server.addResponse("sessionlog.json", request_header, response_header) - -ts = Test.MakeATSProcess("ts", enable_cache=False) - -ts.Disk.records_config.update( - { - 'proxy.config.url_remap.remap_required': 0, - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'header_rewrite|plugin', - }) - -ts.Disk.remap_config.AddLine("map http://example.com http://127.0.0.1:{0}".format(server.Variables.Port)) - -# Write plugin.yaml into the config directory. ATS will prefer this over -# plugin.config. Uses inline config (scalar literal) with header_rewrite.so -# to set a custom response header. xdebug.so is listed but disabled. -ts.Disk.MakeConfigFile("plugin.yaml").update( - { - "plugins": - [ - { - "path": "header_rewrite.so", - "config": "cond %{SEND_RESPONSE_HDR_HOOK}\n set-header X-Plugin-YAML \"loaded-from-inline\"\n", - }, - { - "path": "xdebug.so", - "enabled": False, - "params": ["--enable=x-cache"], - }, - ] - }) - -# Test 1: Verify header_rewrite loaded via plugin.yaml sets the response header. -tr = Test.AddTestRun("Verify inline config sets response header") -tr.Processes.Default.StartBefore(server, ready=When.PortOpen(server.Variables.Port)) -tr.Processes.Default.StartBefore(ts) -tr.MakeCurlCommand('-s -D- -o /dev/null -H "Host: example.com" http://127.0.0.1:{0}/test'.format(ts.Variables.port), ts=ts) -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Streams.stdout = Testers.ContainsExpression( - "X-Plugin-YAML: loaded-from-inline", "Response should contain X-Plugin-YAML header set by inline config") -tr.StillRunningAfter = server -tr.StillRunningAfter = ts - -# Test 2: Verify xdebug is NOT loaded (enabled: false). Send the X-Debug -# header and confirm the X-Cache header is absent from the response. -tr = Test.AddTestRun("Verify disabled plugin is not loaded") -tr.MakeCurlCommand( - '-s -D- -o /dev/null -H "Host: example.com" -H "X-Debug: x-cache" http://127.0.0.1:{0}/test'.format(ts.Variables.port), ts=ts) -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Streams.stdout = Testers.ExcludesExpression( - "X-Cache:", "Response should NOT contain X-Cache header since xdebug is disabled") -tr.StillRunningAfter = server -tr.StillRunningAfter = ts - -# Test 3: Verify the diags.log shows plugin.yaml was used. -tr = Test.AddTestRun("Verify plugin.yaml loading logged") -tr.Processes.Default.Command = "echo check diags.log" -tr.Processes.Default.ReturnCode = 0 -ts.Disk.diags_log.Content += Testers.ContainsExpression("plugin.yaml loading", "diags.log should indicate plugin.yaml was loaded") -ts.Disk.diags_log.Content += Testers.ContainsExpression("skipped", "diags.log should indicate a plugin was skipped") diff --git a/tests/gold_tests/pluginTest/polite_hook_wait/polite_hook_wait.test.py b/tests/gold_tests/pluginTest/polite_hook_wait/polite_hook_wait.test.py deleted file mode 100644 index c43ce842fdd..00000000000 --- a/tests/gold_tests/pluginTest/polite_hook_wait/polite_hook_wait.test.py +++ /dev/null @@ -1,71 +0,0 @@ -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import os - -Test.Summary = ''' -Test spawning a thread in a transaction hook continuation, and getting a result from it, without blocking event task. -''' - -plugin_name = "polite_hook_wait" - -server = Test.MakeOriginServer("server") - -request_header = {"headers": "GET / HTTP/1.1\r\nHost: doesnotmatter\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": "112233"} -server.addResponse("sessionlog.json", request_header, response_header) - -# Disable the cache to make sure each request is forwarded to the origin -# server. -ts = Test.MakeATSProcess("ts", enable_cache=False, block_for_debug=False) - -ts.Disk.records_config.update( - { - 'proxy.config.proxy_name': 'Poxy_Proxy', # This will be the server name. - 'proxy.config.url_remap.remap_required': 1, - 'proxy.config.diags.debug.enabled': 3, - 'proxy.config.diags.debug.tags': f'{plugin_name}', - }) - -rp = os.path.join(Test.Variables.AtsBuildGoldTestsDir, 'pluginTest', 'polite_hook_wait', '.libs', f'{plugin_name}.so') -ts.Setup.Copy(rp, ts.Env['PROXY_CONFIG_PLUGIN_PLUGIN_DIR']) - -ts.Disk.plugin_config.AddLine(f"{plugin_name}.so") - -ts.Disk.remap_config.AddLine("map http://myhost.test http://127.0.0.1:{0}".format(server.Variables.Port)) - -ipv4flag = "" -if not Condition.CurlUsingUnixDomainSocket(): - ipv4flag = "--ipv4" - -tr = Test.AddTestRun() -tr.Processes.Default.StartBefore(server) -tr.Processes.Default.StartBefore(ts) -tr.MakeCurlCommand( - '--verbose {0} --header "Host:myhost.test" http://localhost:{1}/ 2>curl.txt'.format(ipv4flag, ts.Variables.port), ts=ts) -tr.Processes.Default.ReturnCode = 0 - -tr = Test.AddTestRun() -tr.MakeCurlCommand( - '--verbose {0} --header "Host:myhost.test" http://localhost:{1}/ 2>curl.txt'.format(ipv4flag, ts.Variables.port), ts=ts) -tr.Processes.Default.ReturnCode = 0 - -tr = Test.AddTestRun() -# Later versions of curl add a "using HTTP" line to the output, so we filter it -# out to keep this test compatible with old and new versions. -tr.Processes.Default.Command = "grep -F HTTP/ curl.txt | grep -v 'using HTTP'" -tr.Processes.Default.Streams.stdout = "curl.gold" -tr.Processes.Default.ReturnCode = 0 diff --git a/tests/gold_tests/pluginTest/prefetch/prefetch_bad_count_refused.test.py b/tests/gold_tests/pluginTest/prefetch/prefetch_bad_count_refused.test.py deleted file mode 100644 index 421e07e1c5a..00000000000 --- a/tests/gold_tests/pluginTest/prefetch/prefetch_bad_count_refused.test.py +++ /dev/null @@ -1,53 +0,0 @@ -''' -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -Test.Summary = ''' -Test that prefetch.so treats a --fetch-count that does not fit in an unsigned as a configuration -error: ATS refuses to load the remap rule (and fails to start) instead of truncating the value. - -The value below is a string of decimal digits, so it passes a digits-only check, but it exceeds the -range of the unsigned the plugin stores it in. -''' - -ts = Test.MakeATSProcess("ts") -ts.Disk.records_config.update({ - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'prefetch', -}) -ts.Disk.remap_config.AddLine( - "map http://domain.in http://127.0.0.1:8080" + " @plugin=prefetch.so" + " @pparam=--front=true" + - " @pparam=--fetch-policy=simple" + " @pparam=--fetch-count=5000000000") - -ts.ReturnCode = 33 # Emergency exit: remap.config failed to load. -ts.Ready = 0 -# ATS is expected to log the rejection; this ContainsExpression both asserts it and replaces the -# default "diags.log must not contain ERROR:" check (the rejection is logged via TSError). -ts.Disk.diags_log.Content = Testers.ContainsExpression( - "invalid --fetch-count '5000000000'", "an out-of-range fetch-count must be rejected at config load") - -tr = Test.AddTestRun("prefetch rejects an out-of-range fetch-count at load") -# Wait for the rejection message with a separate watcher: gating ts readiness on the log line directly -# can race the process exiting before autest observes the line. -watcher = Test.Processes.Process("watcher") -watcher.Command = "sleep 30" -watcher.Ready = When.FileContains(ts.Disk.diags_log.Name, "invalid --fetch-count '5000000000'") -watcher.StartBefore(ts) - -tr.Processes.Default.Command = "echo done" -tr.TimeOut = 30 -tr.Processes.Default.StartBefore(watcher) diff --git a/tests/gold_tests/pluginTest/prefetch/prefetch_bad_pattern_refused.test.py b/tests/gold_tests/pluginTest/prefetch/prefetch_bad_pattern_refused.test.py deleted file mode 100644 index 01ddbfd1a2e..00000000000 --- a/tests/gold_tests/pluginTest/prefetch/prefetch_bad_pattern_refused.test.py +++ /dev/null @@ -1,54 +0,0 @@ -''' -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -Test.Summary = ''' -Test that prefetch.so treats an unusable fetch-path-pattern as a configuration error: ATS refuses to -load the remap rule (and fails to start) instead of silently running with prefetch disabled. - -The pattern below defines 10 capture groups. The plugin's ovector (OVECOUNT = TOKENCOUNT*3) can hold -offsets for the whole match plus at most TOKENCOUNT-1 (9) groups, so the pattern is rejected at -config-load time; that fails the remap instance, and remap.config fails to load. -''' - -ts = Test.MakeATSProcess("ts") -ts.Disk.records_config.update({ - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'prefetch', -}) -ts.Disk.remap_config.AddLine( - "map http://domain.in http://127.0.0.1:8080" + " @plugin=prefetch.so" + " @pparam=--front=true" + - " @pparam=--fetch-policy=simple" + r" @pparam=--fetch-path-pattern=/(a)(b)(c)(d)(e)(f)(g)(h)(i)(j)/$1/") - -ts.ReturnCode = 33 # Emergency exit: remap.config failed to load. -ts.Ready = 0 -# ATS is expected to log the rejection; this ContainsExpression both asserts it and replaces the -# default "diags.log must not contain ERROR:" check (the rejection is logged via TSError). -ts.Disk.diags_log.Content = Testers.ContainsExpression( - "defines 10 capture groups", "over-limit fetch-path-pattern must be rejected at config load") - -tr = Test.AddTestRun("prefetch rejects an over-limit capture-group pattern at load") -# Wait for the rejection message with a separate watcher: gating ts readiness on the log line directly -# can race the process exiting before autest observes the line. -watcher = Test.Processes.Process("watcher") -watcher.Command = "sleep 30" -watcher.Ready = When.FileContains(ts.Disk.diags_log.Name, "defines 10 capture groups") -watcher.StartBefore(ts) - -tr.Processes.Default.Command = "echo done" -tr.TimeOut = 30 -tr.Processes.Default.StartBefore(watcher) diff --git a/tests/gold_tests/pluginTest/prefetch/prefetch_bignum.test.py b/tests/gold_tests/pluginTest/prefetch/prefetch_bignum.test.py deleted file mode 100644 index bd4cd2bf742..00000000000 --- a/tests/gold_tests/pluginTest/prefetch/prefetch_bignum.test.py +++ /dev/null @@ -1,79 +0,0 @@ -''' -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -Test.Summary = ''' -Test prefetch.so plugin (simple overflow64 mode). -''' - -server = Test.MakeOriginServer("server") -vals = [ - "3842948374928374982374982374", "3842948374928374982374982375", "3842948374928374982374982376", "3842948374928374982374982377" -] -for i in vals: - request_header = { - "headers": - f"GET /texts/demo-{i}.txt HTTP/1.1\r\n" - "Host: does.not.matter\r\n" # But cannot be omitted. - "\r\n", - "timestamp": "1469733493.993", - "body": "" - } - response_header = { - "headers": "HTTP/1.1 200 OK\r\n" - "Connection: close\r\n" - "Cache-control: max-age=85000\r\n" - "\r\n", - "timestamp": "1469733493.993", - "body": f"This is the body for demo-{i}.txt.\n" - } - server.addResponse("sessionlog.json", request_header, response_header) - -dns = Test.MakeDNServer("dns") - -ts = Test.MakeATSProcess("ts") -ts.Disk.records_config.update( - { - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'http|dns|prefetch', - 'proxy.config.dns.nameservers': f"127.0.0.1:{dns.Variables.Port}", - 'proxy.config.dns.resolv_conf': "NULL", - }) -ts.Disk.remap_config.AddLine( - f"map http://domain.in http://127.0.0.1:{server.Variables.Port}" + " @plugin=cachekey.so @pparam=--remove-all-params=true" - " @plugin=prefetch.so" + " @pparam=--front=true" + " @pparam=--fetch-policy=simple" + - r" @pparam=--fetch-path-pattern=/(.*-)(\d+)(.*)/$1{$2+1}$3/" + " @pparam=--fetch-count=3" + " @pparam=--fetch-overflow=bignum") -ts.ReturnCode = Any(0, -2) - -tr = Test.AddTestRun() -tr.Processes.Default.StartBefore(server) -tr.Processes.Default.StartBefore(dns) -tr.Processes.Default.StartBefore(ts) -tr.Processes.Default.Command = 'echo start TS, HTTP server and DNS.' -tr.Processes.Default.ReturnCode = 0 - -tr = Test.AddTestRun() -tr.MakeCurlCommand( - f'--verbose --proxy 127.0.0.1:{ts.Variables.port} http://domain.in/texts/demo-3842948374928374982374982374.txt', ts=ts) -tr.Processes.Default.ReturnCode = 0 - -Test.AddAwaitFileContainsTestRun('Await transactions to finish logging.', ts.Disk.traffic_out.Name, '3842948374928374982374982377') - -tr = Test.AddTestRun() -tr.Processes.Default.Command = (f"grep 'GET http://' {ts.Disk.traffic_out.Name} | grep -v '127.0.0.1' ") -tr.Streams.stdout = "prefetch_bignum.gold" -tr.Processes.Default.ReturnCode = 0 diff --git a/tests/gold_tests/pluginTest/prefetch/prefetch_cmcd.test.py b/tests/gold_tests/pluginTest/prefetch/prefetch_cmcd.test.py deleted file mode 100644 index 40dde89f739..00000000000 --- a/tests/gold_tests/pluginTest/prefetch/prefetch_cmcd.test.py +++ /dev/null @@ -1,290 +0,0 @@ -''' -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import os -import urllib.parse - -Test.Summary = ''' -Test prefetch.so plugin (simple mode). -''' - -origin = Test.MakeOriginServer("origin") - -asset_name = 'request.txt' -pf_name = 'prefetch.txt' -pf_header = f'Cmcd-Request: foo=12,nor="{pf_name}",bar=42' - -request_header = { - "headers": - f"GET /tests/{asset_name} HTTP/1.1\r\n" - "Host: does.not.matter\r\n" # But cant be omitted - f"{pf_header}\r\n" - "\r\n", - "timestamp": "1469733493.993", - "body": "" -} -response_header = { - "headers": "HTTP/1.1 200 OK\r\n" - "Connection: close\r\n" - "Cache-control: max-age=60\r\n" - "\r\n", - "timestamp": "1469733493.993", - "body": f"This is the body for {asset_name}\n" -} -origin.addResponse("sessionlog.json", request_header, response_header) - -# query string -query_name = 'query?this=foo&that' -query_pf_name = 'query?bar=baz' -query_pf_header = f'Cmcd-Request: nor="{query_pf_name}"' - -# nor field may be percent encoded -query_pf_perc_name = urllib.parse.quote(query_pf_name) -query_pf_perc_header = f'Cmcd-Request: nor="{query_pf_perc_name}"' - -request_header = { - "headers": - f"GET /tests/{query_name} HTTP/1.1\r\n" - "Host: does.not.matter\r\n" # But cant be omitted - f"{query_pf_perc_header}\r\n" - "\r\n", - "timestamp": "1469733493.993", - "body": "" -} -response_header = { - "headers": "HTTP/1.1 200 OK\r\n" - "Connection: close\r\n" - "Cache-control: max-age=60\r\n" - "\r\n", - "timestamp": "1469733493.993", - "body": f"This is the body for {query_name}\n" -} -origin.addResponse("sessionlog.json", request_header, response_header) - -# setup the prefetched assets -names = [pf_name, query_pf_name] - -for name in names: - request_header = { - "headers": - f"GET /tests/{name} HTTP/1.1\r\n" - "Host: does.not.matter\r\n" # But cant be omitted - "\r\n", - "timestamp": "1469733493.993", - "body": "" - } - response_header = { - "headers": "HTTP/1.1 200 OK\r\n" - "Connection: close\r\n" - "Cache-control: max-age=60\r\n" - "\r\n", - "timestamp": "1469733493.993", - "body": f"This is the body for {name}\n" - } - origin.addResponse("sessionlog.json", request_header, response_header) - -# prefetch from root -root_name = 'root.txt' -root_header = f'Cmcd-Request: nor="rooted"' - -request_header = { - "headers": - f"GET /{root_name} HTTP/1.1\r\n" - "Host: does.not.matter\r\n" # But cant be omitted - f"{root_header}\r\n" - "\r\n", - "timestamp": "1469733493.993", - "body": "" -} -response_header = { - "headers": "HTTP/1.1 200 OK\r\n" - "Connection: close\r\n" - "Cache-control: max-age=60\r\n" - "\r\n", - "timestamp": "1469733493.993", - "body": f"This is the body for {root_name}\n" -} -origin.addResponse("sessionlog.json", request_header, response_header) - -request_header = { - "headers": - f"GET /rooted HTTP/1.1\r\n" - "Host: does.not.matter\r\n" # But cant be omitted - "\r\n", - "timestamp": "1469733493.993", - "body": "" -} -response_header = { - "headers": "HTTP/1.1 200 OK\r\n" - "Connection: close\r\n" - "Cache-control: max-age=60\r\n" - "\r\n", - "timestamp": "1469733493.993", - "body": f"This is the body for rooted\n" -} -origin.addResponse("sessionlog.json", request_header, response_header) - -# ignore if cmcd-request nrr= found -crr_name = 'crr.txt' -crr_header = f'Cmcd-Request: foo=12,nor="{crr_name}",bar=42,nrr="0-"' -request_header = { - "headers": - f"GET /tests/{crr_name} HTTP/1.1\r\n" - "Host: does.not.matter\r\n" # But cant be omitted - f"{crr_header}\r\n" - "\r\n", - "timestamp": "1469733493.993", - "body": "" -} -response_header = { - "headers": "HTTP/1.1 200 OK\r\n" - "Connection: close\r\n" - "Cache-control: max-age=60\r\n" - "\r\n", - "timestamp": "1469733493.993", - "body": f"This is the body for {crr_name}\n" -} -origin.addResponse("sessionlog.json", request_header, response_header) - -# allows for multiple ats on localhost -dns = Test.MakeDNServer("dns") - -# next hop trafficserver instance -ts1 = Test.MakeATSProcess("ts1", enable_uds=False) -ts1.Disk.records_config.update( - { - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'prefetch|http', - 'proxy.config.dns.nameservers': f"127.0.0.1:{dns.Variables.Port}", - 'proxy.config.dns.resolv_conf': "NULL", - 'proxy.config.http.parent_proxy.self_detect': 0, - }) -dns.addRecords(records={f"ts1": ["127.0.0.1"]}) -ts1.Disk.remap_config.AddLine( - f"map / http://127.0.0.1:{origin.Variables.Port}" + " @plugin=cachekey.so @pparam==--sort-params=true" - " @plugin=prefetch.so @pparam==--front=false") - -ts1.Disk.logging_yaml.AddLines( - ''' -logging: - formats: - - name: custom - format: '% % % % % %<{X-CDN-Prefetch}cqh>' - logs: - - filename: transaction - format: custom -'''.split("\n")) - -ts0 = Test.MakeATSProcess("ts0") -ts0.Disk.records_config.update( - { - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'prefetch|http', - 'proxy.config.dns.nameservers': f"127.0.0.1:{dns.Variables.Port}", - 'proxy.config.dns.resolv_conf': "NULL", - 'proxy.config.http.parent_proxy.self_detect': 0, - }) - -dns.addRecords(records={f"ts0": ["127.0.0.1"]}) -ts0.Disk.remap_config.AddLine( - f"map http://ts0 http://ts1:{ts1.Variables.port}" + " @plugin=cachekey.so @pparam=--sort-params=true" - " @plugin=prefetch.so" + " @pparam=--front=true" + " @pparam=--fetch-policy=simple" + " @pparam=--cmcd-nor=true") - -ts0.Disk.logging_yaml.AddLines( - ''' -logging: - formats: - - name: custom - format: '% % % % % %<{X-CDN-Prefetch}cqh>' - logs: - - filename: transaction - format: custom -'''.split("\n")) - -# start everything up -tr = Test.AddTestRun() -tr.Processes.Default.StartBefore(origin) -tr.Processes.Default.StartBefore(dns) -tr.Processes.Default.StartBefore(ts0) -tr.Processes.Default.StartBefore(ts1) -tr.Processes.Default.Command = 'echo start TS, TSH_N, HTTP origin and DNS.' -tr.Processes.Default.ReturnCode = 0 - -# attempt to get normal asset -tr = Test.AddTestRun() -tr.MakeCurlCommand(f"--verbose --proxy 127.0.0.1:{ts0.Variables.port} http://ts0/tests/{asset_name}", ts=ts0) -tr.Processes.Default.ReturnCode = 0 - -# issue curl form same asset, with prefetch -tr = Test.AddTestRun() -tr.DelayStart = 1 -tr.MakeCurlCommand(f"--verbose --proxy 127.0.0.1:{ts0.Variables.port} http://ts0/tests/{asset_name} -H \'{pf_header}\'", ts=ts0) -tr.Processes.Default.ReturnCode = 0 - -# fetch the prefetched asset (only cached on ts1) -tr = Test.AddTestRun() -tr.DelayStart = 1 -tr.MakeCurlCommand(f"--verbose --proxy 127.0.0.1:{ts0.Variables.port} http://ts0/tests/{pf_name}", ts=ts0) -tr.Processes.Default.ReturnCode = 0 - -# attempt to prefetch again -tr = Test.AddTestRun() -tr.MakeCurlCommand(f"--verbose --proxy 127.0.0.1:{ts0.Variables.port} http://ts0/tests/{asset_name} -H \'{pf_header}\'", ts=ts0) -tr.Processes.Default.ReturnCode = 0 - -# request the prefetched asset -tr = Test.AddTestRun() -tr.MakeCurlCommand(f"--verbose --proxy 127.0.0.1:{ts0.Variables.port} http://ts0/tests/{pf_name}", ts=ts0) -tr.Processes.Default.ReturnCode = 0 - -# prefetch using query params with query prefetch perc encoded -tr = Test.AddTestRun() -tr.MakeCurlCommand( - f"--verbose --proxy 127.0.0.1:{ts0.Variables.port} \'http://ts0/tests/{query_name}\' -H \'{query_pf_perc_header}\'", ts=ts0) -tr.Processes.Default.ReturnCode = 0 - -# request the prefetched asset without perc encoding -tr = Test.AddTestRun() -tr.MakeCurlCommand(f"--verbose --proxy 127.0.0.1:{ts0.Variables.port} \'http://ts0/tests/{query_pf_name}\'", ts=ts0) -tr.Processes.Default.ReturnCode = 0 - -# ensure root path prefetch works -tr = Test.AddTestRun() -tr.MakeCurlCommand(f"--verbose --proxy 127.0.0.1:{ts0.Variables.port} \'http://ts0/{root_name}\' -H \'{root_header}\'", ts=ts0) -tr.Processes.Default.ReturnCode = 0 - -# ensure request with nrr= field is skipped -tr = Test.AddTestRun() -tr.MakeCurlCommand(f"--verbose --proxy 127.0.0.1:{ts0.Variables.port} \'http://ts0/{crr_name}\' -H \'{crr_header}\'", ts=ts0) -tr.Processes.Default.ReturnCode = 0 - -ts0log = os.path.join(ts0.Variables.LOGDIR, 'transaction.log') -Test.AddAwaitFileContainsTestRun('Await ts transactions to finish logging.', ts0log, 'crr.txt') - -ts1log = os.path.join(ts1.Variables.LOGDIR, 'transaction.log') -Test.AddAwaitFileContainsTestRun('Await ts1 transactions to finish logging.', ts1log, 'crr.txt') - -tr = Test.AddTestRun() -tr.Processes.Default.Command = (f"cat {ts0log}") -tr.Streams.stdout = "prefetch_cmcd0.gold" -tr.Processes.Default.ReturnCode = 0 - -tr = Test.AddTestRun() -tr.Processes.Default.Command = (f"cat {ts1log}") -tr.Streams.stdout = "prefetch_cmcd1.gold" -tr.Processes.Default.ReturnCode = 0 diff --git a/tests/gold_tests/pluginTest/prefetch/prefetch_empty_replacement.test.py b/tests/gold_tests/pluginTest/prefetch/prefetch_empty_replacement.test.py deleted file mode 100644 index 4556e52b596..00000000000 --- a/tests/gold_tests/pluginTest/prefetch/prefetch_empty_replacement.test.py +++ /dev/null @@ -1,90 +0,0 @@ -''' -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -Test.Summary = ''' -Test prefetch.so does not self-prefetch when a valid pattern's replacement collapses to an empty path. - -The replacement here is just "$3", where group 3 ("(\\?.*)?") is optional. For a query-less request -that group is absent, so the replacement expands to the empty string. An empty expanded path would -otherwise be scheduled with zero length, which BgFetch skips -- leaving the pristine request path and -prefetching the URL itself. The plugin must instead log and stop; the client request is still served. -''' - -server = Test.MakeOriginServer("server") -for i in list(range(1, 1 + 2)): - request_header = { - "headers": - f"GET /texts/demo-{i} HTTP/1.1\r\n" - "Host: does.not.matter\r\n" # But cannot be omitted. - "\r\n", - "timestamp": "1469733493.993", - "body": "" - } - response_header = { - "headers": "HTTP/1.1 200 OK\r\n" - "Connection: close\r\n" - "Cache-control: max-age=85000\r\n" - "\r\n", - "timestamp": "1469733493.993", - "body": f"This is the body for demo-{i}.\n" - } - server.addResponse("sessionlog.json", request_header, response_header) - -dns = Test.MakeDNServer("dns") - -ts = Test.MakeATSProcess("ts") -ts.Disk.records_config.update( - { - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'http|dns|prefetch', - 'proxy.config.dns.nameservers': f"127.0.0.1:{dns.Variables.Port}", - 'proxy.config.dns.resolv_conf': "NULL", - }) -# A valid pattern (3 defined groups, $3 in range) whose replacement is only the optional $3 group. -ts.Disk.remap_config.AddLine( - f"map http://domain.in http://127.0.0.1:{server.Variables.Port}" + " @plugin=cachekey.so @pparam=--remove-all-params=true" - " @plugin=prefetch.so" + " @pparam=--front=true" + " @pparam=--fetch-policy=simple" + - r" @pparam=--fetch-path-pattern=/(.*-)(\d+)(\?.*)?$/$3/" + " @pparam=--fetch-count=3") -ts.ReturnCode = Any(0, -2) - -# The empty replacement must be logged and skipped. This ContainsExpression both asserts the message -# and replaces the default "diags.log must not contain ERROR:" check (it is logged via TSError). -ts.Disk.diags_log.Content = Testers.ContainsExpression( - "produced an empty path", "an empty replacement must be logged and skipped, not self-prefetched") - -tr = Test.AddTestRun() -tr.Processes.Default.StartBefore(server) -tr.Processes.Default.StartBefore(dns) -tr.Processes.Default.StartBefore(ts) -tr.Processes.Default.Command = 'echo start TS, HTTP server and DNS.' -tr.Processes.Default.ReturnCode = 0 - -# The client request is still served normally even though the prefetch is skipped. -tr = Test.AddTestRun() -tr.MakeCurlCommand(f'--verbose --proxy 127.0.0.1:{ts.Variables.port} http://domain.in/texts/demo-1') -tr.Processes.Default.ReturnCode = 0 - -Test.AddAwaitFileContainsTestRun('Await the empty-path skip to be logged.', ts.Disk.diags_log.Name, 'produced an empty path') - -# The self-prefetch that the old code issued would re-fetch the original path. With the fix the loop -# stops before scheduling, so "failed to process the pattern" (the old second-iteration symptom on the -# emptied working path) must never appear. -tr = Test.AddTestRun() -tr.Processes.Default.Command = (f"grep -c 'failed to process the pattern' {ts.Disk.traffic_out.Name} || true") -tr.Streams.stdout = Testers.ContainsExpression("0", "no per-request pattern-processing failure") -tr.Processes.Default.ReturnCode = 0 diff --git a/tests/gold_tests/pluginTest/prefetch/prefetch_optional_group.test.py b/tests/gold_tests/pluginTest/prefetch/prefetch_optional_group.test.py deleted file mode 100644 index 813a6b640ae..00000000000 --- a/tests/gold_tests/pluginTest/prefetch/prefetch_optional_group.test.py +++ /dev/null @@ -1,95 +0,0 @@ -''' -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -Test.Summary = ''' -Test prefetch.so with an optional trailing capture group that does not participate in the match. - -The fetch-path-pattern below ends in an optional "(\\?.*)?" group that only participates when the -subject carries a query string. For a query-less request that group is absent, so pcre_exec() returns -3 -- one past the highest *participating* group -- even though the pattern defines a 3rd group that the -replacement references as $3. The old code compared $3 against that return value and wrongly rejected -it; a non-participating group must instead substitute an empty string rather than fail the whole -replacement (which previously logged "invalid reference in replacement string: $3" and silently -dropped every prefetch). Mirrors the production hls/mvod remap pattern -"/(.*-)(\\.m3u8)(\\?.*)?$/$1-0.mp4$3/". -''' - -server = Test.MakeOriginServer("server") -for i in list(range(1, 1 + 4)): - request_header = { - "headers": - f"GET /texts/demo-{i} HTTP/1.1\r\n" - "Host: does.not.matter\r\n" # But cannot be omitted. - "\r\n", - "timestamp": "1469733493.993", - "body": "" - } - response_header = { - "headers": "HTTP/1.1 200 OK\r\n" - "Connection: close\r\n" - "Cache-control: max-age=85000\r\n" - "\r\n", - "timestamp": "1469733493.993", - "body": f"This is the body for demo-{i}.\n" - } - server.addResponse("sessionlog.json", request_header, response_header) - -dns = Test.MakeDNServer("dns") - -ts = Test.MakeATSProcess("ts") -ts.Disk.records_config.update( - { - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'http|dns|prefetch', - 'proxy.config.dns.nameservers': f"127.0.0.1:{dns.Variables.Port}", - 'proxy.config.dns.resolv_conf': "NULL", - }) -ts.Disk.remap_config.AddLine( - f"map http://domain.in http://127.0.0.1:{server.Variables.Port}" + " @plugin=cachekey.so @pparam=--remove-all-params=true" - " @plugin=prefetch.so" + " @pparam=--front=true" + " @pparam=--fetch-policy=simple" + - r" @pparam=--fetch-path-pattern=/(.*-)(\d+)(\?.*)?$/$1{$2+1}$3/" + " @pparam=--fetch-count=3") -ts.ReturnCode = Any(0, -2) - -# Belt-and-suspenders next to the gold comparison (which is the primary guard): a regression that -# re-introduces a per-request rejection of the non-participating $3 logs an "invalid reference ..." -# error. This pattern is valid (3 defined groups, $3 in range) so the compile-time validator never -# fires either, hence no "invalid reference" text should ever reach the log. -ts.Disk.traffic_out.Content = Testers.ExcludesExpression( - "invalid reference", "optional non-participating group must not fail the replacement") - -tr = Test.AddTestRun() -tr.Processes.Default.StartBefore(server) -tr.Processes.Default.StartBefore(dns) -tr.Processes.Default.StartBefore(ts) -tr.Processes.Default.Command = 'echo start TS, HTTP server and DNS.' -tr.Processes.Default.ReturnCode = 0 - -tr = Test.AddTestRun() -tr.MakeCurlCommand(f'--verbose --proxy 127.0.0.1:{ts.Variables.port} http://domain.in/texts/demo-1') -tr.Processes.Default.ReturnCode = 0 - -# The original request and the three prefetches are logged independently and may finish out of -# order, so wait for every expected URL to be logged before comparing, and sort both sides so the -# comparison does not depend on completion order. -for tag in ['demo-1', 'demo-2', 'demo-3', 'demo-4']: - Test.AddAwaitFileContainsTestRun(f'Await {tag} to be logged.', ts.Disk.traffic_out.Name, tag) - -tr = Test.AddTestRun() -tr.Processes.Default.Command = (f"grep 'GET http://domain.in' {ts.Disk.traffic_out.Name} | sort") -tr.Streams.stdout = "prefetch_optional_group.gold" -tr.Processes.Default.ReturnCode = 0 diff --git a/tests/gold_tests/pluginTest/prefetch/prefetch_overflow.test.py b/tests/gold_tests/pluginTest/prefetch/prefetch_overflow.test.py deleted file mode 100644 index a3c45380230..00000000000 --- a/tests/gold_tests/pluginTest/prefetch/prefetch_overflow.test.py +++ /dev/null @@ -1,75 +0,0 @@ -''' -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -Test.Summary = ''' -Test prefetch.so plugin (simple overflow64 mode). -''' - -server = Test.MakeOriginServer("server") -for i in list(range(3594967639391, 3594967639391 + 3)): - request_header = { - "headers": - f"GET /texts/demo-{i}.txt HTTP/1.1\r\n" - "Host: does.not.matter\r\n" # But cannot be omitted. - "\r\n", - "timestamp": "1469733493.993", - "body": "" - } - response_header = { - "headers": "HTTP/1.1 200 OK\r\n" - "Connection: close\r\n" - "Cache-control: max-age=85000\r\n" - "\r\n", - "timestamp": "1469733493.993", - "body": f"This is the body for demo-{i}.txt.\n" - } - server.addResponse("sessionlog.json", request_header, response_header) - -dns = Test.MakeDNServer("dns") - -ts = Test.MakeATSProcess("ts") -ts.Disk.records_config.update( - { - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'http|dns|prefetch', - 'proxy.config.dns.nameservers': f"127.0.0.1:{dns.Variables.Port}", - 'proxy.config.dns.resolv_conf': "NULL", - }) -ts.Disk.remap_config.AddLine( - f"map http://domain.in http://127.0.0.1:{server.Variables.Port}" + " @plugin=cachekey.so @pparam=--remove-all-params=true" - " @plugin=prefetch.so" + " @pparam=--front=true" + " @pparam=--fetch-policy=simple" + - r" @pparam=--fetch-path-pattern=/(.*-)(\d+)(.*)/$1{$2+1}$3/" + " @pparam=--fetch-count=3" + " @pparam=--fetch-overflow=64") -ts.ReturnCode = Any(0, -2) - -tr = Test.AddTestRun() -tr.Processes.Default.StartBefore(server) -tr.Processes.Default.StartBefore(dns) -tr.Processes.Default.StartBefore(ts) -tr.Processes.Default.Command = 'echo start TS, HTTP server and DNS.' -tr.Processes.Default.ReturnCode = 0 - -tr = Test.AddTestRun() -tr.MakeCurlCommand(f'--verbose --proxy 127.0.0.1:{ts.Variables.port} http://domain.in/texts/demo-3594967639391.txt', ts=ts) -tr.Processes.Default.ReturnCode = 0 - -Test.AddAwaitFileContainsTestRun('Await transactions to finish logging.', ts.Disk.traffic_out.Name, '3594967639394') - -tr = Test.AddTestRun() -tr.Processes.Default.Command = (f"grep 'GET http://' {ts.Disk.traffic_out.Name} | grep -v '127.0.0.1'") -tr.Streams.stdout = "prefetch_overflow.gold" -tr.Processes.Default.ReturnCode = 0 diff --git a/tests/gold_tests/pluginTest/prefetch/prefetch_simple.test.py b/tests/gold_tests/pluginTest/prefetch/prefetch_simple.test.py deleted file mode 100644 index a171c65db7f..00000000000 --- a/tests/gold_tests/pluginTest/prefetch/prefetch_simple.test.py +++ /dev/null @@ -1,75 +0,0 @@ -''' -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -Test.Summary = ''' -Test prefetch.so plugin (simple mode). -''' - -server = Test.MakeOriginServer("server") -for i in list(range(1, 1 + 3)): - request_header = { - "headers": - f"GET /texts/demo-{i}.txt HTTP/1.1\r\n" - "Host: does.not.matter\r\n" # But cannot be omitted. - "\r\n", - "timestamp": "1469733493.993", - "body": "" - } - response_header = { - "headers": "HTTP/1.1 200 OK\r\n" - "Connection: close\r\n" - "Cache-control: max-age=85000\r\n" - "\r\n", - "timestamp": "1469733493.993", - "body": f"This is the body for demo-{i}.txt.\n" - } - server.addResponse("sessionlog.json", request_header, response_header) - -dns = Test.MakeDNServer("dns") - -ts = Test.MakeATSProcess("ts") -ts.Disk.records_config.update( - { - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'http|dns|prefetch', - 'proxy.config.dns.nameservers': f"127.0.0.1:{dns.Variables.Port}", - 'proxy.config.dns.resolv_conf': "NULL", - }) -ts.Disk.remap_config.AddLine( - f"map http://domain.in http://127.0.0.1:{server.Variables.Port}" + " @plugin=cachekey.so @pparam=--remove-all-params=true" - " @plugin=prefetch.so" + " @pparam=--front=true" + " @pparam=--fetch-policy=simple" + - r" @pparam=--fetch-path-pattern=/(.*-)(\d+)(.*)/$1{$2+1}$3/" + " @pparam=--fetch-count=3") -ts.ReturnCode = Any(0, -2) - -tr = Test.AddTestRun() -tr.Processes.Default.StartBefore(server) -tr.Processes.Default.StartBefore(dns) -tr.Processes.Default.StartBefore(ts) -tr.Processes.Default.Command = 'echo start TS, HTTP server and DNS.' -tr.Processes.Default.ReturnCode = 0 - -tr = Test.AddTestRun() -tr.MakeCurlCommand(f'--verbose --proxy 127.0.0.1:{ts.Variables.port} http://domain.in/texts/demo-1.txt', ts=ts) -tr.Processes.Default.ReturnCode = 0 - -Test.AddAwaitFileContainsTestRun('Await transactions to finish logging.', ts.Disk.traffic_out.Name, 'demo-4.txt') - -tr = Test.AddTestRun() -tr.Processes.Default.Command = (f"grep 'GET http://' {ts.Disk.traffic_out.Name} | grep -v '127.0.0.1'") -tr.Streams.stdout = "prefetch_simple.gold" -tr.Processes.Default.ReturnCode = 0 diff --git a/tests/gold_tests/pluginTest/rate_limit/rate_limit.test.py b/tests/gold_tests/pluginTest/rate_limit/rate_limit.test.py deleted file mode 100644 index 573de3e7730..00000000000 --- a/tests/gold_tests/pluginTest/rate_limit/rate_limit.test.py +++ /dev/null @@ -1,132 +0,0 @@ -''' -Test rate_limit plugin: connection limit enforcement, queue drain, and 429 rejection. -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -Test.Summary = ''' -Test rate_limit plugin: concurrent limit enforcement, queue drain, and independent limiters. -''' - -Test.ContinueOnFail = True - -server = Test.MakeOriginServer("server", delay=3) -ts = Test.MakeATSProcess("ts") - -server.addResponse( - "sessionlog.json", { - "headers": "GET /slow HTTP/1.1\r\nHost: limit.example.com\r\n\r\n", - "timestamp": "1469733493.993", - "body": "" - }, { - "headers": "HTTP/1.1 200 OK\r\n" - "Content-Length: 4\r\n" - "Connection: close\r\n\r\n", - "timestamp": "1469733493.993", - "body": "SLOW" - }) - -server.addResponse( - "sessionlog.json", { - "headers": "GET /fast HTTP/1.1\r\nHost: limit.example.com\r\n\r\n", - "timestamp": "1469733493.993", - "body": "" - }, { - "headers": "HTTP/1.1 200 OK\r\n" - "Content-Length: 4\r\n" - "Connection: close\r\n\r\n", - "timestamp": "1469733493.993", - "body": "FAST" - }) - -ts.Disk.records_config.update( - { - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'rate_limit', - 'proxy.config.http.insert_response_via_str': 0, - 'proxy.config.url_remap.remap_required': 1, - }) - -# Rule 1: limit=1, no queue — immediate rejection -ts.Disk.remap_config.AddLine( - f'map http://limit.example.com/ http://127.0.0.1:{server.Variables.Port}/' - f' @plugin=rate_limit.so @pparam=--limit @pparam=1 @pparam=--queue @pparam=0' - f' @pparam=--error @pparam=429 @pparam=--retry @pparam=1') - -# Rule 2: limit=1, queue=5 — queues excess, resumes when slot freed -ts.Disk.remap_config.AddLine( - f'map http://queued.example.com/ http://127.0.0.1:{server.Variables.Port}/' - f' @plugin=rate_limit.so @pparam=--limit @pparam=1 @pparam=--queue @pparam=5' - f' @pparam=--maxage @pparam=10000 @pparam=--error @pparam=429') - -# Rules 3 & 4: two independent limiters (limit=1 each) -ts.Disk.remap_config.AddLine( - f'map http://limit-a.example.com/ http://127.0.0.1:{server.Variables.Port}/' - f' @plugin=rate_limit.so @pparam=--limit @pparam=1 @pparam=--queue @pparam=0' - f' @pparam=--error @pparam=429') - -ts.Disk.remap_config.AddLine( - f'map http://limit-b.example.com/ http://127.0.0.1:{server.Variables.Port}/' - f' @plugin=rate_limit.so @pparam=--limit @pparam=1 @pparam=--queue @pparam=0' - f' @pparam=--error @pparam=429') - -# Test 1: Concurrent rejection — second request gets 429 -tr = Test.AddTestRun("Concurrent requests: second gets 429") -tr.Processes.Default.StartBefore(server) -tr.Processes.Default.StartBefore(ts) -tr.Processes.Default.Command = f'sh {Test.TestDirectory}/concurrent_reject.sh {ts.Variables.port}' -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Streams.stdout.Content = Testers.ContainsExpression( - "fast=429", "Second concurrent request should be rejected with 429") - -# Test 2: Sequential requests both pass -tr2 = Test.AddTestRun("Sequential requests: both get 200") -tr2.Processes.Default.Command = f'sh {Test.TestDirectory}/sequential_pass.sh {ts.Variables.port}' -tr2.Processes.Default.ReturnCode = 0 -tr2.Processes.Default.Streams.stdout.Content = Testers.ContainsExpression("first=200", "First sequential request should pass") -tr2.Processes.Default.Streams.stdout.Content += Testers.ContainsExpression( - "second=200", "Second sequential request should also pass") - -# Test 3: Retry-After header on 429 rejection -tr3 = Test.AddTestRun("429 response includes Retry-After header") -tr3.Processes.Default.Command = f'sh {Test.TestDirectory}/retry_after.sh {ts.Variables.port}' -tr3.Processes.Default.ReturnCode = 0 -tr3.Processes.Default.Streams.stdout.Content = Testers.ContainsExpression( - "Retry-After: 1", "429 response should include Retry-After header") - -# Test 4: Queue drain — exercises the fixed reserve() loop -tr4 = Test.AddTestRun("Queue drain: queued request resumes with 200") -tr4.Processes.Default.Command = f'sh {Test.TestDirectory}/queue_drain.sh {ts.Variables.port}' -tr4.Processes.Default.ReturnCode = 0 -tr4.Processes.Default.Streams.stdout.Content = Testers.ContainsExpression( - "queued=200", "Queued request should eventually succeed after slot freed") - -# Test 5: Independent limiters — saturating one rule doesn't block the other -tr5 = Test.AddTestRun("Independent limiters: rule B passes while rule A is full") -tr5.Processes.Default.Command = f'sh {Test.TestDirectory}/independent_limiters.sh {ts.Variables.port}' -tr5.Processes.Default.ReturnCode = 0 -tr5.Processes.Default.Streams.stdout.Content = Testers.ContainsExpression( - "independent=200", "Request to rule B should pass despite rule A being full") - -# Test 6: Regression for Finding #106 — queue bypass via incorrect reserve() check. -# With the bug, queued requests are resumed without a valid slot reservation, -# allowing all 3 requests to run concurrently (~3s). With the fix, they serialize -# through the single slot (~9s). We check wall time >= 6s as the pass criterion. -tr6 = Test.AddTestRun("Regression #106: queue does not bypass limit") -tr6.Processes.Default.Command = f'sh {Test.TestDirectory}/queue_bypass_regression.sh {ts.Variables.port}' -tr6.Processes.Default.ReturnCode = 0 -tr6.Processes.Default.Streams.stdout.Content = Testers.ContainsExpression( - "timing=correct", "Queued requests must serialize through the limiter, not bypass it") diff --git a/tests/gold_tests/pluginTest/rate_limit/rate_limit_iprep.test.py b/tests/gold_tests/pluginTest/rate_limit/rate_limit_iprep.test.py deleted file mode 100644 index 1cb14c4e432..00000000000 --- a/tests/gold_tests/pluginTest/rate_limit/rate_limit_iprep.test.py +++ /dev/null @@ -1,120 +0,0 @@ -''' -Test rate_limit plugin: IP reputation initialization (Finding #108). - -Validates that ip-rep buckets are properly initialized. The bug used -vector::reserve() instead of resize(), causing UB on indexed writes. -With the fix, ATS starts cleanly and processes TLS connections through -the ip-rep logic without crashing. -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import os - -Test.Summary = ''' -Test rate_limit ip-rep initialization: reserve() vs resize() regression (Finding #108). -''' - -Test.ContinueOnFail = True - -server = Test.MakeOriginServer("server") -ts = Test.MakeATSProcess("ts", enable_tls=True) - -server.addResponse( - "sessionlog.json", { - "headers": "GET /test HTTP/1.1\r\nHost: iprep.example.com\r\n\r\n", - "timestamp": "1469733493.993", - "body": "" - }, { - "headers": "HTTP/1.1 200 OK\r\n" - "Content-Length: 2\r\n" - "Connection: close\r\n\r\n", - "timestamp": "1469733493.993", - "body": "OK" - }) - -ts.addDefaultSSLFiles() - -# Write the rate_limit YAML config with ip-rep enabled -rate_limit_yaml = os.path.join(ts.Variables.CONFIGDIR, 'rate_limit.yaml') -ts.Disk.File( - rate_limit_yaml, typename="ats:config").AddLines( - [ - 'ip-rep:', - ' - name: test-iprep', - ' buckets: 5', - ' size: 10', - ' percentage: 90', - ' max_age: 300', - '', - 'selector:', - ' - sni: iprep.example.com', - ' limit: 100', - ' ip-rep: test-iprep', - '', - ]) - -ts.Disk.records_config.update( - { - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'rate_limit', - 'proxy.config.http.insert_response_via_str': 0, - 'proxy.config.url_remap.remap_required': 0, - 'proxy.config.ssl.server.cert.path': ts.Variables.SSLDir, - 'proxy.config.ssl.server.private_key.path': ts.Variables.SSLDir, - }) - -ts.Disk.ssl_multicert_yaml.AddLines( - """ -ssl_multicert: - - dest_ip: "*" - ssl_cert_name: server.pem - ssl_key_name: server.key -""".split("\n")) - -ts.Disk.remap_config.AddLine(f'map / http://127.0.0.1:{server.Variables.Port}/') - -ts.Disk.plugin_config.AddLine(f'rate_limit.so {rate_limit_yaml}') - -# Test 1: ATS starts with ip-rep config and handles a TLS request. -# With the reserve() bug, this would crash or produce UB on startup. -tr = Test.AddTestRun("IP reputation init: TLS request through ip-rep selector") -tr.Processes.Default.StartBefore(server) -tr.Processes.Default.StartBefore(ts) -tr.Processes.Default.Command = ( - f"curl -sk -o /dev/null -w '%{{http_code}}' " - f"'https://iprep.example.com:{ts.Variables.ssl_port}/test' " - f"--resolve 'iprep.example.com:{ts.Variables.ssl_port}:127.0.0.1'") -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Streams.stdout.Content = Testers.ContainsExpression( - "200", "TLS request through ip-rep selector should succeed") - -# Test 2: Make multiple requests to exercise the ip-rep increment path. -# Each TLS handshake from the same IP increments the reputation counter. -# If buckets were not properly initialized, this triggers the crash. -tr2 = Test.AddTestRun("IP reputation: multiple requests increment counters") -tr2.Processes.Default.Command = ( - f'for i in 1 2 3 4 5; do ' - f' curl -sk -o /dev/null -w "%{{http_code}} " ' - f' "https://iprep.example.com:{ts.Variables.ssl_port}/test" ' - f' --resolve "iprep.example.com:{ts.Variables.ssl_port}:127.0.0.1"; ' - f'done; echo ""') -tr2.Processes.Default.ReturnCode = 0 -tr2.Processes.Default.Streams.stdout.Content = Testers.ExcludesExpression( - "000", "No request should get a connection failure (code 000)") - -# Verify ATS didn't crash -ts.Disk.diags_log.Content = Testers.ExcludesExpression("FATAL", "ATS should not crash with ip-rep enabled") diff --git a/tests/gold_tests/pluginTest/rate_limit/rate_limit_sni.test.py b/tests/gold_tests/pluginTest/rate_limit/rate_limit_sni.test.py deleted file mode 100644 index c531bd94507..00000000000 --- a/tests/gold_tests/pluginTest/rate_limit/rate_limit_sni.test.py +++ /dev/null @@ -1,121 +0,0 @@ -''' -Test rate_limit plugin: SNI queue expiry does not underflow active counter (Finding #109). - -With the bug, when a queued SNI connection expires via max_age, free() is -called on VCONN_CLOSE even though reserve() never succeeded, underflowing -the active counter and crashing on the next reserve() assertion. -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import os - -Test.Summary = ''' -Test rate_limit SNI queue expiry: active counter underflow regression (Finding #109). -''' - -Test.ContinueOnFail = True - -server = Test.MakeOriginServer("server", delay=4) -ts = Test.MakeATSProcess("ts", enable_tls=True) - -server.addResponse( - "sessionlog.json", { - "headers": "GET /slow HTTP/1.1\r\nHost: queue-expiry.example.com\r\n\r\n", - "timestamp": "1469733493.993", - "body": "" - }, { - "headers": "HTTP/1.1 200 OK\r\n" - "Content-Length: 4\r\n" - "Connection: close\r\n\r\n", - "timestamp": "1469733493.993", - "body": "SLOW" - }) - -server.addResponse( - "sessionlog.json", { - "headers": "GET /test HTTP/1.1\r\nHost: queue-expiry.example.com\r\n\r\n", - "timestamp": "1469733493.993", - "body": "" - }, { - "headers": "HTTP/1.1 200 OK\r\n" - "Content-Length: 2\r\n" - "Connection: close\r\n\r\n", - "timestamp": "1469733493.993", - "body": "OK" - }) - -ts.addDefaultSSLFiles() - -# SNI selector with limit=1, queue=5, max_age=1s (1000ms). -# The short max_age causes queued connections to expire quickly. -rate_limit_yaml = os.path.join(ts.Variables.CONFIGDIR, 'rate_limit.yaml') -ts.Disk.File( - rate_limit_yaml, typename="ats:config").AddLines( - [ - 'selector:', - ' - sni: queue-expiry.example.com', - ' limit: 1', - ' queue:', - ' size: 5', - ' max_age: 1', - '', - ]) - -ts.Disk.records_config.update( - { - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'rate_limit', - 'proxy.config.http.insert_response_via_str': 0, - 'proxy.config.url_remap.remap_required': 0, - 'proxy.config.ssl.server.cert.path': ts.Variables.SSLDir, - 'proxy.config.ssl.server.private_key.path': ts.Variables.SSLDir, - }) - -ts.Disk.ssl_multicert_yaml.AddLines( - """ -ssl_multicert: - - dest_ip: "*" - ssl_cert_name: server.pem - ssl_key_name: server.key -""".split("\n")) - -ts.Disk.remap_config.AddLine(f'map / http://127.0.0.1:{server.Variables.Port}/') - -ts.Disk.plugin_config.AddLine(f'rate_limit.so {rate_limit_yaml}') - -RESOLVE = f"--resolve 'queue-expiry.example.com:{ts.Variables.ssl_port}:127.0.0.1'" -BASE_URL = f"https://queue-expiry.example.com:{ts.Variables.ssl_port}" - -# Test: Queue expiry regression. -# First request holds the slot (4s origin delay), second gets queued and -# expires after max_age (1s). Health check after proves ATS didn't crash. -tr = Test.AddTestRun("Queue expiry: ATS survives without active counter underflow") -tr.Processes.Default.StartBefore(server) -tr.Processes.Default.StartBefore(ts) -tr.Processes.Default.Command = ( - f"curl -sk -o /dev/null '{BASE_URL}/slow' {RESOLVE} & " - f"sleep 0.5; " - f"curl -sk -o /dev/null '{BASE_URL}/queued' {RESOLVE} 2>/dev/null; " - f"wait; sleep 0.5; " - f"curl -sk -o /dev/null -w '%{{http_code}}' '{BASE_URL}/test' {RESOLVE}") -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Streams.stdout.Content = Testers.ContainsExpression( - "200", "Health check after queue expiry should succeed (ATS still alive)") - -# Verify ATS didn't crash -ts.Disk.diags_log.Content = Testers.ExcludesExpression("FATAL", "ATS should not crash from active counter underflow") -ts.Disk.diags_log.Content += Testers.ExcludesExpression("ink_release_assert", "No assertion failure from _active underflow") diff --git a/tests/gold_tests/pluginTest/rate_limit/rate_limit_sni_expiry.test.py b/tests/gold_tests/pluginTest/rate_limit/rate_limit_sni_expiry.test.py deleted file mode 100644 index 8ae995bf1c4..00000000000 --- a/tests/gold_tests/pluginTest/rate_limit/rate_limit_sni_expiry.test.py +++ /dev/null @@ -1,87 +0,0 @@ -''' -Regression test for the max_age expiry branch of the rate_limit SNI queue accounting. A -queued connection never reserves a slot, so when the sweep expires it the plugin must -detach it rather than release a slot it never held; otherwise the expiry underflows the -active-slot counter and the next reserve() trips a release assertion, aborting the server. -ATS must survive the expiry. -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import os - -Test.Summary = __doc__ - -Test.SkipUnless(Condition.PluginExists('rate_limit.so')) - - -class RateLimitSniExpiryTest: - """Age a queued connection out via max_age and assert the active-slot counter stays balanced.""" - - def __init__(self) -> None: - tr = Test.AddTestRun('rate_limit SNI queue max_age expiry') - self._configure_trafficserver() - self._configure_client(tr) - - def _configure_trafficserver(self) -> None: - ts = Test.MakeATSProcess('ts', enable_tls=True, enable_cache=False) - self._ts = ts - ts.addDefaultSSLFiles() - for line in ['ssl_multicert:', ' - dest_ip: "*"', ' ssl_cert_name: server.pem', ' ssl_key_name: server.key']: - ts.Disk.ssl_multicert_yaml.AddLine(line) - - # One concurrent handshake for this SNI, a one-deep queue, and a 1s max age so the - # sweep expires the queued connection. Named .config (not .yaml) so autest treats it - # as a plain config file; the plugin parses it as YAML regardless. - ts.Disk.MakeConfigFile('rate_limit.config').AddLines( - [ - 'selector:', - ' - sni: rate.limited.com', - ' limit: 1', - ' queue:', - ' size: 1', - ' max_age: 1', - ]) - ts.Disk.plugin_config.AddLine(f'rate_limit.so {ts.Variables.CONFIGDIR}/rate_limit.config') - - # Disable the freelist / ProxyAllocator so allocation behavior is not a confound; the - # abort under test is a release-assertion, and this keeps the run representative of CI. - ts.Command += ' -f -F' - - ts.Disk.records_config.update( - { - 'proxy.config.ssl.server.cert.path': ts.Variables.SSLDir, - 'proxy.config.ssl.server.private_key.path': ts.Variables.SSLDir, - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'rate_limit', - }) - - # The expiry branch is actually reached... - ts.Disk.traffic_out.Content = Testers.ContainsExpression('too old', 'a queued connection was expired') - # ...and expiring it does not underflow the active-slot counter into the release assertion. - ts.Disk.traffic_out.Content += Testers.ExcludesExpression( - '_active <= _limit|received signal', 'expiring a queued connection must not underflow and abort ATS') - - def _configure_client(self, tr: 'TestRun') -> None: - ts = self._ts - client = os.path.join(Test.TestDirectory, 'rate_limit_sni_expiry_client.sh') - tr.Processes.Default.Command = f'bash {client} 127.0.0.1 {ts.Variables.ssl_port} rate.limited.com' - tr.Processes.Default.ReturnCode = 0 - tr.Processes.Default.StartBefore(ts) - tr.Processes.Default.Streams.stdout = Testers.ContainsExpression('rate_limit-expiry-done', 'the client ran to completion') - - -RateLimitSniExpiryTest() diff --git a/tests/gold_tests/pluginTest/rate_limit/rate_limit_sni_queue.test.py b/tests/gold_tests/pluginTest/rate_limit/rate_limit_sni_queue.test.py deleted file mode 100644 index 59042752620..00000000000 --- a/tests/gold_tests/pluginTest/rate_limit/rate_limit_sni_queue.test.py +++ /dev/null @@ -1,89 +0,0 @@ -''' -Regression test for a queue-accounting balance bug in the rate_limit SNI limiter: a -queued connection never reserves a slot, but its VCONN_CLOSE unconditionally releases -one, so a queued connection that closes underflows the active-slot counter and the next -reserve() trips a release assertion, aborting the server. ATS must survive the queue -churn. -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import os - -Test.Summary = __doc__ - -Test.SkipUnless(Condition.PluginExists('rate_limit.so')) - - -class RateLimitSniQueueTest: - """Churn the rate_limit SNI queue and assert the active-slot counter never underflows.""" - - def __init__(self) -> None: - tr = Test.AddTestRun('rate_limit SNI queue accounting') - self._configure_trafficserver() - self._configure_client(tr) - - def _configure_trafficserver(self) -> None: - ts = Test.MakeATSProcess('ts', enable_tls=True, enable_cache=False) - self._ts = ts - ts.addDefaultSSLFiles() - for line in ['ssl_multicert:', ' - dest_ip: "*"', ' ssl_cert_name: server.pem', ' ssl_key_name: server.key']: - ts.Disk.ssl_multicert_yaml.AddLine(line) - - # One concurrent handshake for this SNI and a queue that admits exactly one more. - # No rate and no max_age -- the sweep's resume path alone drives the scenario, with - # no rate-bucket or expiry timing to confound it. Named .config (not .yaml) so autest - # treats it as a plain config file; the plugin parses it as YAML regardless. - ts.Disk.MakeConfigFile('rate_limit.config').AddLines( - [ - 'selector:', - ' - sni: rate.limited.com', - ' limit: 1', - ' queue:', - ' size: 1', - ]) - ts.Disk.plugin_config.AddLine(f'rate_limit.so {ts.Variables.CONFIGDIR}/rate_limit.config') - - # Disable the freelist / ProxyAllocator so freed objects are really released rather - # than recycled, keeping allocation reuse from masking a stale access. - ts.Command += ' -f -F' - - ts.Disk.records_config.update( - { - 'proxy.config.ssl.server.cert.path': ts.Variables.SSLDir, - 'proxy.config.ssl.server.private_key.path': ts.Variables.SSLDir, - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'rate_limit', - }) - - # The queue path is reached... - ts.Disk.traffic_out.Content = Testers.ContainsExpression('Queueing the VC', 'a connection was queued') - # ...and the active-slot counter never underflows into the release assertion. Match - # both the specific assertion (pins the failure to this bug) and the generic abort. - ts.Disk.traffic_out.Content += Testers.ExcludesExpression( - '_active <= _limit|received signal', 'the active-slot counter must not underflow and abort ATS') - - def _configure_client(self, tr: 'TestRun') -> None: - ts = self._ts - client = os.path.join(Test.TestDirectory, 'rate_limit_sni_queue_client.sh') - tr.Processes.Default.Command = f'bash {client} 127.0.0.1 {ts.Variables.ssl_port} rate.limited.com' - tr.Processes.Default.ReturnCode = 0 - tr.Processes.Default.StartBefore(ts) - tr.Processes.Default.Streams.stdout = Testers.ContainsExpression( - 'rate_limit-queue-crash-done', 'the client ran to completion') - - -RateLimitSniQueueTest() diff --git a/tests/gold_tests/pluginTest/rate_limit/rate_limit_sni_reject.test.py b/tests/gold_tests/pluginTest/rate_limit/rate_limit_sni_reject.test.py deleted file mode 100644 index 976d320deb1..00000000000 --- a/tests/gold_tests/pluginTest/rate_limit/rate_limit_sni_reject.test.py +++ /dev/null @@ -1,83 +0,0 @@ -''' -Exercise the rate_limit SNI limiter's reject path against a TLS listener, so the -consumer-driven SSLNetVConnection teardown frees every rejected handshake VC -cleanly (no use-after-free or crash). -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import os - -Test.Summary = __doc__ - -Test.SkipUnless(Condition.PluginExists('rate_limit.so')) - - -class RateLimitSniRejectTest: - """Drive rate_limit's SNI reject path and assert ATS frees the VCs without a fault.""" - - def __init__(self) -> None: - tr = Test.AddTestRun('rate_limit SNI reject teardown') - self._configure_trafficserver() - self._configure_client(tr) - - def _configure_trafficserver(self) -> None: - ts = Test.MakeATSProcess('ts', enable_tls=True, enable_cache=False) - self._ts = ts - ts.addDefaultSSLFiles() - for line in ['ssl_multicert:', ' - dest_ip: "*"', ' ssl_cert_name: server.pem', ' ssl_key_name: server.key']: - ts.Disk.ssl_multicert_yaml.AddLine(line) - - # One concurrent handshake for this SNI and no queue, so every further concurrent - # handshake is rejected outright (TS_EVENT_ERROR) rather than queued. Named .config - # (not .yaml) so autest treats it as a plain config file; the plugin parses it as - # YAML regardless (YAML::LoadFile). - ts.Disk.MakeConfigFile('rate_limit.config').AddLines([ - 'selector:', - ' - sni: rate.limited.com', - ' limit: 1', - ]) - ts.Disk.plugin_config.AddLine(f'rate_limit.so {ts.Variables.CONFIGDIR}/rate_limit.config') - - # Disable the freelist / ProxyAllocator so a freed SSLNetVConnection is really - # free()'d rather than recycled; a stale-VC access then hits freed memory - # instead of a still-valid recycled object. - ts.Command += ' -f -F' - - ts.Disk.records_config.update( - { - 'proxy.config.ssl.server.cert.path': ts.Variables.SSLDir, - 'proxy.config.ssl.server.private_key.path': ts.Variables.SSLDir, - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'rate_limit', - }) - - # The reject disposition is reached... - ts.Disk.traffic_out.Content = Testers.ContainsExpression('Rejecting connection', 'over-limit handshakes were rejected') - # ...and ATS tears every rejected handshake VC down without a memory-safety fault. - ts.Disk.traffic_out.Content += Testers.ExcludesExpression( - 'use-after-free|attempting free|SEGV|received signal', 'ATS must survive the reject churn') - - def _configure_client(self, tr: 'TestRun') -> None: - ts = self._ts - client = os.path.join(Test.TestDirectory, 'rate_limit_sni_reject_client.sh') - tr.Processes.Default.Command = f'bash {client} 127.0.0.1 {ts.Variables.ssl_port} rate.limited.com' - tr.Processes.Default.ReturnCode = 0 - tr.Processes.Default.StartBefore(ts) - tr.Processes.Default.Streams.stdout = Testers.ContainsExpression('rate_limit-reject-done', 'the client ran to completion') - - -RateLimitSniRejectTest() diff --git a/tests/gold_tests/pluginTest/redirect_limit/redirect_limit.test.py b/tests/gold_tests/pluginTest/redirect_limit/redirect_limit.test.py deleted file mode 100644 index bfa921ec63d..00000000000 --- a/tests/gold_tests/pluginTest/redirect_limit/redirect_limit.test.py +++ /dev/null @@ -1,134 +0,0 @@ -''' -Plugin-initiated redirects must honor proxy.config.http.number_of_redirections. - -A plugin that calls TSHttpTxnRedirectUrlSet on every response hook (the test -plugin redirect_rearm does exactly this) must not be able to follow more -redirects than the configured limit. The redirect counter is shared with the -core redirect follower, so a plugin that re-sets the redirect URL on each hop -is still capped at number_of_redirections. -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import os - -Test.Summary = 'A plugin re-setting the redirect URL each hop must not bypass number_of_redirections' - -Test.ContinueOnFail = True - -server = Test.MakeOriginServer("server") - -# Chain of 5 hops on the same origin server. Use literal 127.0.0.1:{server.port} -# in the Location headers so ATS's redirect follower doesn't have to do DNS. -ORIGIN = "http://127.0.0.1:{0}".format(server.Variables.Port) - -for i in range(1, 5): - server.addResponse( - "sessionlog.json", { - "headers": "GET /r{i} HTTP/1.1\r\nHost: *\r\n\r\n".format(i=i), - "timestamp": "1", - "body": "" - }, { - "headers": "HTTP/1.1 302 Found\r\nLocation: {0}/r{nxt}\r\nContent-Length: 0\r\n\r\n".format(ORIGIN, nxt=i + 1), - "timestamp": "1", - "body": "" - }) - -server.addResponse( - "sessionlog.json", { - "headers": "GET /r5 HTTP/1.1\r\nHost: *\r\n\r\n", - "timestamp": "1", - "body": "" - }, { - "headers": "HTTP/1.1 200 OK\r\nContent-Length: 5\r\n\r\n", - "timestamp": "1", - "body": "final" - }) - -# A short chain (/s1 -> /s2 -> 200) that stays within number_of_redirections=2. -# This is the positive case: a legitimate plugin-initiated redirect within the -# limit must still be followed all the way to the terminal 200. -server.addResponse( - "sessionlog.json", { - "headers": "GET /s1 HTTP/1.1\r\nHost: *\r\n\r\n", - "timestamp": "1", - "body": "" - }, { - "headers": "HTTP/1.1 302 Found\r\nLocation: {0}/s2\r\nContent-Length: 0\r\n\r\n".format(ORIGIN), - "timestamp": "1", - "body": "" - }) -server.addResponse( - "sessionlog.json", { - "headers": "GET /s2 HTTP/1.1\r\nHost: *\r\n\r\n", - "timestamp": "1", - "body": "" - }, { - "headers": "HTTP/1.1 200 OK\r\nContent-Length: 10\r\n\r\n", - "timestamp": "1", - "body": "shortfinal" - }) - -ts = Test.MakeATSProcess("ts", enable_cache=False) - -ts.Disk.records_config.update( - { - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'redirect_rearm|http_redirect|http', - 'proxy.config.http.number_of_redirections': 2, - 'proxy.config.http.redirect.actions': 'self:follow,private:follow', - }) - -Test.PrepareTestPlugin(os.path.join(Test.Variables.AtsTestPluginsDir, 'redirect_rearm.so'), ts) - -ts.Disk.remap_config.AddLine('map http://127.0.0.1:{0}/ http://127.0.0.1:{0}/'.format(server.Variables.Port)) - -tr = Test.AddTestRun() -tr.MakeCurlCommand( - '-sS -i -x 127.0.0.1:TSPORT http://127.0.0.1:OPORT/r1'.replace('TSPORT', str(ts.Variables.port)).replace( - 'OPORT', str(server.Variables.Port)), - ts=ts) -tr.Processes.Default.StartBefore(ts) -tr.Processes.Default.StartBefore(server) -tr.Processes.Default.ReturnCode = 0 -# With the fix the limit fires and the client sees a 302 returned (the last -# followed hop's response). Without the fix the plugin re-arms the counter on -# every hop and the client sees the final 200 with body "final". -tr.Processes.Default.Streams.stdout = Testers.ContainsExpression( - "HTTP/1.1 302 ", "Client's terminal response must be a 302 from the limit firing, not the final 200") -# Pin the boundary: with number_of_redirections=2 the follower advances r1 -> r2 -# -> r3 and returns r3's response, whose Location points at /r4. Asserting the -# terminal Location is /r4 proves exactly two hops were followed, so this case -# cannot pass if a regression instead followed zero hops (Location /r2) or the -# whole chain. -tr.Processes.Default.Streams.stdout += Testers.ContainsExpression( - "[Ll]ocation: .*/r4", "Terminal 302 must be the third hop's response, proving exactly two redirects were followed") -tr.Processes.Default.Streams.stdout += Testers.ExcludesExpression( - "final", "Client must NOT receive the final body (limit bypassed)") - -# Positive case: a redirect chain within the limit (one hop) must still be -# followed to completion. Guards against the fix over-correcting and refusing -# legitimate plugin-initiated redirects. -tr = Test.AddTestRun() -tr.MakeCurlCommand( - '-sS -i -x 127.0.0.1:TSPORT http://127.0.0.1:OPORT/s1'.replace('TSPORT', str(ts.Variables.port)).replace( - 'OPORT', str(server.Variables.Port)), - ts=ts) -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Streams.stdout = Testers.ContainsExpression( - "HTTP/1.1 200 ", "A within-limit plugin-initiated redirect must reach the terminal 200") -tr.Processes.Default.Streams.stdout += Testers.ContainsExpression( - "shortfinal", "Client must receive the terminal body for a within-limit redirect") diff --git a/tests/gold_tests/pluginTest/regex_remap/regex_remap.test.py b/tests/gold_tests/pluginTest/regex_remap/regex_remap.test.py deleted file mode 100644 index 91d4cb7ab3a..00000000000 --- a/tests/gold_tests/pluginTest/regex_remap/regex_remap.test.py +++ /dev/null @@ -1,249 +0,0 @@ -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import os -import json - -Test.Summary = ''' -Test regex_remap -''' - -# Test description: -# Exercise regex_remap rule matching, redirects, pristine-URL mapping, and the -# regex match limit. Then verify that two map rules naming the same rule file -# share one compiled rule set, and that rewriting that file and reloading -# remap.config compiles a new shared generation instead of reusing the live one. -# No rule here uses a $n / $h substitution, so that path is not covered. - -Test.SkipUnless(Condition.PluginExists('regex_remap.so'),) -Test.ContinueOnFail = False - -# configure origin server -server = Test.MakeOriginServer("server", lookup_key="{%uuid}") -server.addSessionFromFiles("replay") -replay = {} -with open(os.path.join(Test.TestDirectory, 'replay/yts-2819.replay.json')) as src: - replay = json.load(src) - -replay_txns = replay["sessions"][0]["transactions"] - -nameserver = Test.MakeDNServer("dns", default='127.0.0.1') - -# Define ATS and configure -ts = Test.MakeATSProcess("ts", enable_cache=False) - -testName = "regex_remap" - -regex_remap_conf_path = os.path.join(ts.Variables.CONFIGDIR, 'regex_remap.conf') -regex_remap2_conf_path = os.path.join(ts.Variables.CONFIGDIR, 'regex_remap2.conf') -curl_and_args = '-s -D - -v --proxy localhost:{} '.format(ts.Variables.port) - -regex_remap_lines = [ - "# regex_remap configuration\n", - "^/alpha/bravo/[?]((?!action=(newsfeed|calendar|contacts|notepad)).)*$ https://redirect.com/ @status=301\n", - "^/match_limit/(a+)+$ https://redirect.com/ @status=301\n", -] - -ts.Disk.File(regex_remap_conf_path, typename="ats:config").AddLines(regex_remap_lines) - -ts.Disk.File( - regex_remap2_conf_path, typename="ats:config").AddLines( - [ - "# 2nd regex_remap configuration\n" - "^/alpha/bravo/[?]((?!action=(newsfeed|calendar|contacts|notepad)).)*$ " + f"http://localhost:{server.Variables.Port}\n" - ]) - -ts.Disk.remap_config.AddLine( - "map http://example.one/ http://localhost:{}/ @plugin=regex_remap.so @pparam=regex_remap.conf\n".format(server.Variables.Port)) -ts.Disk.remap_config.AddLine( - "map http://example.two/ http://localhost:{}/ ".format(server.Variables.Port) + - "@plugin=regex_remap.so @pparam=regex_remap.conf @pparam=pristine\n") -ts.Disk.remap_config.AddLine( - "map http://example.three/ http://wrong.com/ ".format(server.Variables.Port) + - "@plugin=regex_remap.so @pparam=regex_remap2.conf @pparam=pristine\n") - -# The cache assertions below depend on regex_remap remaining in the debug tags. -ts.Disk.records_config.update( - { - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'http|regex_remap', - 'proxy.config.dns.nameservers': f"127.0.0.1:{nameserver.Variables.Port}", - 'proxy.config.dns.resolv_conf': 'NULL' - }) - -# 0 Test - Load cache (miss) (path1) -tr = Test.AddTestRun("smoke test") -tr.Processes.Default.StartBefore(server) -tr.Processes.Default.StartBefore(nameserver) -tr.Processes.Default.StartBefore(Test.Processes.ts) -creq = replay_txns[0]['client-request'] -tr.MakeCurlCommand(curl_and_args + '--header "uuid: {}" '.format(creq["headers"]["fields"][1][1]) + creq["url"], ts=ts) -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Streams.stdout = "gold/regex_remap_smoke.gold" -tr.StillRunningAfter = ts - -# 1 Test - Match and redirect -tr = Test.AddTestRun("pristine test") -tr.MakeCurlCommand( - curl_and_args + "'http://example.two/alpha/bravo/?action=newsfed;param0001=00003E;param0002=00004E;param0003=00005E'" + - f" | grep -e '^HTTP/' -e '^Location' | sed 's/{server.Variables.Port}/SERVER_PORT/'", - ts=ts) -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Streams.stdout = "gold/regex_remap_redirect.gold" -tr.StillRunningAfter = ts - -# 2 Test - Match and remap -tr = Test.AddTestRun("2nd pristine test") -tr.MakeCurlCommand( - curl_and_args + '--header "uuid: {}" '.format(creq["headers"]["fields"][1][1]) + - " 'http://example.three/alpha/bravo/?action=newsfed;param0001=00003E;param0002=00004E;param0003=00005E'" + - " | grep -e '^HTTP/' -e '^Content-Length'", - ts=ts) -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Streams.stdout = "gold/regex_remap_simple.gold" -tr.StillRunningAfter = ts - -# 3 Test - Match limit test 0 -tr = Test.AddTestRun("match limit 0") -creq = replay_txns[1]['client-request'] -tr.MakeCurlCommand(curl_and_args + \ - '--header "uuid: {}" '.format(creq["headers"]["fields"][1][1]) + '"{}"'.format(creq["url"]), ts=ts) -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Streams.stdout = "gold/regex_remap_crash.gold" -ts.Disk.diags_log.Content = Testers.ContainsExpression( - 'ERROR: .regex_remap. Bad regular expression result -47', "Match limit exceeded") -tr.StillRunningAfter = ts - -# 4 Test - Match limit test 1 -tr = Test.AddTestRun("match limit 1") -creq = replay_txns[2]['client-request'] -tr.MakeCurlCommand(curl_and_args + \ - '--header "uuid: {}" '.format(creq["headers"]["fields"][1][1]) + '"{}"'.format(creq["url"]), ts=ts) -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Streams.stdout = "gold/regex_remap_crash.gold" -ts.Disk.diags_log.Content = Testers.ContainsExpression( - 'ERROR: .regex_remap. Bad regular expression result -47', "Match limit exceeded") -tr.StillRunningAfter = ts - - -class TestRegexRemapRuleCache: - '''Verify shared compiled rules across a remap.config reload.''' - - updated_rule = "^/cache-generation$ https://updated.example/ @status=302\n" - - def __init__(self, ts_process: 'Process', original_rules: str, curl_args: str): - '''Configure the cache and reload TestRuns.''' - self._ts = ts_process - self._original_rules = original_rules - self._curl_args = curl_args - self._regex_remap_path = os.path.join(ts_process.Variables.CONFIGDIR, 'regex_remap.conf') - self._remap_path = os.path.join(ts_process.Variables.CONFIGDIR, 'remap.config') - - self._add_rule_update_run() - self._add_reload_run() - self._add_new_generation_run() - self._add_shared_generation_run() - self._add_isolated_generation_run() - self._add_cache_verification_run() - - def _update_rules(self) -> None: - '''Write a new rule generation and mark remap.config as changed.''' - with open(self._regex_remap_path, 'w') as config_file: - config_file.write(self.updated_rule + self._original_rules) - os.utime(self._remap_path) - - def _add_rule_update_run(self) -> 'TestRun': - '''Change the shared rule file while its first generation is live.''' - tr = Test.AddTestRun("change shared regex_remap rules") - tr.Processes.Default.Command = "echo 'Updating shared regex_remap rules'" - tr.Processes.Default.Setup.Lambda(self._update_rules) - tr.Processes.Default.ReturnCode = 0 - tr.StillRunningAfter = self._ts - return tr - - def _add_reload_run(self) -> 'TestRun': - '''Reload remap.config after the shared rule file changes.''' - tr = Test.AddConfigReload(self._ts, expect_tasks=["remap.config"], description="Reload changed shared regex_remap rules") - tr.StillRunningAfter = self._ts - return tr - - def _add_new_generation_run(self) -> 'TestRun': - '''Verify the new rule generation is active after the reload.''' - tr = Test.AddTestRun("new shared rule generation") - tr.MakeCurlCommand(self._curl_args + "'http://example.one/cache-generation' | grep -e '^HTTP/' -e '^Location'", ts=self._ts) - tr.Processes.Default.ReturnCode = 0 - tr.Processes.Default.Streams.stdout = Testers.ContainsExpression("HTTP/1.1 302", "New rule returns a redirect") - tr.Processes.Default.Streams.stdout += Testers.ContainsExpression( - "Location: https://updated.example/", "New rule generation is active") - tr.StillRunningAfter = self._ts - return tr - - def _add_shared_generation_run(self) -> 'TestRun': - '''Verify the other mapping on this file sees the same generation.''' - tr = Test.AddTestRun("second mapping sees same rule generation") - tr.MakeCurlCommand(self._curl_args + "'http://example.two/cache-generation' | grep -e '^HTTP/' -e '^Location'", ts=self._ts) - tr.Processes.Default.ReturnCode = 0 - tr.Processes.Default.Streams.stdout = Testers.ContainsExpression("HTTP/1.1 302", "Sharing mapping returns a redirect") - tr.Processes.Default.Streams.stdout += Testers.ContainsExpression( - "Location: https://updated.example/", "Sharing mapping is on the new generation") - tr.StillRunningAfter = self._ts - return tr - - def _add_isolated_generation_run(self) -> 'TestRun': - '''Verify a different rule file does not reuse the changed generation.''' - tr = Test.AddTestRun("different rule file remains isolated") - tr.MakeCurlCommand( - self._curl_args + "'http://example.three/cache-generation' | grep -e '^HTTP/' -e '^Location'", ts=self._ts) - tr.Processes.Default.ReturnCode = 0 - tr.Processes.Default.Streams.stdout = Testers.ExcludesExpression("HTTP/1.1 302", "Different file does not redirect") - tr.Processes.Default.Streams.stdout += Testers.ExcludesExpression( - "Location: https://updated.example/", "Different file does not use the changed generation") - tr.StillRunningAfter = self._ts - return tr - - def _add_cache_verification_run(self) -> 'TestRun': - '''Verify each distinct generation is compiled only once.''' - await_tr = Test.AddAwaitFileContainsTestRun( - "await rule cache debug output", self._ts.Disk.traffic_out.Name, "Reusing cached regular expressions from", 3) - await_tr.StillRunningAfter = self._ts - - # Compiles: regex_remap.conf gen1, regex_remap2.conf gen1, and - # regex_remap.conf gen2 == 3 generations and 6 regular expressions. - # Reuses: example.two shares regex_remap.conf on both loads, while - # example.three reuses regex_remap2.conf across the build-then-swap - # reload because the previous remap table is still holding it == 3. - tr = Test.AddTestRun("verify compiled rule cache") - tr.Processes.Default.Command = ( - f"log={self._ts.Disk.traffic_out.Name}; " - "cached=$$(grep -c 'Cached regular expressions from' $$log); " - "reused=$$(grep -c 'Reusing cached regular expressions from' $$log); " - "compiled=$$(grep -c 'Compiling regex:' $$log); " - "cached_primary=$$(grep 'Cached regular expressions from' $$log | grep -F -c '/regex_remap.conf'); " - "cached_secondary=$$(grep 'Cached regular expressions from' $$log | grep -F -c '/regex_remap2.conf'); " - "reused_primary=$$(grep 'Reusing cached regular expressions from' $$log | grep -F -c '/regex_remap.conf'); " - "reused_secondary=$$(grep 'Reusing cached regular expressions from' $$log | grep -F -c '/regex_remap2.conf'); " - "echo cached=$$cached reused=$$reused compiled=$$compiled " - "cached_primary=$$cached_primary cached_secondary=$$cached_secondary " - "reused_primary=$$reused_primary reused_secondary=$$reused_secondary; " - "test $$cached -eq 3 -a $$reused -eq 3 -a $$compiled -eq 6 -a " - "$$cached_primary -eq 2 -a $$cached_secondary -eq 1 -a " - "$$reused_primary -eq 2 -a $$reused_secondary -eq 1") - tr.Processes.Default.ReturnCode = 0 - tr.StillRunningAfter = self._ts - return tr - - -TestRegexRemapRuleCache(ts, ''.join(regex_remap_lines), curl_and_args) diff --git a/tests/gold_tests/pluginTest/regex_revalidate/regex_revalidate.test.py b/tests/gold_tests/pluginTest/regex_revalidate/regex_revalidate.test.py deleted file mode 100644 index aac1205ca8a..00000000000 --- a/tests/gold_tests/pluginTest/regex_revalidate/regex_revalidate.test.py +++ /dev/null @@ -1,273 +0,0 @@ -''' -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import os -import time -from jsonrpc import Request - -Test.Summary = ''' -Test a basic regex_revalidate -''' - -# Test description: -# Load up cache, ensure fresh -# Create regex reval rule, config reload: -# ensure item is staled only once. -# Add a new rule, config reload: -# ensure item isn't restaled again, but rule still in effect. -# -# If the rule disappears from regex_revalidate.conf its still loaded!! -# A rule's expiry can't be changed after the fact! - -Test.SkipUnless(Condition.PluginExists('regex_revalidate.so'), Condition.PluginExists('xdebug.so')) -Test.ContinueOnFail = False - -# configure origin server -server = Test.MakeOriginServer("server") - -# Define ATS and configure -ts = Test.MakeATSProcess("ts") - -Test.testName = "regex_revalidate" -Test.Setup.Copy("metrics.sh") - -# default root -request_header_0 = { - "headers": "GET / HTTP/1.1\r\n" + "Host: www.example.com\r\n" + "\r\n", - "timestamp": "1469733493.993", - "body": "", -} - -response_header_0 = { - "headers": "HTTP/1.1 200 OK\r\n" + "Connection: close\r\n" + "Cache-Control: max-age=300\r\n" + "\r\n", - "timestamp": "1469733493.993", - "body": "xxx", -} - -# cache item path1 -request_header_1 = { - "headers": "GET /path1 HTTP/1.1\r\n" + "Host: www.example.com\r\n" + "\r\n", - "timestamp": "1469733493.993", - "body": "" -} -response_header_1 = { - "headers": - "HTTP/1.1 200 OK\r\n" + "Connection: close\r\n" + 'Etag: "path1"\r\n' + "Cache-Control: max-age=600,public\r\n" + "\r\n", - "timestamp": "1469733493.993", - "body": "abc" -} - -# cache item path1a -request_header_2 = { - "headers": "GET /path1a HTTP/1.1\r\n" + "Host: www.example.com\r\n" + "\r\n", - "timestamp": "1469733493.993", - "body": "" -} -response_header_2 = { - "headers": - "HTTP/1.1 200 OK\r\n" + "Connection: close\r\n" + 'Etag: "path1a"\r\n' + "Cache-Control: max-age=600,public\r\n" + "\r\n", - "timestamp": "1469733493.993", - "body": "cde" -} - -# cache item path2a -request_header_3 = { - "headers": "GET /path2a HTTP/1.1\r\n" + "Host: www.example.com\r\n" + "\r\n", - "timestamp": "1469733493.993", - "body": "" -} -response_header_3 = { - "headers": - "HTTP/1.1 200 OK\r\n" + "Connection: close\r\n" + 'Etag: "path2a"\r\n' + "Cache-Control: max-age=900,public\r\n" + "\r\n", - "timestamp": "1469733493.993", - "body": "efg" -} - -server.addResponse("sessionlog.json", request_header_0, response_header_0) -server.addResponse("sessionlog.json", request_header_1, response_header_1) -server.addResponse("sessionlog.json", request_header_2, response_header_2) -server.addResponse("sessionlog.json", request_header_3, response_header_3) - -# Configure ATS server -ts.Disk.plugin_config.AddLine('xdebug.so --enable=x-cache') -ts.Disk.plugin_config.AddLine('regex_revalidate.so -d -c regex_revalidate.conf') - -regex_revalidate_conf_path = os.path.join(ts.Variables.CONFIGDIR, 'regex_revalidate.conf') -curl_and_args = '-s -D - -v -H "x-debug: x-cache" -H "Host: www.example.com"' - -path1_rule = 'path1 {}\n'.format(int(time.time()) + 600) - -# Define first revision for when trafficserver starts -ts.Disk.File( - regex_revalidate_conf_path, typename="ats:config").AddLines([ - "# Empty\n", - ]) - -ts.Disk.remap_config.AddLine('map / http://127.0.0.1:{}'.format(server.Variables.Port)) - -# minimal configuration -ts.Disk.records_config.update( - { - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'regex_revalidate', - 'proxy.config.http.insert_age_in_response': 0, - 'proxy.config.http.response_via_str': 3, - }) - -# 0 Test - Load cache (miss) (path1) -tr = Test.AddTestRun("Cache miss path1") -ps = tr.Processes.Default -ps.StartBefore(server, ready=When.PortOpen(server.Variables.Port)) -ps.StartBefore(Test.Processes.ts) -ps.StartBefore(Test.Processes.ts) -tr.MakeCurlCommand(curl_and_args + ' http://127.0.0.1:{}/path1'.format(ts.Variables.port), ts=ts) -ps.ReturnCode = 0 -ps.Streams.stdout.Content = Testers.ContainsExpression("X-Cache: miss", "expected cache miss response") -tr.StillRunningAfter = ts - -# 1 Test - Load cache (miss) for later test (path1a) -tr = Test.AddTestRun("Cache miss path1a") -ps = tr.Processes.Default -tr.MakeCurlCommand(curl_and_args + ' http://127.0.0.1:{}/path1a'.format(ts.Variables.port), ts=ts) -ps.ReturnCode = 0 -ps.Streams.stdout.Content = Testers.ContainsExpression("X-Cache: miss", "expected cache miss response") -tr.StillRunningAfter = ts - -# 2 Test - Load cache (miss) for later test (path2a) -tr = Test.AddTestRun("Cache miss path2a") -ps = tr.Processes.Default -tr.MakeCurlCommand(curl_and_args + ' http://127.0.0.1:{}/path2a'.format(ts.Variables.port), ts=ts) -ps.ReturnCode = 0 -ps.Streams.stdout.Content = Testers.ContainsExpression("X-Cache: miss", "expected cache miss response") -tr.StillRunningAfter = ts - -# 3 Test - Cache hit path1 -tr = Test.AddTestRun("Cache hit fresh path1") -ps = tr.Processes.Default -tr.MakeCurlCommand(curl_and_args + ' http://127.0.0.1:{}/path1'.format(ts.Variables.port), ts=ts) -ps.ReturnCode = 0 -ps.Streams.stdout.Content = Testers.ContainsExpression("X-Cache: hit-fresh", "expected cache hit fresh response") -tr.StillRunningAfter = ts - -# 4 Stage - Reload new regex_revalidate -tr = Test.AddTestRun("Reload config add path1") -# Need a sufficient delay so that the modification time difference of the new config file versus -# the old is greater than the granularity of the time stamp used. (The config file write -# happens after the delay.) -tr.DelayStart = 1 -tr.Disk.File( - regex_revalidate_conf_path, typename="ats:config").AddLines([ - path1_rule, - ]) -tr.StillRunningAfter = ts -tr.StillRunningAfter = server -tr.AddJsonRPCClientRequest(ts, Request.admin_config_reload()) -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.TimeOut = 5 -tr.TimeOut = 5 - -# 5 Test - Revalidate path1 -tr = Test.AddTestRun("Revalidate stale path1") -ps = tr.Processes.Default -tr.DelayStart = 5 -tr.MakeCurlCommand(curl_and_args + ' http://127.0.0.1:{}/path1'.format(ts.Variables.port), ts=ts) -ps.ReturnCode = 0 -ps.Streams.stdout.Content = Testers.ContainsExpression("X-Cache: hit-stale", "expected cache hit stale response") -tr.StillRunningAfter = ts - -# 6 Test - Cache hit (path1) -tr = Test.AddTestRun("Cache hit fresh path1") -ps = tr.Processes.Default -tr.MakeCurlCommand(curl_and_args + ' http://127.0.0.1:{}/path1'.format(ts.Variables.port), ts=ts) -ps.ReturnCode = 0 -ps.Streams.stdout.Content = Testers.ContainsExpression("X-Cache: hit-fresh", "expected cache hit fresh response") -tr.StillRunningAfter = ts - -# 7 Stage - Reload new regex_revalidate -tr = Test.AddTestRun("Reload config add path2") -# Need a sufficient delay so that the modification time difference of the new config file versus -# the old is greater than the granularity of the time stamp used. (The config file write -# happens after the delay.) -tr.DelayStart = 1 -tr.Disk.File( - regex_revalidate_conf_path, typename="ats:config").AddLines([ - path1_rule, - 'path2 {}\n'.format(int(time.time()) + 700), - ]) -tr.StillRunningAfter = ts -tr.StillRunningAfter = server -tr.AddJsonRPCClientRequest(ts, Request.admin_config_reload()) -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.TimeOut = 5 -tr.TimeOut = 5 - -# 8 Test - Cache hit (path1) -tr = Test.AddTestRun("Cache hit fresh path1") -tr.DelayStart = 5 -ps = tr.Processes.Default -tr.MakeCurlCommand(curl_and_args + ' http://127.0.0.1:{}/path1'.format(ts.Variables.port), ts=ts) -ps.ReturnCode = 0 -ps.Streams.stdout.Content = Testers.ContainsExpression("X-Cache: hit-fresh", "expected cache hit fresh response") -tr.StillRunningAfter = ts - -# 9 Test - Cache stale (check rule is still loaded) (path1a) -tr = Test.AddTestRun("Revalidate stale path1a") -ps = tr.Processes.Default -tr.MakeCurlCommand(curl_and_args + ' http://127.0.0.1:{}/path1a'.format(ts.Variables.port), ts=ts) -ps.ReturnCode = 0 -ps.Streams.stdout.Content = Testers.ContainsExpression("X-Cache: hit-stale", "expected cache hit stale response") -tr.StillRunningAfter = ts - -# The C version of regex_revalidate doesn't allow an existing rule to -# be changed by a reload. - -# 10 Stage - regex_revalidate rewrite rule early expire -tr = Test.AddTestRun("Reload config change path2") -# Need a sufficient delay so that the modification time difference of the new config file versus -# the old is greater than the granularity of the time stamp used. (The config file write -# happens after the delay.) -tr.DelayStart = 1 -tr.Disk.File( - regex_revalidate_conf_path, typename="ats:config").AddLines([ - path1_rule, - 'path2 {}\n'.format(int(time.time()) - 100), - ]) -tr.StillRunningAfter = ts -tr.StillRunningAfter = server -tr.AddJsonRPCClientRequest(ts, Request.admin_config_reload()) -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.TimeOut = 5 -tr.TimeOut = 5 - -# 11 Test - Cache hit (path2a) -tr = Test.AddTestRun("Cache hit stale path2a") -tr.DelayStart = 5 -ps = tr.Processes.Default -tr.MakeCurlCommand(curl_and_args + ' http://127.0.0.1:{}/path2a'.format(ts.Variables.port), ts=ts) -ps.ReturnCode = 0 -ps.Streams.stdout.Content = Testers.ContainsExpression("X-Cache: hit-stale", "expected cache hit stale response") -tr.StillRunningAfter = ts - -# 12 Stats check -tr = Test.AddTestRun("Check stats") -tr.DelayStart = 5 -ps = tr.Processes.Default -ps.Command = "bash -c ./metrics.sh" -ps.Env = ts.Env -ps.ReturnCode = 0 -tr.StillRunningAfter = ts diff --git a/tests/gold_tests/pluginTest/regex_revalidate/regex_revalidate_miss.test.py b/tests/gold_tests/pluginTest/regex_revalidate/regex_revalidate_miss.test.py deleted file mode 100644 index fb998209816..00000000000 --- a/tests/gold_tests/pluginTest/regex_revalidate/regex_revalidate_miss.test.py +++ /dev/null @@ -1,249 +0,0 @@ -''' -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import os -import time -from jsonrpc import Request - -Test.Summary = ''' -regex_revalidate plugin test, MISS (refetch) functionality -''' - -# Test description: -# If MISS tag encountered, should load rule as refetch instead of IMS. -# If rule switched from MISS to IMS or vice versa, rule should reset. - -Test.SkipUnless(Condition.PluginExists('regex_revalidate.so'), Condition.PluginExists('xdebug.so')) -Test.ContinueOnFail = False - -# configure origin server -server = Test.MakeOriginServer("server") - -# Define ATS and configure -ts = Test.MakeATSProcess("ts") - -Test.testName = "regex_revalidate_miss" -Test.Setup.Copy("metrics_miss.sh") - -# default root -request_header_0 = { - "headers": "GET / HTTP/1.1\r\n" + "Host: www.example.com\r\n" + "\r\n", - "timestamp": "1469733493.993", - "body": "", -} - -response_header_0 = { - "headers": "HTTP/1.1 200 OK\r\n" + "Connection: close\r\n" + "Cache-Control: max-age=300\r\n" + "\r\n", - "timestamp": "1469733493.993", - "body": "xxx", -} - -# cache item path1 -request_header_1 = { - "headers": "GET /path1 HTTP/1.1\r\n" + "Host: www.example.com\r\n" + "\r\n", - "timestamp": "1469733493.993", - "body": "" -} -response_header_1 = { - "headers": - "HTTP/1.1 200 OK\r\n" + "Connection: close\r\n" + 'Etag: "path1"\r\n' + "Cache-Control: max-age=600,public\r\n" + "\r\n", - "timestamp": "1469733493.993", - "body": "abc" -} - -server.addResponse("sessionlog.json", request_header_0, response_header_0) -server.addResponse("sessionlog.json", request_header_1, response_header_1) - -# Configure ATS server -ts.Disk.plugin_config.AddLine('xdebug.so --enable=x-cache') -ts.Disk.plugin_config.AddLine('regex_revalidate.so -d -c regex_revalidate.conf -l revalidate.log -m reval') - -regex_revalidate_conf_path = os.path.join(ts.Variables.CONFIGDIR, 'regex_revalidate.conf') -#curl_and_args = '-s -D - -v -H "x-debug: x-cache" -H "Host: www.example.com"' - -path1_rule = 'path1 {}'.format(int(time.time()) + 600) - -# Define first revision for when trafficserver starts -ts.Disk.File(regex_revalidate_conf_path, typename="ats:config").AddLine("# Empty") - -ts.Disk.remap_config.AddLine('map http://ats/ http://127.0.0.1:{}'.format(server.Variables.Port)) - -# minimal configuration -ts.Disk.records_config.update( - { - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'regex_revalidate', - 'proxy.config.http.insert_age_in_response': 0, - 'proxy.config.http.response_via_str': 3, - 'proxy.config.http.cache.http': 1, - 'proxy.config.http.wait_for_cache': 1, - }) - -curl_and_args = '-s -D /dev/stdout -o /dev/stderr -x http://127.0.0.1:{}'.format(ts.Variables.port) + ' -H "x-debug: x-cache"' - -# 0 Request, cache miss expected -tr = Test.AddTestRun("Cache miss path1") -ps = tr.Processes.Default -ps.StartBefore(server, ready=When.PortOpen(server.Variables.Port)) -ps.StartBefore(Test.Processes.ts) -tr.MakeCurlCommand(curl_and_args + ' http://ats/path1', ts=ts) -ps.ReturnCode = 0 -ps.Streams.stdout.Content = Testers.ContainsExpression("X-Cache: miss", "expected cache miss response") -tr.StillRunningAfter = ts - -# 1 Request, cache hit expected -tr = Test.AddTestRun("Cache hit fresh path1") -ps = tr.Processes.Default -tr.MakeCurlCommand(curl_and_args + ' http://ats/path1', ts=ts) -ps.ReturnCode = 0 -ps.Streams.stdout.Content = Testers.ContainsExpression("X-Cache: hit-fresh", "expected cache hit fresh response") -tr.StillRunningAfter = ts - -# 2 Reload, populated config -tr = Test.AddTestRun("Reload config add path1") -ps = tr.Processes.Default -tr.Disk.File(regex_revalidate_conf_path, typename="ats:config").AddLine(path1_rule + ' MISS') -# keep this for debug -tr.Disk.File(regex_revalidate_conf_path + "_tr2", typename="ats:config").AddLine(path1_rule + ' MISS') -tr.StillRunningAfter = ts -tr.StillRunningAfter = server -tr.AddJsonRPCClientRequest(ts, Request.admin_config_reload()) -ps.ReturnCode = 0 -ps.TimeOut = 5 -tr.TimeOut = 5 -# Delay it so the reload can catch up the diff between config files timestamps. -tr.DelayStart = 1 - -# 3 Request, cache miss expected -tr = Test.AddTestRun("Revalidate MISS path1") -ps = tr.Processes.Default -tr.DelayStart = 7 -tr.MakeCurlCommand(curl_and_args + ' http://ats/path1', ts=ts) -ps.ReturnCode = 0 -ps.Streams.stdout.Content = Testers.ContainsExpression("X-Cache: miss", "expected cache miss response") -tr.StillRunningAfter = ts - -# 4 Request, cache hit (path1) -tr = Test.AddTestRun("Cache hit fresh path1") -ps = tr.Processes.Default -tr.MakeCurlCommand(curl_and_args + ' http://ats/path1', ts=ts) -ps.ReturnCode = 0 -ps.Streams.stdout.Content = Testers.ContainsExpression("X-Cache: hit-fresh", "expected cache hit fresh response") -tr.StillRunningAfter = ts - -# 5 Reload, change MISS to STALE (resets rule) -tr = Test.AddTestRun("Reload config path1 STALE") -ps = tr.Processes.Default -tr.Disk.File(regex_revalidate_conf_path, typename="ats:config").AddLine(path1_rule + ' STALE') -tr.Disk.File(regex_revalidate_conf_path + "_tr5", typename="ats:config").AddLine(path1_rule + ' STALE') -tr.StillRunningAfter = ts -tr.StillRunningAfter = server -tr.AddJsonRPCClientRequest(ts, Request.admin_config_reload()) -ps.ReturnCode = 0 -ps.TimeOut = 5 -tr.TimeOut = 5 -# Delay it so the reload can catch up the diff between config files timestamps. -tr.DelayStart = 1 - -# 6 Request, cache stale expected -tr = Test.AddTestRun("Cache stale path1") -ps = tr.Processes.Default -tr.DelayStart = 7 -tr.MakeCurlCommand(curl_and_args + ' http://ats/path1', ts=ts) -ps.ReturnCode = 0 -ps.Streams.stdout.Content = Testers.ContainsExpression("X-Cache: hit-stale", "expected cache hit stale response") -tr.StillRunningAfter = ts - -# 7 Reload, change STALE to MISS (resets rule) -tr = Test.AddTestRun("Reload config path1 MISS") -ps = tr.Processes.Default -tr.Disk.File(regex_revalidate_conf_path, typename="ats:config").AddLine(path1_rule + ' MISS') -tr.Disk.File(regex_revalidate_conf_path + "_tr7", typename="ats:config").AddLine(path1_rule + ' MISS') -tr.StillRunningAfter = ts -tr.StillRunningAfter = server -tr.AddJsonRPCClientRequest(ts, Request.admin_config_reload()) -ps.ReturnCode = 0 -ps.TimeOut = 5 -tr.TimeOut = 5 -# Delay it so the reload can catch up the diff between config files timestamps. -tr.DelayStart = 1 - -# 8 Request, cache miss expected -tr = Test.AddTestRun("Cache mis path1") -ps = tr.Processes.Default -tr.DelayStart = 7 -tr.MakeCurlCommand(curl_and_args + ' http://ats/path1', ts=ts) -ps.ReturnCode = 0 -ps.Streams.stdout.Content = Testers.ContainsExpression("X-Cache: miss", "expected cache miss response") -tr.StillRunningAfter = ts - -# 9 Request, cache hit expected -tr = Test.AddTestRun("Cache hit path1") -ps = tr.Processes.Default -tr.DelayStart = 5 -tr.MakeCurlCommand(curl_and_args + ' http://ats/path1', ts=ts) -ps.ReturnCode = 0 -ps.Streams.stdout.Content = Testers.ContainsExpression("X-Cache: hit-fresh", "expected cache hit response") -tr.StillRunningAfter = ts - -# 10 Reload, no changes -tr = Test.AddTestRun("Reload no changes") -ps = tr.Processes.Default -tr.StillRunningAfter = ts -tr.StillRunningAfter = server -tr.AddJsonRPCClientRequest(ts, Request.admin_config_reload()) -ps.ReturnCode = 0 -ps.TimeOut = 5 -tr.TimeOut = 5 -# Delay it so the reload can catch up the diff between config files timestamps. -tr.DelayStart = 1 - -# 11 Request again, cache hit expected -tr = Test.AddTestRun("Cache hit path1") -ps = tr.Processes.Default -tr.DelayStart = 7 -tr.MakeCurlCommand(curl_and_args + ' http://ats/path1', ts=ts) -ps.ReturnCode = 0 -ps.Streams.stdout.Content = Testers.ContainsExpression("X-Cache: hit-fresh", "expected cache hit response") -tr.StillRunningAfter = ts - -# 12 Stage - Touch the rules file to trigger reload detection -tr = Test.AddTestRun("Touch regex_revalidate config") -tr.Processes.Default.Command = f'touch {regex_revalidate_conf_path}' -tr.Processes.Default.ReturnCode = 0 -tr.StillRunningAfter = ts -tr.StillRunningAfter = server - -tr = Test.AddConfigReload(ts, description="Reload same config") -tr.StillRunningAfter = server - -# 13 Request, Cache hit expected -tr = Test.AddTestRun("Cache hit path1") -ps = tr.Processes.Default -tr.MakeCurlCommand(curl_and_args + ' http://ats/path1', ts=ts) -ps.ReturnCode = 0 -ps.Streams.stdout.Content = Testers.ContainsExpression("X-Cache: hit-fresh", "expected cache hit response") -tr.StillRunningAfter = ts - -# 14 Stats check -tr = Test.AddTestRun("Check stats") -tr.DelayStart = 5 -tr.Processes.Default.Command = f"bash -c ./metrics_miss.sh" -tr.Processes.Default.Env = ts.Env -tr.Processes.Default.ReturnCode = 0 -tr.StillRunningAfter = ts diff --git a/tests/gold_tests/pluginTest/regex_revalidate/regex_revalidate_state.test.py b/tests/gold_tests/pluginTest/regex_revalidate/regex_revalidate_state.test.py deleted file mode 100644 index f71e854b35b..00000000000 --- a/tests/gold_tests/pluginTest/regex_revalidate/regex_revalidate_state.test.py +++ /dev/null @@ -1,139 +0,0 @@ -''' -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import os -import time - -Test.Summary = ''' -regex_revalidate plugin test, reload epoch state on ats start -''' - -# Test description: -# Ensures that that the regex revalidate config file is loaded, -# then epoch times from the state file are properly merged. - -Test.SkipUnless(Condition.PluginExists('regex_revalidate.so')) -Test.ContinueOnFail = False - -# configure origin server -server = Test.MakeOriginServer("server") - -# Define ATS and configure -ts = Test.MakeATSProcess("ts") - -# **testname is required** -testName = "regex_revalidate_state" - -# default root -request_header_0 = { - "headers": "GET / HTTP/1.1\r\n" + "Host: www.example.com\r\n" + "\r\n", - "timestamp": "1469733493.993", - "body": "", -} - -response_header_0 = { - "headers": "HTTP/1.1 200 OK\r\n" + "Connection: close\r\n" + "Cache-Control: max-age=300\r\n" + "\r\n", - "timestamp": "1469733493.993", - "body": "xxx", -} - -server.addResponse("sessionlog.json", request_header_0, response_header_0) - -reval_conf_path = os.path.join(ts.Variables.CONFIGDIR, 'reval.conf') -reval_state_path = os.path.join(ts.Variables.RUNTIMEDIR, 'reval.state') - -# Configure ATS server -ts.Disk.plugin_config.AddLine(f"regex_revalidate.so -d -c reval.conf -l reval.log -f {reval_state_path}") - -sep = ' ' - -# rule with no initial state -path0_regex = "path0" -path0_expiry = str(time.time() + 90).split('.')[0] -path0_type = "STALE" -path0_rule = sep.join([path0_regex, path0_expiry, path0_type]) - -path1_regex = "path1" -path1_epoch = str(time.time() - 50).split('.')[0] -path1_expiry = str(time.time() + 600).split('.')[0] -path1_type = "MISS" -path1_rule = sep.join([path1_regex, path1_expiry, path1_type]) - -# Create gold files -gold_path_good = reval_state_path + ".good" -ts.Disk.File( - gold_path_good, typename="ats:config").AddLines( - [ - sep.join([path0_regex, "``", path0_expiry, path0_type]), - sep.join([path1_regex, path1_epoch, path1_expiry, path1_type]), - ]) - -# It seems there's no API for negative gold file matching -''' -gold_path_bad = reval_state_path + ".bad" -ts.Disk.File(gold_path_bad, typename="ats:config").AddLines([ - sep.join([path0_regex, path1_epoch, path0_expiry, path0_type]), - sep.join([path1_regex, path1_epoch, path1_expiry, path1_type]), -]) -''' - -# Create a state file, second line will be discarded and not merged -ts.Disk.File( - reval_state_path, typename="ats:config").AddLines( - [ - sep.join([path1_regex, path1_epoch, path1_expiry, path1_type]), - sep.join(["dummy", path1_epoch, path1_expiry, path1_type]), - ]) - -# Write out reval.conf file -ts.Disk.File( - reval_conf_path, typename="ats:config").AddLines([ - path0_rule, - path1_rule, - ]) - -ts.chownForATSProcess(reval_state_path) - -ts.Disk.remap_config.AddLine(f"map http://ats/ http://127.0.0.1:{server.Variables.Port}") - -# minimal configuration -ts.Disk.records_config.update( - { - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'regex_revalidate', - 'proxy.config.http.wait_for_cache': 1, - }) - -# This TestRun creates the state file so it exists when the ts process's Setup -# logic is run so that it can be chowned at that point. -tr = Test.AddTestRun("Populate the regex_revalidate state file") -tr.Processes.Default.Command = f'touch {reval_state_path}' - -# Start ATS and evaluate the new state file -tr = Test.AddTestRun("Initial load, state merged") -ps = tr.Processes.Default -ps.StartBefore(server, ready=When.PortOpen(server.Variables.Port)) - -# Note the ready condition: wait for ATS to modify the contents -# of the file from dummy to path1. -ps.StartBefore(Test.Processes.ts, ready=When.FileContains(reval_state_path, "path0")) - -ps.Command = 'cat ' + reval_state_path -ps.ReturnCode = 0 -ps.Streams.stdout.Content = Testers.GoldFile(gold_path_good) -tr.StillRunningAfter = ts diff --git a/tests/gold_tests/pluginTest/remap_stats/remap_stats.test.py b/tests/gold_tests/pluginTest/remap_stats/remap_stats.test.py deleted file mode 100644 index 0827fc49b2c..00000000000 --- a/tests/gold_tests/pluginTest/remap_stats/remap_stats.test.py +++ /dev/null @@ -1,66 +0,0 @@ -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -Test.Summary = ''' -Test remap_stats plugin -''' -# Skip if plugins not present. -Test.SkipUnless(Condition.PluginExists('remap_stats.so')) - -server = Test.MakeOriginServer("server") - -Test.Setup.Copy("metrics.sh") - -request_header = {"headers": "GET /argh HTTP/1.1\r\nHost: one\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -server.addResponse("sessionlog.json", request_header, response_header) - -ts = Test.MakeATSProcess("ts") - -ts.Disk.plugin_config.AddLine('remap_stats.so') - -ts.Disk.remap_config.AddLine("map http://one http://127.0.0.1:{0}".format(server.Variables.Port)) -ts.Disk.remap_config.AddLine("map http://two http://127.0.0.1:{0}".format(server.Variables.Port)) - -ts.Disk.records_config.update( - { - 'proxy.config.http.transaction_active_timeout_out': 2, - 'proxy.config.http.transaction_no_activity_timeout_out': 2, - 'proxy.config.http.connect_attempts_timeout': 2, - }) - -# 0 Test - Curl host One -tr = Test.AddTestRun("curl host one") -tr.Processes.Default.StartBefore(server) -tr.Processes.Default.StartBefore(Test.Processes.ts) -tr.MakeCurlCommand('-o /dev/null -H "Host: one"' + ' http://127.0.0.1:{}/argh'.format(ts.Variables.port), ts=ts) -tr.Processes.Default.ReturnCode = 0 -tr.StillRunningAfter = ts -tr.StillRunningAfter = server - -# 1 Test - Curl host Two -tr = Test.AddTestRun("curl host two") -tr.MakeCurlCommand('-o /dev/null -H "Host: two"' + ' http://127.0.0.1:{}/badpath'.format(ts.Variables.port), ts=ts) -tr.Processes.Default.ReturnCode = 0 -tr.StillRunningAfter = ts -tr.StillRunningAfter = server - -# 2 Test - Gather output -tr = Test.AddTestRun("analyze stats") -tr.Processes.Default.Command = 'bash -c ./metrics.sh' -tr.Processes.Default.Env = ts.Env -tr.Processes.Default.ReturnCode = 0 -tr.StillRunningAfter = ts diff --git a/tests/gold_tests/pluginTest/remap_stats/remap_stats_post.test.py b/tests/gold_tests/pluginTest/remap_stats/remap_stats_post.test.py deleted file mode 100644 index a0fcf1c2b60..00000000000 --- a/tests/gold_tests/pluginTest/remap_stats/remap_stats_post.test.py +++ /dev/null @@ -1,66 +0,0 @@ -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -Test.Summary = ''' -Test remap_stats plugin -''' -# Skip if plugins not present. -Test.SkipUnless(Condition.PluginExists('remap_stats.so')) - -server = Test.MakeOriginServer("server") - -Test.Setup.Copy("metrics_post.sh") - -request_header = {"headers": "GET /argh HTTP/1.1\r\nHost: one\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -server.addResponse("sessionlog.json", request_header, response_header) - -ts = Test.MakeATSProcess("ts") - -ts.Disk.plugin_config.AddLine('remap_stats.so --post-remap-host') - -ts.Disk.remap_config.AddLine("map http://one http://127.0.0.1:{0}".format(server.Variables.Port)) -ts.Disk.remap_config.AddLine("map http://two http://127.0.0.1:{0}".format(server.Variables.Port)) - -ts.Disk.records_config.update( - { - 'proxy.config.http.transaction_active_timeout_out': 2, - 'proxy.config.http.transaction_no_activity_timeout_out': 2, - 'proxy.config.http.connect_attempts_timeout': 2, - }) - -# 0 Test - Curl host One -tr = Test.AddTestRun("curl host one") -tr.Processes.Default.StartBefore(server) -tr.Processes.Default.StartBefore(Test.Processes.ts) -tr.MakeCurlCommand('-o /dev/null -H "Host: one"' + ' http://127.0.0.1:{}/argh'.format(ts.Variables.port), ts=ts) -tr.Processes.Default.ReturnCode = 0 -tr.StillRunningAfter = ts -tr.StillRunningAfter = server - -# 1 Test - Curl host Two -tr = Test.AddTestRun("curl host two") -tr.MakeCurlCommand('-o /dev/null -H "Host: two"' + ' http://127.0.0.1:{}/badpath'.format(ts.Variables.port), ts=ts) -tr.Processes.Default.ReturnCode = 0 -tr.StillRunningAfter = ts -tr.StillRunningAfter = server - -# 2 Test - Gather output -tr = Test.AddTestRun("analyze stats") -tr.Processes.Default.Command = 'bash -c ./metrics_post.sh' -tr.Processes.Default.Env = ts.Env -tr.Processes.Default.ReturnCode = 0 -tr.StillRunningAfter = ts diff --git a/tests/gold_tests/pluginTest/server_push_preload/server_push_preload.test.py b/tests/gold_tests/pluginTest/server_push_preload/server_push_preload.test.py deleted file mode 100644 index 9ca134705db..00000000000 --- a/tests/gold_tests/pluginTest/server_push_preload/server_push_preload.test.py +++ /dev/null @@ -1,104 +0,0 @@ -''' -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -Test.Summary = ''' -Test for server_push_preload plugin -''' - -Test.SkipUnless( - Condition.PluginExists('server_push_preload.so'), - Condition.HasProgram("nghttp", "Nghttp need to be installed on system for this test to work"), -) -Test.testName = "server_push_preload" -Test.ContinueOnFail = True - -# ---- -# Setup Origin Server -# ---- -microserver = Test.MakeOriginServer("microserver") - -# index.html -microserver.addResponse( - "sessionfile.log", { - "headers": "GET /index.html HTTP/1.1\r\nHost: www.example.com\r\n\r\n", - "body": "" - }, { - "headers": - "HTTP/1.1 200 OK\r\nConnection: close\r\nLink: ; rel=preload; as=style; nopush\r\nLink: ; rel=preload; as=script\r\n\r\n", - "body": - "\r\n\r\n\r\n\r\n\r\n\r\nServer Push Preload Test\r\n\r\n\r\n" - }) - -# /app/style.css -microserver.addResponse( - "sessionfile.log", { - "headers": "GET /app/style.css HTTP/1.1\r\nHost: www.example.com\r\n\r\n", - "body": "" - }, { - "headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", - "body": "body { font-weight: bold; }\r\n" - }) - -# /app/script.js -microserver.addResponse( - "sessionfile.log", { - "headers": "GET /app/script.js HTTP/1.1\r\nHost: www.example.com\r\n\r\n", - "body": "" - }, { - "headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", - "body": "function do_nothing() { return; }\r\n" - }) - -# ---- -# Setup ATS -# ---- -ts = Test.MakeATSProcess("ts", enable_tls=True) - -ts.addDefaultSSLFiles() - -ts.Disk.ssl_multicert_yaml.AddLines( - """ -ssl_multicert: - - dest_ip: "*" - ssl_cert_name: server.pem - ssl_key_name: server.key -""".split("\n")) - -ts.Disk.remap_config.AddLine('map / http://127.0.0.1:{0}/ @plugin=server_push_preload.so'.format(microserver.Variables.Port)) - -ts.Disk.records_config.update( - { - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'http2|server_push_preload', - 'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir), - 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts.Variables.SSLDir), - 'proxy.config.http2.active_timeout_in': 3, - }) - -# ---- -# Test Cases -# ---- - -# Test Case 0: Server Push by Link header -tr = Test.AddTestRun() -tr.Processes.Default.Command = "nghttp -vs --no-dep 'https://127.0.0.1:{0}/index.html'".format(ts.Variables.ssl_port) -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.StartBefore(microserver, ready=When.PortOpen(microserver.Variables.Port)) -tr.Processes.Default.StartBefore(Test.Processes.ts) -tr.Processes.Default.Streams.stdout = "gold/server_push_preload_0_stdout.gold" -tr.StillRunningAfter = microserver diff --git a/tests/gold_tests/pluginTest/slice/slice.test.py b/tests/gold_tests/pluginTest/slice/slice.test.py deleted file mode 100644 index 03d0079b818..00000000000 --- a/tests/gold_tests/pluginTest/slice/slice.test.py +++ /dev/null @@ -1,185 +0,0 @@ -''' -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -Test.Summary = ''' -Basic slice plugin test -''' - -# Test description: -# Preload the cache with the entire asset to be range requested. -# Reload remap rule with slice plugin -# Request content through the slice plugin - -Test.SkipUnless(Condition.PluginExists('slice.so'),) -Test.ContinueOnFail = False - -# configure origin server -server = Test.MakeOriginServer("server") - -# Define ATS and configure -ts = Test.MakeATSProcess("ts") - -# default root -request_header_chk = { - "headers": "GET / HTTP/1.1\r\n" + "Host: ats\r\n" + "\r\n", - "timestamp": "1469733493.993", - "body": "", -} - -response_header_chk = { - "headers": "HTTP/1.1 200 OK\r\n" + "Connection: close\r\n" + "\r\n", - "timestamp": "1469733493.993", - "body": "", -} - -server.addResponse("sessionlog.json", request_header_chk, response_header_chk) - -block_bytes = 7 -body = "lets go surfin now" - -request_header = { - "headers": "GET /path HTTP/1.1\r\n" + "Host: origin\r\n" + "\r\n", - "timestamp": "1469733493.993", - "body": "", -} - -response_header = { - "headers": "HTTP/1.1 200 OK\r\n" + "Connection: close\r\n" + 'Etag: "path"\r\n' + "Cache-Control: max-age=500\r\n" + "\r\n", - "timestamp": "1469733493.993", - "body": body, -} - -server.addResponse("sessionlog.json", request_header, response_header) - -curl_and_args = '-s -D /dev/stdout -o /dev/stderr -x http://127.0.0.1:{}'.format(ts.Variables.port) - -# set up whole asset fetch into cache -ts.Disk.remap_config.AddLines( - [ - f'map http://preload/ http://127.0.0.1:{server.Variables.Port}', - f'map http://slice_only/ http://127.0.0.1:{server.Variables.Port}', - f'map http://slice/ http://127.0.0.1:{server.Variables.Port}' + - f' @plugin=slice.so @pparam=--blockbytes-test={block_bytes}', - f'map http://slicehdr/ http://127.0.0.1:{server.Variables.Port}' + - f' @plugin=slice.so @pparam=--blockbytes-test={block_bytes}' + ' @pparam=--skip-header=SkipSlice', - ]) - -ts.Disk.records_config.update({ - 'proxy.config.diags.debug.enabled': 0, - 'proxy.config.diags.debug.tags': 'slice', -}) - -# 0 Test - Prefetch entire asset into cache -tr = Test.AddTestRun("Fetch first slice range") -ps = tr.Processes.Default -ps.StartBefore(server, ready=When.PortOpen(server.Variables.Port)) -ps.StartBefore(Test.Processes.ts) -tr.MakeCurlCommand(curl_and_args + ' http://preload/path', ts=ts) -ps.ReturnCode = 0 -ps.Streams.stderr = "gold/slice_200.stderr.gold" -ps.Streams.stdout.Content = Testers.ContainsExpression("200 OK", "expected 200 OK response") -tr.StillRunningAfter = ts - -# 1 Test - First complete slice -tr = Test.AddTestRun("Fetch first slice range") -ps = tr.Processes.Default -tr.MakeCurlCommand(curl_and_args + ' http://slice/path' + ' -r 0-6', ts=ts) -ps.ReturnCode = 0 -ps.Streams.stderr = "gold/slice_first.stderr.gold" -ps.Streams.stdout.Content = Testers.ContainsExpression("206 Partial Content", "expected 206 response") -ps.Streams.stdout.Content += Testers.ContainsExpression("Content-Range: bytes 0-6/18", "mismatch byte content response") -tr.StillRunningAfter = ts - -# 2 Test - Last slice auto -tr = Test.AddTestRun("Last slice -- 14-") -ps = tr.Processes.Default -tr.MakeCurlCommand(curl_and_args + ' http://slice/path' + ' -r 14-', ts=ts) -ps.ReturnCode = 0 -ps.Streams.stderr = "gold/slice_last.stderr.gold" -ps.Streams.stdout.Content = Testers.ContainsExpression("206 Partial Content", "expected 206 response") -ps.Streams.stdout.Content += Testers.ContainsExpression("Content-Range: bytes 14-17/18", "mismatch byte content response") -tr.StillRunningAfter = ts - -# 3 Test - Last slice exact -tr = Test.AddTestRun("Last slice 14-17") -ps = tr.Processes.Default -tr.MakeCurlCommand(curl_and_args + ' http://slice/path' + ' -r 14-17', ts=ts) -ps.ReturnCode = 0 -ps.Streams.stderr = "gold/slice_last.stderr.gold" -ps.Streams.stdout.Content = Testers.ContainsExpression("206 Partial Content", "expected 206 response") -ps.Streams.stdout.Content += Testers.ContainsExpression("Content-Range: bytes 14-17/18", "mismatch byte content response") -tr.StillRunningAfter = ts - -# 4 Test - Last slice truncated -tr = Test.AddTestRun("Last truncated slice 14-20") -ps = tr.Processes.Default -tr.MakeCurlCommand(curl_and_args + ' http://slice/path' + ' -r 14-20', ts=ts) -ps.ReturnCode = 0 -ps.Streams.stderr = "gold/slice_last.stderr.gold" -ps.Streams.stdout.Content = Testers.ContainsExpression("206 Partial Content", "expected 206 response") -ps.Streams.stdout.Content += Testers.ContainsExpression("Content-Range: bytes 14-17/18", "mismatch byte content response") -tr.StillRunningAfter = ts - -# 5 Test - Whole asset via slices -tr = Test.AddTestRun("Whole asset via slices") -ps = tr.Processes.Default -tr.MakeCurlCommand(curl_and_args + ' http://slice/path', ts=ts) -ps.ReturnCode = 0 -ps.Streams.stderr = "gold/slice_200.stderr.gold" -ps.Streams.stdout.Content = Testers.ContainsExpression("200 OK", "expected 200 OK response") -tr.StillRunningAfter = ts - -# 6 Test - Whole asset via range -tr = Test.AddTestRun("Whole asset via range") -ps = tr.Processes.Default -tr.MakeCurlCommand(curl_and_args + ' http://slice/path' + ' -r 0-', ts=ts) -ps.ReturnCode = 0 -ps.Streams.stderr = "gold/slice_206.stderr.gold" -ps.Streams.stdout.Content = Testers.ContainsExpression("206 Partial Content", "expected 206 response") -ps.Streams.stdout.Content += Testers.ContainsExpression("Content-Range: bytes 0-17/18", "mismatch byte content response") -tr.StillRunningAfter = ts - -# 7 Test - Non aligned slice request -tr = Test.AddTestRun("Non aligned slice request") -ps = tr.Processes.Default -tr.MakeCurlCommand(curl_and_args + ' http://slice/path' + ' -r 5-16', ts=ts) -ps.ReturnCode = 0 -ps.Streams.stderr = "gold/slice_mid.stderr.gold" -ps.Streams.stdout.Content = Testers.ContainsExpression("206 Partial Content", "expected 206 response") -ps.Streams.stdout.Content += Testers.ContainsExpression("Content-Range: bytes 5-16/18", "mismatch byte content response") -tr.StillRunningAfter = ts - -# 8 Test - special case, begin inside last slice block but outside asset len -tr = Test.AddTestRun("Invalid end range request, 416") -beg = len(body) + 1 -end = beg + block_bytes -ps = tr.Processes.Default -tr.MakeCurlCommand(curl_and_args + ' http://slice/path' + ' -r {}-{}'.format(beg, end), ts=ts) -ps.Streams.stdout.Content = Testers.ContainsExpression("416 Requested Range Not Satisfiable", "expected 416 response") -tr.StillRunningAfter = ts - -# 9 Test - First complete slice using override header -# if this fails it will infinite loop -tr = Test.AddTestRun("Fetch first slice range") -ps = tr.Processes.Default -tr.MakeCurlCommand(curl_and_args + ' http://slicehdr/path' + ' -r 0-6', ts=ts) -ps.ReturnCode = 0 -ps.Streams.stderr = "gold/slice_first.stderr.gold" -ps.Streams.stdout.Content = Testers.ContainsExpression("206 Partial Content", "expected 206 response") -ps.Streams.stdout.Content += Testers.ContainsExpression("Content-Range: bytes 0-6/18", "mismatch byte content response") -tr.StillRunningAfter = ts diff --git a/tests/gold_tests/pluginTest/slice/slice_conditional.test.py b/tests/gold_tests/pluginTest/slice/slice_conditional.test.py deleted file mode 100644 index 27f4c0a64f9..00000000000 --- a/tests/gold_tests/pluginTest/slice/slice_conditional.test.py +++ /dev/null @@ -1,156 +0,0 @@ -''' -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -Test.Summary = ''' -Conditional Slicing test -''' - -# Test description: -# Preload the cache with the entire asset to be range requested. -# Reload remap rule with slice plugin -# Request content through the slice plugin - -Test.SkipUnless( - Condition.PluginExists('slice.so'), - Condition.PluginExists('cache_range_requests.so'), - Condition.PluginExists('xdebug.so'), -) -Test.ContinueOnFail = False - -# configure origin server -server = Test.MakeOriginServer("server", lookup_key="{PATH}{%Range}", options={'-v': None}) - -# Define ATS and configure -ts = Test.MakeATSProcess("ts") - -# small object, should not be sliced -req_small = { - "headers": "GET /small HTTP/1.1\r\n" + "Host: www.example.com\r\n" + "\r\n", - "body": "", -} -res_small = { - "headers": "HTTP/1.1 200 OK\r\n" + "Connection: close\r\n" + "Cache-Control: max-age=10,public\r\n" + "\r\n", - "body": "smol", -} -server.addResponse("sessionlog.json", req_small, res_small) - -# large object, all in one slice -req_large = { - "headers": "GET /large HTTP/1.1\r\n" + "Host: www.example.com\r\n" + "\r\n", - "body": "", -} -res_large = { - "headers": "HTTP/1.1 200 OK\r\n" + "Connection: close\r\n" + "Cache-Control: max-age=10,public\r\n" + "\r\n", - "body": "unsliced large object!" -} -server.addResponse("sessionlog.json", req_large, res_large) - -# large object, this populates the individual slices in the server - -large_body = "large object sliced!" -body_len = len(large_body) -slice_begin = 0 -slice_block_size = 10 -while (slice_begin < body_len): - slice_end = slice_begin + slice_block_size - req_large_slice = { - "headers": "GET /large HTTP/1.1\r\n" + "Host: www.example.com\r\n" + f"Range: bytes={slice_begin}-{slice_end - 1}" + "\r\n", - "body": "", - } - if slice_end > body_len: - slice_end = body_len - res_large_slice = { - "headers": - "HTTP/1.1 206 Partial Content\r\n" + "Connection: close\r\n" + "Accept-Ranges: bytes\r\n" + - f"Content-Range: bytes {slice_begin}-{slice_end - 1}/{body_len}\r\n" + "Cache-Control: max-age=10,public\r\n" + "\r\n", - "body": large_body[slice_begin:slice_end] - } - server.addResponse("sessionlog.json", req_large_slice, res_large_slice) - slice_begin += slice_block_size - -# set up slice plugin with remap host into cache_range_requests -ts.Disk.remap_config.AddLines( - [ - f'map http://slice/ http://127.0.0.1:{server.Variables.Port}/' + - f' @plugin=slice.so @pparam=--blockbytes-test={slice_block_size} @pparam=--minimum-size=8 @pparam=--metadata-cache-size=4 @plugin=cache_range_requests.so' - ]) - -ts.Disk.plugin_config.AddLine('xdebug.so --enable=x-cache') -ts.Disk.records_config.update( - { - 'proxy.config.diags.debug.enabled': '0', - 'proxy.config.diags.debug.tags': 'http|cache|slice|xdebug|cache_range_requests', - }) - -curl_and_args = '-s -D /dev/stdout -o /dev/stderr -x localhost:{}'.format(ts.Variables.port) + ' -H "x-debug: x-cache"' - -# Test case: first request of small object -tr = Test.AddTestRun("Small request 1") -ps = tr.Processes.Default -ps.StartBefore(server, ready=When.PortOpen(server.Variables.Port)) -ps.StartBefore(Test.Processes.ts) -tr.MakeCurlCommand(curl_and_args + ' http://slice/small', ts=ts) -ps.ReturnCode = 0 -ps.Streams.stderr.Content = Testers.ContainsExpression('smol', 'expected smol') -ps.Streams.stdout.Content = Testers.ContainsExpression('X-Cache: miss', 'expected cache miss') -tr.StillRunningAfter = ts - -# Test case: second request of small object - expect cache hit -tr = Test.AddTestRun("Small request 2") -ps = tr.Processes.Default -tr.MakeCurlCommand(curl_and_args + ' http://slice/small', ts=ts) -ps.ReturnCode = 0 -ps.Streams.stderr.Content = Testers.ContainsExpression('smol', 'expected smol') -ps.Streams.stdout.Content = Testers.ContainsExpression('X-Cache: hit-fresh', 'expected cache hit-fresh') -tr.StillRunningAfter = ts - -# Test case: range request of small object - expect cache hit (proxy.config.http.cache.range.lookup = 1) -tr = Test.AddTestRun("Small request - ranged") -ps = tr.Processes.Default -tr.MakeCurlCommand(curl_and_args + ' -r 1-2 http://slice/small', ts=ts) -ps.ReturnCode = 0 -ps.Streams.stderr.Content = Testers.ContainsExpression('mo', 'expected mo') -ps.Streams.stdout.Content = Testers.ContainsExpression('X-Cache: hit-fresh', 'expected cache hit-fresh') -tr.StillRunningAfter = ts - -# Test case: first request of large object - expect unsliced, cache write disabled -tr = Test.AddTestRun("Large request 1") -ps = tr.Processes.Default -tr.MakeCurlCommand(curl_and_args + ' http://slice/large', ts=ts) -ps.ReturnCode = 0 -ps.Streams.stderr.Content = Testers.ContainsExpression('unsliced large object!', 'expected large object') -ps.Streams.stdout.Content = Testers.ContainsExpression('X-Cache: miss', 'expected cache miss') -tr.StillRunningAfter = ts - -# Test case: first request of large object - expect sliced, cache miss -tr = Test.AddTestRun("Large request 2") -ps = tr.Processes.Default -tr.MakeCurlCommand(curl_and_args + ' http://slice/large', ts=ts) -ps.ReturnCode = 0 -ps.Streams.stderr.Content = Testers.ContainsExpression('large object sliced!', 'expected large object') -ps.Streams.stdout.Content = Testers.ContainsExpression('X-Cache: miss', 'expected cache miss') -tr.StillRunningAfter = ts - -## Test case: first request of large object - expect cache hit -tr = Test.AddTestRun("Large request 3") -ps = tr.Processes.Default -tr.MakeCurlCommand(curl_and_args + ' http://slice/large', ts=ts) -ps.ReturnCode = 0 -ps.Streams.stderr.Content = Testers.ContainsExpression('large object sliced!', 'expected large object') -ps.Streams.stdout.Content = Testers.ContainsExpression('X-Cache: hit-fresh', 'expected cache hit-fresh') -tr.StillRunningAfter = ts diff --git a/tests/gold_tests/pluginTest/slice/slice_content_shrink.test.py b/tests/gold_tests/pluginTest/slice/slice_content_shrink.test.py deleted file mode 100644 index 14402670122..00000000000 --- a/tests/gold_tests/pluginTest/slice/slice_content_shrink.test.py +++ /dev/null @@ -1,94 +0,0 @@ -''' -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import sys - -from ports import get_port - -Test.Summary = ''' -Slice plugin: verify no integer underflow when content shrinks below block position. - -When origin reports Content-Range with total length smaller than the requested -block position (content shrunk between fetches), the plugin must fail gracefully -rather than underflowing m_blockskip which would corrupt the response. -''' - -Test.SkipUnless(Condition.PluginExists('slice.so'),) -Test.ContinueOnFail = False - -# Define ATS - no cache so every slice sub-request goes to origin -ts = Test.MakeATSProcess("ts", enable_cache=False) - -# Test: Request bytes 14-20 via slice plugin (blockbytes=7) -# Slice will: -# 1. Fetch block 0 (reference): gets CL=21, etag "old" -# 2. Fetch block 2 (interior, skips block 1): gets CL=10, etag "new" (MISMATCH! m_contentlen=10) -# 3. Refetch block 0 (reference): gets CL=10, etag "new" (matches new m_contentlen) -# 4. Enters ActiveRef: blockpos=14, m_contentlen=10 => guard triggers, Fail state -# -# Client should get an error/empty response, NOT corrupted data. - -tr = Test.AddTestRun("Request triggering content shrink underflow guard") - -# Copy and start custom origin server -tr.Setup.CopyAs("shrink_origin.py") - -origin = tr.Processes.Process("origin") -origin_port = get_port(origin, 'http_port') -origin.Command = f'{sys.executable} shrink_origin.py {origin_port}' -origin.Ready = When.PortOpenv4(origin_port) - -# Configure remap to point at our custom origin -ts.Disk.remap_config.AddLines( - [ - f'map http://slice/ http://127.0.0.1:{origin_port}/' - ' @plugin=slice.so @pparam=--blockbytes-test=7', - ]) - -ts.Disk.records_config.update({ - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'slice', -}) - -ps = tr.Processes.Default -ps.StartBefore(origin) -ps.StartBefore(Test.Processes.ts) -ps.Command = ( - f'curl -s -D /dev/stdout -o /dev/stderr' - f' -x localhost:{ts.Variables.port}' - f' http://slice/shrink -r 14-20' - f' -w "\\nSIZE:%{{size_download}}"') -ps.Streams.stdout = Testers.ContainsExpression(r"SIZE:0\b", "expected zero client-visible body size") -ps.Streams.stderr = Testers.ExcludesExpression(r".", "expected no client-visible response body") -tr.StillRunningAfter = ts - -# Test 2: Non-block-aligned range. blockbytes=7, client requests bytes=16-20. -# firstblock=2, blockpos=14. Content shrinks to 15. -# m_contentlen(15) > blockpos(14) would pass a blockpos-only guard, but -# m_contentlen(15) <= m_req_range.m_beg(16) catches it. -tr = Test.AddTestRun("Mid-block range: content shrinks above blockpos but below range start") -ps = tr.Processes.Default -ps.Command = ( - f'curl -s -D /dev/stdout -o /dev/stderr' - f' -x localhost:{ts.Variables.port}' - f' http://slice/shrink_mid -r 16-20' - f' -w "\\nSIZE:%{{size_download}}"') -tr.StillRunningAfter = ts - -# Verify the error was logged (our new guard message) -ts.Disk.diags_log.Content = Testers.ContainsExpression("shrunk below requested range start", "expected underflow guard error log") diff --git a/tests/gold_tests/pluginTest/slice/slice_crr_ident.test.py b/tests/gold_tests/pluginTest/slice/slice_crr_ident.test.py deleted file mode 100644 index db803deb187..00000000000 --- a/tests/gold_tests/pluginTest/slice/slice_crr_ident.test.py +++ /dev/null @@ -1,220 +0,0 @@ -''' -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. -import os - -Test.Summary = ''' -Combined Slice/crr ident test -''' - -# Test description: -# Preload the cache with the entire asset to be range requested. -# Reload remap rule with slice plugin -# Request content through the slice plugin - -Test.SkipUnless( - Condition.PluginExists('cache_range_requests.so'), - Condition.PluginExists('header_rewrite.so'), - Condition.PluginExists('slice.so'), - Condition.PluginExists('xdebug.so'), -) -Test.ContinueOnFail = False - -# configure origin server -server = Test.MakeOriginServer("server", lookup_key="{%UID}") - -# default root -req_header_chk = { - "headers": "GET / HTTP/1.1\r\n" + "Host: www.example.com\r\n" + "UID: none\r\n" + "\r\n", - "timestamp": "1469733493.993", - "body": "", -} - -res_header_chk = { - "headers": "HTTP/1.1 200 OK\r\n" + "Connection: close\r\n" + "\r\n", - "timestamp": "1469733493.993", - "body": "", -} - -server.addResponse("sessionlog.json", req_header_chk, res_header_chk) - -# Define ATS and configure -ts = Test.MakeATSProcess("ts") - -# set up slice plugin with remap host into cache_range_requests -ts.Disk.remap_config.AddLines( - [ - f'map http://slice/ http://127.0.0.1:{server.Variables.Port}/' + - ' @plugin=slice.so @pparam=--blockbytes-test=3 @pparam=--remap-host=crr', - f'map http://crr/ http://127.0.0.1:{server.Variables.Port}/' + - ' @plugin=cache_range_requests.so @pparam=--consider-ims @pparam=--consider-ident' + - ' @plugin=header_rewrite.so @pparam=hdr_rw.conf', - ]) - -ts.Disk.logging_yaml.AddLines( - ''' -logging: - formats: - - name: custom - format: 'cpuup=% sssc=% pssc=% phr=% range=::%<{Range}cqh>:: x-crr-ident=::%<{X-Crr-Ident}cqh>:: uid=::%<{UID}pqh>:: crc=%' - logs: - - filename: transaction - format: custom -'''.split("\n")) - -ts.Disk.plugin_config.AddLine('xdebug.so --enable=x-cache') - -ts.Disk.records_config.update( - { - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'cache_range_requests|header_rewrite|slice|log', - }) - -ts.Disk.MakeConfigFile("hdr_rw.conf").AddLines( - [ - 'cond %{SEND_REQUEST_HDR_HOOK}', 'cond %{HEADER:Range} ="bytes=0-2" [AND]', 'set-header UID %{CLIENT-HEADER:UID} 0' - '', 'cond %{SEND_REQUEST_HDR_HOOK}', 'cond %{HEADER:Range} ="bytes=3-5" [AND]', 'set-header UID %{CLIENT-HEADER:UID} 1' - ]) - -# Test case: short lived asset. second slice should be HIT_FRESH - -req_header_plain_0 = { - "headers": "GET /plain HTTP/1.1\r\n" + "Host: www.example.com\r\n" + "UID: plain 0\r\n" + "Range: bytes=0-2\r\n" + "\r\n", - "timestamp": "1469733493.993", - "body": "", -} - -res_header_plain_0 = { - "headers": - "HTTP/1.1 206 Partial Content\r\n" + "Accept-Ranges: bytes\r\n" + "Cache-Control: max-age=1\r\n" + "Connection: close\r\n" + - "Content-Range: bytes 0-2/5\r\n" + 'Etag: "plain"\r\n' + "\r\n", - "timestamp": "1469733493.993", - "body": "aaa" -} - -server.addResponse("sessionlog.json", req_header_plain_0, res_header_plain_0) - -req_header_plain_1 = { - "headers": "GET /plain HTTP/1.1\r\n" + "Host: www.example.com\r\n" + "UID: plain 1\r\n" + "Range: bytes=3-5\r\n" + "\r\n", - "timestamp": "1469733493.993", - "body": "", -} - -res_header_plain_1 = { - "headers": - "HTTP/1.1 206 Partial Content\r\n" + "Accept-Ranges: bytes\r\n" + "Cache-Control: max-age=1\r\n" + "Connection: close\r\n" + - "Content-Range: bytes 3-4/5\r\n" + 'Etag: "plain"\r\n' + "\r\n", - "timestamp": "1469733493.993", - "body": "BB" -} - -server.addResponse("sessionlog.json", req_header_plain_1, res_header_plain_1) - -# curl helper -curl_and_args = '-s -D /dev/stdout -o /dev/stderr -x localhost:{}'.format(ts.Variables.port) + ' -H "x-debug: x-cache"' - -# 0 Test - Preload plain asset -tr = Test.AddTestRun("Preload plain") -ps = tr.Processes.Default -ps.StartBefore(server, ready=When.PortOpen(server.Variables.Port)) -ps.StartBefore(Test.Processes.ts) -tr.MakeCurlCommand(curl_and_args + ' http://slice/plain -H "UID: plain"', ts=ts) -ps.ReturnCode = 0 -ps.Streams.stderr.Content = Testers.ContainsExpression("aaaBB", "expected aaaBB") -ps.Streams.stdout.Content = Testers.ContainsExpression('Etag: "plain"', "expected etag plain") -tr.StillRunningAfter = ts - -# 2 Test - Request again, should result in stale asset -tr = Test.AddTestRun("Request 2nd slice (expect slice1 to be fresh)") -tr.DelayStart = 2 # ensure its really stale -ps = tr.Processes.Default -tr.MakeCurlCommand(curl_and_args + ' http://slice/plain -H "UID: plain"', ts=ts) -ps.ReturnCode = 0 -ps.Streams.stderr = Testers.ContainsExpression("aaaBB", "expected aaaBB") -ps.Streams.stdout.Content = Testers.ContainsExpression('Etag: "plain"', "expected etag plain") -tr.StillRunningAfter = ts - -# change out the asset - -req_header_chg_0 = { - "headers": "GET /plain HTTP/1.1\r\n" + "Host: www.example.com\r\n" + "UID: chg 0\r\n" + "Range: bytes=0-2\r\n" + "\r\n", - "timestamp": "1469733493.993", - "body": "", -} - -res_header_chg_0 = { - "headers": - "HTTP/1.1 206 Partial Content\r\n" + "Accept-Ranges: bytes\r\n" + "Cache-Control: max-age=60\r\n" + - "Connection: close\r\n" + "Content-Range: bytes 0-2/5\r\n" + 'Etag: "chg"\r\n' + "\r\n", - "timestamp": "1469733493.993", - "body": "AAA" -} - -server.addResponse("sessionlog.json", req_header_chg_0, res_header_chg_0) - -req_header_chg_1 = { - "headers": "GET /plain HTTP/1.1\r\n" + "Host: www.example.com\r\n" + "UID: chg 1\r\n" + "Range: bytes=3-5\r\n" + "\r\n", - "timestamp": "1469733493.993", - "body": "", -} - -res_header_chg_1 = { - "headers": - "HTTP/1.1 206 Partial Content\r\n" + "Accept-Ranges: bytes\r\n" + "Cache-Control: max-age=60\r\n" + - "Connection: close\r\n" + "Content-Range: bytes 3-4/5\r\n" + 'Etag: "chg"\r\n' + "\r\n", - "timestamp": "1469733493.993", - "body": "bb" -} - -server.addResponse("sessionlog.json", req_header_chg_1, res_header_chg_1) - -# 3 Test - Request again, should result in new asset -tr = Test.AddTestRun("Request again, asset replaced") -tr.DelayStart = 2 # ensure its really stale -ps = tr.Processes.Default -tr.MakeCurlCommand(curl_and_args + ' http://slice/plain -H "UID: chg"', ts=ts) -ps.ReturnCode = 0 -ps.Streams.stderr = Testers.ContainsExpression("AAAbb", "expected AAAbb") -ps.Streams.stdout.Content = Testers.ContainsExpression('Etag: "chg"', "expected etag chg") -tr.StillRunningAfter = ts - -# 4 Test - Request again, should all be hit -tr = Test.AddTestRun("Request again, asset replaced but hit") -tr.DelayStart = 2 # ensure its really stale -ps = tr.Processes.Default -tr.MakeCurlCommand(curl_and_args + ' http://slice/plain -H "UID: chg"', ts=ts) -ps.ReturnCode = 0 -ps.Streams.stderr = Testers.ContainsExpression("AAAbb", "expected AAAbb") -ps.Streams.stdout.Content = Testers.ContainsExpression('Etag: "chg"', "expected etag chg") -tr.StillRunningAfter = ts - -# 5 Test - add token to transaction log -tr = Test.AddTestRun("Fetch 404.txt asset") -ps = tr.Processes.Default -tr.MakeCurlCommand(curl_and_args + ' http://crr/404.txt', ts=ts) -ps.ReturnCode = 0 -ps.Streams.stdout.Content = Testers.ContainsExpression("404", "expected 404 Not Found response") -tr.StillRunningAfter = ts - -tslog = os.path.join(ts.Variables.LOGDIR, 'transaction.log') -Test.AddAwaitFileContainsTestRun('Await ts transactions to finish logging.', tslog, '404.txt') - -# 6 Check logs -tr = Test.AddTestRun() -tr.Processes.Default.Command = (f"cat {tslog}") -tr.Streams.stdout = "gold/slice_crr_ident.gold" -tr.Processes.Default.ReturnCode = 0 diff --git a/tests/gold_tests/pluginTest/slice/slice_error.test.py b/tests/gold_tests/pluginTest/slice/slice_error.test.py deleted file mode 100644 index a99bff794c3..00000000000 --- a/tests/gold_tests/pluginTest/slice/slice_error.test.py +++ /dev/null @@ -1,319 +0,0 @@ -''' -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -Test.Summary = ''' -Slice plugin error.log test -''' - -# Test description: -# Preload the cache with the entire asset to be range requested. -# Reload remap rule with slice plugin -# Request content through the slice plugin - -Test.SkipUnless(Condition.PluginExists('slice.so'),) -Test.ContinueOnFail = False - -# configure origin server -server = Test.MakeOriginServer("server", lookup_key="{%Range}{PATH}") - -# Define ATS and configure -ts = Test.MakeATSProcess("ts", enable_cache=False) - -body = "the quick brown fox" # len 19 - -# default root -request_header_chk = { - "headers": "GET / HTTP/1.1\r\n" + "Host: ats\r\n" + "Range: bytes=0-\r\n" + "\r\n", - "timestamp": "1469733493.993", - "body": "", -} - -response_header_chk = { - "headers": "HTTP/1.1 206 Partial Content\r\n" + "Connection: close\r\n" + "\r\n", - "timestamp": "1469733493.993", - "body": body, -} - -server.addResponse("sessionlog.json", request_header_chk, response_header_chk) - -blockbytes = 9 - -range0 = "{}-{}".format(0, blockbytes - 1) -range1 = "{}-{}".format(blockbytes, (2 * blockbytes) - 1) - -body0 = body[0:blockbytes] -body1 = body[blockbytes:2 * blockbytes] - -# Mismatch etag - -request_header_etag0 = { - "headers": - "GET /etag HTTP/1.1\r\n" + "Host: ats\r\n" + "Range: bytes={}\r\n".format(range0) + - "X-Slicer-Info: full content request\r\n" + "\r\n", - "timestamp": "1469733493.993", - "body": "", -} - -response_header_etag0 = { - "headers": - "HTTP/1.1 206 Partial Content\r\n" + "Connection: close\r\n" + 'Etag: "etag0"\r\n' + - "Content-Range: bytes {}/{}\r\n".format(range0, len(body)) + "Cache-Control: max-age=500\r\n" + "\r\n", - "timestamp": "1469733493.993", - "body": body0, -} - -server.addResponse("sessionlog.json", request_header_etag0, response_header_etag0) - -request_header_etag1 = { - "headers": - "GET /etag HTTP/1.1\r\n" + "Host: ats\r\n" + "Range: bytes={}\r\n".format(range1) + - "X-Slicer-Info: full content request\r\n" + "\r\n", - "timestamp": "1469733493.993", - "body": "", -} - -response_header_etag1 = { - "headers": - "HTTP/1.1 206 Partial Content\r\n" + "Connection: close\r\n" + 'Etag: "etag1"\r\n' + - "Content-Range: bytes {}/{}\r\n".format(range1, len(body)) + "Cache-Control: max-age=500\r\n" + "\r\n", - "timestamp": "1469733493.993", - "body": body1, -} - -server.addResponse("sessionlog.json", request_header_etag1, response_header_etag1) - -# mismatch Last-Modified - -request_header_lm0 = { - "headers": - "GET /lastmodified HTTP/1.1\r\n" + "Host: ats\r\n" + "Range: bytes={}\r\n".format(range0) + - "X-Slicer-Info: full content request\r\n" + "\r\n", - "timestamp": "1469733493.993", - "body": "", -} - -response_header_lm0 = { - "headers": - "HTTP/1.1 206 Partial Content\r\n" + "Connection: close\r\n" + "Last-Modified: Tue, 08 May 2018 15:49:41 GMT\r\n" + - "Content-Range: bytes {}/{}\r\n".format(range0, len(body)) + "Cache-Control: max-age=500\r\n" + "\r\n", - "timestamp": "1469733493.993", - "body": body0, -} - -server.addResponse("sessionlog.json", request_header_lm0, response_header_lm0) - -request_header_lm1 = { - "headers": - "GET /lastmodified HTTP/1.1\r\n" + "Host: ats\r\n" + "Range: bytes={}\r\n".format(range1) + - "X-Slicer-Info: full content request\r\n" + "\r\n", - "timestamp": "1469733493.993", - "body": "", -} - -response_header_lm1 = { - "headers": - "HTTP/1.1 206 Partial Content\r\n" + "Connection: close\r\n" + "Last-Modified: Tue, 08 Apr 2019 18:00:00 GMT\r\n" + - "Content-Range: bytes {}/{}\r\n".format(range1, len(body)) + "Cache-Control: max-age=500\r\n" + "\r\n", - "timestamp": "1469733493.993", - "body": body1, -} - -server.addResponse("sessionlog.json", request_header_lm1, response_header_lm1) - -# non 206 slice block - -request_header_n206_0 = { - "headers": - "GET /non206 HTTP/1.1\r\n" + "Host: ats\r\n" + "Range: bytes={}\r\n".format(range0) + - "X-Slicer-Info: full content request\r\n" + "\r\n", - "timestamp": "1469733493.993", - "body": "", -} - -response_header_n206_0 = { - "headers": - "HTTP/1.1 206 Partial Content\r\n" + "Connection: close\r\n" + 'Etag: "etag"\r\n' + - "Last-Modified: Tue, 08 May 2018 15:49:41 GMT\r\n" + "Content-Range: bytes {}/{}\r\n".format(range0, len(body)) + - "Cache-Control: max-age=500\r\n" + "\r\n", - "timestamp": "1469733493.993", - "body": body0, -} - -server.addResponse("sessionlog.json", request_header_n206_0, response_header_n206_0) - -request_header_n206_1 = { - "headers": - "GET /non206 HTTP/1.1\r\n" + "Host: ats\r\n" + "Range: bytes={}\r\n".format(range1) + - "X-Slicer-Info: full content request\r\n" + "\r\n", - "timestamp": "1469733493.993", - "body": "", -} - -response_header_n206_1 = { - "headers": "HTTP/1.1 502 Bad Gateway\r\n" + "Connection: close\r\n" + "\r\n", - "timestamp": "1469733493.993", - "body": body1, -} - -server.addResponse("sessionlog.json", request_header_n206_1, response_header_n206_1) - -# mismatch content-range - -request_header_crr0 = { - "headers": - "GET /crr HTTP/1.1\r\n" + "Host: ats\r\n" + "Range: bytes={}\r\n".format(range0) + - "X-Slicer-Info: full content request\r\n" + "\r\n", - "timestamp": "1469733493.993", - "body": "", -} - -response_header_crr0 = { - "headers": - "HTTP/1.1 206 Partial Content\r\n" + "Connection: close\r\n" + "Etag: crr\r\n" + - "Content-Range: bytes {}/{}\r\n".format(range0, len(body)) + "Cache-Control: max-age=500\r\n" + "\r\n", - "timestamp": "1469733493.993", - "body": body0, -} - -server.addResponse("sessionlog.json", request_header_crr0, response_header_crr0) - -request_header_crr1 = { - "headers": - "GET /crr HTTP/1.1\r\n" + "Host: ats\r\n" + "Range: bytes={}\r\n".format(range1) + - "X-Slicer-Info: full content request\r\n" + "\r\n", - "timestamp": "1469733493.993", - "body": "", -} - -response_header_crr1 = { - "headers": - "HTTP/1.1 206 Partial Content\r\n" + "Connection: close\r\n" + "Etag: crr\r\n" + - "Content-Range: bytes {}/{}\r\n".format(range1, - len(body) - 1) + "Cache-Control: max-age=500\r\n" + "\r\n", - "timestamp": "1469733493.993", - "body": body1, -} - -server.addResponse("sessionlog.json", request_header_crr1, response_header_crr1) - -# 404 internal block - -request_header_internal404_0 = { - "headers": - "GET /internal404 HTTP/1.1\r\n" + "Host: ats\r\n" + "Range: bytes={}\r\n".format(range0) + - "X-Slicer-Info: full content request\r\n" + "\r\n", - "timestamp": "1469733493.993", - "body": "", -} - -response_header_internal404_0 = { - "headers": - "HTTP/1.1 206 Partial Content\r\n" + "Connection: close\r\n" + 'Etag: "etag"\r\n' + - "Last-Modified: Tue, 08 May 2018 15:49:41 GMT\r\n" + "Content-Range: bytes {}/{}\r\n".format(range0, len(body)) + - "Cache-Control: max-age=500\r\n" + "\r\n", - "timestamp": "1469733493.993", - "body": body0, -} - -server.addResponse("sessionlog.json", request_header_internal404_0, response_header_internal404_0) - -curl_and_args = '-s -D /dev/stdout -o /dev/stderr -x http://127.0.0.1:{}'.format(ts.Variables.port) - -# set up whole asset fetch into cache -ts.Disk.remap_config.AddLine( - 'map / http://127.0.0.1:{}'.format(server.Variables.Port) + ' @plugin=slice.so @pparam=--blockbytes-test={}'.format(blockbytes)) - -# minimal configuration -ts.Disk.records_config.update({ - # 'proxy.config.diags.debug.enabled': 1, - # 'proxy.config.diags.debug.tags': 'slice', -}) - -# Override builtin error check as these cases will fail -# taken from the slice plug code -ts.Disk.diags_log.Content = Testers.ContainsExpression('reason="Mismatch block Etag', "Mismatch block etag") -ts.Disk.diags_log.Content += Testers.ContainsExpression('reason="Mismatch block Last-Modified', "Mismatch block Last-Modified") -ts.Disk.diags_log.Content += Testers.ContainsExpression( - 'reason="Mismatch/Bad block Content-Range', "Mismatch/Bad block Content-Range") -ts.Disk.diags_log.Content += Testers.ContainsExpression('reason="404 internal block response', "404 internal block response") - -# 0 Test - Etag mismatch test -tr = Test.AddTestRun("Etag test") -ps = tr.Processes.Default -ps.StartBefore(server, ready=When.PortOpen(server.Variables.Port)) -ps.StartBefore(Test.Processes.ts) -tr.MakeCurlCommand(curl_and_args + ' http://ats/etag', ts=ts) -# ps.ReturnCode = 0 # curl will return fail status -ps.Streams.stdout.Content = Testers.ContainsExpression("200 OK", "expected 200 OK response") -ps.Streams.stderr = "gold_error/contents.stderr.gold" -tr.StillRunningAfter = ts - -# 1 Check - diags.log message -tr = Test.AddTestRun("Etag error check") -ps = tr.Processes.Default -ps.Command = "grep 'Mismatch block Etag' {}".format(ts.Disk.diags_log.Name) -ps.ReturnCode = 0 -tr.StillRunningAfter = ts - -# 2 Test - Last Modified mismatch test -tr = Test.AddTestRun("Last-Modified test") -ps = tr.Processes.Default -tr.MakeCurlCommand(curl_and_args + ' http://ats/lastmodified', ts=ts) -# ps.ReturnCode = 0 # curl will return fail status -ps.Streams.stderr = "gold_error/contents.stderr.gold" -ps.Streams.stdout.Content = Testers.ContainsExpression("200 OK", "expected 200 OK response") -tr.StillRunningAfter = ts - -# 3 Check - diags.log message -tr = Test.AddTestRun("Last-Modified error check") -ps = tr.Processes.Default -ps.Command = "grep 'Mismatch block Last-Modified' {}".format(ts.Disk.diags_log.Name) -ps.ReturnCode = 0 -tr.StillRunningAfter = ts - -# 4 Test - Block content-range -tr = Test.AddTestRun("Content-Range test") -ps = tr.Processes.Default -tr.MakeCurlCommand(curl_and_args + ' http://ats/crr', ts=ts) -# ps.ReturnCode = 0 # curl will return fail status -ps.Streams.stderr = "gold_error/contents.stderr.gold" -ps.Streams.stdout.Content = Testers.ContainsExpression("200 OK", "expected 200 OK response") -tr.StillRunningAfter = ts - -# 5 Check - diags.log message -tr = Test.AddTestRun("Content-Range error check") -ps = tr.Processes.Default -ps.Command = "grep 'Mismatch/Bad block Content-Range' {}".format(ts.Disk.diags_log.Name) -ps.ReturnCode = 0 -tr.StillRunningAfter = ts - -# 6 Test - 404 internal test -tr = Test.AddTestRun("Internal 404 test") -ps = tr.Processes.Default -tr.MakeCurlCommand(curl_and_args + ' http://ats/internal404', ts=ts) -# ps.ReturnCode = 0 # curl will return fail status -ps.Streams.stderr = "gold_error/contents.stderr.gold" -ps.Streams.stdout.Content = Testers.ContainsExpression("200 OK", "expected 200 OK response") -tr.StillRunningAfter = ts - -# 7 Check - diags.log message -tr = Test.AddTestRun("Internal 404 check") -ps = tr.Processes.Default -ps.Command = "grep '404 internal block response' {}".format(ts.Disk.diags_log.Name) -ps.ReturnCode = 0 -tr.StillRunningAfter = ts diff --git a/tests/gold_tests/pluginTest/slice/slice_ident.test.py b/tests/gold_tests/pluginTest/slice/slice_ident.test.py deleted file mode 100644 index faabb4f9f22..00000000000 --- a/tests/gold_tests/pluginTest/slice/slice_ident.test.py +++ /dev/null @@ -1,187 +0,0 @@ -''' -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import os - -Test.Summary = ''' -Slice plugin test for sending ident header -''' - -# Test description: -# Preload the cache with the entire asset to be range requested. -# Reload remap rule with slice plugin -# Request content through the slice plugin - -Test.SkipUnless(Condition.PluginExists('slice.so'),) -Test.ContinueOnFail = False - -# configure origin server -server = Test.MakeOriginServer("server") - -# default root -request_header_chk = { - "headers": "GET / HTTP/1.1\r\n" + "Host: ats\r\n" + "Range: none\r\n" + "\r\n", - "timestamp": "1469733493.993", - "body": "", -} - -response_header_chk = { - "headers": "HTTP/1.1 200 OK\r\n" + "Connection: close\r\n" + "\r\n", - "timestamp": "1469733493.993", - "body": "", -} - -server.addResponse("sessionlog.json", request_header_chk, response_header_chk) - -block_bytes = 11 -body = "lets go surfin now" - -etag = '"foo"' -last_modified = "Fri, 07 Mar 2025 18:06:58 GMT" - -request_etag = { - "headers": "GET /etag HTTP/1.1\r\n" + "Host: origin\r\n" + "\r\n", - "timestamp": "1469733493.993", - "body": "", -} - -response_etag = { - "headers": - "HTTP/1.1 200 OK\r\n" + "Connection: close\r\n" + f'Etag: {etag}\r\n' + f'Last-Modified: {last_modified}\r\n' + - "Cache-Control: max-age=500\r\n" + "\r\n", - "timestamp": "1469733493.993", - "body": body, -} - -server.addResponse("sessionlog.json", request_etag, response_etag) - -request_lm = { - "headers": "GET /lm HTTP/1.1\r\n" + "Host: origin\r\n" + "\r\n", - "timestamp": "1469733493.993", - "body": "", -} - -response_lm = { - "headers": - "HTTP/1.1 200 OK\r\n" + "Connection: close\r\n" + f'Last-Modified: {last_modified}\r\n' + "Cache-Control: max-age=500\r\n" + - "\r\n", - "timestamp": "1469733493.993", - "body": body, -} - -server.addResponse("sessionlog.json", request_lm, response_lm) - -# use this second ats instance to serve up the slices - -# Define ATS and configure -ts = Test.MakeATSProcess("ts") -ts.Disk.records_config.update({ - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'slice', -}) - -ts.Disk.remap_config.AddLines( - [ - f"map http://preload/ http://127.0.0.1:{server.Variables.Port}", - f'map http://slice/ http://127.0.0.1:{server.Variables.Port}' + - f' @plugin=slice.so @pparam=--blockbytes-test={block_bytes}', - f'map http://slicecustom/ http://127.0.0.1:{server.Variables.Port}' + - f' @plugin=slice.so @pparam=--blockbytes-test={block_bytes}' + ' @pparam=--crr-ident-header=CrrIdent', - ]) - -ts.Disk.logging_yaml.AddLines( - ''' -logging: - formats: - - name: custom - format: '% % % range=::%<{Range}cqh>:: x-crr-ident=::%<{X-Crr-Ident}cqh>:: crrident=::%<{CrrIdent}cqh>::' - logs: - - filename: transaction - format: custom -'''.split("\n")) - -# helpers for curl -curl_and_args = f'-s -D /dev/stdout -o /dev/stderr -x localhost:{ts.Variables.port}' - -# 0 Test - Preload etag asset -tr = Test.AddTestRun("Preload etag asset") -ps = tr.Processes.Default -ps.StartBefore(server, ready=When.PortOpen(server.Variables.Port)) -ps.StartBefore(Test.Processes.ts) -tr.MakeCurlCommand(curl_and_args + ' http://preload/etag', ts=ts) -ps.ReturnCode = 0 -ps.Streams.stdout.Content = Testers.ContainsExpression("200 OK", "expected 200 OK response") -tr.StillRunningAfter = ts - -# 1 Test - Preload last-modified asset -tr = Test.AddTestRun("Preload last-modified asset") -ps = tr.Processes.Default -tr.MakeCurlCommand(curl_and_args + ' http://preload/lm', ts=ts) -ps.ReturnCode = 0 -ps.Streams.stdout.Content = Testers.ContainsExpression("200 OK", "expected 200 OK response") -tr.StillRunningAfter = ts - -# 2 Test - Fetch etag asset -tr = Test.AddTestRun("Fetch etag asset") -ps = tr.Processes.Default -tr.MakeCurlCommand(curl_and_args + ' http://slice/etag', ts=ts) -ps.ReturnCode = 0 -ps.Streams.stdout.Content = Testers.ContainsExpression("200 OK", "expected 200 OK response") -tr.StillRunningAfter = ts - -# 3 Test - Fetch etag asset -tr = Test.AddTestRun("Fetch etag asset, custom") -ps = tr.Processes.Default -tr.MakeCurlCommand(curl_and_args + ' http://slicecustom/etag', ts=ts) -ps.ReturnCode = 0 -ps.Streams.stdout.Content = Testers.ContainsExpression("200 OK", "expected 200 OK response") -tr.StillRunningAfter = ts - -# 4 Test - Fetch last-modified asset -tr = Test.AddTestRun("Fetch last-modified asset, custom") -ps = tr.Processes.Default -tr.MakeCurlCommand(curl_and_args + ' http://slice/lm', ts=ts) -ps.ReturnCode = 0 -ps.Streams.stdout.Content = Testers.ContainsExpression("200 OK", "expected 200 OK response") -tr.StillRunningAfter = ts - -# 5 Test - Fetch last-modified asset, custom -tr = Test.AddTestRun("Fetch last-modified asset") -ps = tr.Processes.Default -tr.MakeCurlCommand(curl_and_args + ' http://slicecustom/lm', ts=ts) -ps.ReturnCode = 0 -ps.Streams.stdout.Content = Testers.ContainsExpression("200 OK", "expected 200 OK response") -tr.StillRunningAfter = ts - -# 6 Test - add token to transaction log -tr = Test.AddTestRun("Add 404 token to transaction log") -ps = tr.Processes.Default -tr.MakeCurlCommand(curl_and_args + ' http://prefetch/404.txt', ts=ts) -ps.ReturnCode = 0 -ps.Streams.stdout.Content = Testers.ContainsExpression("404", "expected 404 Not Found response") -tr.StillRunningAfter = ts - -# 7 - wait for logs -tslog = os.path.join(ts.Variables.LOGDIR, 'transaction.log') -Test.AddAwaitFileContainsTestRun('Await ts transactions to finish logging.', tslog, '404.txt') - -# 8 Check logs -tr = Test.AddTestRun() -tr.Processes.Default.Command = (f"cat {tslog}") -tr.Streams.stdout = "gold/slice_ident.gold" -tr.Processes.Default.ReturnCode = 0 diff --git a/tests/gold_tests/pluginTest/slice/slice_prefetch.test.py b/tests/gold_tests/pluginTest/slice/slice_prefetch.test.py deleted file mode 100644 index 14767b41c02..00000000000 --- a/tests/gold_tests/pluginTest/slice/slice_prefetch.test.py +++ /dev/null @@ -1,214 +0,0 @@ -''' -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. -import os -import re - -Test.Summary = ''' -slice plugin prefetch feature test -''' - -# Test description: -# Fill origin server with range requests -# Request content through slice plugin with varied prefetch counts - -Test.SkipUnless( - Condition.PluginExists('slice.so'), - Condition.PluginExists('cache_range_requests.so'), - Condition.PluginExists('xdebug.so'), -) -Test.ContinueOnFail = False - -# configure origin server, lookup by Range header -server = Test.MakeOriginServer("server", lookup_key="{%Range}") - -# Define ATS and configure -ts = Test.MakeATSProcess("ts") - -block_bytes_1 = 7 -block_bytes_2 = 5 -body = "lets go surfin now" -bodylen = len(body) - -request_header = { - "headers": "GET /path HTTP/1.1\r\n" + "Host: origin\r\n" + "\r\n", - "timestamp": "1469733493.993", - "body": "", -} - -response_header = { - "headers": "HTTP/1.1 200 OK\r\n" + "Connection: close\r\n" + "Cache-Control: public, max-age=5\r\n" + "\r\n", - "timestamp": "1469733493.993", - "body": body, -} - -server.addResponse("sessionlog.json", request_header, response_header) - -# Autest OS doesn't support range request, must manually add requests/responses -for block_bytes in [block_bytes_1, block_bytes_2]: - for i in range(bodylen // block_bytes + 1): - b0 = i * block_bytes - b1 = b0 + block_bytes - 1 - req_header = { - "headers": "GET /path HTTP/1.1\r\n" + "Host: *\r\n" + "Accept: */*\r\n" + f"Range: bytes={b0}-{b1}\r\n" + "\r\n", - "timestamp": "1469733493.993", - "body": "" - } - if (b1 > bodylen - 1): - b1 = bodylen - 1 - resp_header = { - "headers": - "HTTP/1.1 206 Partial Content\r\n" + "Accept-Ranges: bytes\r\n" + "Cache-Control: public, max-age=5\r\n" + - f"Content-Range: bytes {b0}-{b1}/{bodylen}\r\n" + "Connection: close\r\n" + "\r\n", - "timestamp": "1469733493.993", - "body": body[b0:b1 + 1] - } - server.addResponse("sessionlog.json", req_header, resp_header) - -curl_and_args = '{{curl}} -s -D /dev/stdout -o /dev/stderr -x http://127.0.0.1:{} -H "x-debug: x-cache"'.format(ts.Variables.port) - -ts.Disk.remap_config.AddLines( - [ - f'map http://sliceprefetchbytes1/ http://127.0.0.1:{server.Variables.Port}' + - f' @plugin=slice.so @pparam=--blockbytes-test={block_bytes_1} @pparam=--prefetch-count=1 \\' + - ' @plugin=cache_range_requests.so', - f'map http://sliceprefetchbytes2/ http://127.0.0.1:{server.Variables.Port}' + - f' @plugin=slice.so @pparam=--blockbytes-test={block_bytes_2} @pparam=--prefetch-count=3 \\' + - ' @plugin=cache_range_requests.so', - ]) - -ts.Disk.plugin_config.AddLine('xdebug.so --enable=x-cache') -ts.Disk.logging_yaml.AddLines( - [ - 'logging:', - ' formats:', - ' - name: cache', - ' format: "%<{Content-Range}psh> %<{X-Cache}psh>"', - ' logs:', - ' - filename: cache', - ' format: cache', - ' mode: ascii', - ]) - -ts.Disk.records_config.update( - { - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'slice|cache_range_requests|xdebug', - }) - -# 0 Test - Full object slice (miss) with only block 14-20 prefetched in background, block bytes= 7 -tr = Test.AddTestRun("Full object slice: first block is miss, only block 14-20 prefetched") -ps = tr.Processes.Default -ps.StartBefore(server, ready=When.PortOpen(server.Variables.Port)) -ps.StartBefore(Test.Processes.ts) -tr.MakeCurlCommandMulti(curl_and_args + ' http://sliceprefetchbytes1/path', ts=ts) -ps.ReturnCode = 0 -ps.Streams.stderr = "gold/slice_200.stderr.gold" -ps.Streams.stdout.Content = Testers.ContainsExpression("200 OK", "expected 200 OK response") -ps.Streams.stdout.Content += Testers.ContainsExpression("X-Cache: miss", "expected cache miss") -tr.StillRunningAfter = ts - -# 1 Test - Full object slice (hit-fresh) with no prefetched blocks, block bytes= 7 -tr = Test.AddTestRun("Full object slice: first block is hit-fresh, no blocks prefetched") -ps = tr.Processes.Default -tr.MakeCurlCommandMulti('sleep 1; ' + curl_and_args + ' http://sliceprefetchbytes1/path', ts=ts) -ps.ReturnCode = 0 -ps.Streams.stderr = "gold/slice_200.stderr.gold" -ps.Streams.stdout.Content = Testers.ContainsExpression("200 OK", "expected 200 OK response") -ps.Streams.stdout.Content += Testers.ContainsExpression("X-Cache: hit-fresh", "expected cache fresh") -tr.StillRunningAfter = ts - -# 2 Test - Full object slice with only next block prefetched in background, block bytes= 7 -tr = Test.AddTestRun("Full object slice: first block is hit-stale, only block 14-20 prefetched") -ps = tr.Processes.Default -tr.MakeCurlCommandMulti('sleep 5; ' + curl_and_args + ' http://sliceprefetchbytes1/path', ts=ts) -ps.ReturnCode = 0 -ps.Streams.stderr = "gold/slice_200.stderr.gold" -ps.Streams.stdout.Content = Testers.ContainsExpression("200 OK", "expected 200 OK response") -ps.Streams.stdout.Content += Testers.ContainsExpression("X-Cache: hit-stale", "expected cache hit-stale") -tr.StillRunningAfter = ts - -# 3 Test - Full object slice (hit-fresh) with no prefetched blocks, block bytes= 7 -tr = Test.AddTestRun("Full object slice: first block is hit-fresh with range 0-, no blocks prefetched") -ps = tr.Processes.Default -tr.MakeCurlCommandMulti(curl_and_args + ' http://sliceprefetchbytes1/path' + ' -r 0-', ts=ts) -ps.ReturnCode = 0 -ps.Streams.stderr = "gold/slice_200.stderr.gold" -ps.Streams.stdout.Content = Testers.ContainsExpression("206 Partial Content", "expected 206 response") -ps.Streams.stdout.Content += Testers.ContainsExpression("Content-Range: bytes 0-17/18", "mismatch byte content response") -ps.Streams.stdout.Content += Testers.ContainsExpression("X-Cache: hit-fresh", "expected cache hit-fresh") -tr.StillRunningAfter = ts - -# 4 Test - Client range request (hit-stale/miss) enables prefetching -tr = Test.AddTestRun("Client range request") -ps = tr.Processes.Default -tr.MakeCurlCommandMulti(curl_and_args + ' http://sliceprefetchbytes2/path' + ' -r 5-16', ts=ts) -ps.ReturnCode = 0 -ps.Streams.stderr = "gold/slice_mid.stderr.gold" -ps.Streams.stdout.Content = Testers.ContainsExpression("206 Partial Content", "expected 206 response") -ps.Streams.stdout.Content += Testers.ContainsExpression("Content-Range: bytes 5-16/18", "mismatch byte content response") -ps.Streams.stdout.Content += Testers.ContainsExpression("X-Cache: miss", "expected cache miss") -tr.StillRunningAfter = ts - -# 5 Test - special case, begin inside last slice block but outside asset len -tr = Test.AddTestRun("Invalid end range request, 416") -beg = len(body) + 1 -end = beg + block_bytes -ps = tr.Processes.Default -tr.MakeCurlCommandMulti(curl_and_args + f' http://sliceprefetchbytes1/path -r {beg}-{end}', ts=ts) -ps.Streams.stdout.Content = Testers.ContainsExpression("416 Requested Range Not Satisfiable", "expected 416 response") -tr.StillRunningAfter = ts - -# 6 Test - All requests (client & slice internal) logs to see background fetches -cache_file = os.path.join(ts.Variables.LOGDIR, 'cache.log') -# Wait for the final cache log line to be written. -test_run = Test.AddAwaitFileContainsTestRun( - "Checking debug logs for background fetches", - cache_file, - r'\*/18 hit-fresh, none$', -) -cache_log = ts.Disk.File(cache_file) -expected_cache_entries = [ - "bytes 0-6/18 miss", - "bytes 7-13/18 miss", - "bytes 14-17/18 miss", - "bytes 14-17/18 hit-fresh", - "- miss, none", - "bytes 0-6/18 hit-fresh", - "bytes 7-13/18 hit-fresh", - "bytes 14-17/18 hit-fresh", - "- hit-fresh, none", - "bytes 0-6/18 hit-stale", - "bytes 7-13/18 hit-stale", - "bytes 14-17/18 hit-stale", - "- hit-stale, none", - "bytes 0-17/18 hit-fresh, none", - "bytes 0-4/18 miss", - "bytes 5-9/18 miss", - "bytes 10-14/18 miss", - "bytes 15-17/18 miss", - "bytes 10-14/18 hit-fresh", - "bytes 15-17/18 hit-fresh", - "bytes 5-16/18 miss, none", - "*/18 hit-fresh, none", -] -for index, entry in enumerate(expected_cache_entries): - tester = Testers.ContainsExpression(f'(?m)^{re.escape(entry)}$', f'Verify cache log contains: {entry}') - if index == 0: - cache_log.Content = tester - else: - cache_log.Content += tester diff --git a/tests/gold_tests/pluginTest/slice/slice_purge.test.py b/tests/gold_tests/pluginTest/slice/slice_purge.test.py deleted file mode 100644 index 888aeda1914..00000000000 --- a/tests/gold_tests/pluginTest/slice/slice_purge.test.py +++ /dev/null @@ -1,94 +0,0 @@ -''' -Verify ATS slice plugin config accepts PURGE requests -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -Test.Summary = ''' -Verify ATS slice plugin config accepts PURGE requests -''' -Test.SkipUnless( - Condition.PluginExists('slice.so'), - Condition.PluginExists('cache_range_requests.so'), -) - - -class SlicePurgeRequestTest: - replay_file = "replay/slice_purge.replay.yaml" - replay_ref_file = "replay/slice_purge_ref.replay.yaml" - replay_no_ref_file = "replay/slice_purge_no_ref.replay.yaml" - - def __init__(self, ref_block, num): - """Initialize the Test processes for the test runs.""" - self._ref_block = ref_block - self._num = num - self._server = Test.MakeVerifierServerProcess(f"server_{self._num}", SlicePurgeRequestTest.replay_file) - self._configure_trafficserver() - - def _configure_trafficserver(self): - """Configure Traffic Server.""" - self._ts = Test.MakeATSProcess(f"ts_{self._num}", enable_cache=True) - - if (self._ref_block): - self._ts.Disk.remap_config.AddLines( - [ - f"map /ref/block http://127.0.0.1:{self._server.Variables.http_port} \ - @plugin=slice.so @pparam=--blockbytes-test=10 \ - @plugin=cache_range_requests.so", - ]) - else: - self._ts.Disk.remap_config.AddLines( - [ - f"map /ref/block http://127.0.0.1:{self._server.Variables.http_port} \ - @plugin=slice.so @pparam=--blockbytes-test=10 @pparam=--ref-relative \ - @plugin=cache_range_requests.so", - ]) - - self._ts.Disk.records_config.update( - { - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'http|slice|cache_range_requests', - }) - - def slice_purge(self): - tr = Test.AddTestRun() - - tr.AddVerifierClientProcess(f"client_{self._num}", SlicePurgeRequestTest.replay_file, http_ports=[self._ts.Variables.port]) - - tr.Processes.Default.StartBefore(self._server) - tr.Processes.Default.StartBefore(self._ts) - - def slice_purge_ref(self): - tr = Test.AddTestRun() - - tr.AddVerifierClientProcess("client_ref", SlicePurgeRequestTest.replay_ref_file, http_ports=[self._ts.Variables.port]) - - tr.Processes.Default.StartBefore(self._server) - tr.Processes.Default.StartBefore(self._ts) - - def slice_purge_no_ref(self): - tr = Test.AddTestRun() - - tr.AddVerifierClientProcess("client_no_ref", SlicePurgeRequestTest.replay_no_ref_file, http_ports=[self._ts.Variables.port]) - - tr.Processes.Default.StartBefore(self._server) - tr.Processes.Default.StartBefore(self._ts) - - -SlicePurgeRequestTest(True, 0).slice_purge() -SlicePurgeRequestTest(True, 1).slice_purge_ref() -SlicePurgeRequestTest(False, 2).slice_purge() -SlicePurgeRequestTest(False, 3).slice_purge_no_ref() diff --git a/tests/gold_tests/pluginTest/slice/slice_purge_gaps.test.py b/tests/gold_tests/pluginTest/slice/slice_purge_gaps.test.py deleted file mode 100644 index a8ff70c0461..00000000000 --- a/tests/gold_tests/pluginTest/slice/slice_purge_gaps.test.py +++ /dev/null @@ -1,346 +0,0 @@ -"""Verify a PURGE traverses every slice block, not just the ones before a gap.""" - -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -Test.Summary = __doc__ - -Test.SkipUnless( - Condition.PluginExists('slice.so'), - Condition.PluginExists('cache_range_requests.so'), - Condition.PluginExists('header_rewrite.so'), -) -Test.ContinueOnFail = True - - -class SlicePurgeGapsTest: - """Verify a PURGE removes every block it was asked for. - - Slice turns one client PURGE into one PURGE per block. The walk used to stop - at the first block that was not cached, because it had no other end - condition: m_contentlen was only ever set from a 206's Content-Range, a PURGE - response carried none, and so m_req_range.blockIsInside() was true for every - block number. The 404 stood in for a length the plugin never learned. - - ATS now reports the removed object's extent as X-Purged-Content-Range, so the - walk learns where the object ends from the blocks it is already deleting, and - a 404 is merely noted and stepped over. The requested range bounds the walk - throughout; until some block reports an extent, an open ended one is bounded - only by a limit on consecutive misses. A block response that is neither 200 nor - 404 is a failure rather than an absence, and says nothing about the blocks - behind it, so the walk stops there and reports that status. - - Each object below exercises one thing, and each is purged exactly once, - because a purge consumes the state it is measured against. - - /hole blocks 0 and 2 cached, block 1 absent. A 404 mid-walk must not end - it, so block 2 goes too. - /nofirst only block 1 cached. The walk steps over block 0's 404, picks the - extent up from block 1, and the client must not be handed that 404. - /mixed blocks reporting a 30 byte and a 50 byte object, as happens when - the origin object is replaced in place. The walk must follow the - largest extent reported, not the first. - /ranged same gap as /hole, purged with a closed range. Already bounded, so - this isolates the 404 handling from the extent discovery. - /openend purged with "bytes=20-". The start is stated, so only block 2 may - go and block 0 must survive. - /outside only block 1 cached, purged for block 0 alone. No block is removed, - so no extent is ever reported and only the range can end the walk - before it reaches a block the client did not name. - /endbytes purged with "bytes=-10". A suffix range cannot know which block it - starts at, so it is widened to the whole object and every block - goes. - /sparse only block 4 cached, on a proxy whose miss bound is 2, so the - default walk cannot reach it. Covers the bound, its per-request - override, and the fallback when the override is malformed. - /badrange purged with an unparseable range, which must be refused rather - than silently applied to block 0. - /failblock blocks 0 and 3 cached, with a 500 injected for block 1's PURGE. Block - 0 is removed, the walk stops at block 1, block 3 is left alone, and - the client hears the 500 rather than the 200 block 0 earned. - /denied block 0 cached, with a 403 injected for every block PURGE, as - ip_allow refusing PURGE would. The walk stops at its first block, so - nothing is removed and the client must not be told 404. - - Whether a block was purged is measured on the origin, not on the response - body: the origin serves each check phase exactly what the matching fill phase - served, so a purged block and a surviving block are indistinguishable to the - client, and the only difference is whether the origin was asked again. - """ - - _client_replay: str = 'replay/slice_purge_gaps_client.replay.yaml' - _server_replay: str = 'replay/slice_purge_gaps_server.replay.yaml' - - # The core answers a block PURGE with nothing but 200 or 404, so a failure has to - # be injected to be tested at all. - _fail_rules: str = 'purge_block_failure.conf' - - _block_bytes: int = 10 - - # Low enough that the default walk cannot reach /sparse's block 4, which is - # what makes the per-request override observable. - _low_miss_bound: int = 2 - - # Keyed on the block range as well as the phase uuid, so one origin - # transaction answers one block of one phase. - _origin_key_format: str = '--format "{url}{field.range}{field.uuid}"' - - def __init__(self) -> None: - """Declare the origin and the two proxies.""" - self._started = False - self._configure_origin() - self._ts = self._make_ts('ts', fail_rules=True) - self._ts_bound = self._make_ts('ts-bound', miss_bound=self._low_miss_bound) - - self._ts.Disk.traffic_out.Content = Testers.ContainsExpression( - 'Purge suffix range widened to the whole object', - 'A suffix range purge should be widened rather than guessing at its start.') - - self._ts_bound.Disk.traffic_out.Content = Testers.ContainsExpression( - f'gave up after {self._low_miss_bound} consecutive uncached block', - 'The walk should stop at its configured miss bound rather than scanning the whole range.') - self._ts_bound.Disk.diags_log.Content = Testers.ContainsExpression( - 'Ignoring invalid X-Slice-Purge-Probe', 'A malformed override should be rejected, not acted on.') - - # A purge issues nothing but block PURGEs. request_block logs every request - # header it builds at debug, so an only-if-cached here would mean a - # read-only length probe had been reintroduced. - for ts in (self._ts, self._ts_bound): - ts.Disk.traffic_out.Content += Testers.ExcludesExpression( - 'only-if-cached', 'A purge should not issue a read-only length probe.') - - def _configure_origin(self) -> None: - """Configure the origin.""" - self._origin = Test.MakeVerifierServerProcess('origin', self._server_replay, other_args=self._origin_key_format) - - # ATS answers PURGE itself and the plugin issues no other request kind, so - # neither may ever be seen upstream. - self._origin.Streams.stdout += Testers.ExcludesExpression( - 'PURGE', 'A PURGE should be answered by ATS and never forwarded to the origin.') - self._origin.Streams.stdout += Testers.ExcludesExpression('HEAD /', 'A purge should never issue a HEAD upstream.') - - def _make_ts(self, label: str, miss_bound: int = None, fail_rules: bool = False) -> 'Process': - """Create a proxy that slices in front of cache_range_requests. - - --ref-relative keeps a ranged GET from dragging block 0 in as a reference - block, which is what lets a fill phase leave a chosen block uncached. - - :param label: process name suffix. - :param miss_bound: --purge-probe-blocks value, or None for the default. - :param fail_rules: load the header_rewrite rules that fail a block PURGE. - """ - ts = Test.MakeATSProcess(label, enable_cache=True) - - rules = '' - if fail_rules: - ts.Setup.CopyAs(f'rules/{self._fail_rules}', Test.RunDirectory) - rules = f' @plugin=header_rewrite.so @pparam={Test.RunDirectory}/{self._fail_rules}' - - bound = '' if miss_bound is None else f' @pparam=--purge-probe-blocks={miss_bound}' - ts.Disk.remap_config.AddLine( - f'map http://slice/ http://127.0.0.1:{self._origin.Variables.http_port}/' - f'{rules} @plugin=slice.so @pparam=--blockbytes-test={self._block_bytes} @pparam=--ref-relative{bound}' - ' @plugin=cache_range_requests.so') - ts.Disk.records_config.update( - { - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'slice|cache_range_requests', - }) - return ts - - def _run(self, summary: str, phases: str, ts: 'Process' = None) -> None: - """Add a TestRun replaying one or more phases, in replay file order. - - :param summary: TestRun description. - :param phases: space separated verifier-client keys. - :param ts: proxy to replay against, defaulting to the ordinary one. - """ - ts = self._ts if ts is None else ts - tr = Test.AddTestRun(summary) - if not self._started: - tr.Processes.Default.StartBefore(self._origin) - tr.Processes.Default.StartBefore(self._ts) - tr.Processes.Default.StartBefore(self._ts_bound) - self._started = True - tr.AddVerifierClientProcess( - f"client-{phases.replace(' ', '-')}", self._client_replay, http_ports=[ts.Variables.port], keys=phases) - tr.StillRunningAfter = ts - - def _origin_saw(self, phase: str, block: str, why: str) -> None: - """Assert the origin was asked for a block under a given phase.""" - self._origin.Streams.stdout += Testers.ContainsExpression(f'request with key /{block}{phase}', why) - - def _fill(self, summary: str, url: str, blocks: list, ts: 'Process' = None) -> None: - """Cache the given blocks of an object, and prove each fill happened. - - The fill assertions carry weight: a check phase only observes that the - origin was asked, which is equally true of a block that was purged and one - that was never cached at all. Asserting the fill reached the origin is what - makes the later check mean "removed" rather than merely "absent". - """ - self._run(summary, ' '.join(f'{url}-fill-{block}' for block in blocks), ts) - for block in blocks: - self._origin_saw( - f'{url}-fill-{block}', f'{url}bytes={block * 10}-{block * 10 + 9}', - f'Block {block} of /{url} should have been fetched and cached.') - - def _purged(self, phase: str, block: str, why: str) -> None: - """Assert a phase's block request reached the origin, so it was purged.""" - self._origin_saw(phase, block, why) - - def _survived(self, phase: str, block: str, why: str) -> None: - """Assert a phase's block request never reached the origin, so it survived. - - The origin has no transaction registered for such a phase either, so a - wrongly purged block fails the client's own expectation as well. - """ - self._origin.Streams.stdout += Testers.ExcludesExpression(f'request with key /{block}{phase}', why) - - def _gap_mid_walk(self) -> None: - """A 404 in the middle of the walk must not end it.""" - self._fill('Cache blocks 0 and 2 of /hole, leaving block 1 uncached', 'hole', [0, 2]) - self._run('PURGE the whole /hole object', 'hole-purge') - self._run('Both cached blocks of /hole were purged', 'hole-check-0 hole-check-2') - self._purged('hole-check-0', 'holebytes=0-9', 'Block 0 is in front of the gap, so it should be purged.') - self._purged('hole-check-2', 'holebytes=20-29', 'A PURGE should traverse blocks behind an uncached one.') - - def _uncached_first_block(self) -> None: - """A miss on the first block must not stop the purge before it starts.""" - self._fill('Cache only block 1 of /nofirst', 'nofirst', [1]) - self._run('PURGE /nofirst, whose block 0 is not cached', 'nofirst-purge') - self._run('Block 1 of /nofirst was purged', 'nofirst-check-1') - self._purged('nofirst-check-1', 'nofirstbytes=10-19', 'An uncached first block should not stop the purge.') - - def _largest_extent_wins(self) -> None: - """A block reporting a longer object widens the walk.""" - self._fill('Cache blocks 0, 1 and 4 of /mixed, disagreeing about its length', 'mixed', [0, 1, 4]) - self._run('PURGE /mixed', 'mixed-purge') - self._run('Block 4 of /mixed was purged, so the walk took the longer extent', 'mixed-check-4') - self._purged('mixed-check-4', 'mixedbytes=40-49', 'The walk should follow the largest extent any block reports.') - - def _closed_range(self) -> None: - """A closed range bounds the walk itself, isolating the 404 step-over.""" - self._fill('Cache blocks 0 and 2 of /ranged, leaving block 1 uncached', 'ranged', [0, 2]) - self._run('PURGE /ranged with a closed range spanning the whole object', 'ranged-purge') - self._run('Both cached blocks of /ranged were purged', 'ranged-check-0 ranged-check-2') - self._purged('ranged-check-0', 'rangedbytes=0-9', 'A closed range purge should remove the blocks it covers.') - self._purged('ranged-check-2', 'rangedbytes=20-29', 'A 404 should not end a closed range purge either.') - - def _open_ended_range(self) -> None: - """A "bytes=N-" purge states its start, so it purges only what it names.""" - self._fill('Cache blocks 0 and 2 of /openend', 'openend', [0, 2]) - self._run('PURGE /openend from byte 20 on', 'openend-purge') - self._run('Block 2 of /openend went and block 0 stayed', 'openend-check-2 openend-check-0') - self._purged('openend-check-2', 'openendbytes=20-29', 'The block covering the range should be purged.') - self._survived('openend-check-0', 'openendbytes=0-9', 'A purge should not remove blocks before its stated start.') - - def _range_bounds_the_walk(self) -> None: - """A range ends the walk even before any block has reported an extent. - - /outside has only block 1 cached and is purged for block 0 alone. Nothing - is ever removed, so no block reports an extent, and the miss bound would - otherwise carry the walk past the end of the named range and into a block - the client never asked to remove. - """ - self._fill('Cache only block 1 of /outside', 'outside', [1]) - self._run('PURGE /outside for block 0 only, which is not cached', 'outside-purge') - self._run('Block 1 of /outside survived a purge that did not name it', 'outside-check-1') - self._survived( - 'outside-check-1', 'outsidebytes=10-19', 'A purge should stop at the end of its range, not at the miss bound.') - - def _suffix_range(self) -> None: - """A "bytes=-N" purge is widened to the whole object.""" - self._fill('Cache blocks 0 and 2 of /endbytes', 'endbytes', [0, 2]) - self._run('PURGE the last 10 bytes of /endbytes', 'endbytes-purge') - self._run('Every cached block of /endbytes went, not just the named tail', 'endbytes-check-0 endbytes-check-2') - self._purged('endbytes-check-2', 'endbytesbytes=20-29', 'The block covering the suffix range must be purged.') - self._purged( - 'endbytes-check-0', 'endbytesbytes=0-9', - 'A widened suffix purge removes the whole object, which is a superset of what was named.') - - def _miss_bound_and_override(self) -> None: - """The miss bound stops a walk that has found nothing, and is overridable. - - /sparse has only block 4 of five cached, out of reach of this proxy's - configured bound of two. Nothing about the remap changes between the two - purges below; only the request header does. - """ - ts = self._ts_bound - self._fill('Cache only block 4 of /sparse, out of reach of the configured bound', 'sparse', [4], ts) - - self._run('A purge with a malformed override falls back to the configured bound', 'sparse-purge-narrow', ts) - self._run('Block 4 of /sparse survived the too-narrow purge', 'sparse-check-alive', ts) - self._survived('sparse-check-alive', 'sparsebytes=40-49', 'A walk that gave up before block 4 should not have purged it.') - - self._run('PURGE /sparse with an override wide enough to reach block 4', 'sparse-purge-wide', ts) - self._run('Block 4 of /sparse was purged once the bound reached it', 'sparse-check-gone', ts) - self._purged('sparse-check-gone', 'sparsebytes=40-49', 'A request supplied bound should let the walk reach block 4.') - - def _unparseable_range(self) -> None: - """A purge whose range cannot be parsed is refused, not guessed at. - - An unparseable range leaves the plugin's range covering block 0 only, so - walking it would delete the head of the object and report success. A purge - is destructive, so it is rejected instead. - """ - self._fill('Cache block 0 of /badrange', 'badrange', [0]) - self._run('A PURGE with an unparseable range is refused', 'badrange-purge') - self._run('Block 0 of /badrange survived the refused purge', 'badrange-check-0') - self._survived('badrange-check-0', 'badrangebytes=0-9', 'A refused purge must not have removed anything.') - self._ts.Disk.diags_log.Content = Testers.ContainsExpression( - 'Refusing PURGE with an unparseable range', 'The refusal should be visible in the error log.') - - def _block_failure(self) -> None: - """A block that could not be purged is not a block that was absent. - - Only a 404 says the block was not cached. Any other status says the walk could - not tell, and says nothing about the blocks behind it either, so it stops there - and the client hears that status rather than the 200 the earlier blocks earned. - Both failures are injected with header_rewrite, since the core answers a block - PURGE with nothing but 200 or 404 on its own. - """ - self._fill('Cache blocks 0 and 3 of /failblock', 'failblock', [0, 3]) - self._run('PURGE /failblock, whose block 1 answers 500', 'failblock-purge') - self._run('/failblock was purged up to the failing block only', 'failblock-check-0 failblock-check-3') - self._purged('failblock-check-0', 'failblockbytes=0-9', 'A block removed before the failure should stay removed.') - self._survived( - 'failblock-check-3', 'failblockbytes=30-39', - 'The walk should stop at the failing block, leaving what is behind it cached.') - self._ts.Disk.diags_log.Content += Testers.ContainsExpression( - 'Purge of block 1 failed', 'The block that ended the walk should be logged.') - - self._fill('Cache block 0 of /denied', 'denied', [0]) - self._run('PURGE /denied, whose every block answers 403', 'denied-purge') - self._run('Block 0 of /denied survived the refused purge', 'denied-check-0') - self._survived('denied-check-0', 'deniedbytes=0-9', 'A purge refused at its first block must not remove anything.') - self._ts.Disk.diags_log.Content += Testers.ContainsExpression( - 'Purge of block 0 failed', 'A purge refused outright should be logged rather than reported as a 404.') - - def run(self) -> None: - """Configure the test runs.""" - self._gap_mid_walk() - self._uncached_first_block() - self._largest_extent_wins() - self._closed_range() - self._open_ended_range() - self._range_bounds_the_walk() - self._suffix_range() - self._unparseable_range() - self._miss_bound_and_override() - self._block_failure() - - -SlicePurgeGapsTest().run() diff --git a/tests/gold_tests/pluginTest/slice/slice_regex.test.py b/tests/gold_tests/pluginTest/slice/slice_regex.test.py deleted file mode 100644 index abc0d40dded..00000000000 --- a/tests/gold_tests/pluginTest/slice/slice_regex.test.py +++ /dev/null @@ -1,147 +0,0 @@ -''' -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -Test.Summary = ''' -slice regex plugin test -''' - -# Test description: -# Preload the cache with the entire asset to be range requested. -# Reload remap rule with slice plugin -# Request content through the slice plugin - -Test.SkipUnless(Condition.PluginExists('slice.so'),) -Test.ContinueOnFail = False - -# configure origin server -server = Test.MakeOriginServer("server") - -# Define ATS and configure. -ts = Test.MakeATSProcess("ts", enable_cache=False) - -# default root -request_header_chk = { - "headers": "GET / HTTP/1.1\r\n" + "Host: www.example.com\r\n" + "\r\n", - "timestamp": "1469733493.993", - "body": "", -} - -response_header_chk = { - "headers": "HTTP/1.1 200 OK\r\n" + "Connection: close\r\n" + "\r\n", - "timestamp": "1469733493.993", - "body": "", -} - -server.addResponse("sessionlog.json", request_header_chk, response_header_chk) - -body = "lets go surfin now" - -request_header_txt = { - "headers": "GET /slice.txt HTTP/1.1\r\n" + "Host: slice\r\n" + "\r\n", - "timestamp": "1469733493.993", - "body": "", -} - -response_header_txt = { - "headers": - "HTTP/1.1 200 OK\r\n" + "Connection: close\r\n" + 'Etag: "path"\r\n' + "Cache-Control: max-age=500\r\n" + - "X-Info: notsliced\r\n" + "\r\n", - "timestamp": "1469733493.993", - "body": body, -} - -server.addResponse("sessionlog.json", request_header_txt, response_header_txt) - -request_header_mp4 = { - "headers": "GET /slice.mp4 HTTP/1.1\r\n" + "Host: sliced\r\n" + "Range: bytes=0-99\r\n" - "\r\n", - "timestamp": "1469733493.993", - "body": "", -} - -response_header_mp4 = { - "headers": - "HTTP/1.1 206 Partial Content\r\n" + "Connection: close\r\n" + 'Etag: "path"\r\n' + - "Content-Range: bytes 0-{}/{}\r\n".format(len(body) - 1, len(body)) + "Cache-Control: max-age=500\r\n" + - "X-Info: sliced\r\n" + "\r\n", - "timestamp": "1469733493.993", - "body": body, -} - -server.addResponse("sessionlog.json", request_header_mp4, response_header_mp4) - -curl_and_args = '-s -D /dev/stdout -o /dev/stderr -x localhost:{} -H "x-debug: x-cache"'.format(ts.Variables.port) - -block_bytes = 100 - -# set up whole asset fetch into cache -ts.Disk.remap_config.AddLines( - [ - 'map http://exclude/ http://127.0.0.1:{}/'.format(server.Variables.Port) + ' @plugin=slice.so' + - ' @pparam=--blockbytes-test={}'.format(block_bytes) + ' @pparam=--exclude-regex=\\.txt' - ' @pparam=--remap-host=sliced', - 'map http://include/ http://127.0.0.1:{}/'.format(server.Variables.Port) + ' @plugin=slice.so' + - ' @pparam=--blockbytes-test={}'.format(block_bytes) + ' @pparam=--include-regex=\\.mp4' - ' @pparam=--remap-host=sliced', - 'map http://sliced/ http://127.0.0.1:{}/'.format(server.Variables.Port), - ]) - -# minimal configuration -ts.Disk.records_config.update( - { - # 'proxy.config.diags.debug.enabled': 1, - # 'proxy.config.diags.debug.tags': 'slice', - 'proxy.config.http.insert_age_in_response': 0, - 'proxy.config.http.response_via_str': 0, - }) - -# 0 Test - Exclude: ensure txt passes through -tr = Test.AddTestRun("Exclude - asset passed through") -ps = tr.Processes.Default -ps.StartBefore(server, ready=When.PortOpen(server.Variables.Port)) -ps.StartBefore(Test.Processes.ts) -tr.MakeCurlCommand(curl_and_args + ' http://exclude/slice.txt', ts=ts) -ps.ReturnCode = 0 -ps.Streams.stdout.Content = Testers.ContainsExpression("X-Info: notsliced", "expected not sliced header") -tr.StillRunningAfter = ts - -# 1 Test - Exclude mp4 gets sliced -tr = Test.AddTestRun("Exclude - asset is sliced") -ps = tr.Processes.Default -tr.MakeCurlCommand(curl_and_args + ' http://exclude/slice.mp4', ts=ts) -ps.ReturnCode = 0 -ps.Streams.stdout.Content = Testers.ContainsExpression("X-Info: sliced", "expected sliced header") -tr.StillRunningAfter = ts -tr.StillRunningAfter = ts - -# 2 Test - Exclude: ensure txt passes through -tr = Test.AddTestRun("Include - asset passed through") -ps = tr.Processes.Default -tr.MakeCurlCommand(curl_and_args + ' http://include/slice.txt', ts=ts) -ps.ReturnCode = 0 -ps.Streams.stdout.Content = Testers.ContainsExpression("X-Info: notsliced", "expected not sliced header") -tr.StillRunningAfter = ts - -# 3 Test - Exclude mp4 gets sliced -tr = Test.AddTestRun("Include - asset is sliced") -ps = tr.Processes.Default -tr.MakeCurlCommand(curl_and_args + ' http://include/slice.mp4', ts=ts) -ps.ReturnCode = 0 -ps.Streams.stdout.Content = Testers.ContainsExpression("X-Info: sliced", "expected sliced header") -tr.StillRunningAfter = ts -tr.StillRunningAfter = ts diff --git a/tests/gold_tests/pluginTest/slice/slice_rm_range.test.py b/tests/gold_tests/pluginTest/slice/slice_rm_range.test.py deleted file mode 100644 index 7c122fac390..00000000000 --- a/tests/gold_tests/pluginTest/slice/slice_rm_range.test.py +++ /dev/null @@ -1,69 +0,0 @@ -''' -Verify ATS slice plugin config: @pparam=--strip-range-for-head -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -Test.Summary = ''' -Verify ATS slice plugin config: @pparam=--strip-range-for-head -''' -Test.SkipUnless( - Condition.PluginExists('slice.so'), - Condition.PluginExists('cache_range_requests.so'), -) - - -class SliceStripRangeForHeadRequestTest: - replay_file = "replay/slice_range.replay.yaml" - - def __init__(self): - """Initialize the Test processes for the test runs.""" - self._server = Test.MakeVerifierServerProcess("server", SliceStripRangeForHeadRequestTest.replay_file) - self._configure_trafficserver() - - def _configure_trafficserver(self): - """Configure Traffic Server.""" - self._ts = Test.MakeATSProcess("ts", enable_cache=False) - - self._ts.Disk.remap_config.AddLines( - [ - f"map /no/range http://127.0.0.1:{self._server.Variables.http_port} \ - @plugin=slice.so @pparam=--blockbytes-test=10 @pparam=--strip-range-for-head \ - @plugin=cache_range_requests.so", - f"map /with/range http://127.0.0.1:{self._server.Variables.http_port} \ - @plugin=slice.so @pparam=--blockbytes-test=10 \ - @plugin=cache_range_requests.so", - ]) - - self._ts.Disk.records_config.update( - { - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'http|slice|cache_range_requests', - }) - - def _test_head_request_range_header(self): - tr = Test.AddTestRun() - - tr.AddVerifierClientProcess("client", SliceStripRangeForHeadRequestTest.replay_file, http_ports=[self._ts.Variables.port]) - - tr.Processes.Default.StartBefore(self._server) - tr.Processes.Default.StartBefore(self._ts) - - def run(self): - self._test_head_request_range_header() - - -SliceStripRangeForHeadRequestTest().run() diff --git a/tests/gold_tests/pluginTest/slice/slice_selfhealing.test.py b/tests/gold_tests/pluginTest/slice/slice_selfhealing.test.py deleted file mode 100644 index 5f2ef1a70b8..00000000000 --- a/tests/gold_tests/pluginTest/slice/slice_selfhealing.test.py +++ /dev/null @@ -1,423 +0,0 @@ -''' -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. -import time -import datetime - -Test.Summary = ''' -Slice selfhealing test -''' - - -def to_httpdate(dt): - # string representation of a date according to RFC 1123 (HTTP/1.1). - weekday = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"][dt.weekday()] - month = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"][dt.month - 1] - return "%s, %02d %s %04d %02d:%02d:%02d GMT" % (weekday, dt.day, month, dt.year, dt.hour, dt.minute, dt.second) - - -# Test description: -# Preload the cache with the entire asset to be range requested. -# Reload remap rule with slice plugin -# Request content through the slice plugin - -Test.SkipUnless( - Condition.PluginExists('slice.so'), - Condition.PluginExists('cache_range_requests.so'), - Condition.PluginExists('xdebug.so'), -) -Test.ContinueOnFail = False - -# configure origin server -server = Test.MakeOriginServer("server", lookup_key="{%uuid}") - -# Define ATS and configure -ts = Test.MakeATSProcess("ts") - -# default root -req_header_chk = { - "headers": "GET / HTTP/1.1\r\n" + "Host: www.example.com\r\n" + "uuid: none\r\n" + "\r\n", - "timestamp": "1469733493.993", - "body": "", -} - -res_header_chk = { - "headers": "HTTP/1.1 200 OK\r\n" + "Connection: close\r\n" + "\r\n", - "timestamp": "1469733493.993", - "body": "", -} - -server.addResponse("sessionlog.json", req_header_chk, res_header_chk) - -# set up slice plugin with remap host into cache_range_requests -ts.Disk.remap_config.AddLines( - [ - f'map http://slice/ http://127.0.0.1:{server.Variables.Port}/' + - ' @plugin=slice.so @pparam=--blockbytes-test=3 @pparam=--remap-host=crr', - f'map http://crr/ http://127.0.0.1:{server.Variables.Port}/' + ' @plugin=cache_range_requests.so @pparam=--consider-ident', - f'map http://slicehdr/ http://127.0.0.1:{server.Variables.Port}/' + ' @plugin=slice.so @pparam=--blockbytes-test=3' + - ' @pparam=--remap-host=crrhdr @pparam=--crr-ident-header=crr-foo', - f'map http://crrhdr/ http://127.0.0.1:{server.Variables.Port}/' - ' @plugin=cache_range_requests.so @pparam=--ident-header=crr-foo', - ]) - -ts.Disk.plugin_config.AddLine('xdebug.so --enable=x-cache') - -ts.Disk.records_config.update( - { - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'cache_range_requests|slice', - }) - -curl_and_args = '-s -D /dev/stdout -o /dev/stderr -x localhost:{}'.format(ts.Variables.port) + ' -H "x-debug: x-cache"' - -# Test case: 2nd slice out of date (refetch and continue) - -req_header_2ndold1 = { - "headers": "GET /second HTTP/1.1\r\n" + "Host: www.example.com\r\n" + "uuid: etagold-1\r\n" + "Range: bytes=3-5\r\n" - "\r\n", - "timestamp": "1469733493.993", - "body": "", -} - -res_header_2ndold1 = { - "headers": - "HTTP/1.1 206 Partial Content\r\n" + "Accept-Ranges: bytes\r\n" + "Cache-Control: max-age=5000\r\n" + - "Connection: close\r\n" + "Content-Range: bytes 3-4/5\r\n" + 'Etag: "etagold"\r\n' + "\r\n", - "timestamp": "1469733493.993", - "body": "aa" -} - -server.addResponse("sessionlog.json", req_header_2ndold1, res_header_2ndold1) - -req_header_2ndnew0 = { - "headers": "GET /second HTTP/1.1\r\n" + "Host: www.example.com\r\n" + "uuid: etagnew-0\r\n" + "Range: bytes=0-2\r\n" - "\r\n", - "timestamp": "1469733493.993", - "body": "", -} - -res_header_2ndnew0 = { - "headers": - "HTTP/1.1 206 Partial Content\r\n" + "Accept-Ranges: bytes\r\n" + "Cache-Control: max-age=5000\r\n" + - "Connection: close\r\n" + "Content-Range: bytes 0-2/5\r\n" + 'Etag: "etagnew"\r\n' + "\r\n", - "timestamp": "1469733493.993", - "body": "bbb" -} - -server.addResponse("sessionlog.json", req_header_2ndnew0, res_header_2ndnew0) - -req_header_2ndnew1 = { - "headers": "GET /second HTTP/1.1\r\n" + "Host: www.example.com\r\n" + "uuid: etagnew-1\r\n" + "Range: bytes=3-5\r\n" - "\r\n", - "timestamp": "1469733493.993", - "body": "", -} - -res_header_2ndnew1 = { - "headers": - "HTTP/1.1 206 Partial Content\r\n" + "Accept-Ranges: bytes\r\n" + "Cache-Control: max-age=5000\r\n" + - "Connection: close\r\n" + "Content-Range: bytes 3-4/5\r\n" + 'Etag: "etagnew"\r\n' + "\r\n", - "timestamp": "1469733493.993", - "body": "bb" -} - -server.addResponse("sessionlog.json", req_header_2ndnew1, res_header_2ndnew1) - -# 0 Test - Preload reference etagnew-0 -tr = Test.AddTestRun("Preload reference etagnew-0") -ps = tr.Processes.Default -ps.StartBefore(server, ready=When.PortOpen(server.Variables.Port)) -ps.StartBefore(Test.Processes.ts) -tr.MakeCurlCommand(curl_and_args + ' http://crr/second -r 0-2 -H "uuid: etagnew-0"', ts=ts) -ps.ReturnCode = 0 -ps.Streams.stderr = "gold/bbb.gold" -ps.Streams.stdout.Content = Testers.ContainsExpression("etagnew", "expected etagnew") -tr.StillRunningAfter = ts - -# 1 Test - Preload slice etagold-1 -tr = Test.AddTestRun("Preload slice etagold-1") -ps = tr.Processes.Default -tr.MakeCurlCommand(curl_and_args + ' http://crr/second -r 3-5 -H "uuid: etagold-1"', ts=ts) -ps.ReturnCode = 0 -ps.Streams.stderr = "gold/aa.gold" -ps.Streams.stdout.Content = Testers.ContainsExpression("etagold", "expected etagold") -tr.StillRunningAfter = ts - -# 2 Test - Request second slice via slice plugin, with instructions to fetch new 2nd slice -tr = Test.AddTestRun("Request 2nd slice (expect refetch)") -ps = tr.Processes.Default -tr.MakeCurlCommand(curl_and_args + ' http://slice/second -r 3- -H "uuid: etagnew-1"', ts=ts) -ps.ReturnCode = 0 -ps.Streams.stderr = "gold/bb.gold" -ps.Streams.stdout.Content = Testers.ContainsExpression("etagnew", "expected etagnew") -tr.StillRunningAfter = ts - -# 3 Test - Request fully healed asset via slice plugin -tr = Test.AddTestRun("Request full healed slice") -ps = tr.Processes.Default -tr.MakeCurlCommand(curl_and_args + ' http://slice/second', ts=ts) -ps.ReturnCode = 0 -ps.Streams.stderr.Content = Testers.ContainsExpression("bbbbb", "expected bbbbb content") -ps.Streams.stdout.Content = Testers.ContainsExpression("etagnew", "expected etagnew") -tr.StillRunningAfter = ts - -# Test case: reference slice out of date (abort connection, heal reference) - -req_header_refold0 = { - "headers": "GET /reference HTTP/1.1\r\n" + "Host: www.example.com\r\n" + "uuid: etagold-0\r\n" + "Range: bytes=0-2\r\n" - "\r\n", - "timestamp": "1469733493.993", - "body": "", -} - -res_header_refold0 = { - "headers": - "HTTP/1.1 206 Partial Content\r\n" + "Accept-Ranges: bytes\r\n" + "Cache-Control: max-age=5000\r\n" + - "Connection: close\r\n" + "Content-Range: bytes 0-2/5\r\n" + 'Etag: "etagold"\r\n' + "\r\n", - "timestamp": "1469733493.993", - "body": "aaa" -} - -server.addResponse("sessionlog.json", req_header_refold0, res_header_refold0) - -req_header_refnew0 = { - "headers": "GET /reference HTTP/1.1\r\n" + "Host: www.example.com\r\n" + "uuid: etagnew-0\r\n" + "Range: bytes=0-2\r\n" - "\r\n", - "timestamp": "1469733493.993", - "body": "", -} - -res_header_refnew0 = { - "headers": - "HTTP/1.1 206 Partial Content\r\n" + "Accept-Ranges: bytes\r\n" + "Cache-Control: max-age=5000\r\n" + - "Connection: close\r\n" + "Content-Range: bytes 0-2/5\r\n" + 'Etag: "etagnew"\r\n' + "\r\n", - "timestamp": "1469733493.993", - "body": "bbb" -} - -server.addResponse("sessionlog.json", req_header_refnew0, res_header_refnew0) - -req_header_refnew1 = { - "headers": "GET /reference HTTP/1.1\r\n" + "Host: www.example.com\r\n" + "uuid: etagnew-1\r\n" + "Range: bytes=3-5\r\n" - "\r\n", - "timestamp": "1469733493.993", - "body": "", -} - -res_header_refnew1 = { - "headers": - "HTTP/1.1 206 Partial Content\r\n" + "Accept-Ranges: bytes\r\n" + "Cache-Control: max-age=5000\r\n" + - "Connection: close\r\n" + "Content-Range: bytes 3-4/5\r\n" + 'Etag: "etagnew"\r\n' + "\r\n", - "timestamp": "1469733493.993", - "body": "bb" -} - -server.addResponse("sessionlog.json", req_header_refnew1, res_header_refnew1) - -# 4 Test - Preload reference etagold-0 -tr = Test.AddTestRun("Preload reference etagold-0") -ps = tr.Processes.Default -tr.MakeCurlCommand(curl_and_args + ' http://crr/reference -r 0-2 -H "uuid: etagold-0"', ts=ts) -ps.ReturnCode = 0 -ps.Streams.stderr = "gold/aaa.gold" -ps.Streams.stdout.Content = Testers.ContainsExpression("etagold", "expected etagold") -tr.StillRunningAfter = ts - -# 5 Test - Preload slice 1 etagnew-1 -tr = Test.AddTestRun("Preload slice etagnew-1") -ps = tr.Processes.Default -tr.MakeCurlCommand(curl_and_args + ' http://crr/reference -r 3-5 -H "uuid: etagnew-1"', ts=ts) -ps.ReturnCode = 0 -ps.Streams.stderr = "gold/bb.gold" -ps.Streams.stdout.Content = Testers.ContainsExpression("etagnew", "expected etagnew") -tr.StillRunningAfter = ts - -# 6 Test - Request reference slice via slice plugin, with instructions to -# fetch new 2nd slice -- this will send the old header, but abort and -# refetch it -tr = Test.AddTestRun("Request 2nd slice (expect abort)") -ps = tr.Processes.Default -tr.MakeCurlCommand(curl_and_args + ' http://slice/reference -r 3- -H "uuid: etagnew-0" -w "SENT: \'%{size_download}\'"', ts=ts) -# ps.ReturnCode = 0 # curl will fail here -ps.Streams.stdout.Content = Testers.ContainsExpression("etagold", "expected etagold") -ps.Streams.stdout.Content += Testers.ContainsExpression("SENT: '0'", "expected empty payload") -tr.StillRunningAfter = ts - -# 7 Test - Request full healed asset via slice plugin -tr = Test.AddTestRun("Request full healed slice") -ps = tr.Processes.Default -tr.MakeCurlCommand(curl_and_args + ' http://slice/reference', ts=ts) -ps.ReturnCode = 0 -ps.Streams.stderr.Content = Testers.ContainsExpression("bbbbb", "expected bbbbb content") -ps.Streams.stdout.Content = Testers.ContainsExpression("etagnew", "expected etagnew") -tr.StillRunningAfter = ts - -# Request results in 200, not 206 (server not support range requests) - -req_header_200 = { - "headers": "GET /code200 HTTP/1.1\r\n" + "Host: www.example.com\r\n" + "uuid: code200\r\n" + "Range: bytes=3-5\r\n" - "\r\n", - "timestamp": "1469733493.993", - "body": "", -} - -res_header_200 = { - "headers": "HTTP/1.1 200 OK\r\n" + "Cache-Control: max-age=5000\r\n" + "Connection: close\r\n" + 'Etag: "etag"\r\n' + "\r\n", - "timestamp": "1469733493.993", - "body": "ccccc" -} - -server.addResponse("sessionlog.json", req_header_200, res_header_200) - -# 8 test - Request through slice but get a 200 back -tr = Test.AddTestRun("Request gets a 200") -ps = tr.Processes.Default -tr.MakeCurlCommand(curl_and_args + ' http://slice/code200 -r 3-5 -H "uuid: code200"', ts=ts) -ps.ReturnCode = 0 -ps.Streams.stderr.Content = Testers.ContainsExpression("ccccc", "expected full ccccc content") -ps.Streams.stdout.Content = Testers.ContainsExpression("200 OK", "expected 200") -tr.StillRunningAfter = ts - -# Test for asset gone - -# Preload -req_header_assetgone0 = { - "headers": "GET /assetgone HTTP/1.1\r\n" + "Host: www.example.com\r\n" + "uuid: assetgone-0\r\n" + "Range: bytes=0-2\r\n" - "\r\n", - "timestamp": "1469733493.993", - "body": "", -} - -res_header_assetgone0 = { - "headers": - "HTTP/1.1 206 Partial Content\r\n" + "Accept-Ranges: bytes\r\n" + "Cache-Control: max-age=5000\r\n" + - "Connection: close\r\n" + "Content-Range: bytes 0-2/5\r\n" + 'Etag: "etag"\r\n' + "\r\n", - "timestamp": "1469733493.993", - "body": "aaa" -} - -server.addResponse("sessionlog.json", req_header_assetgone0, res_header_assetgone0) - -# 9 test - Preload reference slice -tr = Test.AddTestRun("Preload reference assetgone-0") -ps = tr.Processes.Default -tr.MakeCurlCommand(curl_and_args + ' http://slice/assetgone -r 0-2 -H "uuid: assetgone-0"', ts=ts) -ps.ReturnCode = 0 -ps.Streams.stderr = "gold/aaa.gold" -ps.Streams.stdout.Content = Testers.ContainsExpression("etag", "expected etag") -tr.StillRunningAfter = ts - -# 10 test - Fetch full asset, 2nd slice should trigger 404 response -tr = Test.AddTestRun("Fetch full asset") -ps = tr.Processes.Default -tr.MakeCurlCommand(curl_and_args + ' http://slice/assetgone', ts=ts) -# ps.ReturnCode = 0 # curl will return non zero -ps.Streams.stderr = "gold/aaa.gold" -ps.Streams.stdout.Content = Testers.ContainsExpression("etag", "expected etag") -ps.Streams.stdout.Content += Testers.ContainsExpression("Content-Length: 5", "expected header of content-length 5") -tr.StillRunningAfter = ts - -# 11 test - Fetch full asset again, full blown 404 -tr = Test.AddTestRun("Fetch full asset, 404") -ps = tr.Processes.Default -tr.MakeCurlCommand(curl_and_args + ' http://slice/assetgone', ts=ts) -# ps.ReturnCode = 0 # curl will return non zero -ps.Streams.stdout.Content = Testers.ContainsExpression("404 Not Found", "Expected 404") -tr.StillRunningAfter = ts - -## custom headers - -# Test case: 2nd slice out of date (refetch and continue) - -req_header_custom_2ndold1 = { - "headers": - "GET /second-custom HTTP/1.1\r\n" + "Host: www.example.com\r\n" + "uuid: etagold-custom-1\r\n" + "Range: bytes=3-5\r\n" - "\r\n", - "timestamp": "1469733493.993", - "body": "", -} - -res_header_custom_2ndold1 = { - "headers": - "HTTP/1.1 206 Partial Content\r\n" + "Accept-Ranges: bytes\r\n" + "Cache-Control: max-age=5000\r\n" + - "Connection: close\r\n" + "Content-Range: bytes 3-4/5\r\n" + 'Etag: "etagold-custom"\r\n' + "\r\n", - "timestamp": "1469733493.993", - "body": "aa" -} - -server.addResponse("sessionlog.json", req_header_custom_2ndold1, res_header_custom_2ndold1) - -req_header_custom_2ndnew0 = { - "headers": - "GET /second-custom HTTP/1.1\r\n" + "Host: www.example.com\r\n" + "uuid: etagnew-custom-0\r\n" + "Range: bytes=0-2\r\n" - "\r\n", - "timestamp": "1469733493.993", - "body": "", -} - -res_header_custom_2ndnew0 = { - "headers": - "HTTP/1.1 206 Partial Content\r\n" + "Accept-Ranges: bytes\r\n" + "Cache-Control: max-age=5000\r\n" + - "Connection: close\r\n" + "Content-Range: bytes 0-2/5\r\n" + 'Etag: "etagnew-custom"\r\n' + "\r\n", - "timestamp": "1469733493.993", - "body": "bbb" -} - -server.addResponse("sessionlog.json", req_header_custom_2ndnew0, res_header_custom_2ndnew0) - -req_header_custom_2ndnew1 = { - "headers": - "GET /second-custom HTTP/1.1\r\n" + "Host: www.example.com\r\n" + "uuid: etagnew-custom-1\r\n" + "Range: bytes=3-5\r\n" - "\r\n", - "timestamp": "1469733493.993", - "body": "", -} - -res_header_custom_2ndnew1 = { - "headers": - "HTTP/1.1 206 Partial Content\r\n" + "Accept-Ranges: bytes\r\n" + "Cache-Control: max-age=5000\r\n" + - "Connection: close\r\n" + "Content-Range: bytes 3-4/5\r\n" + 'Etag: "etagnew-custom"\r\n' + "\r\n", - "timestamp": "1469733493.993", - "body": "bb" -} - -server.addResponse("sessionlog.json", req_header_custom_2ndnew1, res_header_custom_2ndnew1) - -edt = datetime.datetime.fromtimestamp(time.time() + 100) -edate = to_httpdate(edt) - -# 12 Test - Preload reference etagold-1 -tr = Test.AddTestRun("Preload slice etagold-custom-1") -ps = tr.Processes.Default -tr.MakeCurlCommand(curl_and_args + f' http://crrhdr/second-custom -r 3-5 -H "uuid: etagold-custom-1" -H "crr-foo: {edate}"', ts=ts) -ps.ReturnCode = 0 -ps.Streams.stderr = "gold/aa.gold" -ps.Streams.stdout.Content = Testers.ContainsExpression("etagold-custom", "expected etagold-custom") -tr.StillRunningAfter = ts - -# 13 Test - Request second slice via slice plugin, with instructions to fetch new 2nd slice -tr = Test.AddTestRun("Request 2nd slice (expect refetch)") -ps = tr.Processes.Default -tr.MakeCurlCommand(curl_and_args + ' http://slicehdr/second-custom -r 3- -H "uuid: etagnew-custom-1"', ts=ts) -ps.ReturnCode = 0 -ps.Streams.stderr = "gold/bb.gold" -ps.Streams.stdout.Content = Testers.ContainsExpression("etagnew-custom", "expected etagnew-custom") -tr.StillRunningAfter = ts - -# Over riding the built in ERROR check since we expect to see logSliceErrors -ts.Disk.diags_log.Content = Testers.ContainsExpression("logSliceError", "logSliceErrors generated") diff --git a/tests/gold_tests/pluginTest/slice/slice_stale_generation.test.py b/tests/gold_tests/pluginTest/slice/slice_stale_generation.test.py deleted file mode 100644 index 05381279391..00000000000 --- a/tests/gold_tests/pluginTest/slice/slice_stale_generation.test.py +++ /dev/null @@ -1,327 +0,0 @@ -"""Verify slice serves a stale object identity after the origin object changes.""" - -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -Test.Summary = __doc__ - -Test.SkipUnless( - Condition.PluginExists('slice.so'), - Condition.PluginExists('cache_range_requests.so'), - Condition.PluginExists('xdebug.so'), -) -Test.ContinueOnFail = False - - -class SliceHierarchyTest: - """Build the child/parent hierarchy the incident ran on. - - Both tiers load slice with the same block size, and both put - cache_range_requests behind it, as the affected property does. - - The parent only behaves as a slicing proxy for requests that arrive without - slice's skip header. The child stamps that header onto every block request it - issues (client.cc:66), so slice returns immediately on the parent - (slice.cc:48) and a child block request is handled there by - cache_range_requests alone: look up this exact Range, forward it on a miss, - store whatever 206 comes back. The parent therefore holds N independent - per-Range objects with no shared identity, and cannot notice, refuse or - reconcile a version mix. That is where the mixed set lived in the incident. - - A client hitting the parent directly is a different path: the parent does - slice that request, forms its own reference block and clamps against it. - """ - - _server_replay: str = 'replay/slice_stale_generation_server.replay.yaml' - _client_replay: str = 'replay/slice_stale_generation_client.replay.yaml' - - _block_bytes: int = 16 - - _origin_key_format: str = '--format "{url}{field.range}{field.uuid}"' - - def __init__(self, name: str) -> None: - """Declare the origin, the parent and the child. - - :param name: suffix distinguishing this hierarchy's processes. - """ - self._name = name - self._configure_dns() - self._configure_origin() - self._configure_parent() - self._configure_child() - - def _configure_dns(self) -> None: - """Configure a DNS server so neither tier consults resolv.conf.""" - self._dns = Test.MakeDNServer(f'dns-{self._name}', default='127.0.0.1') - - def _configure_origin(self) -> None: - """Configure the origin. - - The server is keyed on the block's byte range and on the phase uuid that - slice propagates from the client request, so one replay file answers - every block request of every phase and can replace the object between - phases without holding any state. - """ - self._origin = Test.MakeVerifierServerProcess( - f'origin-{self._name}', self._server_replay, other_args=self._origin_key_format) - - def _slice_remap(self, source: str, upstream: str) -> str: - """Build a remap rule carrying slice in front of cache_range_requests.""" - return ( - f'map {source} {upstream}' - f' @plugin=slice.so @pparam=--blockbytes-test={self._block_bytes}' - ' @plugin=cache_range_requests.so') - - def _records(self, ts: 'Process', debug: int) -> None: - """Apply the records.yaml settings common to both tiers.""" - ts.Disk.records_config.update( - { - 'proxy.config.diags.debug.enabled': debug, - 'proxy.config.diags.debug.tags': 'slice|cache_range_requests', - 'proxy.config.dns.nameservers': f'127.0.0.1:{self._dns.Variables.Port}', - 'proxy.config.dns.resolv_conf': 'NULL', - 'proxy.config.http.parent_proxy.self_detect': 0, - }) - - def _configure_parent(self) -> None: - """Configure the parent, which the child's block requests reach.""" - self._parent = Test.MakeATSProcess(f'ts-parent-{self._name}') - self._parent.Disk.remap_config.AddLine( - self._slice_remap('http://origin.test/', f'http://127.0.0.1:{self._origin.Variables.http_port}/')) - self._parent.Disk.plugin_config.AddLine('xdebug.so --enable=x-cache') - self._records(self._parent, debug=1) - - # slice is loaded on the parent, but every block request the child sends - # carries the skip header, so slice returns immediately and the request - # is handled by cache_range_requests alone. Plugin debug output lands in - # traffic.out, not diags.log. - self._parent.Disk.traffic_out.Content = Testers.ContainsExpression( - 'slice passing GET or HEAD request through to next plugin', - "The child's block requests should bypass the parent's slice.") - self._parent.Disk.traffic_out.Content += Testers.ExcludesExpression( - 'slice accepting and slicing', 'The parent should never slice a child block request.') - - def _make_child(self, label: str) -> 'Process': - """Create a child tier that slices and forwards to the parent.""" - ts = Test.MakeATSProcess(f'ts-{label}-{self._name}') - ts.Disk.remap_config.AddLine(self._slice_remap('http://slice/', 'http://origin.test/')) - ts.Disk.parent_config.AddLine( - f'dest_domain=. parent=127.0.0.1:{self._parent.Variables.port}' - ' round_robin=consistent_hash go_direct=false') - ts.Disk.plugin_config.AddLine('xdebug.so --enable=x-cache') - self._records(ts, debug=1) - return ts - - def _configure_child(self) -> None: - """Configure the child, which slices and forwards to the parent.""" - self._child = self._make_child('child') - - def _start_hierarchy(self, tr: 'TestRun') -> None: - """Bring up origin, parent and child for the first TestRun.""" - tr.Processes.Default.StartBefore(self._dns) - tr.Processes.Default.StartBefore(self._origin) - tr.Processes.Default.StartBefore(self._parent) - tr.Processes.Default.StartBefore(self._child) - - def _replay_phase(self, tr: 'TestRun', phase: str, ts: 'Process' = None) -> 'Process': - """Replay the client transaction for one phase against a child.""" - ts = self._child if ts is None else ts - return tr.AddVerifierClientProcess( - f'client-{phase}-{self._name}', self._client_replay, http_ports=[ts.Variables.port], keys=phase) - - def _still_running(self, tr: 'TestRun') -> None: - """Assert both tiers survive the TestRun.""" - tr.StillRunningAfter = self._child - tr.StillRunningAfter = self._parent - - -class SliceStaleGenerationTest(SliceHierarchyTest): - """Verify a cached reference block pins a stale object identity. - - The plugin takes the whole object length from the reference block's - Content-Range and clips the client range to it, so the cached reference - block, not the origin, defines the object's identity for every request:: - - server.cc handleFirstServerHeader: - data->m_contentlen = blockcr.m_length; - data->m_req_range.m_end = std::min(data->m_contentlen, data->m_req_range.m_end); - - Replacing the object under the same URL therefore forks every cache into one - that filled before the replacement and one that filled after, for as long as - the reference block stays fresh. On the stale side, ranges that exist in the - current object are answered against the stale length with the stale ETag: - clipped short, or refused with a 416. Neither path logs a block stitch error, - because handleNextServerHeader only complains when blocks disagree with each - other and here they are uniformly stale. - - Modelled on an incident where a versioned, year-cacheable object was replaced - in place. Two edges seven hours apart on either side of the replacement served - object lengths 4043309056 and 7031250004 for the same URL, the stale one - clipping a 64 MiB range request down to 16 MiB. - """ - - # The reference block the origin holds at the new generation for each phase - # that reads the cached object. The test asserts the origin never gets these. - _unreachable_keys = ('/objbytes=0-15clipped', '/objbytes=0-15unsatisfiable') - - def __init__(self) -> None: - """Declare the hierarchy and its assertions.""" - super().__init__('stale') - - for key in self._unreachable_keys: - self._origin.Streams.stdout += Testers.ExcludesExpression( - f'request with key {key}', 'The stale object should never be refetched after the origin object changed.') - - # Debug is enabled on the child, so Config::canLogError cannot suppress a - # block stitch error by pacing. The stale response is served with none. - self._child.Disk.diags_log.Content = Testers.ExcludesExpression( - 'logSliceError', 'The stale response should be served with no block stitch error.') - self._child.Disk.diags_log.Content += Testers.ExcludesExpression( - 'Mismatch/Bad block Content-Range', 'The stale blocks agree with each other, so nothing should mismatch.') - - def _fill_cache(self) -> None: - """Cache the whole object while the origin holds the first generation.""" - tr = Test.AddTestRun('Cache the object while the origin holds the first generation') - self._start_hierarchy(tr) - self._replay_phase(tr, 'fill') - self._still_running(tr) - - def _verify_clipped_range(self) -> None: - """A range inside the current object is clipped to the stale length.""" - tr = Test.AddTestRun('A range inside the current object is clipped to the stale length') - self._replay_phase(tr, 'clipped') - self._still_running(tr) - - def _verify_unsatisfiable_range(self) -> None: - """A range past the stale length is refused with a 416.""" - tr = Test.AddTestRun('A range past the stale length is refused with a 416') - self._replay_phase(tr, 'unsatisfiable') - self._still_running(tr) - - def _verify_uncached_object(self) -> None: - """An object first fetched after the replacement is served correctly.""" - tr = Test.AddTestRun('An object first fetched after the replacement is served correctly') - self._replay_phase(tr, 'control') - self._still_running(tr) - - def run(self) -> None: - """Configure the test runs.""" - self._fill_cache() - self._verify_clipped_range() - self._verify_unsatisfiable_range() - self._verify_uncached_object() - - -class SliceMixedGenerationTest(SliceHierarchyTest): - """Verify the parent stores a version mix and the child cannot recover. - - The other failure mode from the same origin object replacement, and the one - the parent's per-Range cache makes possible. Only the reference block is - refetched after the replacement, so the parent ends up holding two blocks of - one object at two different generations, both fresh, with nothing to relate - them. It serves each on request without complaint. - - The child is the only tier that compares blocks, and only against its own - reference block. It forms the client response header from the reference block, - which is correct for the current object, and only then discovers that the - interior block belongs to the previous one:: - - server.cc handleNextServerHeader: - if (!blockcr.isValid() || blockcr.m_length != data->m_contentlen) { - logSliceError("Mismatch/Bad block Content-Range", data, header); - - The self heal refetches the reference block, which is already the newest one - the parent holds, so the same block comes back and the interior block still - disagrees with it. The second mismatch is where slice gives up. - - The upstream is aborted. Slice can abort but cannot evict, so the mixed pair - on the parent survives. The final TestRun proves where the damage actually - lives: a second child with a completely cold cache, pointed at the same - parent, fails identically. It never saw the previous generation; it simply - inherits the mix from the one place that holds it. That is the incident's - shape, where all 105 blocks hashed to a single parent and every one of the 32 - child nodes served the same broken object. - """ - - # The reference block is cached with a one second lifetime, so let it expire. - _expiry_wait: int = 2 - - def __init__(self) -> None: - """Declare the hierarchy and its assertions.""" - super().__init__('mixed') - self._cold_child = self._make_child('cold-child') - - # Unlike the stale case, the child does report this one: first the interior - # block against the reference block, then the refetch against the interior. - self._child.Disk.diags_log.Content = Testers.ContainsExpression( - 'Mismatch/Bad block Content-Range.*blk_range="16-31".*etag_got="%22v1%22"', - 'The interior block should disagree with the reference block.') - self._child.Disk.diags_log.Content += Testers.ContainsExpression( - 'Mismatch/Bad block Content-Range.*blk_range="0-15".*etag_got="%22v2%22"', - 'The refetched reference block should disagree in turn, leaving no way out.') - - # The parent never compares blocks, so it never complains about the mix - # it is storing and serving to the child. - self._parent.Disk.diags_log.Content += Testers.ExcludesExpression( - 'logSliceError', 'The parent should not notice the version mix it holds.') - self._parent.Disk.diags_log.Content += Testers.ExcludesExpression( - 'Mismatch/Bad block Content-Range', 'The parent should not compare blocks at all.') - - def _fill_interior_block(self) -> None: - """Cache an interior block at the first generation, on both tiers.""" - tr = Test.AddTestRun('Cache an interior block at the first generation') - self._start_hierarchy(tr) - self._replay_phase(tr, 'fill-interior') - self._still_running(tr) - - def _verify_aborted_response(self) -> None: - """The reference block moves on and the transaction cannot be completed.""" - tr = Test.AddTestRun('A mixed generation object cannot be delivered and the request fails') - client = self._replay_phase(tr, 'mixed') - # Let the reference block go stale so that only it is revalidated. - tr.Processes.Default.Command = f'sleep {self._expiry_wait}; ' + tr.Processes.Default.Command - # Slice aborts the transaction, so the client never reads a response at - # all: not a short body, no response header. verifier-client exits 1. - client.ReturnCode = 1 - client.Streams.stdout += Testers.ContainsExpression( - 'Failed to find a well-formed, completed HTTP response: PARSE_INCOMPLETE', - 'The client should not receive a parsable response.') - client.Streams.stdout += Testers.ContainsExpression( - 'Failed HTTP/1 transaction with key: mixed', 'The transaction should fail.') - self._still_running(tr) - - def _verify_mix_is_on_the_parent(self) -> None: - """A cold child fails identically, because the mix lives on the parent.""" - tr = Test.AddTestRun('A second child with a cold cache inherits the mix from the parent') - tr.Processes.Default.StartBefore(self._cold_child) - client = self._replay_phase(tr, 'cold-child', ts=self._cold_child) - client.ReturnCode = 1 - client.Streams.stdout += Testers.ContainsExpression( - 'Failed HTTP/1 transaction with key: cold-child', 'A node that never saw the old generation should fail the same way.') - self._cold_child.Disk.diags_log.Content = Testers.ContainsExpression( - 'Mismatch/Bad block Content-Range', 'The cold child should hit the same mismatch.') - tr.StillRunningAfter = self._cold_child - self._still_running(tr) - - def run(self) -> None: - """Configure the test runs.""" - self._fill_interior_block() - self._verify_aborted_response() - self._verify_mix_is_on_the_parent() - - -SliceStaleGenerationTest().run() -SliceMixedGenerationTest().run() diff --git a/tests/gold_tests/pluginTest/sslheaders/sslheaders.test.py b/tests/gold_tests/pluginTest/sslheaders/sslheaders.test.py deleted file mode 100644 index b5cdf9b2cc5..00000000000 --- a/tests/gold_tests/pluginTest/sslheaders/sslheaders.test.py +++ /dev/null @@ -1,78 +0,0 @@ -''' -Test the sslheaders plugin. -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -Test.Summary = ''' -Test sslheaders plugin. -''' - -Test.SkipIf(Condition.CurlUsingUnixDomainSocket()) -Test.SkipUnless( - Condition.PluginExists('sslheaders.so'), - Condition.HasCurlFeature('http2'), -) - -Test.Disk.File('sslheaders.log').Content = 'sslheaders.gold' - -server = Test.MakeOriginServer("server", options={'--load': Test.TestDirectory + '/observer.py'}) - -request_header = {"headers": "GET / HTTP/1.1\r\nHost: doesnotmatter\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -server.addResponse("sessionlog.json", request_header, response_header) - -# Disable the cache to make sure each request is forwarded to the origin -# server. -ts = Test.MakeATSProcess("ts", enable_tls=True, enable_cache=False) - -ts.addDefaultSSLFiles() -# ts.addSSLfile("ssl/signer.pem") - -ts.Disk.records_config.update( - { - 'proxy.config.diags.debug.enabled': 0, - 'proxy.config.diags.debug.tags': 'http', - 'proxy.config.proxy_name': 'Poxy_Proxy', # This will be the server name. - 'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir), - 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts.Variables.SSLDir), - 'proxy.config.http.server_ports': - ( - 'ipv4:{0} ipv4:{1}:proto=http2;http:ssl ipv6:{0} ipv6:{1}:proto=http2;http:ssl'.format( - ts.Variables.port, ts.Variables.ssl_port)), - }) - -ts.Disk.ssl_multicert_yaml.AddLines( - """ -ssl_multicert: - - dest_ip: "*" - ssl_cert_name: server.pem - ssl_key_name: server.key -""".split("\n")) - -ts.Disk.remap_config.AddLine('map http://bar.com http://127.0.0.1:{0}'.format(server.Variables.Port)) -ts.Disk.remap_config.AddLine('map https://bar.com http://127.0.0.1:{0}'.format(server.Variables.Port)) - -ts.Disk.plugin_config.AddLine('sslheaders.so SSL-Client-ID=client.subject') - -tr = Test.AddTestRun() -tr.Processes.Default.StartBefore(server) -tr.Processes.Default.StartBefore(Test.Processes.ts) -tr.MakeCurlCommand( - '-H "SSL-Client-ID: My Fake Client ID" --verbose --ipv4 --insecure --header "Host: bar.com"' + - ' https://localhost:{}'.format(ts.Variables.ssl_port), - ts=ts) -tr.Processes.Default.ReturnCode = 0 diff --git a/tests/gold_tests/pluginTest/stale_response/stale_response.test.py b/tests/gold_tests/pluginTest/stale_response/stale_response.test.py deleted file mode 100644 index 246b55daccd..00000000000 --- a/tests/gold_tests/pluginTest/stale_response/stale_response.test.py +++ /dev/null @@ -1,189 +0,0 @@ -''' -Verify correct stale_response plugin behavior -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -from enum import Enum -import os - -Test.Summary = ''' -Verify correct stale_response plugin behavior -''' - -Test.SkipUnless(Condition.PluginExists('stale_response.so'),) - - -class OptionType(Enum): - """Describe what options to pass to the plugin.""" - - NONE = 0 - DEFAULT_DIRECTIVES = 1 - FORCE_SWR = 2 - FORCE_SIE = 3 - MAX_MEMORY_USAGE = 4 - - -assert len({option.value for option in OptionType.__members__.values()}) == len(OptionType.__members__) - - -class TestStaleResponse: - """Verify correct stale_response.so plugin behavior.""" - - _replay_file: str - _ts_counter: int = 0 - _server_counter: int = 0 - _client_counter: int = 0 - - def __init__(self, option_type: OptionType, is_global: bool) -> None: - """Initialize the test. - - :param option_type: The type of options to pass to the stale_response plugin. - """ - self._option_type = option_type - self._is_global = is_global - - plugin_type_description = "global" if is_global else "per-remap" - if option_type == OptionType.NONE: - self._replay_file = "stale_response_no_default.replay.yaml" - option_description = f"no stale_response plugin options: {plugin_type_description}" - elif option_type == OptionType.DEFAULT_DIRECTIVES: - self._replay_file = "stale_response_with_defaults.replay.yaml" - option_description = f"--stale-while-revalidate-default 30 --stale-if-error-default 30: {plugin_type_description}" - elif option_type == OptionType.FORCE_SWR: - self._replay_file = "stale_response_with_force_swr.replay.yaml" - option_description = f"--force-stale-while-revalidate 30: {plugin_type_description}" - elif option_type == OptionType.FORCE_SIE: - self._replay_file = "stale_response_with_force_sie.replay.yaml" - option_description = f"--force-stale-if-error 30: {plugin_type_description}" - elif option_type == OptionType.MAX_MEMORY_USAGE: - self._replay_file = "stale_response_max_memory.replay.yaml" - option_description = f"--max-memory-usage 256: {plugin_type_description}" - - tr = Test.AddTestRun(f"stale_response.so Options: {option_description}") - - self.setupOriginServer(tr) - self.setupTS() - self.setupClient(tr) - self.verify_plugin_log() - - def setupOriginServer(self, tr: 'TestRun') -> None: - """ Configure the server. - - :param tr: The test run to add the server to. - """ - name = f'server_{TestStaleResponse._server_counter}' - TestStaleResponse._server_counter += 1 - self._server = tr.AddVerifierServerProcess(name, self._replay_file) - - def setupTS(self) -> None: - """Configure the traffic server. - - ATS is not configured for a TestRun because we need it to last longer to - ensure that the plugin's log is created. - """ - name = f'ts_{TestStaleResponse._ts_counter}' - TestStaleResponse._ts_counter += 1 - ts = Test.MakeATSProcess(name) - self._ts = ts - - log_path = os.path.join(ts.Variables.LOGDIR, 'stale_responses.log') - ts.Disk.File(log_path, id='stale_responses_log') - - remap_plugin_config = "" - if self._is_global: - plugin_command = 'stale_response.so --log-all --log-filename stale_responses' - if self._option_type == OptionType.DEFAULT_DIRECTIVES: - plugin_command += ' --stale-while-revalidate-default 30 --stale-if-error-default 30' - elif self._option_type == OptionType.FORCE_SWR: - plugin_command += ' --force-stale-while-revalidate 30' - elif self._option_type == OptionType.FORCE_SIE: - plugin_command += ' --force-stale-if-error 30' - elif self._option_type == OptionType.MAX_MEMORY_USAGE: - plugin_command += ' --max-memory-usage 256' - ts.Disk.plugin_config.AddLine(plugin_command) - else: - # Configure the stale_response plugin for the remap rule. - remap_plugin_config = "@plugin=stale_response.so @pparam=--log-all @pparam=--log-filename @pparam=stale_responses" - if self._option_type == OptionType.DEFAULT_DIRECTIVES: - remap_plugin_config += ' @pparam=--stale-while-revalidate-default @pparam=30 @pparam=--stale-if-error-default @pparam=30' - elif self._option_type == OptionType.FORCE_SWR: - remap_plugin_config += ' @pparam=--force-stale-while-revalidate @pparam=30' - elif self._option_type == OptionType.FORCE_SIE: - remap_plugin_config += ' @pparam=--force-stale-if-error @pparam=30' - elif self._option_type == OptionType.MAX_MEMORY_USAGE: - remap_plugin_config += ' @pparam=--max-memory-usage @pparam=256' - - ts.Disk.records_config.update( - { - "proxy.config.diags.debug.enabled": 1, - "proxy.config.diags.debug.tags": "http|stale_response", - "proxy.config.http.server_session_sharing.pool": "global", - # Turn off negative revalidating so that we can test stale-if-error. - "proxy.config.http.negative_revalidating_enabled": 0, - # Keep the active log filename available for the final content check if the test spans UTC midnight. - "proxy.config.log.rolling_enabled": 0, - }) - ts.Disk.remap_config.AddLine(f"map / http://127.0.0.1:{self._server.Variables.http_port}/ {remap_plugin_config}") - - def setupClient(self, tr: 'TestRun') -> None: - - name = f'client_{TestStaleResponse._client_counter}' - TestStaleResponse._client_counter += 1 - p = tr.AddVerifierClientProcess(name, self._replay_file, http_ports=[self._ts.Variables.port]) - p.StartBefore(self._server) - p.StartBefore(self._ts) - p.StillRunningAfter = self._ts - - def verify_plugin_log(self) -> None: - """Verify the contents of the stale_response plugin log.""" - if self._option_type == OptionType.MAX_MEMORY_USAGE: - diagnostic = "response exceeded memory limit; sending stale data" - Test.AddAwaitFileContainsTestRun( - "Verify stale_response max-memory diagnostic", self._ts.Disk.traffic_out.Name, diagnostic) - self._ts.Disk.traffic_out.Content += Testers.ContainsExpression( - diagnostic, "Verify max-memory stale-if-error fallback is logged") - return - - swr_log_pattern = "stale-while-revalidate:.*stale.jpeg" - sie_log_pattern = "stale-if-error:.*error.jpeg" - - def expect_log_entry(pattern: str, description: str) -> None: - tr = Test.AddAwaitFileContainsTestRun(f"Await {description}", self._ts.Disk.stale_responses_log.AbsPath, pattern) - tr.StillRunningBefore = self._ts - tr.StillRunningAfter = self._ts - self._ts.Disk.stale_responses_log.Content += Testers.ContainsExpression(pattern, f"Verify {description}") - - if self._option_type == OptionType.FORCE_SWR: - expect_log_entry(swr_log_pattern, "stale-while-revalidate directive is logged") - elif self._option_type == OptionType.FORCE_SIE: - expect_log_entry(sie_log_pattern, "stale-if-error directive is logged") - else: - expect_log_entry(swr_log_pattern, "stale-while-revalidate directive is logged") - expect_log_entry(sie_log_pattern, "stale-if-error directive is logged") - - -TestStaleResponse(OptionType.NONE, is_global=True) -TestStaleResponse(OptionType.DEFAULT_DIRECTIVES, is_global=True) -TestStaleResponse(OptionType.FORCE_SWR, is_global=True) -TestStaleResponse(OptionType.FORCE_SIE, is_global=True) -TestStaleResponse(OptionType.MAX_MEMORY_USAGE, is_global=True) - -TestStaleResponse(OptionType.NONE, is_global=False) -TestStaleResponse(OptionType.DEFAULT_DIRECTIVES, is_global=False) -TestStaleResponse(OptionType.FORCE_SWR, is_global=False) -TestStaleResponse(OptionType.FORCE_SIE, is_global=False) -TestStaleResponse(OptionType.MAX_MEMORY_USAGE, is_global=False) diff --git a/tests/gold_tests/pluginTest/statichit/statichit.test.py b/tests/gold_tests/pluginTest/statichit/statichit.test.py deleted file mode 100644 index 795f397b098..00000000000 --- a/tests/gold_tests/pluginTest/statichit/statichit.test.py +++ /dev/null @@ -1,111 +0,0 @@ -''' -Verify correct statichit plugin behavior -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import os - -Test.Summary = ''' -Verify correct statichit plugin behavior -''' - -Test.SkipUnless(Condition.PluginExists('statichit.so'),) - - -class RemapData: - """Data for each remap rule""" - - @staticmethod - def setup(): - # Path relative to config directory. The statichit directory will be created, and small_body.txt copied to it, - # by the 2nd (dummy) test run. - RemapData.small_body_path = os.path.join('statichit', 'small_body.txt') - - Test.Setup.Copy('story_16.json', Test.RunDirectory) - RemapData.json_body_path = os.path.join(Test.RunDirectory, 'story_16.json') - - Test.Setup.Copy('empty.txt', Test.RunDirectory) - RemapData.empty_body_path = os.path.join(Test.RunDirectory, 'empty.txt') - - # This directory will be created, and a file copied to it, by the 1st (dummy) test run. - RemapData.dir_path = os.path.join(Test.RunDirectory, 'body_dir') - - def __init__(self, from_url, *args) -> None: - self.remap_from = from_url - self.plugin_args = args - self.body = RemapData.small_body_path - - def bad_body(self): - self.body = 'invalid' - return self - - def json_body(self): - self.body = RemapData.json_body_path - return self - - def empty_body(self): - self.body = RemapData.empty_body_path - return self - - def dir(self): - self.body = RemapData.dir_path - return self - - -RemapData.setup() - -remap_data = [ - RemapData('http://fqdn1'), - RemapData('http://fqdn2', '--max-age=123'), - RemapData('http://fqdn3', '--success-code=200', '--disable-exact', '--failure-code=222'), - RemapData('http://fqdn4', '--success-code=200', '--failure-code=412').bad_body(), - RemapData('http://fqdn5', '--success-code=200', '--failure-code=412', "--mime-type=application/json").json_body(), - RemapData('http://fqdn6', '--success-code=200', '--failure-code=222').empty_body(), - RemapData('http://fqdn7', '--success-code', '200', '--disable-exact', '--failure-code', '222'), - RemapData('http://fqdn8', '-s', '200', '-d', '-c', '222'), - RemapData('http://fqdn9').dir(), -] - -ts = Test.MakeATSProcess('ts') - -ts.Disk.records_config.update({ - "proxy.config.diags.debug.enabled": 1, - "proxy.config.diags.debug.tags": "http|statichit", -}) - -for d in remap_data: - arg_str = '' - for arg in d.plugin_args: - arg_str += ' @pparam=' + arg - ts.Disk.remap_config.AddLine(f'map {d.remap_from} http://127.0.0.1/ @plugin=statichit.so @pparam=--file-path={d.body}{arg_str}') - -# Dummy test run for copying a file. -tr = Test.AddTestRun() -p = tr.Processes.Default -p.Command = (f'mkdir {Test.RunDirectory}/body_dir ; ' + f'cp {Test.TestDirectory}/small_body.txt {Test.RunDirectory}/body_dir/.') - -# Dummy test run for copying a file. -tr = Test.AddTestRun() -p = tr.Processes.Default -p.Command = ( - f'mkdir {ts.Variables.CONFIGDIR}/statichit ; ' + f'cp {Test.TestDirectory}/small_body.txt {ts.Variables.CONFIGDIR}/statichit/.') -p.StartBefore(ts) -p.StillRunningAfter = ts - -tr = Test.AddTestRun() -p = tr.AddVerifierClientProcess('client', 'statichit.replay.yaml', http_ports=[ts.Variables.port]) -p.StillRunningAfter = ts diff --git a/tests/gold_tests/pluginTest/stats_over_http/stats_over_http.test.py b/tests/gold_tests/pluginTest/stats_over_http/stats_over_http.test.py deleted file mode 100644 index a93ef0491c5..00000000000 --- a/tests/gold_tests/pluginTest/stats_over_http/stats_over_http.test.py +++ /dev/null @@ -1,400 +0,0 @@ -''' -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -from enum import Enum -import re -import sys - -Test.Summary = 'Exercise stats-over-http plugin' -Test.SkipUnless(Condition.PluginExists('stats_over_http.so')) -Test.ContinueOnFail = True - -# Skip until plugin supports pp or uds path -Test.SkipIf(Condition.CurlUsingUnixDomainSocket()) - - -class StatsOverHttpPluginTest: - """ - https://docs.trafficserver.apache.org/en/latest/admin-guide/plugins/stats_over_http.en.html - """ - - class State(Enum): - """ - State of process - """ - INIT = 0 - RUNNING = 1 - - def __init__(self): - self.state = self.State.INIT - self.__setupTS() - - def __setupTS(self): - self.ts = Test.MakeATSProcess("ts") - - self.ts.Disk.plugin_config.AddLine('stats_over_http.so _stats') - - self.ts.Disk.records_config.update( - { - "proxy.config.http.server_ports": f"{self.ts.Variables.port} {self.ts.Variables.uds_path}", - "proxy.config.diags.debug.enabled": 1, - "proxy.config.diags.debug.tags": "stats_over_http" - }) - - def __checkProcessBefore(self, tr): - if self.state == self.State.RUNNING: - tr.StillRunningBefore = self.ts - else: - tr.Processes.Default.StartBefore(self.ts) - self.state = self.State.RUNNING - - def __checkProcessAfter(self, tr): - assert (self.state == self.State.RUNNING) - tr.StillRunningAfter = self.ts - - def __containsLiteral(self, p: "Test.Process", expression: str, description: str): - p.Streams.stdout += Testers.ContainsExpression(re.escape(expression), description) - - def __excludesLiteral(self, p: "Test.Process", expression: str, description: str): - p.Streams.stdout += Testers.ExcludesExpression(re.escape(expression), description) - - def __checkPrometheusMetrics(self, p: 'Test.Process', from_prometheus: bool): - '''Check the Prometheus metrics output. - :param p: The process whose output to check. - :param from_prometheus: Whether the output is from Prometheus. Otherwise it's from ATS. - ''' - p.Streams.stdout += Testers.ContainsExpression( - 'HELP proxy_process_http2_current_client_connections proxy.process.http2.current_client_connections', - 'Output should have a help line for a gauge.') - p.Streams.stdout += Testers.ContainsExpression( - 'TYPE proxy_process_http2_current_client_connections gauge', 'Output should have a type line for a gauge.') - p.Streams.stdout += Testers.ContainsExpression( - 'proxy_process_http2_current_client_connections 0', 'Verify the successful parsing of Prometheus metrics for a gauge.') - - p.Streams.stdout += Testers.ContainsExpression( - 'HELP proxy_process_http_delete_requests proxy.process.http.delete_requests', - 'Output should have a help line for a counter.') - p.Streams.stdout += Testers.ContainsExpression( - 'TYPE proxy_process_http_delete_requests counter', 'Output should have a type line for a counter.') - - # Curiosly, Prometheus appaneds _total to counter metrics. - if from_prometheus: - p.Streams.stdout += Testers.ContainsExpression( - 'proxy_process_http_delete_requests_total 0', 'Verify the successful parsing of Prometheus metrics for a counter.') - else: - p.Streams.stdout += Testers.ContainsExpression( - 'proxy_process_http_delete_requests 0', 'Verify the successful parsing of Prometheus metrics for a counter.') - - def __checkPrometheusV2Metrics(self, p: "Test.Process"): - """Check the Prometheus v2 metrics output. - :param p: The process whose output to check. - """ - p.Streams.stdout += Testers.ContainsExpression( - "# HELP proxy_process_http_requests", - "Output should have a help line for the base metric name.", - ) - p.Streams.stdout += Testers.ContainsExpression( - "# TYPE proxy_process_http_requests counter", - "Output should have a type line for the base metric name.", - ) - - self.__containsLiteral( - p, - 'proxy_process_http_requests{method="delete"}', - "Verify that HTTP method labels (GET, POST, DELETE, etc.) are extracted correctly.", - ) - - self.__containsLiteral( - p, - 'proxy_process_http_requests{method="extension_method"}', - "Verify that multi-token HTTP method labels are extracted correctly.", - ) - - self.__containsLiteral( - p, - 'proxy_process_http_requests{method="invalid_client"}', - "Verify that invalid client request labels are extracted correctly.", - ) - - self.__containsLiteral( - p, - 'proxy_process_http_requests{direction="incoming"}', - "Verify that direction labels (incoming / outgoing) are extracted correctly.", - ) - - p.Streams.stdout += Testers.ContainsExpression( - "proxy_process_http_completed_requests", - "Verify that completed_requests remains its own lifecycle counter.", - ) - self.__excludesLiteral( - p, - 'method="completed"', - "completed is not an HTTP method label.", - ) - - self.__containsLiteral( - p, - 'proxy_process_http_cache_fresh{result="hit"}', - "Verify that result labels are extracted correctly.", - ) - - self.__containsLiteral( - p, - 'proxy_process_http_disallowed_continue{method="post", status="100"}', - "Verify that status code labels are extracted correctly.", - ) - - self.__containsLiteral( - p, - 'proxy_process_cache_volume_lookup_active{volume="0"}', - "Verify that volume labels are extracted from volume_N patterns.", - ) - - self.__containsLiteral( - p, - 'proxy_process_eventloop_count{le="', - "Verify that time buckets are correctly transformed into le labels.", - ) - - def __checkParsedPrometheusV2Metrics(self, p: "Test.Process"): - """Check the Prometheus parser's view of the v2 metrics output. - :param p: The process whose output to check. - """ - p.Streams.stdout += Testers.ContainsExpression( - "# TYPE proxy_process_http_requests counter", - "Prometheus parser should recognize HTTP request metrics as one counter family.", - ) - self.__containsLiteral( - p, - 'proxy_process_http_requests_total{method="delete"}', - "Parsed output should retain HTTP method labels.", - ) - self.__containsLiteral( - p, - 'proxy_process_http_requests_total{method="extension_method"}', - "Parsed output should retain multi-token HTTP method labels.", - ) - self.__containsLiteral( - p, - 'proxy_process_http_requests_total{direction="incoming"}', - "Parsed output should retain direction labels.", - ) - p.Streams.stdout += Testers.ContainsExpression( - "proxy_process_http_completed_requests_total", - "Parsed output should keep completed_requests as its own counter family.", - ) - self.__excludesLiteral( - p, - 'method="completed"', - "completed should not be parsed as an HTTP method label.", - ) - self.__containsLiteral( - p, - 'proxy_process_http_disallowed_continue_total{method="post",status="100"}', - "Parsed output should preserve multiple labels.", - ) - p.Streams.stdout += Testers.ContainsExpression( - "# TYPE proxy_process_http_responses counter", - "Prometheus parser should recognize HTTP response metrics as one counter family.", - ) - self.__containsLiteral( - p, - 'proxy_process_http_responses_total{direction="incoming"}', - "Parsed output should retain direction labels on response metrics.", - ) - self.__containsLiteral( - p, - 'proxy_process_http_responses_total{status="2xx"}', - "Parsed output should retain status code class labels.", - ) - self.__containsLiteral( - p, - 'proxy_process_http_cache_ims_total{result="miss"}', - "Parsed output should retain cache result labels.", - ) - self.__containsLiteral( - p, - 'proxy_process_http_transaction_counts_failed_total{result="errors",method="connect"}', - "Parsed output should preserve combined result and method labels.", - ) - self.__containsLiteral( - p, - 'proxy_process_eventloop_count{le="100s"}', - "Parsed output should retain bucket labels.", - ) - self.__containsLiteral( - p, - 'proxy_process_cache_volume_lookup_active{volume="0"}', - "Parsed output should preserve gauge labels.", - ) - self.__containsLiteral( - p, - 'proxy_process_cache_volume_lookup_success_total{volume="0"}', - "Parsed output should preserve counter labels for volume metrics.", - ) - - def __testCaseNoAccept(self): - tr = Test.AddTestRun('Fetch stats over HTTP in JSON format: no Accept and default path') - self.__checkProcessBefore(tr) - tr.MakeCurlCommand(f"-vs --http1.1 http://127.0.0.1:{self.ts.Variables.port}/_stats", ts=self.ts) - tr.Processes.Default.ReturnCode = 0 - tr.Processes.Default.Streams.stdout += Testers.ContainsExpression('{ "global": {', 'Output should have the JSON header.') - tr.Processes.Default.Streams.stdout += Testers.ContainsExpression( - '"proxy.process.http.delete_requests": "0",', 'Output should be JSON formatted.') - tr.Processes.Default.Streams.stderr = "gold/stats_over_http_json_stderr.gold" - tr.Processes.Default.TimeOut = 3 - self.__checkProcessAfter(tr) - - def __testCaseAcceptCSV(self): - tr = Test.AddTestRun('Fetch stats over HTTP in CSV format') - self.__checkProcessBefore(tr) - tr.MakeCurlCommand(f"-vs -H'Accept: text/csv' --http1.1 http://127.0.0.1:{self.ts.Variables.port}/_stats", ts=self.ts) - tr.Processes.Default.ReturnCode = 0 - tr.Processes.Default.Streams.stdout += Testers.ContainsExpression( - 'proxy.process.http.delete_requests,0', 'Output should be CSV formatted.') - tr.Processes.Default.Streams.stderr = "gold/stats_over_http_csv_stderr.gold" - tr.Processes.Default.TimeOut = 3 - self.__checkProcessAfter(tr) - - def __testCaseAcceptPrometheus(self): - tr = Test.AddTestRun('Fetch stats over HTTP in Prometheus format') - self.__checkProcessBefore(tr) - tr.MakeCurlCommand( - f"-vs -H'Accept: text/plain; version=0.0.4' --http1.1 http://127.0.0.1:{self.ts.Variables.port}/_stats", ts=self.ts) - tr.Processes.Default.ReturnCode = 0 - self.__checkPrometheusMetrics(tr.Processes.Default, from_prometheus=False) - tr.Processes.Default.Streams.stderr = "gold/stats_over_http_prometheus_stderr.gold" - tr.Processes.Default.TimeOut = 3 - self.__checkProcessAfter(tr) - - def __testCaseAcceptPrometheusV2(self): - tr = Test.AddTestRun("Fetch stats over HTTP in Prometheus v2 format via Accept header") - self.__checkProcessBefore(tr) - tr.MakeCurlCommand( - f"-vs -H'Accept: text/plain; version=2.0.0' --http1.1 http://127.0.0.1:{self.ts.Variables.port}/_stats", - ts=self.ts, - ) - tr.Processes.Default.ReturnCode = 0 - self.__checkPrometheusV2Metrics(tr.Processes.Default) - tr.Processes.Default.Streams.stderr = ("gold/stats_over_http_prometheus_v2_accept_stderr.gold") - tr.Processes.Default.TimeOut = 3 - self.__checkProcessAfter(tr) - - def __testCasePathJSON(self): - tr = Test.AddTestRun('Fetch stats over HTTP in JSON format via /_stats/json') - self.__checkProcessBefore(tr) - tr.MakeCurlCommand(f"-vs --http1.1 http://127.0.0.1:{self.ts.Variables.port}/_stats/json", ts=self.ts) - tr.Processes.Default.ReturnCode = 0 - tr.Processes.Default.Streams.stdout += Testers.ContainsExpression('{ "global": {', 'JSON header expected.') - tr.Processes.Default.Streams.stdout += Testers.ContainsExpression( - '"proxy.process.http.delete_requests": "0",', 'JSON field expected.') - tr.Processes.Default.Streams.stderr = "gold/stats_over_http_json_stderr.gold" - tr.Processes.Default.TimeOut = 3 - self.__checkProcessAfter(tr) - - def __testCasePathCSV(self): - tr = Test.AddTestRun('Fetch stats over HTTP in CSV format via /_stats/csv') - self.__checkProcessBefore(tr) - tr.MakeCurlCommand(f"-vs --http1.1 http://127.0.0.1:{self.ts.Variables.port}/_stats/csv", ts=self.ts) - tr.Processes.Default.ReturnCode = 0 - tr.Processes.Default.Streams.stdout += Testers.ContainsExpression( - 'proxy.process.http.delete_requests,0', 'CSV output expected.') - tr.Processes.Default.Streams.stderr = "gold/stats_over_http_csv_stderr.gold" - tr.Processes.Default.TimeOut = 3 - self.__checkProcessAfter(tr) - - def __testCasePathPrometheus(self): - tr = Test.AddTestRun('Fetch stats over HTTP in Prometheus format via /_stats/prometheus') - self.__checkProcessBefore(tr) - tr.MakeCurlCommand(f"-vs --http1.1 http://127.0.0.1:{self.ts.Variables.port}/_stats/prometheus", ts=self.ts) - tr.Processes.Default.ReturnCode = 0 - self.__checkPrometheusMetrics(tr.Processes.Default, from_prometheus=False) - tr.Processes.Default.Streams.stderr = "gold/stats_over_http_prometheus_stderr.gold" - tr.Processes.Default.TimeOut = 3 - self.__checkProcessAfter(tr) - - def __testCasePathPrometheusV2(self): - tr = Test.AddTestRun("Fetch stats over HTTP in Prometheus v2 format via /_stats/prometheus_v2") - self.__checkProcessBefore(tr) - tr.MakeCurlCommand( - f"-vs --http1.1 http://127.0.0.1:{self.ts.Variables.port}/_stats/prometheus_v2", - ts=self.ts, - ) - tr.Processes.Default.ReturnCode = 0 - self.__checkPrometheusV2Metrics(tr.Processes.Default) - tr.Processes.Default.Streams.stderr = ("gold/stats_over_http_prometheus_v2_stderr.gold") - tr.Processes.Default.TimeOut = 3 - self.__checkProcessAfter(tr) - - def __testCaseAcceptIgnoredIfPathExplicit(self): - tr = Test.AddTestRun('Fetch stats over HTTP in Prometheus format with Accept csv header') - self.__checkProcessBefore(tr) - tr.MakeCurlCommand( - f"-vs -H'Accept: text/csv' --http1.1 http://127.0.0.1:{self.ts.Variables.port}/_stats/prometheus", ts=self.ts) - tr.Processes.Default.ReturnCode = 0 - tr.Processes.Default.Streams.stdout += Testers.ContainsExpression( - 'proxy_process_http_delete_requests 0', 'Prometheus output expected.') - tr.Processes.Default.Streams.stderr = "gold/stats_over_http_prometheus_stderr.gold" - tr.Processes.Default.TimeOut = 3 - self.__checkProcessAfter(tr) - - def __queryAndParsePrometheusMetrics(self): - """ - Query the ATS stats over HTTP in Prometheus format and parse the output. - """ - tr = Test.AddTestRun('Query and parse Prometheus metrics') - ingester = 'prometheus_stats_ingester.py' - tr.Setup.CopyAs(ingester) - self.__checkProcessBefore(tr) - p = tr.Processes.Default - p.Command = f'{sys.executable} {ingester} http://127.0.0.1:{self.ts.Variables.port}/_stats/prometheus' - p.ReturnCode = 0 - self.__checkPrometheusMetrics(p, from_prometheus=True) - self.__checkProcessAfter(tr) - - def __queryAndParsePrometheusV2Metrics(self): - """ - Query the ATS stats over HTTP in Prometheus v2 format and parse the output. - """ - tr = Test.AddTestRun('Query and parse Prometheus v2 metrics') - ingester = 'prometheus_stats_ingester.py' - tr.Setup.CopyAs(ingester) - self.__checkProcessBefore(tr) - p = tr.Processes.Default - p.Command = ( - f'{sys.executable} {ingester} --validate-v2-format --strict-family-metadata ' - f'http://127.0.0.1:{self.ts.Variables.port}/_stats/prometheus_v2') - p.ReturnCode = 0 - self.__checkParsedPrometheusV2Metrics(p) - self.__checkProcessAfter(tr) - - def run(self): - self.__testCaseNoAccept() - self.__testCaseAcceptCSV() - self.__testCaseAcceptPrometheus() - self.__testCaseAcceptPrometheusV2() - self.__testCasePathJSON() - self.__testCasePathCSV() - self.__testCasePathPrometheus() - self.__testCasePathPrometheusV2() - self.__testCaseAcceptIgnoredIfPathExplicit() - self.__queryAndParsePrometheusMetrics() - self.__queryAndParsePrometheusV2Metrics() - - -StatsOverHttpPluginTest().run() diff --git a/tests/gold_tests/pluginTest/stek_share/stek_share.test.py b/tests/gold_tests/pluginTest/stek_share/stek_share.test.py deleted file mode 100644 index 8d2fb94d745..00000000000 --- a/tests/gold_tests/pluginTest/stek_share/stek_share.test.py +++ /dev/null @@ -1,331 +0,0 @@ -''' -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import os -import re - -Test.Summary = 'Test the STEK Share plugin' -Test.testName = "stek_share" - -Test.SkipUnless(Condition.PluginExists('stek_share.so')) - -server = Test.MakeOriginServer('server') - -ts1 = Test.MakeATSProcess("ts1", enable_tls=True) -ts2 = Test.MakeATSProcess("ts2", enable_tls=True) -ts3 = Test.MakeATSProcess("ts3", enable_tls=True) -ts4 = Test.MakeATSProcess("ts4", enable_tls=True) -ts5 = Test.MakeATSProcess("ts5", enable_tls=True) - -Test.Setup.Copy('ssl/self_signed.crt') -Test.Setup.Copy('ssl/self_signed.key') -Test.Setup.Copy('server_list.yaml') - -cert_path = os.path.join(Test.RunDirectory, 'self_signed.crt') -key_path = os.path.join(Test.RunDirectory, 'self_signed.key') -server_list_path = os.path.join(Test.RunDirectory, 'server_list.yaml') - -request_header1 = {'headers': 'GET / HTTP/1.1\r\nHost: www.example.com\r\n\r\n', 'timestamp': '1469733493.993', 'body': ''} -response_header1 = {'headers': 'HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n', 'timestamp': '1469733493.993', 'body': 'curl test'} -server.addResponse('sessionlog.json', request_header1, response_header1) - -stek_share_conf_path_1 = os.path.join(ts1.Variables.CONFIGDIR, 'stek_share_conf.yaml') -stek_share_conf_path_2 = os.path.join(ts2.Variables.CONFIGDIR, 'stek_share_conf.yaml') -stek_share_conf_path_3 = os.path.join(ts3.Variables.CONFIGDIR, 'stek_share_conf.yaml') -stek_share_conf_path_4 = os.path.join(ts4.Variables.CONFIGDIR, 'stek_share_conf.yaml') -stek_share_conf_path_5 = os.path.join(ts5.Variables.CONFIGDIR, 'stek_share_conf.yaml') - -ts1.Disk.File(stek_share_conf_path_1, id="stek_share_conf_1", typename="ats:config") -ts2.Disk.File(stek_share_conf_path_2, id="stek_share_conf_2", typename="ats:config") -ts3.Disk.File(stek_share_conf_path_3, id="stek_share_conf_3", typename="ats:config") -ts4.Disk.File(stek_share_conf_path_4, id="stek_share_conf_4", typename="ats:config") -ts5.Disk.File(stek_share_conf_path_5, id="stek_share_conf_5", typename="ats:config") - -ts1.Disk.stek_share_conf_1.AddLines( - [ - 'server_id: 1', - 'address: 127.0.0.1', - 'port: 10001', - 'asio_thread_pool_size: 4', - 'heart_beat_interval: 100', - 'election_timeout_lower_bound: 200', - 'election_timeout_upper_bound: 400', - 'reserved_log_items: 5', - 'snapshot_distance: 5', - 'client_req_timeout: 3000', # this is in milliseconds - 'key_update_interval: 3600', # this is in seconds - 'server_list_file: {0}'.format(server_list_path), - 'root_cert_file: {0}'.format(cert_path), - 'server_cert_file: {0}'.format(cert_path), - 'server_key_file: {0}'.format(key_path), - 'cert_verify_str: /C=US/ST=IL/O=Yahoo/OU=Edge/CN=stek-share', - ]) - -ts2.Disk.stek_share_conf_2.AddLines( - [ - 'server_id: 2', - 'address: 127.0.0.1', - 'port: 10002', - 'asio_thread_pool_size: 4', - 'heart_beat_interval: 100', - 'election_timeout_lower_bound: 200', - 'election_timeout_upper_bound: 400', - 'reserved_log_items: 5', - 'snapshot_distance: 5', - 'client_req_timeout: 3000', # this is in milliseconds - 'key_update_interval: 3600', # this is in seconds - 'server_list_file: {0}'.format(server_list_path), - 'root_cert_file: {0}'.format(cert_path), - 'server_cert_file: {0}'.format(cert_path), - 'server_key_file: {0}'.format(key_path), - 'cert_verify_str: /C=US/ST=IL/O=Yahoo/OU=Edge/CN=stek-share', - ]) - -ts3.Disk.stek_share_conf_3.AddLines( - [ - 'server_id: 3', - 'address: 127.0.0.1', - 'port: 10003', - 'asio_thread_pool_size: 4', - 'heart_beat_interval: 100', - 'election_timeout_lower_bound: 200', - 'election_timeout_upper_bound: 400', - 'reserved_log_items: 5', - 'snapshot_distance: 5', - 'client_req_timeout: 3000', # this is in milliseconds - 'key_update_interval: 3600', # this is in seconds - 'server_list_file: {0}'.format(server_list_path), - 'root_cert_file: {0}'.format(cert_path), - 'server_cert_file: {0}'.format(cert_path), - 'server_key_file: {0}'.format(key_path), - 'cert_verify_str: /C=US/ST=IL/O=Yahoo/OU=Edge/CN=stek-share', - ]) - -ts4.Disk.stek_share_conf_4.AddLines( - [ - 'server_id: 4', - 'address: 127.0.0.1', - 'port: 10004', - 'asio_thread_pool_size: 4', - 'heart_beat_interval: 100', - 'election_timeout_lower_bound: 200', - 'election_timeout_upper_bound: 400', - 'reserved_log_items: 5', - 'snapshot_distance: 5', - 'client_req_timeout: 3000', # this is in milliseconds - 'key_update_interval: 3600', # this is in seconds - 'server_list_file: {0}'.format(server_list_path), - 'root_cert_file: {0}'.format(cert_path), - 'server_cert_file: {0}'.format(cert_path), - 'server_key_file: {0}'.format(key_path), - 'cert_verify_str: /C=US/ST=IL/O=Yahoo/OU=Edge/CN=stek-share', - ]) - -ts5.Disk.stek_share_conf_5.AddLines( - [ - 'server_id: 5', - 'address: 127.0.0.1', - 'port: 10005', - 'asio_thread_pool_size: 4', - 'heart_beat_interval: 100', - 'election_timeout_lower_bound: 200', - 'election_timeout_upper_bound: 400', - 'reserved_log_items: 5', - 'snapshot_distance: 5', - 'client_req_timeout: 3000', # this is in milliseconds - 'key_update_interval: 3600', # this is in seconds - 'server_list_file: {0}'.format(server_list_path), - 'root_cert_file: {0}'.format(cert_path), - 'server_cert_file: {0}'.format(cert_path), - 'server_key_file: {0}'.format(key_path), - 'cert_verify_str: /C=US/ST=IL/O=Yahoo/OU=Edge/CN=stek-share', - ]) - -ts1.Disk.records_config.update( - { - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'stek_share', - 'proxy.config.exec_thread.autoconfig.enabled': 0, - 'proxy.config.exec_thread.limit': 4, - 'proxy.config.ssl.server.cert.path': '{0}'.format(Test.RunDirectory), - 'proxy.config.ssl.server.private_key.path': '{0}'.format(Test.RunDirectory), - 'proxy.config.ssl.server.session_ticket.enable': 1, - 'proxy.config.ssl.server.cipher_suite': - 'ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-DSS-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-DSS-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA256:DHE-RSA-AES128-SHA256:DHE-DSS-AES128-SHA256:DHE-RSA-AES256-SHA:DHE-DSS-AES256-SHA:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!aECDH:!EDH-DSS-DES-CBC3-SHA:!EDH-RSA-DES-CBC3-SHA:!KRB5-DES-CBC3-SHA' - }) -ts1.Disk.plugin_config.AddLine('stek_share.so {0}'.format(stek_share_conf_path_1)) -ts1.Disk.ssl_multicert_yaml.AddLines( - """ -ssl_multicert: - - dest_ip: "*" - ssl_cert_name: self_signed.crt - ssl_key_name: self_signed.key -""".split("\n")) -ts1.Disk.remap_config.AddLine('map / http://127.0.0.1:{0}'.format(server.Variables.Port)) - -ts2.Disk.records_config.update( - { - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'stek_share', - 'proxy.config.exec_thread.autoconfig.enabled': 0, - 'proxy.config.exec_thread.limit': 4, - 'proxy.config.ssl.server.cert.path': '{0}'.format(Test.RunDirectory), - 'proxy.config.ssl.server.private_key.path': '{0}'.format(Test.RunDirectory), - 'proxy.config.ssl.server.session_ticket.enable': 1, - 'proxy.config.ssl.server.cipher_suite': - 'ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-DSS-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-DSS-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA256:DHE-RSA-AES128-SHA256:DHE-DSS-AES128-SHA256:DHE-RSA-AES256-SHA:DHE-DSS-AES256-SHA:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!aECDH:!EDH-DSS-DES-CBC3-SHA:!EDH-RSA-DES-CBC3-SHA:!KRB5-DES-CBC3-SHA' - }) -ts2.Disk.plugin_config.AddLine('stek_share.so {0}'.format(stek_share_conf_path_2)) -ts2.Disk.ssl_multicert_yaml.AddLines( - """ -ssl_multicert: - - dest_ip: "*" - ssl_cert_name: self_signed.crt - ssl_key_name: self_signed.key -""".split("\n")) -ts2.Disk.remap_config.AddLine('map / http://127.0.0.1:{0}'.format(server.Variables.Port)) - -ts3.Disk.records_config.update( - { - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'stek_share', - 'proxy.config.exec_thread.autoconfig.enabled': 0, - 'proxy.config.exec_thread.limit': 4, - 'proxy.config.ssl.server.cert.path': '{0}'.format(Test.RunDirectory), - 'proxy.config.ssl.server.private_key.path': '{0}'.format(Test.RunDirectory), - 'proxy.config.ssl.server.session_ticket.enable': 1, - 'proxy.config.ssl.server.cipher_suite': - 'ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-DSS-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-DSS-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA256:DHE-RSA-AES128-SHA256:DHE-DSS-AES128-SHA256:DHE-RSA-AES256-SHA:DHE-DSS-AES256-SHA:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!aECDH:!EDH-DSS-DES-CBC3-SHA:!EDH-RSA-DES-CBC3-SHA:!KRB5-DES-CBC3-SHA' - }) -ts3.Disk.plugin_config.AddLine('stek_share.so {0}'.format(stek_share_conf_path_3)) -ts3.Disk.ssl_multicert_yaml.AddLines( - """ -ssl_multicert: - - dest_ip: "*" - ssl_cert_name: self_signed.crt - ssl_key_name: self_signed.key -""".split("\n")) -ts3.Disk.remap_config.AddLine('map / http://127.0.0.1:{0}'.format(server.Variables.Port)) - -ts4.Disk.records_config.update( - { - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'stek_share', - 'proxy.config.exec_thread.autoconfig.enabled': 0, - 'proxy.config.exec_thread.limit': 4, - 'proxy.config.ssl.server.cert.path': '{0}'.format(Test.RunDirectory), - 'proxy.config.ssl.server.private_key.path': '{0}'.format(Test.RunDirectory), - 'proxy.config.ssl.server.session_ticket.enable': 1, - 'proxy.config.ssl.server.cipher_suite': - 'ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-DSS-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-DSS-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA256:DHE-RSA-AES128-SHA256:DHE-DSS-AES128-SHA256:DHE-RSA-AES256-SHA:DHE-DSS-AES256-SHA:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!aECDH:!EDH-DSS-DES-CBC3-SHA:!EDH-RSA-DES-CBC3-SHA:!KRB5-DES-CBC3-SHA' - }) -ts4.Disk.plugin_config.AddLine('stek_share.so {0}'.format(stek_share_conf_path_4)) -ts4.Disk.ssl_multicert_yaml.AddLines( - """ -ssl_multicert: - - dest_ip: "*" - ssl_cert_name: self_signed.crt - ssl_key_name: self_signed.key -""".split("\n")) -ts4.Disk.remap_config.AddLine('map / http://127.0.0.1:{0}'.format(server.Variables.Port)) - -ts5.Disk.records_config.update( - { - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'stek_share', - 'proxy.config.exec_thread.autoconfig.enabled': 0, - 'proxy.config.exec_thread.limit': 4, - 'proxy.config.ssl.server.cert.path': '{0}'.format(Test.RunDirectory), - 'proxy.config.ssl.server.private_key.path': '{0}'.format(Test.RunDirectory), - 'proxy.config.ssl.server.session_ticket.enable': 1, - 'proxy.config.ssl.server.cipher_suite': - 'ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-DSS-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-DSS-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA256:DHE-RSA-AES128-SHA256:DHE-DSS-AES128-SHA256:DHE-RSA-AES256-SHA:DHE-DSS-AES256-SHA:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!aECDH:!EDH-DSS-DES-CBC3-SHA:!EDH-RSA-DES-CBC3-SHA:!KRB5-DES-CBC3-SHA' - }) -ts5.Disk.plugin_config.AddLine('stek_share.so {0}'.format(stek_share_conf_path_5)) -ts5.Disk.ssl_multicert_yaml.AddLines( - """ -ssl_multicert: - - dest_ip: "*" - ssl_cert_name: self_signed.crt - ssl_key_name: self_signed.key -""".split("\n")) -ts5.Disk.remap_config.AddLine('map / http://127.0.0.1:{0}'.format(server.Variables.Port)) - - -def check_session(ev, test): - retval = False - f = open(test.GetContent(ev), 'r') - err = "Session ids match" - if not f: - err = "Failed to open {0}".format(openssl_output) - return (retval, "Check that session ids match", err) - - content = f.read() - match = re.findall('Session-ID: ([0-9A-F]+)', content) - - if match: - if all(i == j for i, j in zip(match, match[1:])): - err = "{0} reused successfully {1} times".format(match[0], len(match) - 1) - retval = True - else: - err = "Session is not being reused as expected" - else: - err = "Didn't find session id" - return (retval, "Check that session ids match", err) - - -tr1 = Test.AddTestRun('Basic Curl test, and give it enough time for all ATS to start up and sync STEK') -tr1.Processes.Default.Command = 'sleep 10 && curl https://127.0.0.1:{0} -k'.format(ts1.Variables.ssl_port) -tr1.Processes.Default.ReturnCode = 0 -tr1.Processes.Default.StartBefore(server) -tr1.Processes.Default.StartBefore(ts1) -tr1.Processes.Default.StartBefore(ts2) -tr1.Processes.Default.StartBefore(ts3) -tr1.Processes.Default.StartBefore(ts4) -tr1.Processes.Default.StartBefore(ts5) -tr1.Processes.Default.Streams.All = Testers.ContainsExpression('curl test', 'Making sure the basics still work') -ts1.Disk.traffic_out.Content = Testers.ContainsExpression('Generate initial STEK succeeded', 'should succeed') -ts2.Disk.traffic_out.Content = Testers.ContainsExpression('Generate initial STEK succeeded', 'should succeed') -ts3.Disk.traffic_out.Content = Testers.ContainsExpression('Generate initial STEK succeeded', 'should succeed') -ts4.Disk.traffic_out.Content = Testers.ContainsExpression('Generate initial STEK succeeded', 'should succeed') -ts5.Disk.traffic_out.Content = Testers.ContainsExpression('Generate initial STEK succeeded', 'should succeed') -tr1.StillRunningAfter = server -tr1.StillRunningAfter += ts1 -tr1.StillRunningAfter += ts2 -tr1.StillRunningAfter += ts3 -tr1.StillRunningAfter += ts4 -tr1.StillRunningAfter += ts5 - -tr2 = Test.AddTestRun("TLSv1.2 Session Ticket") -tr2.Command = \ - 'printf "GET / HTTP/1.1\r\n" | openssl s_client -tls1_2 -connect 127.0.0.1:{0} -sess_out {5} && ' \ - 'printf "GET / HTTP/1.1\r\n" | openssl s_client -tls1_2 -connect 127.0.0.1:{0} -sess_in {5} && ' \ - 'printf "GET / HTTP/1.1\r\n" | openssl s_client -tls1_2 -connect 127.0.0.1:{1} -sess_in {5} && ' \ - 'printf "GET / HTTP/1.1\r\n" | openssl s_client -tls1_2 -connect 127.0.0.1:{2} -sess_in {5} && ' \ - 'printf "GET / HTTP/1.1\r\n" | openssl s_client -tls1_2 -connect 127.0.0.1:{3} -sess_in {5} && ' \ - 'printf "GET / HTTP/1.1\r\n" | openssl s_client -tls1_2 -connect 127.0.0.1:{4} -sess_in {5}' \ - .format( - ts1.Variables.ssl_port, - ts2.Variables.ssl_port, - ts3.Variables.ssl_port, - ts4.Variables.ssl_port, - ts5.Variables.ssl_port, - os.path.join(Test.RunDirectory, 'sess.dat') - ) -tr2.ReturnCode = 0 -tr2.Processes.Default.Streams.All.Content = Testers.Lambda(check_session) diff --git a/tests/gold_tests/pluginTest/strategies/strategies_plugins.test.py b/tests/gold_tests/pluginTest/strategies/strategies_plugins.test.py deleted file mode 100644 index d718431994a..00000000000 --- a/tests/gold_tests/pluginTest/strategies/strategies_plugins.test.py +++ /dev/null @@ -1,411 +0,0 @@ -''' -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. -import os - -Test.Summary = ''' -Combined header_rewrite/regex_remap/tslua strategies tests -''' - -# Test description: -# Preload the cache with the entire asset to be range requested. -# Reload remap rule with slice plugin -# Request content through the slice plugin - -Test.SkipUnless( - Condition.PluginExists('header_rewrite.so'), - Condition.PluginExists('regex_remap.so'), - Condition.PluginExists('tslua.so'), -) -Test.ContinueOnFail = False - -dns = Test.MakeDNServer("dns") - -origins = [] - -chars = ['0', '1', '2', 'p', 's'] -for char in chars: - name = f"nh{char}" - origin = Test.MakeOriginServer(name, options={"--verbose": ""}) - request_header = { - "headers": f"GET / HTTP/1.1\r\nHost: origin\r\n\r\n", - "timestamp": "1469733493.993", - "body": "", - } - response_header = { - "headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", - "timestamp": "1469733493.993", - "body": "", - } - origin.addResponse("sessionfile.log", request_header, response_header) - request_header = { - "headers": "GET /path HTTP/1.1\r\nHost: origin\r\n\r\n", - "timestamp": "1469733493.993", - "body": "", - } - response_header = { - "headers": f"HTTP/1.1 200 OK\r\nConnection: close\r\nOrigin: {name}\r\n\r\n", - "timestamp": "1469733493.993", - "body": name, - } - origin.addResponse("sessionfile.log", request_header, response_header) - request_header = { - "headers": f"GET /path/{name} HTTP/1.1\r\nHost: origin\r\n\r\n", - "timestamp": "1469733493.993", - "body": "", - } - response_header = { - "headers": f"HTTP/1.1 200 OK\r\nConnection: close\r\nOrigin: {name}\r\n\r\n", - "timestamp": "1469733493.993", - "body": name, - } - origin.addResponse("sessionfile.log", request_header, response_header) - origin.ReturnCode = 0 - origins.append(origin) - dns.addRecords(records={name: ["127.0.0.1"]}) - -# Define ATS and configure -ts = Test.MakeATSProcess("ts", enable_cache=False) -ts.ReturnCode = 0 -ts.Disk.records_config.update( - { - 'proxy.config.dns.nameservers': f"127.0.0.1:{dns.Variables.Port}", - 'proxy.config.dns.resolv_conf': "NULL", - 'proxy.config.http.cache.http': 0, - "proxy.config.http.insert_response_via_str": 1, - 'proxy.config.http.uncacheable_requests_bypass_parent': 0, - 'proxy.config.http.no_dns_just_forward_to_parent': 1, - 'proxy.config.http.parent_proxy.mark_down_hostdb': 0, - 'proxy.config.http.parent_proxy.self_detect': 0, - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': "next_hop|dns|http|parent|regex_remap|header_rewrite|tslua", - }) - -ts.Disk.MakeConfigFile("hdr_rw.config").AddLines( - [ - 'cond %{CLIENT-HEADER:Strategy} ="nemo"', - "set-next-hop-strategy nemo", - 'cond %{CLIENT-HEADER:Strategy} ="nh0"', - "set-next-hop-strategy nh0", - 'cond %{CLIENT-HEADER:Strategy} ="nh1"', - "set-next-hop-strategy nh1", - 'cond %{CLIENT-HEADER:Strategy} ="null"', - "set-next-hop-strategy null", - 'cond %{CLIENT-HEADER:Strategy} ="clear"', - 'set-next-hop-strategy ""', - ]) -ts.Disk.MakeConfigFile("regex_remap.config").AddLines( - [ - "/nh0 http://origin/path @strategy=nh1", - '/nh1 http://origin/path @strategy=', - "/nh2 http://origin/path @strategy=nh0", - '/null http://origin/path @strategy=null', - "/nemo http://origin/path @strategy=nemo", - "# fallthrough", - "/ http://origin/path", - ]) -ts.Disk.MakeConfigFile("strategies.lua").AddLines( - [ - 'function do_remap()', - ' local uri = ts.client_request.get_uri()', - ' if uri:find("nh0") then', - ' ts.http.set_next_hop_strategy("nh1")', - ' elseif uri:find("nh1") then', - ' ts.http.set_next_hop_strategy("")', - ' elseif uri:find("nh2") then', - ' ts.http.set_next_hop_strategy("nh0")', - ' elseif uri:find("null") then', - ' ts.http.set_next_hop_strategy("null")', - ' elseif uri:find("nemo") then', - ' ts.http.set_next_hop_strategy("nemo")', - ' end', - ' ts.client_request.set_uri("path")', - ' return 0', - 'end', - ]) - -# parent.config -ts.Disk.parent_config.AddLines( - [f'dest_domain=. parent="nh2:{origins[2].Variables.Port}" round_robin=false go_direct=false parent_is_proxy=false']) - -# build strategies.yaml file -ts.Disk.File(ts.Variables.CONFIGDIR + "/strategies.yaml", id="strategies", typename="ats:config") - -s = ts.Disk.strategies -s.AddLine("groups:") -for ind in range(len(origins)): - char = chars[ind] - org = origins[ind] - name = f"nh{chars[ind]}" - s.AddLines( - [ - f" - &g{char}", - f" - host: {name}", - f" protocol:", - f" - scheme: http", - f" port: {org.Variables.Port}", - f" weight: 1.0", - ]) - -s.AddLine("strategies:") - -# third ts_nh -for char in chars: - s.AddLines( - [ - f" - strategy: nh{char}", - f" policy: consistent_hash", - f" hash_key: path", - f" go_direct: false", - f" parent_is_proxy: false", - f" ignore_self_detect: true", - f" groups:", - f" - *g{char}", - f" scheme: http", - ]) - -ts.Disk.remap_config.AddLines( - [ - "# header rewrite", - "map http://nhp_hr http://origin @plugin=header_rewrite.so @pparam=hdr_rw.config", - "map http://nhs_hr http://origin @strategy=nh0 @plugin=header_rewrite.so @pparam=hdr_rw.config", - "# modify strategy/parent", - "map http://nh0_hr http://origin @strategy=nh0 @plugin=header_rewrite.so @pparam=hdr_rw.config", - "map http://nh1_hr http://origin @strategy=nh1 @plugin=header_rewrite.so @pparam=hdr_rw.config", - "map http://nh2_hr http://origin @plugin=header_rewrite.so @pparam=hdr_rw.config", - "", - "# regex_remap", - "map http://nhp_rr http://origin @plugin=regex_remap.so @pparam=regex_remap.config", - "map http://nhs_rr http://origin @strategy=nh0 @plugin=regex_remap.so @pparam=regex_remap.config", - "# modify strategy/parent", - "map http://nh0_rr http://origin @strategy=nh0 @plugin=regex_remap.so @pparam=regex_remap.config", - "map http://nh1_rr http://origin @strategy=nh1 @plugin=regex_remap.so @pparam=regex_remap.config", - "map http://nh2_rr http://origin @plugin=regex_remap.so @pparam=regex_remap.config", - "", - "# tslua", - "map http://nhp_lua http://origin @plugin=tslua.so @pparam=strategies.lua", - "map http://nhs_lua http://origin @strategy=nh0 @plugin=tslua.so @pparam=strategies.lua", - "# modify strategy/parent", - "map http://nh0_lua http://origin @strategy=nh0 @plugin=tslua.so @pparam=strategies.lua", - "map http://nh1_lua http://origin @strategy=nh1 @plugin=tslua.so @pparam=strategies.lua", - "map http://nh2_lua http://origin @plugin=tslua.so @pparam=strategies.lua", - ]) - -# Tests - -# stdout for body, stderr for headers -curl_and_args = '-s -o /dev/stdout -D /dev/stderr -x localhost:{}'.format(ts.Variables.port) - -# header rewrite - -# 0 - nhp request to parent.config -tr = Test.AddTestRun("nhp_hr parent.config through request") -ps = tr.Processes.Default -for ind in range(len(origins)): - origin = origins[ind] - ps.StartBefore(origin, ready=When.PortOpen(origin.Variables.Port)) - tr.StillRunningAfter = origin -ps.StartBefore(dns) -ps.StartBefore(Test.Processes.ts) -tr.MakeCurlCommand(curl_and_args + " http://nhp_hr/path", ts=ts) -ps.ReturnCode = 0 -ps.Streams.stdout.Content = Testers.ContainsExpression("nh2", "expected nh2") -tr.StillRunningAfter = ts -tr.StillRunningAfter = dns - -# 1 - nhs_hr default request -tr = Test.AddTestRun("nhs_hr straight through request") -ps = tr.Processes.Default -tr.MakeCurlCommand(curl_and_args + " http://nhs_hr/path", ts=ts) -ps.ReturnCode = 0 -ps.Streams.stdout.Content = Testers.ContainsExpression("nh0", "expected nh0") -tr.StillRunningAfter = ts - -# 2 - nh0_hr default request -tr = Test.AddTestRun("nh0_hr straight through request") -ps = tr.Processes.Default -tr.MakeCurlCommand(curl_and_args + " http://nh0_hr/path", ts=ts) -ps.ReturnCode = 0 -ps.Streams.stdout.Content = Testers.ContainsExpression("nh0", "expected nh0") -tr.StillRunningAfter = ts - -# 3 - nh1_hr default request -tr = Test.AddTestRun("nh1_hr straight through request") -ps = tr.Processes.Default -tr.MakeCurlCommand(curl_and_args + " http://nh1_hr/path", ts=ts) -ps.ReturnCode = 0 -ps.Streams.stdout.Content = Testers.ContainsExpression("nh1", "expected nh1") -tr.StillRunningAfter = ts - -# 4 - nh2_hr default request -tr = Test.AddTestRun("nh2_hr straight through request") -ps = tr.Processes.Default -tr.MakeCurlCommand(curl_and_args + " http://nh2_hr/path", ts=ts) -ps.ReturnCode = 0 -ps.Streams.stdout.Content = Testers.ContainsExpression("nh2", "expected nh2") -tr.StillRunningAfter = ts - -# 5 switch strategies -tr = Test.AddTestRun("nh0_hr switch to nh1") -ps = tr.Processes.Default -tr.MakeCurlCommand(curl_and_args + ' http://nh0_hr/path -H "Strategy: nh1"', ts=ts) -ps.ReturnCode = 0 -ps.Streams.stdout.Content = Testers.ContainsExpression("nh1", "expected nh1") -tr.StillRunningAfter = ts -tr.StillRunningAfter = dns - -# 6 strategy to parent.config -tr = Test.AddTestRun("nh1_hr switch to parent.config") -ps = tr.Processes.Default -tr.MakeCurlCommand(curl_and_args + ' http://nh1_hr/path -H "Strategy: null"', ts=ts) -ps.ReturnCode = 0 -ps.Streams.stdout.Content = Testers.ContainsExpression("nh2", "expected nh2") -tr.StillRunningAfter = ts -tr.StillRunningAfter = dns - -# 7 parent.config strategy to strategy -tr = Test.AddTestRun("nh2_hr switch to nh0") -ps = tr.Processes.Default -tr.MakeCurlCommand(curl_and_args + ' http://nh2_hr/path -H "Strategy: nh0"', ts=ts) -ps.ReturnCode = 0 -ps.Streams.stdout.Content = Testers.ContainsExpression("nh0", "expected nh0") -tr.StillRunningAfter = ts -tr.StillRunningAfter = dns - -# 8 try to switch to non existent strategy -tr = Test.AddTestRun("nh0_hr switch to nemo (fail)") -ps = tr.Processes.Default -tr.MakeCurlCommand(curl_and_args + ' http://nh0_hr/path -H "Strategy: nemo"', ts=ts) -ps.ReturnCode = 0 -ps.Streams.stdout.Content = Testers.ContainsExpression("nh0", "expected nh0") -tr.StillRunningAfter = ts -tr.StillRunningAfter = dns - -# regex_remap - -# 9 use parent.config -tr = Test.AddTestRun("nhp_rr parent.config") -ps = tr.Processes.Default -tr.MakeCurlCommand(curl_and_args + ' http://nhp_rr/nhp', ts=ts) -ps.ReturnCode = 0 -ps.Streams.stdout.Content = Testers.ContainsExpression("nh2", "expected nh2") -tr.StillRunningAfter = ts -tr.StillRunningAfter = dns - -# 10 use strategies -tr = Test.AddTestRun("nhs_rr strategies.yaml") -ps = tr.Processes.Default -tr.MakeCurlCommand(curl_and_args + ' http://nhs_rr/nh', ts=ts) -ps.ReturnCode = 0 -ps.Streams.stdout.Content = Testers.ContainsExpression("nh0", "expected nh0") -tr.StillRunningAfter = ts -tr.StillRunningAfter = dns - -# 11 switch strategies -tr = Test.AddTestRun("nh0_rr switch to nh1") -ps = tr.Processes.Default -tr.MakeCurlCommand(curl_and_args + ' http://nh0_rr/nh0', ts=ts) -ps.ReturnCode = 0 -ps.Streams.stdout.Content = Testers.ContainsExpression("nh1", "expected nh1") -tr.StillRunningAfter = ts -tr.StillRunningAfter = dns - -# 12 strategy to parent.config -tr = Test.AddTestRun("nh1_rr switch to parent.config") -ps = tr.Processes.Default -tr.MakeCurlCommand(curl_and_args + ' http://nh1_rr/nh1', ts=ts) -ps.ReturnCode = 0 -ps.Streams.stdout.Content = Testers.ContainsExpression("nh2", "expected nh2") -tr.StillRunningAfter = ts -tr.StillRunningAfter = dns - -# 13 parent.config strategy to strategy -tr = Test.AddTestRun("nh2_rr switch to nh0") -ps = tr.Processes.Default -tr.MakeCurlCommand(curl_and_args + ' http://nh2_rr/nh2', ts=ts) -ps.ReturnCode = 0 -ps.Streams.stdout.Content = Testers.ContainsExpression("nh0", "expected nh0") -tr.StillRunningAfter = ts -tr.StillRunningAfter = dns - -# 14 switch strategies (fail) -tr = Test.AddTestRun("nh0_rr switch to nemo") -ps = tr.Processes.Default -tr.MakeCurlCommand(curl_and_args + ' http://nh0_rr/nemo', ts=ts) -ps.ReturnCode = 0 -ps.Streams.stdout.Content = Testers.ContainsExpression("nh0", "expected nh0") -tr.StillRunningAfter = ts -tr.StillRunningAfter = dns - -# tslua - -# 15 parent.config -tr = Test.AddTestRun("nhp_lua parent.config") -ps = tr.Processes.Default -tr.MakeCurlCommand(curl_and_args + ' http://nhp_lua/nh', ts=ts) -ps.ReturnCode = 0 -ps.Streams.stdout.Content = Testers.ContainsExpression("nh2", "expected nh2") -tr.StillRunningAfter = ts -tr.StillRunningAfter = dns - -# 16 strategies.yaml -tr = Test.AddTestRun("nhs_lua strategies.yaml") -ps = tr.Processes.Default -tr.MakeCurlCommand(curl_and_args + ' http://nhs_lua/nh', ts=ts) -ps.ReturnCode = 0 -ps.Streams.stdout.Content = Testers.ContainsExpression("nh0", "expected nh0") -tr.StillRunningAfter = ts -tr.StillRunningAfter = dns - -# 17 switch strategies -tr = Test.AddTestRun("nh0_lua switch to nh1") -ps = tr.Processes.Default -tr.MakeCurlCommand(curl_and_args + ' http://nh0_lua/nh0', ts=ts) -ps.ReturnCode = 0 -ps.Streams.stdout.Content = Testers.ContainsExpression("nh1", "expected nh1") -tr.StillRunningAfter = ts -tr.StillRunningAfter = dns - -# 18 strategy to parent.config -tr = Test.AddTestRun("nh1_lua switch to parent.config") -ps = tr.Processes.Default -tr.MakeCurlCommand(curl_and_args + ' http://nh1_lua/nh1', ts=ts) -ps.ReturnCode = 0 -ps.Streams.stdout.Content = Testers.ContainsExpression("nh2", "expected nh2") -tr.StillRunningAfter = ts -tr.StillRunningAfter = dns - -# 19 parent.config strategy to strategy -tr = Test.AddTestRun("nh2_lua switch to nh0") -ps = tr.Processes.Default -tr.MakeCurlCommand(curl_and_args + ' http://nh2_lua/nh2', ts=ts) -ps.ReturnCode = 0 -ps.Streams.stdout.Content = Testers.ContainsExpression("nh0", "expected nh0") -tr.StillRunningAfter = ts -tr.StillRunningAfter = dns - -# 20 switch strategies, fail -tr = Test.AddTestRun("nh0_lua switch to nemo") -ps = tr.Processes.Default -tr.MakeCurlCommand(curl_and_args + ' http://nh0_lua/nemo', ts=ts) -ps.ReturnCode = 0 -ps.Streams.stdout.Content = Testers.ContainsExpression("nh0", "expected nh0") -tr.StillRunningAfter = ts -tr.StillRunningAfter = dns - -# Overriding the built in ERROR check since we expect some ERROR messages -ts.Disk.diags_log.Content = Testers.ContainsExpression("ERROR", "Some tests are failure tests") diff --git a/tests/gold_tests/pluginTest/test_hooks/hook_add.test.py b/tests/gold_tests/pluginTest/test_hooks/hook_add.test.py deleted file mode 100644 index 595e37e311e..00000000000 --- a/tests/gold_tests/pluginTest/test_hooks/hook_add.test.py +++ /dev/null @@ -1,58 +0,0 @@ -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import os - -Test.Summary = ''' -Test adding hooks -''' - -Test.ContinueOnFail = True - -server = Test.MakeOriginServer("server") - -request_header = {"headers": "GET /argh HTTP/1.1\r\nHost: doesnotmatter\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -server.addResponse("sessionlog.json", request_header, response_header) - -ts = Test.MakeATSProcess("ts", enable_cache=False) - -ts.Disk.records_config.update( - { - 'proxy.config.diags.debug.tags': 'test', - 'proxy.config.diags.show_location': 0, - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.url_remap.remap_required': 0, - }) - -Test.PrepareTestPlugin(os.path.join(Test.Variables.AtsTestPluginsDir, 'hook_add_plugin.so'), ts) - -ts.Disk.remap_config.AddLine("map http://one http://127.0.0.1:{0}".format(server.Variables.Port)) - -ipv4flag = "" -if not Condition.CurlUsingUnixDomainSocket(): - ipv4flag = "--ipv4" - -tr = Test.AddTestRun() -# Probe server port to check if ready. -tr.Processes.Default.StartBefore(server, ready=When.PortOpen(server.Variables.Port)) -tr.Processes.Default.StartBefore(Test.Processes.ts) -# -tr.MakeCurlCommand('--verbose {0} --header "Host: one" http://localhost:{1}/argh'.format(ipv4flag, ts.Variables.port), ts=ts) -tr.Processes.Default.ReturnCode = 0 - -# Look at the debug output from the plugin -ts.Disk.traffic_out.Content = "hook_add.gold" diff --git a/tests/gold_tests/pluginTest/test_hooks/ssn_start_delay_hook.test.py b/tests/gold_tests/pluginTest/test_hooks/ssn_start_delay_hook.test.py deleted file mode 100644 index 7a10b9a2993..00000000000 --- a/tests/gold_tests/pluginTest/test_hooks/ssn_start_delay_hook.test.py +++ /dev/null @@ -1,60 +0,0 @@ -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import os - -Test.Summary = ''' -Test adding hooks, and rescheduling the ssn start hook from a non-net thread -''' - -Test.ContinueOnFail = True - -server = Test.MakeOriginServer("server") - -request_header = {"headers": "GET /argh HTTP/1.1\r\nHost: doesnotmatter\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -server.addResponse("sessionlog.json", request_header, response_header) - -ts = Test.MakeATSProcess("ts") - -ts.Disk.records_config.update( - { - 'proxy.config.diags.debug.tags': 'test', - 'proxy.config.diags.show_location': 0, - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.http.cache.http': 0, - 'proxy.config.url_remap.remap_required': 0, - }) - -Test.PrepareTestPlugin(os.path.join(Test.Variables.AtsTestPluginsDir, 'hook_add_plugin.so'), ts, '-delay') - -ts.Disk.remap_config.AddLine("map http://one http://127.0.0.1:{0}".format(server.Variables.Port)) - -ipv4flag = "" -if not Condition.CurlUsingUnixDomainSocket(): - ipv4flag = "--ipv4" - -tr = Test.AddTestRun() -# Probe server port to check if ready. -tr.Processes.Default.StartBefore(server, ready=When.PortOpen(server.Variables.Port)) -# Probe TS cleartext port to check if ready (probing TLS port causes spurious VCONN hook triggers). -tr.Processes.Default.StartBefore(Test.Processes.ts, ready=When.PortOpen(ts.Variables.port)) -# -tr.MakeCurlCommand('--verbose {0} --header "Host: one" http://localhost:{1}/argh'.format(ipv4flag, ts.Variables.port), ts=ts) -tr.Processes.Default.ReturnCode = 0 - -# Look at the debug output from the plugin -ts.Disk.traffic_out.Content = "ssn_delay.gold" diff --git a/tests/gold_tests/pluginTest/test_hooks/test_hooks.test.py b/tests/gold_tests/pluginTest/test_hooks/test_hooks.test.py deleted file mode 100644 index b0cb46daf7d..00000000000 --- a/tests/gold_tests/pluginTest/test_hooks/test_hooks.test.py +++ /dev/null @@ -1,104 +0,0 @@ -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import os - -Test.Summary = ''' -Test TS API Hooks. -''' - -Test.SkipUnless(Condition.HasCurlFeature('http2'),) -Test.ContinueOnFail = True - -server = Test.MakeOriginServer("server") - -request_header = {"headers": "GET /argh HTTP/1.1\r\nHost: doesnotmatter\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -server.addResponse("sessionlog.json", request_header, response_header) - -# Disable the cache to make sure each request is forwarded to the origin -# server. -ts = Test.MakeATSProcess("ts", enable_tls=True, enable_cache=False) - -# test_hooks.so will output test logging to this file. -log_path = os.path.join(ts.Variables.LOGDIR, "log.txt") -Test.Env["OUTPUT_FILE"] = log_path - -ts.addDefaultSSLFiles() - -ts.Disk.records_config.update( - { - 'proxy.config.proxy_name': 'Poxy_Proxy', # This will be the server name. - 'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir), - 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts.Variables.SSLDir), - 'proxy.config.url_remap.remap_required': 0, - 'proxy.config.diags.debug.enabled': 0, - 'proxy.config.diags.debug.tags': 'http|test_hooks', - }) - -ts.Disk.ssl_multicert_yaml.AddLines( - """ -ssl_multicert: - - dest_ip: "*" - ssl_cert_name: server.pem - ssl_key_name: server.key -""".split("\n")) - -Test.PrepareTestPlugin(os.path.join(Test.Variables.AtsTestPluginsDir, 'test_hooks.so'), ts) - -ts.Disk.remap_config.AddLine("map http://one http://127.0.0.1:{0}".format(server.Variables.Port)) -ts.Disk.remap_config.AddLine("map https://one http://127.0.0.1:{0}".format(server.Variables.Port)) - -ipv4flag = "" -if not Condition.CurlUsingUnixDomainSocket(): - ipv4flag = "--ipv4" - -tr = Test.AddTestRun() -# Probe server port to check if ready. -tr.Processes.Default.StartBefore(server, ready=When.PortOpen(server.Variables.Port)) -tr.Processes.Default.StartBefore(Test.Processes.ts) -# -tr.MakeCurlCommand('--verbose {0} --header "Host: one" http://localhost:{1}/argh'.format(ipv4flag, ts.Variables.port), ts=ts) -tr.Processes.Default.ReturnCode = 0 - -if not Condition.CurlUsingUnixDomainSocket(): - tr = Test.AddTestRun() - tr.MakeCurlCommand( - '--verbose --ipv4 --http2 --insecure --header "Host: one" https://localhost:{0}/argh'.format(ts.Variables.ssl_port), ts=ts) - tr.Processes.Default.ReturnCode = 0 - - tr = Test.AddTestRun() - tr.MakeCurlCommand( - '--verbose --ipv4 --http1.1 --insecure --header "Host: one" https://localhost:{0}/argh'.format(ts.Variables.ssl_port), - ts=ts) - tr.Processes.Default.ReturnCode = 0 - -# The probing of the ATS port to detect when ATS is ready may be seen by ATS as a VCONN start/close, so filter out these -# events from the log file. -# -tr = Test.AddTestRun() -tr.Processes.Default.Command = "cd " + Test.RunDirectory + " ; . " + Test.TestDirectory + "/clean.sh" -tr.Processes.Default.ReturnCode = 0 - -tr = Test.AddTestRun() -tr.Processes.Default.Command = "echo check log" -tr.Processes.Default.ReturnCode = 0 -f = tr.Disk.File(log_path) -if Condition.CurlUsingUnixDomainSocket(): - f.Content = "log_uds.gold" -else: - f.Content = "log.gold" - f.Content += Testers.ContainsExpression("Global: event=TS_EVENT_VCONN_CLOSE", "VCONN_CLOSE should trigger 2 times") diff --git a/tests/gold_tests/pluginTest/traffic_dump/traffic_dump.test.py b/tests/gold_tests/pluginTest/traffic_dump/traffic_dump.test.py deleted file mode 100644 index 6886b8b1ea6..00000000000 --- a/tests/gold_tests/pluginTest/traffic_dump/traffic_dump.test.py +++ /dev/null @@ -1,429 +0,0 @@ -""" -Verify traffic_dump functionality. -""" -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import os -import sys - -Test.Summary = ''' -Verify traffic_dump functionality. -''' - -Test.SkipUnless(Condition.PluginExists('traffic_dump.so'),) - -schema_path = os.path.join(Test.Variables.AtsTestToolsDir, 'lib', 'replay_schema.json') - -# Configure the origin server. -replay_file = "replay/traffic_dump.yaml" -server = Test.MakeVerifierServerProcess( - "server", "replay/traffic_dump_server.yaml", ssl_cert="ssl/server_combined.pem", ca_cert="ssl/signer.pem") - -# Define ATS and configure it. -ts = Test.MakeATSProcess("ts", enable_tls=True) -replay_dir = os.path.join(ts.RunDirectory, "ts", "log") - -ts.addSSLfile("ssl/server.pem") -ts.addSSLfile("ssl/server.key") -ts.addSSLfile("ssl/signer.pem") - -ts.Setup.Copy("ssl/signed-foo.pem") -ts.Setup.Copy("ssl/signed-foo.key") - -ts.Disk.records_config.update( - { - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'traffic_dump|http', - 'proxy.config.http.insert_age_in_response': 0, - 'proxy.config.ssl.server.cert.path': ts.Variables.SSLDir, - 'proxy.config.ssl.server.private_key.path': ts.Variables.SSLDir, - 'proxy.config.url_remap.pristine_host_hdr': 1, - 'proxy.config.ssl.CA.cert.filename': f'{ts.Variables.SSLDir}/signer.pem', - 'proxy.config.exec_thread.autoconfig.scale': 1.0, - 'proxy.config.http.host_sni_policy': 2, - 'proxy.config.ssl.TLSv1_3.enabled': 0, - 'proxy.config.ssl.client.verify.server.policy': 'PERMISSIVE', - 'proxy.config.http.connect_ports': f"{server.Variables.http_port}", - }) - -ts.Disk.ssl_multicert_yaml.AddLines( - """ -ssl_multicert: - - dest_ip: "*" - ssl_cert_name: server.pem - ssl_key_name: server.key -""".split("\n")) - -ts.Disk.remap_config.AddLines( - [ - f'map https://www.client_only_tls.com/ http://127.0.0.1:{server.Variables.http_port}', - f'map https://www.tls.com/ https://127.0.0.1:{server.Variables.https_port}', - f'map http://www.connect_target.com/ http://127.0.0.1:{server.Variables.http_port}', - f'map / http://127.0.0.1:{server.Variables.http_port}', - ]) -ts.addPrivateConnectAllowYaml(methods='[ CONNECT, GET, POST ]') - -# Configure traffic_dump. -ts.Disk.plugin_config.AddLine( - f'traffic_dump.so --logdir {replay_dir} --sample 1 --limit 1000000000 ' - '--sensitive-fields "cookie,set-cookie,x-request-1,x-request-2"') -# Configure logging of transactions. This is helpful for the cache test below. -ts.Disk.logging_yaml.AddLines( - ''' -logging: - formats: - - name: basic - format: "%: Read result: %:%:%, Write result: %" - logs: - - filename: transactions - format: basic -'''.split('\n')) - -# Set up trafficserver expectations. -ts.Disk.diags_log.Content = Testers.ContainsExpression( - "loading plugin.*traffic_dump.so", "Verify the traffic_dump plugin got loaded.") -ts.Disk.traffic_out.Content = Testers.ContainsExpression( - f"Initialized with log directory: {replay_dir}", "Verify traffic_dump initialized with the configured directory.") -ts.Disk.traffic_out.Content += Testers.ContainsExpression( - "Initialized with sample pool size of 1 bytes and disk limit of 1000000000 bytes", - "Verify traffic_dump initialized with the configured disk limit.") -ts.Disk.traffic_out.Content += Testers.ContainsExpression( - "Finish a session with log file of.*bytes", "Verify traffic_dump sees the end of sessions and accounts for it.") -ts.Disk.traffic_out.Content += Testers.ContainsExpression("Dumping body bytes: false", "Verify that dumping body bytes is enabled.") - -# Set up the json replay file expectations. -replay_file_session_1 = os.path.join(replay_dir, "127", "0000000000000000") -ts.Disk.File(replay_file_session_1, exists=True) -replay_file_session_2 = os.path.join(replay_dir, "127", "0000000000000001") -ts.Disk.File(replay_file_session_2, exists=True) -replay_file_session_3 = os.path.join(replay_dir, "127", "0000000000000002") -ts.Disk.File(replay_file_session_3, exists=True) -replay_file_session_4 = os.path.join(replay_dir, "127", "0000000000000003") -ts.Disk.File(replay_file_session_4, exists=True) -replay_file_session_5 = os.path.join(replay_dir, "127", "0000000000000004") -ts.Disk.File(replay_file_session_5, exists=True) -replay_file_session_6 = os.path.join(replay_dir, "127", "0000000000000005") -ts.Disk.File(replay_file_session_6, exists=True) -replay_file_session_7 = os.path.join(replay_dir, "127", "0000000000000006") -ts.Disk.File(replay_file_session_7, exists=True) -replay_file_session_8 = os.path.join(replay_dir, "127", "0000000000000007") -ts.Disk.File(replay_file_session_8, exists=True) -replay_file_session_9 = os.path.join(replay_dir, "127", "0000000000000008") -ts.Disk.File(replay_file_session_9, exists=True) -replay_file_session_10 = os.path.join(replay_dir, "127", "0000000000000009") -ts.Disk.File(replay_file_session_10, exists=True) -replay_file_session_11 = os.path.join(replay_dir, "127", "000000000000000a") -ts.Disk.File(replay_file_session_11, exists=True) - -# The following will not be written to disk because of a restricted disk limit. -replay_file_session_12 = os.path.join(replay_dir, "127", "000000000000000b") -ts.Disk.File(replay_file_session_12, exists=False) - -# The following will be written to disk because the disk restriction will be -# removed. -replay_file_session_13 = os.path.join(replay_dir, "127", "000000000000000c") -ts.Disk.File(replay_file_session_13, exists=True) - -# The following will not be written to disk because the restriction will be -# re-added. -replay_file_session_14 = os.path.join(replay_dir, "127", "000000000000000d") -ts.Disk.File(replay_file_session_14, exists=False) - -# Run our test traffic. -tr = Test.AddTestRun("Run the test traffic.") -tr.AddVerifierClientProcess( - "client", - replay_file, - http_ports=[ts.Variables.port], - https_ports=[ts.Variables.ssl_port], - ssl_cert="ssl/server_combined.pem", - ca_cert="ssl/signer.pem") - -tr.Processes.Default.StartBefore(server) -tr.Processes.Default.StartBefore(ts) -tr.StillRunningAfter = server -tr.StillRunningAfter = ts - -# -# Test 1: Verify the correct behavior of two transactions across two sessions. -# - -# Verify the properties of the replay file for the first transaction. -tr = Test.AddTestRun("Verify the json content of the first session") -http_protocols = "tcp,ip" -verify_replay = "verify_replay.py" -sensitive_fields_arg = ( - "--sensitive-fields cookie " - "--sensitive-fields set-cookie " - "--sensitive-fields x-request-1 " - "--sensitive-fields x-request-2 ") -tr.Setup.CopyAs(verify_replay, Test.RunDirectory) -tr.Processes.Default.Command = \ - (f'{sys.executable} {verify_replay} {schema_path} {replay_file_session_1} ' - f'{sensitive_fields_arg} --client-http-version "1.1" ' - f'--client-protocols "{http_protocols}"') -tr.Processes.Default.ReturnCode = 0 -tr.StillRunningAfter = server -tr.StillRunningAfter = ts - -# Verify the properties of the replay file for the second transaction. -tr = Test.AddTestRun("Verify the json content of the second session") -tr.Setup.CopyAs(verify_replay, Test.RunDirectory) -tr.Processes.Default.Command = \ - (f'{sys.executable} {verify_replay} {schema_path} {replay_file_session_2} ' - f'{sensitive_fields_arg} --client-http-version "1.1" ' - '--request-target "/two"') -tr.Processes.Default.ReturnCode = 0 -tr.StillRunningAfter = server -tr.StillRunningAfter = ts - -# -# Test 2: Verify the correct behavior of an explicit path in the request line. -# - -# Verify recording of a request target with the host specified. -tr = Test.AddTestRun("Verify the replay file has the explicit target.") -tr.Setup.CopyAs(verify_replay, Test.RunDirectory) - -tr.Processes.Default.Command = \ - (f"{sys.executable} {verify_replay} {schema_path} {replay_file_session_3} {sensitive_fields_arg} " - "--request-target 'http://www.some.host.com/candy'") -tr.Processes.Default.ReturnCode = 0 -tr.StillRunningAfter = server -tr.StillRunningAfter = ts - -# -# Test 3: Verify correct handling of a POST with body data. -# - -tr = Test.AddTestRun("Verify the client-request size node for a request with a body.") -tr.Setup.CopyAs(verify_replay, Test.RunDirectory) - -size_of_verify_replay_file = os.path.getsize(os.path.join(Test.TestDirectory, verify_replay)) -expected_body_size = 12345 -tr.Processes.Default.Command = \ - (f"{sys.executable} {verify_replay} {schema_path} {replay_file_session_4} {sensitive_fields_arg} " - f"--client-request-size {expected_body_size}") -tr.Processes.Default.ReturnCode = 0 -tr.StillRunningAfter = server -tr.StillRunningAfter = ts - -# -# Test 4: Verify correct handling of a response produced out of the cache. -# -tr = Test.AddTestRun("Verify that the cached response's replay file looks appropriate.") -tr.Setup.CopyAs(verify_replay, Test.RunDirectory) -tr.Processes.Default.Command = \ - (f'{sys.executable} {verify_replay} {schema_path} {replay_file_session_6} ' - f'--client-protocols "{http_protocols}"') -tr.Processes.Default.ReturnCode = 0 -tr.StillRunningAfter = server -tr.StillRunningAfter = ts - -# -# Test 5: Verify correct handling of two transactions in a session. -# -tr = Test.AddTestRun("Verify the dump file of two transactions in a session.") -tr.Setup.CopyAs(verify_replay, Test.RunDirectory) -tr.Processes.Default.Command = \ - (f'{sys.executable} {verify_replay} {schema_path} {replay_file_session_7} ' - f'--client-protocols "{http_protocols}"') -tr.Processes.Default.ReturnCode = 0 -tr.StillRunningAfter = server -tr.StillRunningAfter = ts - -# -# Test 6: Verify correct protocol dumping of a TLS connection. -# -tr = Test.AddTestRun("Verify the client protocol stack of a TLS session.") -https_protocols = "tls,tcp,ip" -client_tls_features = "sni:www.tls.com,proxy-verify-mode:0,proxy-provided-cert:true" -tr.Setup.CopyAs(verify_replay, Test.RunDirectory) -tr.Processes.Default.Command = \ - (f'{sys.executable} {verify_replay} {schema_path} {replay_file_session_8} ' - f'--client-protocols "{https_protocols}" --client-tls-features "{client_tls_features}"') -tr.Processes.Default.ReturnCode = 0 -tr.StillRunningAfter = server -tr.StillRunningAfter = ts - -tr = Test.AddTestRun("Verify the server TLS protocol stack.") -https_server_stack = "http,tls,tcp,ip" -tr.Setup.CopyAs(verify_replay, Test.RunDirectory) -server_tls_features = 'proxy-provided-cert:false,sni:www.tls.com,proxy-verify-mode:1' -tr.Processes.Default.Command = \ - (f'{sys.executable} {verify_replay} {schema_path} {replay_file_session_8} --server-protocols ' - f'"{https_server_stack}" --server-tls-features "{server_tls_features}"') -tr.Processes.Default.ReturnCode = 0 -tr.StillRunningAfter = server -tr.StillRunningAfter = ts - -# -# Test 7: Verify correct protocol dumping of TLS and HTTP/2 connections. -# -tr = Test.AddTestRun("Verify the client HTTP/2 protocol stack.") -h2_protocols = "http,tls,tcp,ip" -tr.Setup.CopyAs(verify_replay, Test.RunDirectory) -tr.Processes.Default.Command = \ - (f'{sys.executable} {verify_replay} {schema_path} {replay_file_session_9} ' - f'--client-http-version "2" --client-protocols "{h2_protocols}" ' - f'--client-tls-features "{client_tls_features}"') -tr.Processes.Default.ReturnCode = 0 -tr.StillRunningAfter = server -tr.StillRunningAfter = ts - -tr = Test.AddTestRun("Verify the server HTTP/2 protocol stack.") -tr.Setup.CopyAs(verify_replay, Test.RunDirectory) -tr.Processes.Default.Command = \ - (f'{sys.executable} {verify_replay} {schema_path} {replay_file_session_9} ' - f'--server-protocols "{https_server_stack}" --server-tls-features "{server_tls_features}"') -tr.Processes.Default.ReturnCode = 0 -tr.StillRunningAfter = server -tr.StillRunningAfter = ts - -# -# Test 8: Verify correct protocol dumping of client-side TLS and server-side HTTP. -# -tr = Test.AddTestRun("Verify the client TLS protocol stack.") -tr.Setup.CopyAs(verify_replay, Test.RunDirectory) -tr.Processes.Default.Command = \ - (f'{sys.executable} {verify_replay} {schema_path} {replay_file_session_10} ' - f'--client-http-version "1.1" --client-protocols "{https_protocols}"') -tr.Processes.Default.ReturnCode = 0 -tr.StillRunningAfter = server -tr.StillRunningAfter = ts - -tr = Test.AddTestRun("Verify the server HTTP protocol stack.") -tr.Setup.CopyAs(verify_replay, Test.RunDirectory) -http_server_stack = "http,tcp,ip" -tr.Processes.Default.Command = \ - (f'{sys.executable} {verify_replay} {schema_path} {replay_file_session_10} ' - f'--server-protocols "{http_server_stack}"') -tr.Processes.Default.ReturnCode = 0 -tr.StillRunningAfter = server -tr.StillRunningAfter = ts - -# -# Test 9: Verify correct handling of a CONNECT request. -# - -tr = Test.AddTestRun("Verify handling of a CONNECT request.") -tr.Setup.CopyAs(verify_replay, Test.RunDirectory) - -tr.Processes.Default.Command = \ - (f"{sys.executable} {verify_replay} {schema_path} {replay_file_session_11} {sensitive_fields_arg} ") -tr.Processes.Default.ReturnCode = 0 -tr.StillRunningAfter = server -tr.StillRunningAfter = ts - -# -# Test 10: Verify that we can change the --limit value. -# -tr = Test.AddTestRun("Verify changing --limit via traffic_ctl.") -tr.Processes.Default.Command = "traffic_ctl plugin msg traffic_dump.limit 0" -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Env = ts.Env -tr.StillRunningAfter = ts - -# Give ATS some time to process the change -tr = Test.AddTestRun("Give ATS some time to process the change") -tr.Processes.Default.Command = "sleep 2" -tr.Processes.Default.ReturnCode = 0 - -tr = Test.AddTestRun("Run some more test traffic with the restricted disk limit.") -tr.AddVerifierClientProcess( - "client-2", - replay_file, - http_ports=[ts.Variables.port], - https_ports=[ts.Variables.ssl_port], - ssl_cert="ssl/server_combined.pem", - ca_cert="ssl/signer.pem", - other_args='--keys 1') - -# Since the limit is zero, we should not see any new replay file created. -tr = Test.AddTestRun("Verify no new traffic was dumped") -# Sleep 2 seconds to give the replay plugin plenty of time to write the file. -tr.Processes.Default.Command = "sleep 2" -tr.Processes.Default.ReturnCode = 0 -file = tr.Disk.File(replay_file_session_12) -file.Exists = False - -# -# Test 11: Verify that we can remove the disk limit. -# -tr = Test.AddTestRun("Removing the disk limit via traffic_ctl.") -tr.Processes.Default.Command = "traffic_ctl plugin msg traffic_dump.unlimit" -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Env = ts.Env -tr.StillRunningAfter = ts - -# Give ATS some time to process the change -tr = Test.AddTestRun("Give ATS some time to process the change") -tr.Processes.Default.Command = "sleep 2" -tr.Processes.Default.ReturnCode = 0 - -tr = Test.AddTestRun("Run some more test traffic with no disk limit.") -tr.AddVerifierClientProcess( - "client-3", - replay_file, - http_ports=[ts.Variables.port], - https_ports=[ts.Variables.ssl_port], - ssl_cert="ssl/server_combined.pem", - ca_cert="ssl/signer.pem", - other_args='--keys 1') - -# Since the limit is zero, we should not see any new replay file created. -tr = Test.AddTestRun("Verify the new traffic was dumped") -# Sleep 2 seconds to give the replay plugin plenty of time to write the file. -tr.Processes.Default.Command = "sleep 2" -tr.Processes.Default.ReturnCode = 0 -file = tr.Disk.File(replay_file_session_13) -file.Exists = True - -# -# Test 11: Verify that we can again restrict the disk limit. -# -# Verify that the restriction can be re-added after unlimit was set. This -# verifies correct handling of the boolean controlling unlimited disk space. -# -tr = Test.AddTestRun("Verify re-adding --limit via traffic_ctl.") -tr.Processes.Default.Command = "traffic_ctl plugin msg traffic_dump.limit 0" -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Env = ts.Env -tr.StillRunningAfter = ts - -# Give ATS some time to process the change -tr = Test.AddTestRun("Give ATS some time to process the change") -tr.Processes.Default.Command = "sleep 2" -tr.Processes.Default.ReturnCode = 0 - -tr = Test.AddTestRun("Run test traffic with newly restricted disk limit.") -tr.AddVerifierClientProcess( - "client-4", - replay_file, - http_ports=[ts.Variables.port], - https_ports=[ts.Variables.ssl_port], - ssl_cert="ssl/server_combined.pem", - ca_cert="ssl/signer.pem", - other_args='--keys 1') - -# Since the limit is zero, we should not see any new replay file created. -tr = Test.AddTestRun("Verify no new traffic was dumped") -# Sleep 2 seconds to give the replay plugin plenty of time to write the file. -tr.Processes.Default.Command = "sleep 2" -tr.Processes.Default.ReturnCode = 0 -file = tr.Disk.File(replay_file_session_14) -file.Exists = False diff --git a/tests/gold_tests/pluginTest/traffic_dump/traffic_dump_http3.test.py b/tests/gold_tests/pluginTest/traffic_dump/traffic_dump_http3.test.py deleted file mode 100644 index 758b469979e..00000000000 --- a/tests/gold_tests/pluginTest/traffic_dump/traffic_dump_http3.test.py +++ /dev/null @@ -1,142 +0,0 @@ -""" -Verify traffic_dump HTTP/3 functionality. -""" -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import os -import sys - -Test.Summary = ''' -Verify traffic_dump HTTP/3 functionality. -''' - -Test.SkipUnless( - Condition.PluginExists('traffic_dump.so'), - Condition.HasATSFeature('TS_USE_QUIC'), -) -Test.SkipIf(Condition.true("Skip this test until the TS_EVENT_HTTP_SSN are supported for QUIC connections."),) - -schema_path = os.path.join(Test.Variables.AtsTestToolsDir, 'lib', 'replay_schema.json') - -# Configure the origin server. -replay_file = "replay/http3.yaml" -server = Test.MakeVerifierServerProcess("server", replay_file, ssl_cert="ssl/server_combined.pem", ca_cert="ssl/signer.pem") - -# Define ATS and configure it. -ts = Test.MakeATSProcess("ts", enable_tls=True, enable_quic=True) -ts_log_dir = os.path.join(ts.RunDirectory, "ts", "log") -qlog_dir = os.path.join(ts_log_dir, "qlog_dir") - -ts.addSSLfile("ssl/server.pem") -ts.addSSLfile("ssl/server.key") -ts.addSSLfile("ssl/signer.pem") - -ts.Disk.records_config.update( - { - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'traffic_dump|quic', - 'proxy.config.http.insert_age_in_response': 0, - 'proxy.config.quic.qlog.file_base': qlog_dir, - 'proxy.config.ssl.server.cert.path': ts.Variables.SSLDir, - 'proxy.config.ssl.server.private_key.path': ts.Variables.SSLDir, - 'proxy.config.url_remap.pristine_host_hdr': 1, - 'proxy.config.ssl.CA.cert.filename': f'{ts.Variables.SSLDir}/signer.pem', - 'proxy.config.exec_thread.autoconfig.scale': 1.0, - 'proxy.config.http.host_sni_policy': 2, - 'proxy.config.ssl.client.verify.server.policy': 'PERMISSIVE', - }) - -ts.Disk.ssl_multicert_yaml.AddLines( - """ -ssl_multicert: - - dest_ip: "*" - ssl_cert_name: server.pem - ssl_key_name: server.key -""".split("\n")) - -ts.Disk.remap_config.AddLine(f'map https://www.client_only_tls.com/ http://127.0.0.1:{server.Variables.http_port}') -ts.Disk.remap_config.AddLine(f'map https://www.tls.com/ https://127.0.0.1:{server.Variables.https_port}') -ts.Disk.remap_config.AddLine(f'map / http://127.0.0.1:{server.Variables.http_port}') - -# Configure traffic_dump. -ts.Disk.plugin_config.AddLine( - f'traffic_dump.so --logdir {ts_log_dir} --sample 1 --limit 1000000000 ' - '--sensitive-fields "cookie,set-cookie,x-request-1,x-request-2"') -# Configure logging of transactions. This is helpful for the cache test below. -ts.Disk.logging_yaml.AddLines( - ''' -logging: - formats: - - name: basic - format: "%: Read result: %:%:%, Write result: %" - logs: - - filename: transactions - format: basic -'''.split('\n')) - -# Set up trafficserver expectations. -ts.Disk.diags_log.Content = Testers.ContainsExpression( - "loading plugin.*traffic_dump.so", "Verify the traffic_dump plugin got loaded.") -ts.Disk.traffic_out.Content = Testers.ContainsExpression( - f"Initialized with log directory: {ts_log_dir}", "Verify traffic_dump initialized with the configured directory.") -ts.Disk.traffic_out.Content += Testers.ContainsExpression( - "Initialized with sample pool size of 1 bytes and disk limit of 1000000000 bytes", - "Verify traffic_dump initialized with the configured disk limit.") -ts.Disk.traffic_out.Content += Testers.ContainsExpression( - "Finish a session with log file of.*bytes", "Verify traffic_dump sees the end of sessions and accounts for it.") - -# Set up the json replay file expectations. -replay_file_session_1 = os.path.join(ts_log_dir, "127", "0000000000000000") -ts.Disk.File(replay_file_session_1, exists=True) - -# Execute the first transaction. -tr = Test.AddTestRun("Run the test traffic.") -tr.AddVerifierClientProcess( - "client", - replay_file, - http_ports=[ts.Variables.port], - https_ports=[ts.Variables.ssl_port], - http3_ports=[ts.Variables.ssl_port], - ssl_cert="ssl/server_combined.pem", - ca_cert="ssl/signer.pem") - -tr.Processes.Default.StartBefore(server) -tr.Processes.Default.StartBefore(ts) -tr.StillRunningAfter = server -tr.StillRunningAfter = ts - -# -# Test 1: Verify the correct behavior of two transactions across two sessions. -# - -# Verify the properties of the replay file for the first transaction. -tr = Test.AddTestRun("Verify the json content of the first session") -http_protocols = "tcp,ip" -verify_replay = "verify_replay.py" -sensitive_fields_arg = ( - "--sensitive-fields cookie " - "--sensitive-fields set-cookie " - "--sensitive-fields x-request-1 " - "--sensitive-fields x-request-2 ") -tr.Setup.CopyAs(verify_replay, Test.RunDirectory) -tr.Processes.Default.Command = \ - (f'{sys.executable} {verify_replay} {schema_path} {replay_file_session_1} ' - f'{sensitive_fields_arg} --client-http-version "3" ' - f'--client-protocols "{http_protocols}"') -tr.Processes.Default.ReturnCode = 0 -tr.StillRunningAfter = server -tr.StillRunningAfter = ts diff --git a/tests/gold_tests/pluginTest/traffic_dump/traffic_dump_ip_filter.test.py b/tests/gold_tests/pluginTest/traffic_dump/traffic_dump_ip_filter.test.py deleted file mode 100644 index 60bc55cda62..00000000000 --- a/tests/gold_tests/pluginTest/traffic_dump/traffic_dump_ip_filter.test.py +++ /dev/null @@ -1,116 +0,0 @@ -""" -Verify traffic_dump IP filter functionality. -""" -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import os -import sys - -Test.Summary = ''' -Verify traffic_dump IP filter functionality. -''' - -Test.SkipUnless(Condition.PluginExists('traffic_dump.so'),) - -# Configure the origin server. -replay_file = "replay/traffic_dump.yaml" -server = Test.MakeVerifierServerProcess("server", "replay/traffic_dump_ip_filter_server.yaml") - - -def get_common_ats_process(name, plugin_command, replay_exists): - """ - Get an ATS process with some common configuration. - - These tests have different log expectations, but have generally the same - ATS Process configuration. This function returns a Process with that common - configuration. - - Returns: - A configured ATS Process. - The replay file. - """ - ts = Test.MakeATSProcess(name) - replay_dir = os.path.join(ts.RunDirectory, name, "log") - ts.Disk.records_config.update({ - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'traffic_dump', - }) - ts.Disk.remap_config.AddLine(f'map / http://127.0.0.1:{server.Variables.http_port}') - # Configure traffic_dump as specified. - ts.Disk.plugin_config.AddLine(plugin_command.format(replay_dir)) - - ts_dump_fil = os.path.join(replay_dir, "127", "0000000000000000") - ts.Disk.File(ts_dump_fil, exists=replay_exists) - return ts, ts_dump_fil - - -# Common verification variables. -verify_replay = "verify_replay.py" -schema = os.path.join(Test.Variables.AtsTestToolsDir, 'lib', 'replay_schema.json') -verify_command_prefix = f'{sys.executable} {verify_replay} {schema}' - -# -# Test 1: Verify -4 works for a specified address. -# -tr = Test.AddTestRun("Verify that -4 matches 127.0.0.1 as expected") -ts1, ts1_replay_file = get_common_ats_process( - "ts1", 'traffic_dump.so --logdir {0} --sample 1 --limit 1000000000 -4 127.0.0.1', replay_exists=True) -ts1.Disk.traffic_out.Content += Testers.ContainsExpression( - "Filtering to only dump connections with ip: 127.0.0.1", "Verify the IP filter status message.") -tr.AddVerifierClientProcess("client0", replay_file, http_ports=[ts1.Variables.port], other_args='--keys 1') - -tr.Processes.Default.StartBefore(server) -tr.Processes.Default.StartBefore(ts1) -tr.StillRunningAfter = server -tr.StillRunningAfter = ts1 - -# Verify that the expected request body was recorded. -tr = Test.AddTestRun("Verify that the expected request body was recorded.") -tr.Setup.CopyAs(verify_replay, Test.RunDirectory) -tr.Processes.Default.Command = f'{verify_command_prefix} {ts1_replay_file}' -tr.Processes.Default.ReturnCode = 0 -tr.StillRunningAfter = server -tr.StillRunningAfter = ts1 - -# -# Test 2: Verify -4 filters out other addresses. -# -tr = Test.AddTestRun("Verify that -4 filters out our non-matching IP as expected") -ts2, ts2_replay_file = get_common_ats_process( - "ts2", 'traffic_dump.so --logdir {0} --sample 1 --limit 1000000000 -4 1.2.3.4', replay_exists=False) -ts2.Disk.traffic_out.Content += Testers.ContainsExpression( - "Filtering to only dump connections with ip: 1.2.3.4", "Verify the IP filter status message.") -tr.AddVerifierClientProcess("client1", replay_file, http_ports=[ts2.Variables.port], other_args='--keys 1') - -tr.Processes.Default.StartBefore(ts2) -tr.StillRunningAfter = server -tr.StillRunningAfter = ts2 - -# -# Test 3: Verify -4 recognizes an invalid IP address string. -# -tr = Test.AddTestRun("Verify that -4 detects an invalid IP string") -invalid_ip = "this_is_not_a_valid_ip_string" -ts3, ts3_replay_file = get_common_ats_process( - "ts3", 'traffic_dump.so --logdir {0} --sample 1 --limit 1000000000 -4 ' + invalid_ip, replay_exists=False) -ts3.Disk.diags_log.Content = Testers.ContainsExpression( - f"Problems parsing IP filter address argument: {invalid_ip}", "Verify traffic_dump detects an invalid IPv4 address.") -tr.AddVerifierClientProcess("client2", replay_file, http_ports=[ts3.Variables.port], other_args='--keys 1') - -tr.Processes.Default.StartBefore(ts3) -tr.StillRunningAfter = server -tr.StillRunningAfter = ts3 diff --git a/tests/gold_tests/pluginTest/traffic_dump/traffic_dump_response_body.test.py b/tests/gold_tests/pluginTest/traffic_dump/traffic_dump_response_body.test.py deleted file mode 100644 index 1b296b5e6a0..00000000000 --- a/tests/gold_tests/pluginTest/traffic_dump/traffic_dump_response_body.test.py +++ /dev/null @@ -1,144 +0,0 @@ -""" -Verify traffic_dump response body functionality. -""" -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import os -import sys - -Test.Summary = ''' -Verify traffic_dump response body functionality. -''' - -Test.SkipUnless(Condition.PluginExists('traffic_dump.so'),) - -# Configure the origin server. -replay_file = "replay/response_body.yaml" -server = Test.MakeVerifierServerProcess("server", replay_file) - -# Configure ATS. -ts = Test.MakeATSProcess("ts", enable_tls=True) -replay_dir = os.path.join(ts.RunDirectory, "ts", "log") - -ts.addSSLfile("ssl/server.pem") -ts.addSSLfile("ssl/server.key") -ts.addSSLfile("ssl/signer.pem") - -ts.Disk.records_config.update( - { - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'traffic_dump', - 'proxy.config.ssl.server.cert.path': ts.Variables.SSLDir, - 'proxy.config.ssl.server.private_key.path': ts.Variables.SSLDir, - 'proxy.config.url_remap.pristine_host_hdr': 1, - 'proxy.config.ssl.CA.cert.filename': f'{ts.Variables.SSLDir}/signer.pem', - 'proxy.config.exec_thread.autoconfig.scale': 1.0, - 'proxy.config.http.host_sni_policy': 2, - 'proxy.config.ssl.TLSv1_3.enabled': 0, - 'proxy.config.ssl.client.verify.server.policy': 'PERMISSIVE', - }) - -ts.Disk.ssl_multicert_yaml.AddLines( - """ -ssl_multicert: - - dest_ip: "*" - ssl_cert_name: server.pem - ssl_key_name: server.key -""".split("\n")) - -ts.Disk.remap_config.AddLines([ - f'map / http://127.0.0.1:{server.Variables.http_port}', -]) - -# Configure traffic_dump to dump body bytes (-b). -ts.Disk.plugin_config.AddLine(f'traffic_dump.so --logdir {replay_dir} --sample 1 --limit 1000000000 -b') - -ts_dump_0 = os.path.join(replay_dir, "127", "0000000000000000") -ts.Disk.File(ts_dump_0, exists=True) - -ts_dump_1 = os.path.join(replay_dir, "127", "0000000000000001") -ts.Disk.File(ts_dump_1, exists=True) - -ts_dump_2 = os.path.join(replay_dir, "127", "0000000000000002") -ts.Disk.File(ts_dump_2, exists=True) - -ts_dump_3 = os.path.join(replay_dir, "127", "0000000000000003") -ts.Disk.File(ts_dump_3, exists=True) - -ts.Disk.traffic_out.Content = Testers.ContainsExpression("Dumping body bytes: true", "Verify that dumping body bytes is enabled.") - -# Run our test traffic. -tr = Test.AddTestRun("Run the test traffic.") -tr.AddVerifierClientProcess( - "client", - replay_file, - http_ports=[ts.Variables.port], - https_ports=[ts.Variables.ssl_port], - ssl_cert="ssl/server_combined.pem", - ca_cert="ssl/signer.pem") - -tr.Processes.Default.StartBefore(server) -tr.Processes.Default.StartBefore(ts) -tr.StillRunningAfter = server -tr.StillRunningAfter = ts - -# Common verification variables. -verify_replay = "verify_replay.py" -schema = os.path.join(Test.Variables.AtsTestToolsDir, 'lib', 'replay_schema.json') -verify_command_prefix = f'{sys.executable} {verify_replay} {schema}' - -# -# Verify a response without a body is dumped correctly. -# -tr = Test.AddTestRun("Verify the json content of the transaction with no response body.") -tr.Setup.CopyAs(verify_replay, Test.RunDirectory) -tr.Processes.Default.Command = f'{verify_command_prefix} {ts_dump_0}' -tr.Processes.Default.ReturnCode = 0 -tr.StillRunningAfter = server -tr.StillRunningAfter = ts - -# -# Verify a response with a body is dumped correctly. -# -tr = Test.AddTestRun("Verify the json content of the transaction with a response body.") -tr.Setup.CopyAs(verify_replay, Test.RunDirectory) -tr.Processes.Default.Command = \ - f'{verify_command_prefix} {ts_dump_1} --response_body "0000000 0000001 "' -tr.Processes.Default.ReturnCode = 0 -tr.StillRunningAfter = server -tr.StillRunningAfter = ts - -# -# Verify a response with a character to be escaped is dumped correctly. -# -tr = Test.AddTestRun("Verify the json content of the response body with a character to be escaped.") -tr.Setup.CopyAs(verify_replay, Test.RunDirectory) -tr.Processes.Default.Command = \ - rf'{verify_command_prefix} {ts_dump_2} --response_body 12\"34' -tr.Processes.Default.ReturnCode = 0 -tr.StillRunningAfter = server -tr.StillRunningAfter = ts -# -# Verify an HTTP/2 response with a body is dumped correctly. -# -tr = Test.AddTestRun("Verify the json content of the response body of an HTTP/2 transaction.") -tr.Setup.CopyAs(verify_replay, Test.RunDirectory) -tr.Processes.Default.Command = \ - f'{verify_command_prefix} {ts_dump_3} --response_body "0000000 0000001 0000002 "' -tr.Processes.Default.ReturnCode = 0 -tr.StillRunningAfter = server -tr.StillRunningAfter = ts diff --git a/tests/gold_tests/pluginTest/traffic_dump/traffic_dump_sni_filter.test.py b/tests/gold_tests/pluginTest/traffic_dump/traffic_dump_sni_filter.test.py deleted file mode 100644 index b3ff3ca00e0..00000000000 --- a/tests/gold_tests/pluginTest/traffic_dump/traffic_dump_sni_filter.test.py +++ /dev/null @@ -1,128 +0,0 @@ -""" -Verify traffic_dump functionality. -""" -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import os -import sys - -Test.Summary = ''' -Verify traffic_dump functionality. -''' - -Test.SkipUnless(Condition.PluginExists('traffic_dump.so'),) - -schema_path = os.path.join(Test.Variables.AtsTestToolsDir, 'lib', 'replay_schema.json') -replay_file = "replay/various_sni.yaml" -server = Test.MakeVerifierServerProcess( - "server-various-sni", replay_file, ssl_cert="ssl/server_combined.pem", ca_cert="ssl/signer.pem") - -# Define ATS and configure -ts = Test.MakeATSProcess("ts", enable_tls=True) -replay_dir = os.path.join(ts.RunDirectory, "ts", "log") - -ts.addSSLfile("ssl/server.pem") -ts.addSSLfile("ssl/server.key") -ts.addSSLfile("ssl/signer.pem") - -ts.Disk.records_config.update( - { - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'traffic_dump', - 'proxy.config.ssl.server.cert.path': ts.Variables.SSLDir, - 'proxy.config.ssl.server.private_key.path': ts.Variables.SSLDir, - 'proxy.config.url_remap.pristine_host_hdr': 1, - 'proxy.config.ssl.CA.cert.filename': f'{ts.Variables.SSLDir}/signer.pem', - 'proxy.config.exec_thread.autoconfig.scale': 1.0, - 'proxy.config.http.host_sni_policy': 2, - 'proxy.config.ssl.TLSv1_3.enabled': 0, - 'proxy.config.ssl.client.verify.server.policy': 'PERMISSIVE', - }) - -ts.Disk.ssl_multicert_yaml.AddLines( - """ -ssl_multicert: - - dest_ip: "*" - ssl_cert_name: server.pem - ssl_key_name: server.key -""".split("\n")) - -ts.Disk.remap_config.AddLine(f'map / https://127.0.0.1:{server.Variables.https_port}') - -ts.Disk.sni_yaml.AddLines([ - 'sni:', - '- fqdn: bob.com', - ' verify_client: NONE', - ' host_sni_policy: PERMISSIVE', -]) - -# Configure traffic_dump's SNI filter to only dump connections with SNI bob.com. -sni_filter = "bob.com" -ts.Disk.plugin_config.AddLine(f'traffic_dump.so --logdir {replay_dir} --sample 1 ' - f'--sni-filter "{sni_filter}"') - -# Set up trafficserver expectations. -ts.Disk.traffic_out.Content += Testers.ContainsExpression( - f"Filtering to only dump connections with SNI: {sni_filter}", "Verify filtering for the expected SNI.") - -ts.Disk.traffic_out.Content += Testers.ContainsExpression( - "Ignore HTTPS session with non-filtered SNI: dave", "Verify that the non-desired SNI session was filtered out.") - -ts.Disk.traffic_out.Content += Testers.ContainsExpression( - "Initialized with sample pool size of 1 bytes and unlimited disk utilization", - "Verify traffic_dump initialized with the configured disk limit.") - -# Set up the json replay file expectations. -replay_file_session_1 = os.path.join(replay_dir, "127", "0000000000000000") -ts.Disk.File(replay_file_session_1, exists=True) - -# The second session should be filtered out because it doesn't have the -# expected SNI (note exists is set to False). -replay_file_session_2 = os.path.join(replay_dir, "127", "0000000000000001") -ts.Disk.File(replay_file_session_2, exists=False) - -# The third session should also be filtered out because it doesn't have any -# SNI (note exists is set to False). -replay_file_session_2 = os.path.join(replay_dir, "127", "0000000000000002") -ts.Disk.File(replay_file_session_2, exists=False) - -# Run the traffic with connections containing various SNI values. -tr = Test.AddTestRun("Test SNI filter with various SNI values in the handshakes.") -# Use the same port across the two servers so that the remap config will work -# across both. -server_port = server.Variables.http_port -tr.AddVerifierClientProcess( - "client-various-sni", - replay_file, - https_ports=[ts.Variables.ssl_port], - ssl_cert="ssl/server_combined.pem", - ca_cert="ssl/signer.pem") -tr.Processes.Default.StartBefore(server) -tr.Processes.Default.StartBefore(ts) -tr.StillRunningAfter = server -tr.StillRunningAfter = ts - -# Verify the properties of the replay file for the dumped transaction. -tr = Test.AddTestRun("Verify the json content of the first session") -session_1_protocols = "http,tls,tcp,ip" -session_1_tls_features = 'sni:bob.com,proxy-verify-mode:0,proxy-provided-cert:true' -verify_replay = "verify_replay.py" -tr.Setup.CopyAs(verify_replay, Test.RunDirectory) -tr.Processes.Default.Command = \ - (f'{sys.executable} {verify_replay} {schema_path} {replay_file_session_1} ' - f'--client-protocols "{session_1_protocols}" --client-tls-features "{session_1_tls_features}"') -tr.Processes.Default.ReturnCode = 0 diff --git a/tests/gold_tests/pluginTest/transform/transaction_data_sink.test.py b/tests/gold_tests/pluginTest/transform/transaction_data_sink.test.py deleted file mode 100644 index b6260f33a6b..00000000000 --- a/tests/gold_tests/pluginTest/transform/transaction_data_sink.test.py +++ /dev/null @@ -1,108 +0,0 @@ -''' -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -Test.Summary = ''' -Verify transaction data sink. -''' - -Test.SkipUnless(Condition.PluginExists('txn_data_sink.so'),) - - -class TransactionDataSyncTest: - - replay_file = "transaction-with-body.replays.yaml" - - def __init__(self): - self._setupOriginServer() - self._setupNameserver() - self._setupTS() - - def _setupOriginServer(self): - self.server = Test.MakeVerifierServerProcess("server", self.replay_file) - - def _setupNameserver(self): - self.nameserver = Test.MakeDNServer("dns", default='127.0.0.1') - - def _setupTS(self): - self.ts = Test.MakeATSProcess("ts", enable_cache=False, enable_tls=True) - self.ts.Disk.records_config.update( - { - "proxy.config.ssl.server.cert.path": f'{self.ts.Variables.SSLDir}', - "proxy.config.ssl.server.private_key.path": f'{self.ts.Variables.SSLDir}', - "proxy.config.ssl.client.verify.server.policy": 'PERMISSIVE', - "proxy.config.dns.nameservers": f"127.0.0.1:{self.nameserver.Variables.Port}", - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'http|txn_data_sink', - }) - self.ts.addDefaultSSLFiles() - self.ts.Disk.remap_config.AddLine(f'map / http://localhost:{self.server.Variables.http_port}/') - self.ts.Disk.ssl_multicert_yaml.AddLines( - """ -ssl_multicert: - - dest_ip: "*" - ssl_cert_name: server.pem - ssl_key_name: server.key -""".split("\n")) - self.ts.Disk.plugin_config.AddLine('txn_data_sink.so') - - # All of the bodies that contained "not_dumped" were not configured to - # be dumped. Therefore it is a bug if they show up in the logs. - self.ts.Disk.traffic_out.Content += Testers.ExcludesExpression('body_not_dumped', "An unexpected body was dumped.") - - # Verify that each of the configured transaction bodies were dumped. - self.ts.Disk.traffic_out.Content += Testers.ContainsExpression( - 'http1.1_cl_response_body_dumped', "The expected HTTP/1.1 Content-Length response body was dumped.") - self.ts.Disk.traffic_out.Content += Testers.ContainsExpression( - 'http1.1_chunked_response_body_dumped', "The expected HTTP/1.1 chunked response body was dumped.") - self.ts.Disk.traffic_out.Content += Testers.ContainsExpression( - 'http1.1_cl_request_body_dumped', "The expected HTTP/1.1 Content-Length request body was dumped.") - self.ts.Disk.traffic_out.Content += Testers.ContainsExpression( - 'http1.1_chunked_request_body_dumped', "The expected HTTP/1.1 chunked request body was dumped.") - self.ts.Disk.traffic_out.Content += Testers.ContainsExpression( - '"http2_response_body_dumped"', "The expected HTTP/2 response body was dumped.") - self.ts.Disk.traffic_out.Content += Testers.ContainsExpression( - 'http2_request_body_dumped', "The expected HTTP/2 request body was dumped.") - - def run(self): - """Configure a TestRun for the test.""" - tr = Test.AddTestRun() - tr.Processes.Default.StartBefore(self.server) - tr.Processes.Default.StartBefore(self.nameserver) - tr.Processes.Default.StartBefore(self.ts) - tr.AddVerifierClientProcess( - "client", self.replay_file, http_ports=[self.ts.Variables.port], https_ports=[self.ts.Variables.ssl_port]) - tr.StillRunningAfter = self.server - tr.StillRunningAfter = self.nameserver - tr.StillRunningAfter = self.ts - - tr = Test.AddTestRun("Wait for transaction data sink output") - timeout = 30 - watcher = tr.Processes.Process("watcher") - watcher.Command = f"sleep {timeout}" - watcher.Ready = When.FileContains(self.ts.Disk.traffic_out.Name, "http2_response_body_dumped") - watcher.TimeOut = timeout - tr.TimeOut = timeout - tr.StillRunningAfter = self.server - tr.StillRunningAfter = self.nameserver - tr.StillRunningAfter = self.ts - tr.Processes.Default.StartBefore(watcher) - tr.Processes.Default.Command = "echo await_transaction_data_sink_output" - tr.Processes.Default.ReturnCode = 0 - - -TransactionDataSyncTest().run() diff --git a/tests/gold_tests/pluginTest/tsapi/test_TSHttpSsnInfo.test.py b/tests/gold_tests/pluginTest/tsapi/test_TSHttpSsnInfo.test.py deleted file mode 100644 index 2e9964b9bf1..00000000000 --- a/tests/gold_tests/pluginTest/tsapi/test_TSHttpSsnInfo.test.py +++ /dev/null @@ -1,113 +0,0 @@ -''' -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import os - -Test.Summary = ''' -Test TS API to get H2 Session info and H2 metrics -''' - -Test.SkipUnless(Condition.HasProgram("nghttp", "Nghttp need to be installed on system for this test to work"),) -Test.ContinueOnFail = True - -# ---- -# Setup Origin Server -# ---- -httpbin = Test.MakeHttpBinServer("httpbin") - -# 128ytes -post_body = "0123456789abcdef" * 8 -post_body_file = open(os.path.join(Test.RunDirectory, "post_body"), "w") -post_body_file.write(post_body) -post_body_file.close() - -# ---- -# Setup ATS -# ---- -if Condition.HasATSFeature('TS_USE_QUIC') and Condition.HasCurlFeature('http3'): - ts = Test.MakeATSProcess("ts", enable_tls=True, enable_quic=True) -else: - ts = Test.MakeATSProcess("ts", enable_tls=True) - -# add ssl materials like key, certificates for the server -ts.addDefaultSSLFiles() - -ts.Disk.remap_config.AddLines(['map /httpbin/ http://127.0.0.1:{0}/'.format(httpbin.Variables.Port)]) - -ts.Disk.ssl_multicert_yaml.AddLines( - """ -ssl_multicert: - - dest_ip: "*" - ssl_cert_name: server.pem - ssl_key_name: server.key -""".split("\n")) - -Test.PrepareTestPlugin( - os.path.join(Test.Variables.AtsBuildGoldTestsDir, 'pluginTest', 'tsapi', '.libs', 'test_TSHttpSsnInfo.so'), ts) - -ts.Disk.records_config.update( - { - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'http2|http3|quic|test_TSHttpSsnInfo', - 'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir), - 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts.Variables.SSLDir) - }) - -# http2_info.so will output test logging to this file. -log_path = os.path.join(ts.Variables.LOGDIR, "test_TSHttpSsnInfo_plugin_log.txt") -Test.Env["OUTPUT_FILE"] = log_path - -# ---- -# Test Cases -# ---- - -# H2 SETTINGS, PRIORITY, HEADERS, CONTINUATION, DATA, GOAWAY -tr = Test.AddTestRun('HTTP/2 Test') -tr.TimeOut = 10 -# grep out the continuation lines because that significantly reduces the output -# size which exponentially reduces the gold file processing time. -tr.Processes.Default.Command = f"nghttp -vn --continuation 'https://localhost:{ts.Variables.ssl_port}/httpbin/post' -d 'post_body' | grep -v 'continuation-test'" -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.StartBefore(httpbin) -tr.Processes.Default.StartBefore(Test.Processes.ts) -tr.Processes.Default.Streams.stdout = "test_TSHttpSsnInfo_nghttp0.gold" -tr.StillRunningAfter = httpbin -tr.StillRunningAfter = ts - -# H3 -if Condition.HasATSFeature('TS_USE_QUIC') and Condition.HasCurlFeature('http3'): - tr = Test.AddTestRun('HTTP/3 Test') - tr.TimeOut = 10 - tr.MakeCurlCommand(f"-k --http3 'https://localhost:{ts.Variables.ssl_port}/httpbin/post' -d 'post_body'") - tr.Processes.Default.ReturnCode = 0 - tr.Processes.Default.Streams.stdout = "test_TSHttpSsnInfo_curl0.gold" - tr.StillRunningAfter = httpbin - tr.StillRunningAfter = ts - -tr = Test.AddTestRun('Check the log output') -tr.Processes.Default.Command = "echo check log" -tr.Processes.Default.ReturnCode = 0 -f = tr.Disk.File(log_path) -f.Content = "test_TSHttpSsnInfo_plugin_log.gold" -f.Content += Testers.ContainsExpression( - "H2 Frames Received:D1,H1,PR.,RS0,S2,PP0,P0,G1,WU0,C1,U0", "Expected numbers of frames should be received") -f.Content += Testers.ContainsExpression( - "H2 OOB\\(11\\)=0,OOB\\(1000\\)=0", "OOB sub_key values should map to UNKNOWN bucket without crashing") -# We cannot test this on H3 now because the test plugin does not work on H3 sessions -# f.Content += Testers.ContainsExpression("H3 Frames Received:D1,H1,Ra0,CP0,S1,PP0,Rb0,G0,Rc0,Rd0,UND0,UND0,UND0,MPI0,U0", -# "Expected numbers of frames should be received") diff --git a/tests/gold_tests/pluginTest/tsapi/test_TSHttpTxnServerAddrSet.test.py b/tests/gold_tests/pluginTest/tsapi/test_TSHttpTxnServerAddrSet.test.py deleted file mode 100644 index 4f0d85eca29..00000000000 --- a/tests/gold_tests/pluginTest/tsapi/test_TSHttpTxnServerAddrSet.test.py +++ /dev/null @@ -1,91 +0,0 @@ -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import os -from ports import get_port - -Test.Summary = ''' -Verify TSHttpTxnServerAddrSet() works as expected. -''' - -plugin_name = "test_TSHttpTxnServerAddrSet" - - -class TestTSHttpTxnServerAddrSet: - """Verify that TSHttpTxnServerAddrSet() works as expected.""" - _replay_file = "test_TSHttpTxnServerAddrSet.replay.yaml" - - def _configure_server(self, tr: 'TestRun') -> 'Process': - """Configure the server process for a TestRun. - - :param tr: The TestRun for which to configure the server process. - """ - self._server = tr.AddVerifierServerProcess("server", self._replay_file) - self._server.Streams.stdout += Testers.ContainsExpression("redirect-succeeded", "Verify that the server sent the response.") - return self._server - - def _configure_dns(self, tr: 'TestRun') -> 'Process': - """Configure the DNS process for a TestRun. - - :param tr: The TestRun for which to configure the DNS process. - """ - self._dns = tr.MakeDNServer("dns", default=['127.0.0.1']) - return self._dns - - def _configure_traffic_server(self, tr: 'TestRun') -> 'Process': - """Configure the Traffic Server process for a TestRun. - - :param tr: The TestRun for which to configure the Traffic Server process. - """ - self._ts = tr.MakeATSProcess("ts") - ts = self._ts - - plugin_path = os.path.join(Test.Variables.AtsBuildGoldTestsDir, 'pluginTest', 'tsapi', '.libs', f'{plugin_name}.so') - ts.Setup.Copy(plugin_path, ts.Env['PROXY_CONFIG_PLUGIN_PLUGIN_DIR']) - ts.Disk.plugin_config.AddLine(f"{plugin_path} 127.0.0.1 {self._server.Variables.http_port}") - - ts.Disk.records_config.update( - { - 'proxy.config.dns.nameservers': f'127.0.0.1:{self._dns.Variables.Port}', - 'proxy.config.dns.resolv_conf': 'NULL', - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'http|dns|hostdb|test_plugin', - }) - - # Remap to a nonexisting server and port. The plugin should use - # TSHttpTxnServerAddrSet() to set an actual server address and port. We - # use get_port to guarantee that no one is listing on this port. - bogus_port = get_port(self._server, "bogus_port") - ts.Disk.remap_config.AddLine(f'map / http://non.existent.server.com:{bogus_port}') - - def run(self): - """ Configure the TestRun.""" - tr = Test.AddTestRun("Verify TSHttpTxnServerAddrSet() works as expected.") - self._configure_dns(tr) - self._configure_server(tr) - self._configure_traffic_server(tr) - tr.AddVerifierClientProcess("client", self._replay_file, http_ports=[self._ts.Variables.port]) - - tr.Processes.Default.Streams.stdout += Testers.ContainsExpression( - "redirect-succeeded", "Verify that the client received the response.") - - tr.Processes.Default.StartBefore(self._dns) - tr.Processes.Default.StartBefore(self._server) - tr.Processes.Default.StartBefore(self._ts) - - -test = TestTSHttpTxnServerAddrSet() -test.run() diff --git a/tests/gold_tests/pluginTest/tsapi/test_TSHttpTxnServerAddrSet_retry.test.py b/tests/gold_tests/pluginTest/tsapi/test_TSHttpTxnServerAddrSet_retry.test.py deleted file mode 100644 index adf0ba7c6a1..00000000000 --- a/tests/gold_tests/pluginTest/tsapi/test_TSHttpTxnServerAddrSet_retry.test.py +++ /dev/null @@ -1,91 +0,0 @@ -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. -''' -Tests for TSHttpTxnServerAddrSet() retry behavior with and without cache enabled. -''' - -import os -from ports import get_port - -Test.Summary = ''' -Verify TSHttpTxnServerAddrSet() retry works with and without cache enabled -''' - -plugin_name = "test_TSHttpTxnServerAddrSet_retry" - - -class TestServerAddrSetRetry: - """Verify retry behavior for TSHttpTxnServerAddrSet() with and without cache.""" - - def _configure_dns(self, tr: 'TestRun', name: str) -> 'Process': - """Configure the DNS process for a TestRun.""" - return tr.MakeDNServer(name, default=['127.0.0.1']) - - def _configure_traffic_server(self, tr: 'TestRun', name: str, dns: 'Process', enable_cache: bool) -> 'Process': - """Configure the Traffic Server process for a TestRun.""" - ts = tr.MakeATSProcess(name, enable_cache=enable_cache) - - plugin_path = os.path.join(Test.Variables.AtsBuildGoldTestsDir, 'pluginTest', 'tsapi', '.libs', f'{plugin_name}.so') - ts.Setup.Copy(plugin_path, ts.Env['PROXY_CONFIG_PLUGIN_PLUGIN_DIR']) - ts.Disk.plugin_config.AddLine(f"{plugin_path}") - - ts.Disk.records_config.update( - { - 'proxy.config.dns.nameservers': f'127.0.0.1:{dns.Variables.Port}', - 'proxy.config.dns.resolv_conf': 'NULL', - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'http|dns|test_TSHttpTxnServerAddrSet_retry', - # Enable retries so we can see if OS_DNS is called multiple times - 'proxy.config.http.connect_attempts_max_retries': 3, - 'proxy.config.http.connect_attempts_timeout': 1, - }) - - # Remap to a nonexisting server - the plugin will set addresses - bogus_port = get_port(ts, "bogus_port") - ts.Disk.remap_config.AddLine(f'map / http://non.existent.server.com:{bogus_port}') - return ts - - def run(self, enable_cache: bool, description: str, name_suffix: str) -> None: - """Configure a TestRun.""" - tr = Test.AddTestRun(description) - dns = self._configure_dns(tr, f"dns_{name_suffix}") - ts = self._configure_traffic_server(tr, f"ts_{name_suffix}", dns, enable_cache) - - # Make a simple request - it should fail since first address is non-routable - # but the plugin should log whether OS_DNS was called multiple times - tr.Processes.Default.Command = f'curl -vs --connect-timeout 5 http://127.0.0.1:{ts.Variables.port}/ -o /dev/null 2>&1; true' - tr.Processes.Default.ReturnCode = 0 - - tr.Processes.Default.StartBefore(dns) - tr.Processes.Default.StartBefore(ts) - - # Override the default diags.log error check - we use TSError() for test output - # Using '=' instead of '+=' replaces the default "no errors" check - ts.Disk.diags_log.Content = Testers.ContainsExpression("OS_DNS hook called, count=1", "First OS_DNS call logged") - - ts.Disk.diags_log.Content += Testers.ContainsExpression( - "SUCCESS: OS_DNS hook was called", "Plugin was able to retry with different address") - - if enable_cache: - ts.Disk.traffic_out.Content = Testers.ExcludesExpression( - "failed assertion", "ATS should not hit the redirect write-lock assertion") - ts.Disk.traffic_out.Content += Testers.ExcludesExpression( - "received signal 6", "ATS should not abort while retrying origin selection") - - -test = TestServerAddrSetRetry() -test.run(enable_cache=False, description="Reproduce issue #12611 without cache", name_suffix="nocache") -test.run(enable_cache=True, description="Verify TSHttpTxnServerAddrSet retry is safe with cache enabled", name_suffix="cache") diff --git a/tests/gold_tests/pluginTest/tsapi/test_TSHttpTxnVerifiedAddr.test.py b/tests/gold_tests/pluginTest/tsapi/test_TSHttpTxnVerifiedAddr.test.py deleted file mode 100644 index a8b3417bf6b..00000000000 --- a/tests/gold_tests/pluginTest/tsapi/test_TSHttpTxnVerifiedAddr.test.py +++ /dev/null @@ -1,65 +0,0 @@ -''' -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import os - -Test.Summary = ''' -Test TS API to get and set a verified address -''' - -Test.ContinueOnFail = True - -# ---- -# Setup Origin Server -# ---- -httpbin = Test.MakeHttpBinServer("httpbin") - -# ---- -# Setup ATS -# ---- -ts = Test.MakeATSProcess("ts") - -ts.Disk.remap_config.AddLine(f'map /httpbin/ http://127.0.0.1:{httpbin.Variables.Port}/') - -Test.PrepareTestPlugin( - os.path.join(Test.Variables.AtsBuildGoldTestsDir, 'pluginTest', 'tsapi', '.libs', 'test_TSHttpTxnVerifiedAddr.so'), ts) - -ts.Setup.CopyAs('hrw_verified_addr.conf', Test.RunDirectory) -ts.Disk.plugin_config.AddLine(f'header_rewrite.so --inbound-ip-source=PLUGIN {Test.RunDirectory}/hrw_verified_addr.conf') - -ts.Disk.records_config.update( - { - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'http|test_TSHttpTxnVerifiedAddr', - }) - -# ---- -# Test Cases -# ---- - -# The test plugin sets an hard coded IP address as a verified address. -# The verified address should be read by header_rewrite plugin and returned in "ip" response header. -tr = Test.AddTestRun("Test that a verified address is set by a plugin and read by another plugin") -tr.MakeCurlCommand(f'-v http://127.0.0.1:{ts.Variables.port}/httpbin/get', ts=ts) -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.StartBefore(httpbin) -tr.Processes.Default.StartBefore(Test.Processes.ts) -tr.Processes.Default.Streams.stderr.Content = Testers.ContainsExpression( - "ip: 1.1.1.1", "Verifiy header_rewrite picked the verified address") -tr.StillRunningAfter = httpbin -tr.StillRunningAfter = ts diff --git a/tests/gold_tests/pluginTest/tsapi/test_TSVConnPPInfo.test.py b/tests/gold_tests/pluginTest/tsapi/test_TSVConnPPInfo.test.py deleted file mode 100644 index a70a103cadf..00000000000 --- a/tests/gold_tests/pluginTest/tsapi/test_TSVConnPPInfo.test.py +++ /dev/null @@ -1,112 +0,0 @@ -''' -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import os - -Test.Summary = ''' -Test TS API to get PROXY protocol info -''' - -Test.SkipUnless( - Condition.HasProgram("nghttp", "Nghttp need to be installed on system for this test to work"), - Condition.HasCurlOption("--haproxy-clientip"), -) -Test.ContinueOnFail = True - -# ---- -# Setup Origin Server -# ---- -httpbin = Test.MakeHttpBinServer("httpbin") - -# 128ytes -post_body = "0123456789abcdef" * 8 -post_body_file = open(os.path.join(Test.RunDirectory, "post_body"), "w") -post_body_file.write(post_body) -post_body_file.close() - -# ---- -# Setup ATS -# ---- -ts = Test.MakeATSProcess("ts", enable_tls=True, enable_proxy_protocol=True) - -# add ssl materials like key, certificates for the server -ts.addDefaultSSLFiles() - -ts.Disk.remap_config.AddLines(['map /httpbin/ http://127.0.0.1:{0}/'.format(httpbin.Variables.Port)]) - -ts.Disk.ssl_multicert_yaml.AddLines( - """ -ssl_multicert: - - dest_ip: "*" - ssl_cert_name: server.pem - ssl_key_name: server.key -""".split("\n")) - -Test.PrepareTestPlugin( - os.path.join(Test.Variables.AtsBuildGoldTestsDir, 'pluginTest', 'tsapi', '.libs', 'test_TSVConnPPInfo.so'), ts) - -ts.Disk.records_config.update( - { - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'http|proxyprotocol|test_TSVConnPPInfo', - 'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir), - 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts.Variables.SSLDir) - }) - -# http2_info.so will output test logging to this file. -log_path = os.path.join(ts.Variables.LOGDIR, "test_TSVConnPPInfo_plugin_log.txt") -Test.Env["OUTPUT_FILE"] = log_path - -# ---- -# Test Cases -# ---- - -# plaintext HTTP -tr = Test.AddTestRun() -tr.TimeOut = 10 -tr.Processes.Default.Command = ( - f"curl --haproxy-protocol --haproxy-clientip 1.2.3.4 " - f"'http://127.0.0.1:{ts.Variables.proxy_protocol_port}/httpbin/get'") -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.StartBefore(httpbin) -tr.Processes.Default.StartBefore(Test.Processes.ts) -tr.Processes.Default.Streams.stdout = "test_TSVConnPPInfo_curl0.gold" -tr.StillRunningAfter = httpbin -tr.StillRunningAfter = ts - -# HTTPS -tr = Test.AddTestRun() -tr.TimeOut = 10 -tr.Processes.Default.Command = ( - f"curl --haproxy-protocol --haproxy-clientip 5.6.7.8 -k " - f"'https://127.0.0.1:{ts.Variables.proxy_protocol_ssl_port}/httpbin/get'") -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Streams.stdout = "test_TSVConnPPInfo_curl1.gold" -tr.StillRunningAfter = httpbin -tr.StillRunningAfter = ts - -tr = Test.AddTestRun() -tr.Processes.Default.Command = "echo check log" -tr.Processes.Default.ReturnCode = 0 -f = tr.Disk.File(log_path) -f.Content = "test_TSVConnPPInfo_plugin_log.gold" -# curl 8.20+ intentionally uses --haproxy-clientip for both PROXY addresses so the address family matches. -f.Content += Testers.ContainsExpression( - r"PP Info Received:V1,P2,T1,SRC1\.2\.3\.4,DST(127\.0\.0\.1|1\.2\.3\.4)", "Expected information should be received") -f.Content += Testers.ContainsExpression( - r"PP Info Received:V1,P2,T1,SRC5\.6\.7\.8,DST(127\.0\.0\.1|5\.6\.7\.8)", "Expected information should be received") diff --git a/tests/gold_tests/pluginTest/tsapi/tsapi.test.py b/tests/gold_tests/pluginTest/tsapi/tsapi.test.py deleted file mode 100644 index 2b4e2e278f5..00000000000 --- a/tests/gold_tests/pluginTest/tsapi/tsapi.test.py +++ /dev/null @@ -1,112 +0,0 @@ -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import os - -Test.Summary = ''' -Test TS API. -''' - -Test.SkipUnless(Condition.HasCurlFeature('http2'),) -Test.ContinueOnFail = True - -plugin_name = "test_tsapi" - -server = Test.MakeOriginServer("server") - -request_header = {"headers": "GET / HTTP/1.1\r\nHost: doesnotmatter\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": "112233"} -server.addResponse("sessionlog.json", request_header, response_header) - -request_header = {"headers": "GET /xYz HTTP/1.1\r\nHost: doesnotmatter\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": "445566"} -server.addResponse("sessionlog.json", request_header, response_header) - -# Disable the cache to make sure each request is forwarded to the origin -# server. -ts = Test.MakeATSProcess("ts", enable_tls=True, enable_cache=False) - -# The test plugin will output test logging to this file. -log_file_name = os.path.join(ts.Variables.LOGDIR, "log.txt") -Test.Env["OUTPUT_FILE"] = log_file_name - -ts.addDefaultSSLFiles() - -ts.Disk.records_config.update( - { - 'proxy.config.proxy_name': 'Poxy_Proxy', # This will be the server name. - 'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir), - 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts.Variables.SSLDir), - 'proxy.config.url_remap.remap_required': 1, - 'proxy.config.diags.debug.enabled': 3, - 'proxy.config.diags.debug.tags': f'http|{plugin_name}', - }) - -ts.Disk.ssl_multicert_yaml.AddLines( - """ -ssl_multicert: - - dest_ip: "*" - ssl_cert_name: server.pem - ssl_key_name: server.key -""".split("\n")) - -rp = os.path.join(Test.Variables.AtsBuildGoldTestsDir, 'pluginTest', 'tsapi', '.libs', f'{plugin_name}.so') -ts.Setup.Copy(rp, ts.Env['PROXY_CONFIG_PLUGIN_PLUGIN_DIR']) - -ts.Disk.remap_config.AddLine( - "map http://myhost.test http://127.0.0.1:{0} @plugin={1} @plugin={1}".format(server.Variables.Port, f"{plugin_name}.so")) -ts.Disk.remap_config.AddLine( - "map https://myhost.test:123 http://127.0.0.1:{0} @plugin={1} @plugin={1}".format(server.Variables.Port, f"{plugin_name}.so")) - -ipv4flag = "" -if not Condition.CurlUsingUnixDomainSocket(): - ipv4flag = "--ipv4" - -# For some reason, without this delay, traffic_server cannot reliably open the cleartext port for listening without an -# error. -# -tr = Test.AddTestRun() -tr.Processes.Default.Command = "sleep 3" -tr.Processes.Default.ReturnCode = 0 - -tr = Test.AddTestRun() -tr.Processes.Default.StartBefore(server) -tr.Processes.Default.StartBefore(ts) -# -tr.MakeCurlCommand('--verbose {0} --header "Host: mYhOsT.teSt" hTtP://loCalhOst:{1}/'.format(ipv4flag, ts.Variables.port), ts=ts) -tr.Processes.Default.ReturnCode = 0 - -tr = Test.AddTestRun() -tr.MakeCurlCommand('--verbose {0} --proxy localhost:{1} http://mYhOsT.teSt/xYz'.format(ipv4flag, ts.Variables.port), ts=ts) - -if not Condition.CurlUsingUnixDomainSocket(): - tr = Test.AddTestRun() - tr.MakeCurlCommand( - '--verbose --ipv4 --http2 --insecure --header ' + - '"Host: myhost.test:123" HttPs://LocalHost:{}/'.format(ts.Variables.ssl_port), - ts=ts) - tr.Processes.Default.ReturnCode = 0 - -tr = Test.AddTestRun() -# Change server port number (which can vary) to a fixed string for compare to gold file. -second_log_file_name = os.path.join(ts.Variables.LOGDIR, "log2.txt") -tr.Processes.Default.Command = f"sed 's/{server.Variables.Port}/SERVER_PORT/' < {log_file_name} > {second_log_file_name}" -tr.Processes.Default.ReturnCode = 0 -f = tr.Disk.File(second_log_file_name) -if Condition.CurlUsingUnixDomainSocket(): - f.Content = "log_uds.gold" -else: - f.Content = "log.gold" diff --git a/tests/gold_tests/pluginTest/txn_box/basic/txn_box_basic.test.py b/tests/gold_tests/pluginTest/txn_box/basic/txn_box_basic.test.py deleted file mode 100644 index 99653afa528..00000000000 --- a/tests/gold_tests/pluginTest/txn_box/basic/txn_box_basic.test.py +++ /dev/null @@ -1,44 +0,0 @@ -# @file -# -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. -''' -Basic smoke tests. -''' -Test.Summary = ''' -Test basic functions and directives. -''' - -Test.SkipUnless(Condition.PluginExists('txn_box.so')) - -tr = Test.TxnBoxTestAndRun( - "Test basics", - "basic.replay.yaml", - config_path='Auto', - config_key="meta.txn_box.global", - remap=[ - ['http://remap.ex', 'http://remapped.ex', ['--key=meta.txn_box.remap-1', 'basic.replay.yaml']], - ['http://one.ex/path/', 'http://two.ex/path/'], ['http://one.ex'] - ]) -ts = tr.Variables.TS -ts.Disk.records_config.update( - { - 'proxy.config.log.max_secs_per_buffer': 1, - 'proxy.config.http.per_server.connection.max': 500, - 'proxy.config.http.background_fill_completed_threshold': 0.4, - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'txn_box' - }) diff --git a/tests/gold_tests/pluginTest/txn_box/basic/txn_box_mod.test.py b/tests/gold_tests/pluginTest/txn_box/basic/txn_box_mod.test.py deleted file mode 100644 index 24103d49d93..00000000000 --- a/tests/gold_tests/pluginTest/txn_box/basic/txn_box_mod.test.py +++ /dev/null @@ -1,42 +0,0 @@ -# @file -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. -# -# Copyright 2020, Verizon Media -# -Test.Summary = ''' -Modifier checks. -''' - -Test.SkipUnless(Condition.PluginExists("txn_box.so")) - -replay_file = 'mod.replay.yaml' -tr = Test.TxnBoxTestAndRun( - "Test modifiers", - replay_file, - remap=[ - ['http://alpha.ex', ['--key=meta.txn-box.alpha', replay_file]], - ['http://bravo.ex', ['--key=meta.txn-box.bravo', replay_file]], - ['http://charlie.ex', ['--key=meta.txn-box.charlie', replay_file]] - ]) -ts = tr.Variables.TS -ts.Setup.Copy(replay_file, ts.Variables.CONFIGDIR) # because it's remap only - not auto-copied. -ts.Disk.records_config.update( - { - 'proxy.config.http.cache.http': 0, - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'txn_box' - }) diff --git a/tests/gold_tests/pluginTest/txn_box/basic/txn_box_multi-cfg.test.py b/tests/gold_tests/pluginTest/txn_box/basic/txn_box_multi-cfg.test.py deleted file mode 100644 index 87b3059e14f..00000000000 --- a/tests/gold_tests/pluginTest/txn_box/basic/txn_box_multi-cfg.test.py +++ /dev/null @@ -1,43 +0,0 @@ -# @file -# -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. -# -# Copyright 2020, Verizon Media - -Test.Summary = ''' -Multiple Remap Configurations. -''' - -Test.SkipUnless(Condition.PluginExists("txn_box.so")) - -# Point of this is to test two remap configs in isolation and then both as separate remap configs. -tr = Test.TxnBoxTestAndRun( - "Multiple remap configurations", - "multi-cfg.replay.yaml", - remap=[ - ["http://one.ex", ['multi-cfg.1.yaml']], ["http://two.ex", ['multi-cfg.2.yaml']], - ["http://both.ex", ['multi-cfg.1.yaml', 'multi-cfg.2.yaml']] - ]) -ts = tr.Variables.TS -ts.Setup.Copy("multi-cfg.1.yaml", ts.Variables.CONFIGDIR) -ts.Setup.Copy("multi-cfg.2.yaml", ts.Variables.CONFIGDIR) -ts.Disk.records_config.update( - { - 'proxy.config.log.max_secs_per_buffer': 1, - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'txn_box' - }) diff --git a/tests/gold_tests/pluginTest/txn_box/basic/txn_box_redirect.test.py b/tests/gold_tests/pluginTest/txn_box/basic/txn_box_redirect.test.py deleted file mode 100644 index dc80a718dea..00000000000 --- a/tests/gold_tests/pluginTest/txn_box/basic/txn_box_redirect.test.py +++ /dev/null @@ -1,52 +0,0 @@ -# @file -# -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. -# -# Copyright 2020, Verizon Media - -import os.path - -Test.Summary = ''' -Test that redirect can be used to clip the path for specific URL paths. -''' - -Test.SkipUnless(Condition.PluginExists("txn_box.so")) - -replay_file = "redirect.replay.yaml" - -tr = Test.TxnBoxTestAndRun( - "Redirect", - replay_file, - remap=[ - ['http://base.ex/', ('--key=meta.txn_box.remap', replay_file)], - ['http://unmatched.ex/'] # no TxnBox for this rule. - , - ['http://encode.ex/', ('--key=meta.txn_box.remap-encode', replay_file)], - ['http://decode.ex/', ('--key=meta.txn_box.remap-decode', replay_file)] - ], - verifier_client_args="--verbose info") - -ts = tr.Variables.TS - -ts.Setup.Copy(replay_file, ts.Variables.CONFIGDIR) # because it's remap only - not auto-copied. - -ts.Disk.records_config.update( - { - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'txn_box', - 'proxy.config.http.cache.http': 0 - }) diff --git a/tests/gold_tests/pluginTest/txn_box/basic/txn_box_reply.test.py b/tests/gold_tests/pluginTest/txn_box/basic/txn_box_reply.test.py deleted file mode 100644 index 9bf2dc8bdf5..00000000000 --- a/tests/gold_tests/pluginTest/txn_box/basic/txn_box_reply.test.py +++ /dev/null @@ -1,47 +0,0 @@ -# @file -# -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. -# -# Copyright 2020, Verizon Media - -import os.path - -Test.Summary = ''' -Test the proxy-reply directive. -''' -Test.SkipUnless(Condition.PluginExists("txn_box.so")) - -replay_file = "reply.replay.yaml" - -tr = Test.TxnBoxTestAndRun( - "proxy-reply", - replay_file, - config_path='Auto', - config_key='meta.txn-box.global', - remap=[['http://base.ex/', ('--key=meta.txn-box.remap', replay_file)], ['http://unmatched.ex/']], - verifier_client_args="--verbose info") - -ts = tr.Variables.TS - -ts.Setup.Copy(replay_file, ts.Variables.CONFIGDIR) # because it's remap only - not auto-copied. - -ts.Disk.records_config.update( - { - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'txn_box|http', - 'proxy.config.http.cache.http': 0 - }) diff --git a/tests/gold_tests/pluginTest/txn_box/basic/txn_box_rxp.test.py b/tests/gold_tests/pluginTest/txn_box/basic/txn_box_rxp.test.py deleted file mode 100644 index 0394777574a..00000000000 --- a/tests/gold_tests/pluginTest/txn_box/basic/txn_box_rxp.test.py +++ /dev/null @@ -1,37 +0,0 @@ -# @file -# -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. -# -# Copyright 2020, Verizon Media -# -Test.Summary = ''' -Regular expression. -''' - -Test.SkipUnless(Condition.PluginExists("txn_box.so")) - -tr = Test.TxnBoxTestAndRun( - "Regular Expressions", - "rxp.replay.yaml", - config_path='Auto', - config_key="meta.txn_box.global", - remap=[ - ['http://app.ex', 'http://app.ex', ['--key=meta.txn_box.remap', 'rxp.replay.yaml']], - ['http://one.ex/path/', 'http://two.ex/path/'], ['http://one.ex'] - ]) -ts = tr.Variables.TS -ts.Disk.records_config.update({'proxy.config.diags.debug.enabled': 1, 'proxy.config.diags.debug.tags': 'txn_box'}) diff --git a/tests/gold_tests/pluginTest/txn_box/basic/txn_box_stat.test.py b/tests/gold_tests/pluginTest/txn_box/basic/txn_box_stat.test.py deleted file mode 100644 index 030113f13ed..00000000000 --- a/tests/gold_tests/pluginTest/txn_box/basic/txn_box_stat.test.py +++ /dev/null @@ -1,60 +0,0 @@ -# @file -# -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. -# -# Copyright 2020, Verizon Media -# -import os.path - -Test.Summary = ''' -Plugin statistics. -''' - -Test.SkipUnless(Condition.PluginExists("txn_box.so")) -Test.SkipIf(Condition.true("This needs to be revisit.")) - -tr = Test.TxnBoxTestAndRun( - "Plugin Stats", - "stat.replay.yaml", - config_path='Auto', - config_key='meta.txn_box.global', - verifier_client_args="--verbose info", - command="traffic_server") - -ts = tr.Variables.TS -ts.Setup.Copy("stat.replay.yaml", ts.Variables.CONFIGDIR) - -ts.Disk.records_config.update( - { - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'txn_box', - 'proxy.config.http.cache.http': 0, - 'proxy.config.http.server_ports': '{0}'.format(ts.Variables.port) - }) - -probe_r = tr.Variables.TEST.AddTestRun() -probe_r.DelayStart = 20 -probe_r.Processes.Default.Command = "traffic_ctl metric get plugin.txn_box.stat-1" -probe_r.Processes.Default.Env = ts.Env -probe_r.Processes.Default.ReturnCode = 0 -probe_r.Processes.Default.Streams.stdout = Testers.ContainsExpression("stat-1 3", "Checking stat-1 value") - -probe_r = tr.Variables.TEST.AddTestRun() -probe_r.Processes.Default.Command = "traffic_ctl metric get plugin.test.stat-2" -probe_r.Processes.Default.Env = ts.Env -probe_r.Processes.Default.ReturnCode = 0 -probe_r.Processes.Default.Streams.stdout = Testers.ContainsExpression("stat-2 105", "Checking stat-2 value") diff --git a/tests/gold_tests/pluginTest/txn_box/basic/txn_box_tls-cert.test.py b/tests/gold_tests/pluginTest/txn_box/basic/txn_box_tls-cert.test.py deleted file mode 100644 index 496e5f08954..00000000000 --- a/tests/gold_tests/pluginTest/txn_box/basic/txn_box_tls-cert.test.py +++ /dev/null @@ -1,67 +0,0 @@ -# @file -# -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. -# -# Copyright 2022, Apache Software Foundation - -import os.path - -Test.Summary = ''' -Test certificate handling. -''' - -Test.SkipUnless(Condition.PluginExists("txn_box.so")) - -tr = Test.TxnBoxTestAndRun( - "TLS Certs", - "tls-cert.replay.yaml", - config_path='Auto', - config_key="meta.txn_box.global", - enable_tls=True, - remap=[ - ['https://alpha.ex/', "https://alpha.ex/"], ['http://alpha.ex/', 'https://alpha.ex/'], - ['http://charlie.ex/', 'https://charlie.ex/'] - ]) - -ts = tr.Variables.TS - -ts.Setup.Copy("../ssl/server.key", os.path.join(ts.Variables.SSLDir, "server.key")) -ts.Setup.Copy("../ssl/server.pem", os.path.join(ts.Variables.SSLDir, "server.pem")) -ts.Setup.Copy("../ssl/bravo-signed.cert", os.path.join(ts.Variables.SSLDir, "bravo-signed.cert")) - -ts.Disk.records_config.update( - { - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'txn_box|http|ssl', - 'proxy.config.http.cache.http': 0, - 'proxy.config.ssl.server.cert.path': ts.Variables.SSLDir, - 'proxy.config.ssl.server.private_key.path': ts.Variables.SSLDir - # enable ssl port - , - 'proxy.config.http.server_ports': '{0} {1}:ssl'.format(ts.Variables.port, ts.Variables.ssl_port), - 'proxy.config.ssl.client.certification_level': 0, - 'proxy.config.ssl.client.verify.server.policy': 'DISABLED', - 'proxy.config.ssl.client.cert.path': ts.Variables.SSLDir, - 'proxy.config.ssl.client.cert.filename': "bravo-signed.cert" - }) -ts.Disk.ssl_multicert_yaml.AddLines( - """ -ssl_multicert: - - dest_ip: "*" - ssl_cert_name: server.pem - ssl_key_name: server.key -""".split("\n")) diff --git a/tests/gold_tests/pluginTest/txn_box/basic/txn_box_tls.test.py b/tests/gold_tests/pluginTest/txn_box/basic/txn_box_tls.test.py deleted file mode 100644 index e23165d4ce5..00000000000 --- a/tests/gold_tests/pluginTest/txn_box/basic/txn_box_tls.test.py +++ /dev/null @@ -1,62 +0,0 @@ -# @file -# -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. -# -# Copyright 2020, Verizon Media - -import os.path - -Test.Summary = ''' -Basic TLS testing. -''' - -Test.SkipUnless(Condition.PluginExists("txn_box.so")) - -tr = Test.TxnBoxTestAndRun( - "Basic TLS", - "tls.replay.yaml", - remap=[ - ['http://base.ex/', ('--key=meta.txn_box.remap', 'tls.replay.yaml')], - ['https://base.ex/', ('--key=meta.txn_box.remap', 'tls.replay.yaml')] - ], - verifier_client_args="--verbose info", - enable_tls=True) - -ts = tr.Variables.TS - -ts.Setup.Copy("tls.replay.yaml", ts.Variables.CONFIGDIR) # because it's remap only - not auto-copied. -ts.addDefaultSSLFiles() - -ts.Disk.records_config.update( - { - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'txn_box|http|ssl', - 'proxy.config.http.cache.http': 0, - 'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir), - 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts.Variables.SSLDir) - # enable ssl port - , - 'proxy.config.http.server_ports': '{0} {1}:ssl'.format(ts.Variables.port, ts.Variables.ssl_port), - 'proxy.config.ssl.client.verify.server.policy': 'DISABLED' - }) -ts.Disk.ssl_multicert_yaml.AddLines( - """ -ssl_multicert: - - dest_ip: "*" - ssl_cert_name: server.pem - ssl_key_name: server.key -""".split("\n")) diff --git a/tests/gold_tests/pluginTest/txn_box/basic/txn_box_tuple.test.py b/tests/gold_tests/pluginTest/txn_box/basic/txn_box_tuple.test.py deleted file mode 100644 index 4b640751560..00000000000 --- a/tests/gold_tests/pluginTest/txn_box/basic/txn_box_tuple.test.py +++ /dev/null @@ -1,29 +0,0 @@ -# @file -# -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. -# -# Copyright 2020, Verizon Media -# -Test.Summary = ''' -Tuple and related elements checks. -''' - -Test.SkipUnless(Condition.PluginExists("txn_box.so")) - -tr = Test.TxnBoxTestAndRun("Tuples and such", "tuple.replay.yaml", config_path='Auto', config_key="meta.txn_box.global") -ts = tr.Variables.TS -ts.Disk.records_config.update({'proxy.config.diags.debug.enabled': 1, 'proxy.config.diags.debug.tags': 'txn_box'}) diff --git a/tests/gold_tests/pluginTest/txn_box/basic/txn_box_txn-debug.test.py b/tests/gold_tests/pluginTest/txn_box/basic/txn_box_txn-debug.test.py deleted file mode 100644 index 96ac0e17bf5..00000000000 --- a/tests/gold_tests/pluginTest/txn_box/basic/txn_box_txn-debug.test.py +++ /dev/null @@ -1,66 +0,0 @@ -# @file -# -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. -# -# Copyright 2021, Yahoo Inc -# -Test.Summary = ''' -txn-debug directive -''' - -Test.SkipUnless(Condition.PluginExists("txn_box.so")) -Test.SkipIf(Condition.true("This needs to be revisit. TS not finishing up gracefully.")) - -replay_file = "txn-debug.replay.yaml" - -tr = Test.TxnBoxTestAndRun( - "Test txn-debug enabled", - replay_file, - config_path='Auto', - verifier_client_args="--verbose diag --keys debug-expected", - verifier_server_args="--verbose diag", - config_key="meta.txn_box.global", - suffix="debug-enabled") -ts = tr.Variables.TS -ts.Disk.records_config.update( - { - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'txn_box', - 'proxy.config.crash_log_helper': - '/home/dmeden/code/git/trafficserver/build/_sandbox/txn-debug/ts-debug-enabled/bin/traffic_crashlog' - }) - -ts.Disk.traffic_out.Content += Testers.ContainsExpression(r"DIAG: ", -} - -server.addResponse("sessionfile.log", req_header, res_header) - -# http://user:password@host:port/path;params?query#fragment - -# Define default ATS -ts = Test.MakeATSProcess("ts", enable_cache=False) -#ts = Test.MakeATSProcess("ts", "traffic_server_valgrind.sh") - -ts.Disk.records_config.update( - { - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'uri_signing|http', - # 'proxy.config.plugin.dynamic_reload_mode': 0, - # 'proxy.config.diags.debug.tags': 'uri_signing', - }) - -# Use unchanged incoming URL. -ts.Disk.remap_config.AddLine( - 'map http://somehost/ http://127.0.0.1:{}/'.format(server.Variables.Port) + - ' @plugin=uri_signing.so @pparam={}/config.json'.format(Test.RunDirectory)) - -# Install configuration -ts.Setup.CopyAs('config.json', Test.RunDirectory) -ts.Setup.CopyAs('run_sign.sh', Test.RunDirectory) -ts.Setup.CopyAs('signer.json', Test.RunDirectory) -#ts.Setup.CopyAs('traffic_server_valgrind.sh', Test.RunDirectory) - -curl_and_args = '-q -v -x localhost:{} '.format(ts.Variables.port) - -# 0 - reject unsigned request -tr = Test.AddTestRun("unsigned request") -ps = tr.Processes.Default -ps.StartBefore(ts) -ps.StartBefore(server, ready=When.PortOpen(server.Variables.Port)) -tr.MakeCurlCommand(curl_and_args + 'http://somehost/someasset.ts', ts=ts) -ps.ReturnCode = 0 -ps.Streams.stderr = "gold/403.gold" -tr.StillRunningAfter = server -tr.StillRunningAfter = ts - -# 1 - accept a passthru request -tr = Test.AddTestRun("passthru request") -ps = tr.Processes.Default -tr.MakeCurlCommand(curl_and_args + 'http://somehost/crossdomain.xml', ts=ts) -ps.ReturnCode = 0 -ps.Streams.stderr = "gold/200.gold" -tr.StillRunningAfter = server -tr.StillRunningAfter = ts - -# 2 - good token, signed "forever" (run_sign.sh 0) -tr = Test.AddTestRun("good signed") -ps = tr.Processes.Default -tr.MakeCurlCommand( - curl_and_args + - '"http://somehost/someasset.ts?URISigningPackage=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJpc3N1ZXIiLCJleHAiOjE5MjMwNTYwODR9.zw_wFQ-wvrWmfPLGj3hAUWn-GOHkiJZi2but4KV0paY"', - ts=ts) -ps.ReturnCode = 0 -ps.Streams.stderr = "gold/200.gold" -tr.StillRunningAfter = server -tr.StillRunningAfter = ts - -# 3 - expired token (run_sign.sh 1) -tr = Test.AddTestRun("expired signed") -ps = tr.Processes.Default -tr.MakeCurlCommand( - curl_and_args + - '"http://somehost/someasset.ts?URISigningPackage=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJpc3N1ZXIiLCJleHAiOjF9.GkdlOPHQc6BqS4Q6x79GeYuVFO2zuGbaPZZsJfD6ir8"', - ts=ts) -ps.ReturnCode = 0 -ps.Streams.stderr = "gold/403.gold" -tr.StillRunningAfter = server -tr.StillRunningAfter = ts - -# 4 - good token, different key (run_sign.sh 2) -tr = Test.AddTestRun("good token, second key") -ps = tr.Processes.Default -tr.MakeCurlCommand( - curl_and_args + - '"http://somehost/someasset.ts?URISigningPackage=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJpc3N1ZXIiLCJleHAiOjE5MjMwNTYwODR9.ozH4sNwgcOlTZT0l4RQlVCH_osxz9yI1HCBesEv-jYg"', - ts=ts) -ps.ReturnCode = 0 -ps.Streams.stderr = "gold/200.gold" -tr.StillRunningAfter = server -tr.StillRunningAfter = ts - -# 5 - good token, inline -tr = Test.AddTestRun("good signed") -ps = tr.Processes.Default -tr.MakeCurlCommand( - curl_and_args + - '"http://somehost/URISigningPackage=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJpc3N1ZXIiLCJleHAiOjE5MjMwNTYwODR9.zw_wFQ-wvrWmfPLGj3hAUWn-GOHkiJZi2but4KV0paY/someasset.ts"', - ts=ts) -ps.ReturnCode = 0 -ps.Streams.stderr = "gold/200.gold" -tr.StillRunningAfter = server -tr.StillRunningAfter = ts - -# 6 - expired token, inline -tr = Test.AddTestRun("expired signed") -ps = tr.Processes.Default -tr.MakeCurlCommand( - curl_and_args + - '"http://somehost/URISigningPackage=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJpc3N1ZXIiLCJleHAiOjF9.GkdlOPHQc6BqS4Q6x79GeYuVFO2zuGbaPZZsJfD6ir8/someasset.ts"', - ts=ts) -ps.ReturnCode = 0 -ps.Streams.stderr = "gold/403.gold" -tr.StillRunningAfter = server -tr.StillRunningAfter = ts - -# 7 - good token, param -tr = Test.AddTestRun("good signed, param") -ps = tr.Processes.Default -tr.MakeCurlCommand( - curl_and_args + - '"http://somehost/someasset.ts;URISigningPackage=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJpc3N1ZXIiLCJleHAiOjE5MjMwNTYwODR9.zw_wFQ-wvrWmfPLGj3hAUWn-GOHkiJZi2but4KV0paY"', - ts=ts) -ps.ReturnCode = 0 -ps.Streams.stderr = "gold/200.gold" -tr.StillRunningAfter = server -tr.StillRunningAfter = ts - -# 8 - expired token, param -tr = Test.AddTestRun("expired signed, param") -ps = tr.Processes.Default -tr.MakeCurlCommand( - curl_and_args + - '"http://somehost/someasset.ts;URISigningPackage=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJpc3N1ZXIiLCJleHAiOjF9.GkdlOPHQc6BqS4Q6x79GeYuVFO2zuGbaPZZsJfD6ir8"', - ts=ts) -ps.ReturnCode = 0 -ps.Streams.stderr = "gold/403.gold" -tr.StillRunningAfter = server -tr.StillRunningAfter = ts - -# 9 - let's cookie this -tr = Test.AddTestRun("good signed cookie") -ps = tr.Processes.Default -tr.MakeCurlCommand( - curl_and_args + - '"http://somehost/someasset.ts" -H "Cookie: URISigningPackage=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJpc3N1ZXIiLCJleHAiOjE5MjMwNTYwODR9.zw_wFQ-wvrWmfPLGj3hAUWn-GOHkiJZi2but4KV0paY"', - ts=ts) -ps.ReturnCode = 0 -ps.Streams.stderr = "gold/200.gold" -tr.StillRunningAfter = server -tr.StillRunningAfter = ts - -# 10 - expired cookie token -tr = Test.AddTestRun("expired signed cooked") -ps = tr.Processes.Default -tr.MakeCurlCommand( - curl_and_args + - '"http://somehost/someasset.ts" -H "Cookie: URISigningPackage=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJpc3N1ZXIiLCJleHAiOjF9.GkdlOPHQc6BqS4Q6x79GeYuVFO2zuGbaPZZsJfD6ir8"', - ts=ts) -ps.ReturnCode = 0 -ps.Streams.stderr = "gold/403.gold" -tr.StillRunningAfter = server -tr.StillRunningAfter = ts - -# 11 - multiple cookies -tr = Test.AddTestRun("multiple cookies, expired then good") -ps = tr.Processes.Default -tr.MakeCurlCommand( - curl_and_args + - '"http://somehost/someasset.ts" -H "Cookie: URISigningPackage=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJpc3N1ZXIiLCJleHAiOjF9.GkdlOPHQc6BqS4Q6x79GeYuVFO2zuGbaPZZsJfD6ir8;URISigningPackage=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJpc3N1ZXIiLCJleHAiOjE5MjMwNTYwODR9.zw_wFQ-wvrWmfPLGj3hAUWn-GOHkiJZi2but4KV0paY"', - ts=ts) -ps.ReturnCode = 0 -ps.Streams.stderr = "gold/200.gold" -tr.StillRunningAfter = server -tr.StillRunningAfter = ts - -# 12 - Check missing iss from the payload -tr = Test.AddTestRun("Missing iss field in the payload") -ps = tr.Processes.Default -tr.MakeCurlCommand( - curl_and_args + - '"http://somehost/someasset.ts?URISigningPackage=ewogICJ0eXAiOiAiSldUIiwKICAiYWxnIjogIkhTMjU2Igp9.ewogICJleHAiOiAxOTIzMDU2MDg0Cn0.zw_wFQ-wvrWmfPLGj3hAUWn-GOHkiJZi2but4KV0paY"', - ts=ts) -ps.ReturnCode = 0 -ps.Streams.stderr = "gold/403.gold" -ts.Disk.traffic_out.Content = Testers.ContainsExpression( - "Initial JWT Failure: iss is missing, must be present", "should fail the validation") -tr.StillRunningAfter = server -tr.StillRunningAfter = ts diff --git a/tests/gold_tests/pluginTest/url_sig/url_sig.test.py b/tests/gold_tests/pluginTest/url_sig/url_sig.test.py deleted file mode 100644 index d4724485978..00000000000 --- a/tests/gold_tests/pluginTest/url_sig/url_sig.test.py +++ /dev/null @@ -1,406 +0,0 @@ -''' -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import hashlib -import hmac - -Test.Summary = ''' -Test url_sig plugin -''' - -Test.ContinueOnFail = True - -# Skip if plugins not present. -Test.SkipUnless(Condition.PluginExists('url_sig.so')) - -# Skip since tests assume client addresses are IPs -Test.SkipIf(Condition.CurlUsingUnixDomainSocket()) - -# Set up to check the output after the tests have run. -# -url_sig_log_id = Test.Disk.File("url_sig_short.log") -url_sig_log_id.Content = "url_sig.gold" - -server = Test.MakeOriginServer("server") - -request_header = { - "headers": "GET /foo/abcde/qrstuvwxyz HTTP/1.1\r\nHost: just.any.thing\r\n\r\n", - "timestamp": "1469733493.993", - "body": "" -} -# expected response from the origin server -response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -# add response to the server dictionary -server.addResponse("sessionfile.log", request_header, response_header) - -# Add responses for excl_regex URLs that should bypass signature checks -crossdomain_request = { - "headers": "GET /crossdomain.xml HTTP/1.1\r\nHost: just.any.thing\r\n\r\n", - "timestamp": "1469733493.993", - "body": "" -} -crossdomain_response = { - "headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", - "timestamp": "1469733493.993", - "body": "crossdomain" -} -server.addResponse("sessionfile.log", crossdomain_request, crossdomain_response) - -clientaccess_request = { - "headers": "GET /clientaccesspolicy.xml HTTP/1.1\r\nHost: just.any.thing\r\n\r\n", - "timestamp": "1469733493.993", - "body": "" -} -clientaccess_response = { - "headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", - "timestamp": "1469733493.993", - "body": "clientaccess" -} -server.addResponse("sessionfile.log", clientaccess_request, clientaccess_response) - -test_html_request = { - "headers": "GET /test.html HTTP/1.1\r\nHost: just.any.thing\r\n\r\n", - "timestamp": "1469733493.993", - "body": "" -} -test_html_response = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": "test"} -server.addResponse("sessionfile.log", test_html_request, test_html_response) - -# Define default ATS. Disable the cache to make sure each request is forwarded -# to the origin server. -ts = Test.MakeATSProcess("ts", enable_tls=True, enable_cache=False) - -ts.addDefaultSSLFiles() - -ts.Disk.records_config.update( - { - # 'proxy.config.diags.debug.enabled': 1, - # 'proxy.config.diags.debug.tags': 'http|url_sig', - 'proxy.config.proxy_name': 'Poxy_Proxy', # This will be the server name. - 'proxy.config.ssl.server.cert.path': ts.Variables.SSLDir, - 'proxy.config.ssl.server.private_key.path': ts.Variables.SSLDir, - }) - -ts.Disk.ssl_multicert_yaml.AddLines( - """ -ssl_multicert: - - dest_ip: "*" - ssl_cert_name: server.pem - ssl_key_name: server.key -""".split("\n")) - -# Use unchanged incoming URL. -# -ts.Setup.Copy("url_sig.config", ts.Variables.CONFIGDIR) -ts.Disk.remap_config.AddLine( - f'map http://one.two.three/ http://127.0.0.1:{server.Variables.Port}/' + ' @plugin=url_sig.so @pparam=url_sig.config') - -# Use unchanged incoming HTTPS URL. -# -ts.Disk.remap_config.AddLine( - f'map https://one.two.three/ http://127.0.0.1:{server.Variables.Port}/' + ' @plugin=url_sig.so @pparam=url_sig.config') - -# Use pristine URL, incoming URL unchanged. -# -ts.Disk.remap_config.AddLine( - f'map http://four.five.six/ http://127.0.0.1:{server.Variables.Port}/' + - ' @plugin=url_sig.so @pparam=url_sig.config @pparam=pristineurl') - -# Use pristine URL, incoming URL changed. -# -ts.Disk.remap_config.AddLine( - f'map http://seven.eight.nine/ http://127.0.0.1:{server.Variables.Port}' + - ' @plugin=url_sig.so @pparam=url_sig.config @pparam=PristineUrl') - -# Use config with all settings set -# -ts.Setup.Copy("url_sig.all.config", ts.Variables.CONFIGDIR) -ts.Disk.remap_config.AddLine( - f'map http://ten.eleven.twelve/ http://127.0.0.1:{server.Variables.Port}/' + ' @plugin=url_sig.so @pparam=url_sig.all.config') - -# Validation failure tests. - -LogTee = f" 2>&1 | grep '^<' | tee -a {Test.RunDirectory}/url_sig_long.log" - -# Bad client / MD5 / P=101 / URL pristine / URL altered. -# -tr = Test.AddTestRun("Bad client IP should fail signature check") -p = tr.MakeCurlCommand( - f"--verbose --proxy http://127.0.0.1:{ts.Variables.port} 'http://seven.eight.nine/" + - "foo/abcde/qrstuvwxyz?C=127.0.0.2&E=33046620008&A=2&K=13&P=101&S=d1f352d4f1d931ad2f441013402d93f8'" + LogTee, - ts=ts) -p.StartBefore(ts) -p.StartBefore(server, ready=When.PortOpen(server.Variables.Port)) -p.ReturnCode = 0 -p.Streams.stdout = Testers.ContainsExpression("HTTP.*403", "Should receive 403 Forbidden") - -# With client / MD5 / P=010 / URL pristine / URL altered -- Expired. -# -tr = Test.AddTestRun("Expired signature should fail") -p = tr.MakeCurlCommand( - f"--verbose --proxy http://127.0.0.1:{ts.Variables.port} 'http://seven.eight.nine/" + - "foo/abcde/qrstuvwxyz?C=127.0.0.1&E=1&A=2&K=13&P=010&S=f237aad1fa010234d7bf8108a0e36387'" + LogTee, - ts=ts) -p.ReturnCode = 0 -p.Streams.stdout = Testers.ContainsExpression("HTTP.*403", "Should receive 403 Forbidden") - -# With client / No algorithm / P=101 / URL pristine / URL altered. -# -tr = Test.AddTestRun("Missing algorithm parameter should fail") -p = tr.MakeCurlCommand( - f"--verbose --proxy http://127.0.0.1:{ts.Variables.port} 'http://seven.eight.nine/" + - "foo/abcde/qrstuvwxyz?C=127.0.0.1&E=33046620008&K=13&P=101&S=d1f352d4f1d931ad2f441013402d93f8'" + LogTee, - ts=ts) -p.ReturnCode = 0 -p.Streams.stdout = Testers.ContainsExpression("HTTP.*403", "Should receive 403 Forbidden") - -# With client / Bad algorithm / P=101 / URL pristine / URL altered. -# -tr = Test.AddTestRun("Invalid algorithm (A=3) should fail") -p = tr.MakeCurlCommand( - f"--verbose --proxy http://127.0.0.1:{ts.Variables.port} 'http://seven.eight.nine/" + - "foo/abcde/qrstuvwxyz?C=127.0.0.1&E=33046620008&A=3&K=13&P=101&S=d1f352d4f1d931ad2f441013402d93f8'" + LogTee, - ts=ts) -p.ReturnCode = 0 -p.Streams.stdout = Testers.ContainsExpression("HTTP.*403", "Should receive 403 Forbidden") - -# With client / MD5 / No parts / URL pristine / URL altered. -# -tr = Test.AddTestRun("Missing parts parameter should fail") -p = tr.MakeCurlCommand( - f"--verbose --proxy http://127.0.0.1:{ts.Variables.port} 'http://seven.eight.nine/" + - "foo/abcde/qrstuvwxyz?C=127.0.0.1&E=33046620008&A=2&K=13&S=d1f352d4f1d931ad2f441013402d93f8'" + LogTee, - ts=ts) -p.ReturnCode = 0 -p.Streams.stdout = Testers.ContainsExpression("HTTP.*403", "Should receive 403 Forbidden") - -# With client / MD5 / P=10 (bad) / URL pristine / URL altered. -# -tr = Test.AddTestRun("Invalid parts value (P=10) should fail") -p = tr.MakeCurlCommand( - f"--verbose --proxy http://127.0.0.1:{ts.Variables.port} 'http://seven.eight.nine/" + - "foo/abcde/qrstuvwxyz?C=127.0.0.1&E=33046620008&A=2&K=13&P=10&S=d1f352d4f1d931ad2f441013402d93f8'" + LogTee, - ts=ts) -p.ReturnCode = 0 -p.Streams.stdout = Testers.ContainsExpression("HTTP.*403", "Should receive 403 Forbidden") - -# With client / MD5 / P=101 / URL pristine / URL altered -- No signature. -# -tr = Test.AddTestRun("Missing signature parameter should fail") -p = tr.MakeCurlCommand( - f"--verbose --proxy http://127.0.0.1:{ts.Variables.port} 'http://seven.eight.nine/" + - "foo/abcde/qrstuvwxyz?C=127.0.0.1&E=33046620008&A=2&K=13&P=101'" + LogTee, - ts=ts) -p.ReturnCode = 0 -p.Streams.stdout = Testers.ContainsExpression("HTTP.*403", "Should receive 403 Forbidden") - -# With client / MD5 / P=101 / URL pristine / URL altered -- Bad signature. -# -tr = Test.AddTestRun("Incorrect signature should fail") -p = tr.MakeCurlCommand( - f"--verbose --proxy http://127.0.0.1:{ts.Variables.port} 'http://seven.eight.nine/" + - "foo/abcde/qrstuvwxyz?C=127.0.0.1&E=33046620008&A=2&K=13&P=101&S=d1f452d4f1d931ad2f441013402d93f8'" + LogTee, - ts=ts) -p.ReturnCode = 0 -p.Streams.stdout = Testers.ContainsExpression("HTTP.*403", "Should receive 403 Forbidden") - -# With client / MD5 / P=101 / URL pristine / URL altered -- Spurious &. -# -tr = Test.AddTestRun("Spurious ampersand should fail signature check") -p = tr.MakeCurlCommand( - f"--verbose --proxy http://127.0.0.1:{ts.Variables.port} 'http://seven.eight.nine/" + - "foo/abcde/qrstuvwxyz?C=127.0.0.1&E=33046620008&A=2&&K=13&P=101&S=d1f352d4f1d931ad2f441013402d93f8#'" + LogTee, - ts=ts) -p.ReturnCode = 0 -p.Streams.stdout = Testers.ContainsExpression("HTTP.*403", "Should receive 403 Forbidden") - -# With client / MD5 / Only C parameter -- truncated query string. -# -tr = Test.AddTestRun("Truncated query string with only client IP should fail") -p = tr.MakeCurlCommand( - f"--verbose --proxy http://127.0.0.1:{ts.Variables.port} 'http://seven.eight.nine/" + "foo/abcde/qrstuvwxyz?C=127.0.0.1'" + - LogTee, - ts=ts) -p.ReturnCode = 0 -p.Streams.stdout = Testers.ContainsExpression("HTTP.*403", "Should receive 403 Forbidden") - -# With client / MD5 / C parameter last in query -- missing trailing delimiter. -# -tr = Test.AddTestRun("Client IP as final query parameter should fail") -p = tr.MakeCurlCommand( - f"--verbose --proxy http://127.0.0.1:{ts.Variables.port} 'http://seven.eight.nine/" + - "foo/abcde/qrstuvwxyz?E=33046620008&A=2&K=13&P=101&S=d1f352d4f1d931ad2f441013402d93f8&C=127.0.0.1'" + LogTee, - ts=ts) -p.ReturnCode = 0 -p.Streams.stdout = Testers.ContainsExpression("HTTP.*403", "Should receive 403 Forbidden") - -# With client / MD5 / C parameter has empty value. -# -tr = Test.AddTestRun("Empty client IP value should fail") -p = tr.MakeCurlCommand( - f"--verbose --proxy http://127.0.0.1:{ts.Variables.port} 'http://seven.eight.nine/" + - "foo/abcde/qrstuvwxyz?C=&E=33046620008&A=2&K=13&P=101&S=d1f352d4f1d931ad2f441013402d93f8'" + LogTee, - ts=ts) -p.ReturnCode = 0 -p.Streams.stdout = Testers.ContainsExpression("HTTP.*403", "Should receive 403 Forbidden") - -# Success tests. - -# Test excl_regex feature - URLs matching the exclusion regex should bypass signature checks. -# The url_sig.all.config has: excl_regex = (/crossdomain.xml|/clientaccesspolicy.xml|/test.html) -# -tr = Test.AddTestRun("Excluded URL /crossdomain.xml should bypass signature check") -p = tr.MakeCurlCommand( - f"--verbose --proxy http://127.0.0.1:{ts.Variables.port} 'http://ten.eleven.twelve/crossdomain.xml'" + LogTee, ts=ts) -p.ReturnCode = 0 -p.Streams.stdout = Testers.ContainsExpression("HTTP.*200", "Should receive 200 OK without signature") - -# Test another excluded URL. -# -tr = Test.AddTestRun("Excluded URL /clientaccesspolicy.xml should bypass signature check") -p = tr.MakeCurlCommand( - f"--verbose --proxy http://127.0.0.1:{ts.Variables.port} 'http://ten.eleven.twelve/clientaccesspolicy.xml'" + LogTee, ts=ts) -p.ReturnCode = 0 -p.Streams.stdout = Testers.ContainsExpression("HTTP.*200", "Should receive 200 OK without signature") - -# Test third excluded URL. -# -tr = Test.AddTestRun("Excluded URL /test.html should bypass signature check") -p = tr.MakeCurlCommand( - f"--verbose --proxy http://127.0.0.1:{ts.Variables.port} 'http://ten.eleven.twelve/test.html'" + LogTee, ts=ts) -p.ReturnCode = 0 -p.Streams.stdout = Testers.ContainsExpression("HTTP.*200", "Should receive 200 OK without signature") - -# Test that non-excluded URL still requires signature. -# -tr = Test.AddTestRun("Non-excluded URL /other.html should require signature and fail") -p = tr.MakeCurlCommand( - f"--verbose --proxy http://127.0.0.1:{ts.Variables.port} 'http://ten.eleven.twelve/other.html'" + LogTee, ts=ts) -p.ReturnCode = 0 -p.Streams.stdout = Testers.ContainsExpression("HTTP.*403", "Should receive 403 Forbidden without signature") - -# With client / SHA1 / P=1 / URL pristine / URL not altered. -# -tr = Test.AddTestRun("Valid SHA1 signature with client IP should succeed") -p = tr.MakeCurlCommand( - f"--verbose --proxy http://127.0.0.1:{ts.Variables.port} 'http://four.five.six/" + - "foo/abcde/qrstuvwxyz?C=127.0.0.1&E=33046618556&A=1&K=15&P=1&S=f4103561a23adab7723a89b9831d77e0afb61d92'" + LogTee, - ts=ts) -p.ReturnCode = 0 -p.Streams.stdout = Testers.ContainsExpression("HTTP.*200", "Should receive 200 OK") - -# No client / MD5 / P=1 / URL pristine / URL altered. -# -tr = Test.AddTestRun("Valid MD5 signature without client IP should succeed") -p = tr.MakeCurlCommand( - f"--verbose --proxy http://127.0.0.1:{ts.Variables.port} 'http://seven.eight.nine/" + - "foo/abcde/qrstuvwxyz?E=33046618586&A=2&K=0&P=1&S=0364efa28afe345544596705b92d20ac'" + LogTee, - ts=ts) -p.ReturnCode = 0 -p.Streams.stdout = Testers.ContainsExpression("HTTP.*200", "Should receive 200 OK") - -# With client / MD5 / P=010 / URL pristine / URL altered. -# -tr = Test.AddTestRun("Valid MD5 signature with client IP and P=010 should succeed") -p = tr.MakeCurlCommand( - f"--verbose --proxy http://127.0.0.1:{ts.Variables.port} 'http://seven.eight.nine/" + - "foo/abcde/qrstuvwxyz?C=127.0.0.1&E=33046619717&A=2&K=13&P=010&S=f237aad1fa010234d7bf8108a0e36387'" + LogTee, - ts=ts) -p.ReturnCode = 0 -p.Streams.stdout = Testers.ContainsExpression("HTTP.*200", "Should receive 200 OK") - -# With client / MD5 / P=101 / URL pristine / URL altered. -# -tr = Test.AddTestRun("Valid MD5 signature with client IP and P=101 should succeed") -p = tr.MakeCurlCommand( - f"--verbose --proxy http://127.0.0.1:{ts.Variables.port} 'http://seven.eight.nine/" + - "foo/abcde/qrstuvwxyz?C=127.0.0.1&E=33046620008&A=2&K=13&P=101&S=d1f352d4f1d931ad2f441013402d93f8'" + LogTee, - ts=ts) -p.ReturnCode = 0 -p.Streams.stdout = Testers.ContainsExpression("HTTP.*200", "Should receive 200 OK") - - -def sign(payload, key): - secret = bytes(key, 'utf-8') - data = bytes(payload, 'utf-8') - md = bytes(hmac.new(secret, data, digestmod=hashlib.sha1).digest().hex(), 'utf-8') - return md.decode("utf-8") - - -# No client / SHA1 / P=1 / URL not pristine / URL not altered. -# -path = "foo/abcde/qrstuvwxyz?E=33046618506&A=1&K=7&P=1&S=" -to_sign = f"127.0.0.1:{server.Variables.Port}/{path}" -url = "http://one.two.three/" + path + sign(to_sign, "dqsgopTSM_doT6iAysasQVUKaPykyb6e") - -tr = Test.AddTestRun("Valid SHA1 signature without client IP (non-pristine URL) should succeed") -p = tr.MakeCurlCommand(f"--verbose --proxy http://127.0.0.1:{ts.Variables.port} '{url}'" + LogTee, ts=ts) -p.ReturnCode = 0 -p.Streams.stdout = Testers.ContainsExpression("HTTP.*200", "Should receive 200 OK") - -# With client / MD5 / P=101 / URL pristine / URL altered. -# uses url_type pristine in config -tr = Test.AddTestRun("Valid MD5 signature with pristine URL config should succeed") -p = tr.MakeCurlCommand( - f"--verbose --proxy http://127.0.0.1:{ts.Variables.port} 'http://ten.eleven.twelve/" + - "foo/abcde/qrstuvwxyz?C=127.0.0.1&E=33046620008&A=2&K=13&P=101&S=586ef8e808caeeea025c525c89ff2638'" + LogTee, - ts=ts) -p.ReturnCode = 0 -p.Streams.stdout = Testers.ContainsExpression("HTTP.*200", "Should receive 200 OK") - -# Single fail test - check for bad path param inserted -# With client / MD5 / P=101 / URL pristine / URL altered. Bad Path Param -# uses url_type pristine in config -tr = Test.AddTestRun("Bad path parameter injection should fail signature check") -p = tr.MakeCurlCommand( - f"--verbose --proxy http://127.0.0.1:{ts.Variables.port} 'http://ten.eleven.twelve/" + - "foo/abcde/qrstuvwxyz;badparam=true?C=127.0.0.1&E=33046620008&A=2&K=13&P=101&S=586ef8e808caeeea025c525c89ff2638'" + LogTee, - ts=ts) -p.ReturnCode = 0 -p.Streams.stdout = Testers.ContainsExpression("HTTP.*403", "Should receive 403 Forbidden") - -# Success -# With client / SHA1 / P=1 / URL pristine / URL altered. Base64 Encoded Path Param -tr = Test.AddTestRun("Valid base64 encoded path parameter signature should succeed") -p = tr.MakeCurlCommand( - f"--verbose --proxy http://127.0.0.1:{ts.Variables.port} 'http://ten.eleven.twelve/" + - "foo/abcde;urlsig=Qz0xMjcuMC4wLjE7RT0zMzA0NjYyMDAwODtBPTI7Sz0xMztQPTEwMTtTPTA1MDllZjljY2VlNjUxZWQ1OTQxM2MyZjE3YmVhODZh/qrstuvwxyz'" - + LogTee, - ts=ts) -p.ReturnCode = 0 -p.Streams.stdout = Testers.ContainsExpression("HTTP.*200", "Should receive 200 OK") - -# Success -# This test must be last since it converts from the long to the short log output -# No client / SHA1 / P=1 / URL not pristine / URL not altered -- HTTPS. -# -path = "foo/abcde/qrstuvwxyz?E=33046618506&A=1&K=7&P=1&S=" -to_sign = f"127.0.0.1:{server.Variables.Port}/{path}" -url = f"https://127.0.0.1:{ts.Variables.ssl_port}/{path}" + sign(to_sign, "dqsgopTSM_doT6iAysasQVUKaPykyb6e") - -tr = Test.AddTestRun("Valid SHA1 signature over HTTPS should succeed") -p = tr.MakeCurlCommandMulti( - f"{{curl_base}} --verbose --http1.1 --insecure --header 'Host: one.two.three' '{url}'" + LogTee + - f" ; grep -F -e '< HTTP' -e Authorization {ts.RunDirectory}/url_sig_long.log > {ts.RunDirectory}/url_sig_short.log ", - ts=ts) -p.ReturnCode = 0 -p.Streams.stdout = Testers.ContainsExpression("HTTP.*200", "Should receive 200 OK") - -# Overriding the built in ERROR check since we expect some ERROR messages -ts.Disk.diags_log.Content = Testers.ContainsExpression("ERROR", "Some tests are failure tests") -ts.Disk.diags_log.Content += Testers.ExcludesExpression("Error parsing", "Verify that we can accept long comment lines") diff --git a/tests/gold_tests/pluginTest/webp_transform/webp_transform_cache_refused.test.py b/tests/gold_tests/pluginTest/webp_transform/webp_transform_cache_refused.test.py deleted file mode 100644 index ae9c431b84d..00000000000 --- a/tests/gold_tests/pluginTest/webp_transform/webp_transform_cache_refused.test.py +++ /dev/null @@ -1,90 +0,0 @@ -''' -An over-cap response that webp_transform refuses with a 502 must not poison the -cache. - -The refused path produces a zero-length body and rewrites the client status to -502 in the send-response-headers hook. The cacheable object, however, is driven -by the origin's 200 server response plus the transform's (empty) output, and the -read hook already stamped a transformed Content-Type onto that server response. -If nothing marks the refused response uncacheable, ATS can store a 200 with an -empty body labeled image/webp and serve that poisoned entry to later clients -while the original requester saw a 502. - -This test enables caching (and forces caching of responses without explicit -freshness headers), drives the over-cap chunked body twice through the same URL, -and asserts BOTH requests are refused with a 502 and an empty body. A cached -poisoned 200 would show up as a 200 (and/or a non-zero body) on the second -request. -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -Test.Summary = 'An over-cap refused (502) response must not poison the cache' - -Test.SkipUnless(Condition.PluginExists('webp_transform.so')) - -Test.ContinueOnFail = True - -server = Test.MakeVerifierServerProcess("server", "replay/webp_chunked_cap.replay.yaml") - -ts = Test.MakeATSProcess("ts", enable_cache=True) -ts.Disk.records_config.update( - { - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'webp_transform', - # Cache aggressively: store 200s even without explicit freshness headers, - # so a refused response that is not marked no-store WOULD be cached. This - # makes the poisoning observable if the fix regresses. - 'proxy.config.http.cache.required_headers': 0, - 'proxy.config.http.cache.ignore_client_cc_max_age': 1, - }) -ts.Disk.plugin_config.AddLine('webp_transform.so convert_to_webp') -ts.Disk.remap_config.AddLine('map http://127.0.0.1:{0}/ http://127.0.0.1:{0}/'.format(server.Variables.http_port)) - -ts.Disk.diags_log.Content = Testers.ContainsExpression( - "response body exceeds cap", "The in-transform cap must trip for the chunked body with no Content-Length") - -# Request 1: origin is contacted, the cap trips mid-stream, client gets a 502. -tr = Test.AddTestRun("first request: over-cap chunked body refused with 502") -tr.MakeCurlCommand( - '-sS -D - -o /dev/null -w "size_download=%{{size_download}}" -x 127.0.0.1:{0} ' - '-H "Accept: image/webp" -H "uuid: chunked-huge" http://127.0.0.1:{1}/chunked-huge.jpg'.format( - ts.Variables.port, server.Variables.http_port), - ts=ts) -tr.Processes.Default.StartBefore(server) -tr.Processes.Default.StartBefore(ts) -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Streams.stdout = Testers.ContainsExpression( - "HTTP/1.1 502", "First over-cap request must be refused with a 502") -tr.Processes.Default.Streams.stdout += Testers.ContainsExpression( - "size_download=0", "First refused response must have an empty body") - -# Request 2: if the refused response was cached, this hit would serve a poisoned -# 200 with an empty (or mislabeled) body. The fix marks the refused response -# uncacheable, so this must also be a fresh 502 with an empty body. -tr2 = Test.AddTestRun("second request: must not be served a cached poisoned 200") -tr2.MakeCurlCommand( - '-sS -D - -o /dev/null -w "size_download=%{{size_download}}" -x 127.0.0.1:{0} ' - '-H "Accept: image/webp" -H "uuid: chunked-huge" http://127.0.0.1:{1}/chunked-huge.jpg'.format( - ts.Variables.port, server.Variables.http_port), - ts=ts) -tr2.Processes.Default.ReturnCode = 0 -tr2.Processes.Default.Streams.stdout = Testers.ContainsExpression( - "HTTP/1.1 502", "Second request must also be a 502, not a cached poisoned 200") -tr2.Processes.Default.Streams.stdout += Testers.ExcludesExpression( - "HTTP/1.1 200", "Second request must not be served a cached 200 (cache poisoning)") -tr2.Processes.Default.Streams.stdout += Testers.ContainsExpression( - "size_download=0", "Second response body must be empty; no cached oversized/empty image") diff --git a/tests/gold_tests/pluginTest/webp_transform/webp_transform_chunked_cap.test.py b/tests/gold_tests/pluginTest/webp_transform/webp_transform_chunked_cap.test.py deleted file mode 100644 index 5c599c34d44..00000000000 --- a/tests/gold_tests/pluginTest/webp_transform/webp_transform_chunked_cap.test.py +++ /dev/null @@ -1,74 +0,0 @@ -''' -webp_transform must refuse an over-cap chunked response -rather than forward an oversized, mislabeled body. - -When the origin advertises a Content-Length over the cap, webp_transform -declines the transform up front and the original response passes through. A -chunked response has no Content-Length, so that early check cannot fire and the -per-transaction cap inside consume() is the only bound. The transform produces -nothing until handleInputComplete, so when the cap is exceeded it drops the -buffer, produces no body, and rewrites the status to 502 in the -send-response-headers hook. The client gets a 502 with an empty body rather -than the oversized image. (Transaction::error() cannot be used; it asserts once -the response is in flight, so the status is changed at send time instead.) - -This test drives a 20 MiB image/jpeg framed with Transfer-Encoding: chunked -through the plugin and confirms the cap trips mid-stream, ATS does not buffer -the whole body or crash, and the client receives a 502 with a zero-length body -rather than the full 20 MiB image. -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -Test.Summary = 'webp_transform returns 502 for an over-cap chunked no-Content-Length response' - -Test.SkipUnless(Condition.PluginExists('webp_transform.so')) - -Test.ContinueOnFail = True - -server = Test.MakeVerifierServerProcess("server", "replay/webp_chunked_cap.replay.yaml") - -ts = Test.MakeATSProcess("ts", enable_cache=False) -ts.Disk.records_config.update({ - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'webp_transform', -}) -ts.Disk.plugin_config.AddLine('webp_transform.so convert_to_webp') -ts.Disk.remap_config.AddLine('map http://127.0.0.1:{0}/ http://127.0.0.1:{0}/'.format(server.Variables.http_port)) - -# No Content-Length, so the up-front decline cannot fire; the cap trips inside -# consume(), which emits this message at ERROR level to diags.log. Asserting it -# both confirms the in-transform cap engaged and tells autest the ERROR line is -# expected (the default check fails on any ERROR in diags.log). -ts.Disk.diags_log.Content = Testers.ContainsExpression( - "response body exceeds cap", "The in-transform cap must trip for a chunked body with no Content-Length") - -tr = Test.AddTestRun("over-cap chunked body is refused with a 502 and no body") -# -w reports the downloaded body size; braces are doubled so str.format leaves -# the curl %{...} variable intact. -tr.MakeCurlCommand( - '-sS -D - -o /dev/null -w "size_download=%{{size_download}}" -x 127.0.0.1:{0} ' - '-H "Accept: image/webp" -H "uuid: chunked-huge" http://127.0.0.1:{1}/chunked-huge.jpg'.format( - ts.Variables.port, server.Variables.http_port), - ts=ts) -tr.Processes.Default.StartBefore(server) -tr.Processes.Default.StartBefore(ts) -tr.Processes.Default.ReturnCode = 0 -# The client gets a 502 with a zero-length body: the oversized image is refused, -# not forwarded. A full response would download 20 MiB. -tr.Processes.Default.Streams.stdout = Testers.ContainsExpression("HTTP/1.1 502", "Over-cap image must be refused with a 502") -tr.Processes.Default.Streams.stdout += Testers.ContainsExpression( - "size_download=0", "Body must be empty; the full 20 MiB image must not be forwarded") diff --git a/tests/gold_tests/pluginTest/webp_transform/webp_transform_decode_limit.test.py b/tests/gold_tests/pluginTest/webp_transform/webp_transform_decode_limit.test.py deleted file mode 100644 index 936a0cd5722..00000000000 --- a/tests/gold_tests/pluginTest/webp_transform/webp_transform_decode_limit.test.py +++ /dev/null @@ -1,117 +0,0 @@ -''' -A small image that declares dimensions over the ImageMagick decode limits must -not be decoded into a giant pixel buffer; the transform reverts to the original -bytes instead of crashing or exhausting memory. - -The byte cap (max_buffer_size) does not help here: a tiny encoded image can sit -well under the cap yet declare enormous dimensions that decode into gigabytes of -pixels. TSPluginInit installs Magick::ResourceLimits (width/height/area/memory/ -map and disk(0)) so such an image fails as a caught Magick::Error and the plugin -reverts to the original bytes. - -This test serves a minimal, over-wide WebP (VP8L) image: a real RIFF/WEBP/VP8L -signature followed by a bit-packed header declaring 16129x2 (16129 > the -plugin's 16000 px width limit), with no bitstream payload beyond the header. -ImageMagick's decoder reads the declared width/height straight out of that -header and throws before it would ever need pixel data; the plugin catches it, -logs an ImageMagick error, and forwards the original bytes. The test asserts -ATS does not crash (the client still gets a 200), the decode-limit error is -logged, and the original body is returned unchanged (not a converted jpeg). - -The plugin's has_signature_for() guard checks the declared encoding's magic -bytes before ImageMagick ever sees the body, so the served body must carry a -real signature -- an arbitrary/mislabeled body would be caught by that guard -instead and never reach the decode-limit code path this test targets. WebP is -used (rather than PNG or JPEG) because it is the only one of the three -signatures the plugin recognizes that can be built entirely from bytes <=0x7f; -the origin server writes the body via a UTF-8 encode, so any byte over 0x7f -would not survive the round trip unchanged. -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import struct - -Test.Summary = 'An over-dimension image is rejected by the decode limits and reverts to the original' - -Test.SkipUnless(Condition.PluginExists('webp_transform.so')) - -Test.ContinueOnFail = True - -# A minimal WebP (VP8L) image, 16129 px wide by 2 px tall. 16129 > the plugin's -# 16000 px width ResourceLimit, so ImageMagick rejects it on read. There is no -# bitstream data beyond the 5-byte VP8L header (signature byte + packed -# width/height), so the body is a few dozen bytes, far under the buffer cap -- -# the decode-side limit is what must catch it, not the byte cap. 16129x2 is -# also chosen so every byte of the packed header is <=0x7f (see module -# docstring for why that matters). -W, H = 16129, 2 -vp8l_payload = b'\x2f' + struct.pack('jpeg conversion; the body -# carries a real RIFF/WEBP/VP8L signature so has_signature_for() lets it -# through to ImageMagick, which reads the over-limit dimensions from the VP8L -# header. -server.addResponse( - "sessionlog.json", { - "headers": "GET /overwide.webp HTTP/1.1\r\nHost: *\r\n\r\n", - "timestamp": "1", - "body": "" - }, { - "headers": - "HTTP/1.1 200 OK\r\nContent-Type: image/webp\r\nContent-Length: {0}\r\nConnection: close\r\n\r\n".format(WEBP_LEN), - "timestamp": "1", - "body": webp_body - }) - -ts = Test.MakeATSProcess("ts", enable_cache=False) -ts.Disk.records_config.update({ - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'webp_transform', -}) -ts.Disk.plugin_config.AddLine('webp_transform.so convert_to_jpeg') -ts.Disk.remap_config.AddLine('map http://127.0.0.1:{0}/ http://127.0.0.1:{0}/'.format(server.Variables.Port)) - -# The decode-limit failure is caught and logged as an ImageMagick error at ERROR -# level. Asserting it confirms the resource limit engaged (not the byte cap) and -# tells autest the ERROR line is expected. -ts.Disk.diags_log.Content = Testers.ContainsExpression( - "ImageMagick.. error", "The decode-side ResourceLimit must reject the over-dimension image") - -tr = Test.AddTestRun("over-dimension image reverts to original instead of crashing") -tr.MakeCurlCommand( - '-sS -D - -o /dev/null -w "size_download=%{{size_download}}" -x 127.0.0.1:{0} ' - '-H "Accept: image/jpeg" http://127.0.0.1:{1}/overwide.webp'.format(ts.Variables.port, server.Variables.Port), - ts=ts) -tr.Processes.Default.StartBefore(server) -tr.Processes.Default.StartBefore(ts) -tr.Processes.Default.ReturnCode = 0 -# ATS must stay up and return the original image, not crash and not 502 (the byte -# cap is not exceeded). The reverted body is the original bytes, so its size -# equals the source image, not a smaller converted jpeg. -tr.Processes.Default.Streams.stdout = Testers.ContainsExpression( - "HTTP/1.1 200", "Client must get a 200 (decode failed over the limit, original forwarded), not a crash or 502") -tr.Processes.Default.Streams.stdout += Testers.ContainsExpression( - "size_download={0}".format(WEBP_LEN), "Original bytes must be forwarded unchanged, not a converted jpeg") diff --git a/tests/gold_tests/pluginTest/webp_transform/webp_transform_max_buffer_size.test.py b/tests/gold_tests/pluginTest/webp_transform/webp_transform_max_buffer_size.test.py deleted file mode 100644 index 7cec80ca25e..00000000000 --- a/tests/gold_tests/pluginTest/webp_transform/webp_transform_max_buffer_size.test.py +++ /dev/null @@ -1,108 +0,0 @@ -''' -The max_buffer_size plugin argument overrides the default 16 MiB buffer cap, and -malformed values are rejected so a bad config cannot silently disable the cap. - -Two ATS instances: - - one loads webp_transform with max_buffer_size=1M and is driven with a 2 MiB - Content-Length image, proving the override is parsed (K/M/G suffix) and - applied: the transform is declined at the 1 MiB cap, not the 16 MiB default. - - one loads webp_transform with several malformed max_buffer_size values - (negative, bad suffix, suffix-multiply overflow) and asserts each is rejected - with "keeping default 16777216", proving a bad value falls back to the safe - default rather than wrapping to a huge or unintended cap. -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -Test.Summary = 'max_buffer_size overrides the cap and rejects malformed values' - -Test.SkipUnless(Condition.PluginExists('webp_transform.so')) - -Test.ContinueOnFail = True - -# ---- Instance 1: a valid 1M override takes effect ---- -server = Test.MakeOriginServer("server") - -# 2 MiB image/jpeg: over the 1 MiB override, well under the 16 MiB default. With -# the override in effect the up-front decline fires at 1 MiB; with the default it -# would not. -TWO_MIB = 2 * 1024 * 1024 -server.addResponse( - "sessionlog.json", { - "headers": "GET /two_mib.jpg HTTP/1.1\r\nHost: *\r\n\r\n", - "timestamp": "1", - "body": "" - }, { - "headers": - "HTTP/1.1 200 OK\r\nContent-Type: image/jpeg\r\nContent-Length: {0}\r\nConnection: close\r\n\r\n".format(TWO_MIB), - "timestamp": "1", - "body": "A" * TWO_MIB - }) - -ts = Test.MakeATSProcess("ts", enable_cache=False) -ts.Disk.records_config.update({ - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'webp_transform', -}) -ts.Disk.plugin_config.AddLine('webp_transform.so convert_to_webp max_buffer_size=1M') -ts.Disk.remap_config.AddLine('map http://127.0.0.1:{0}/ http://127.0.0.1:{0}/'.format(server.Variables.Port)) - -# 1M == 1048576. Seeing the decline name 1048576 (not the 16777216 default) -# proves parse_size parsed the M suffix and the override is what is enforced. -ts.Disk.traffic_out.Content = Testers.ContainsExpression( - "exceeds cap 1048576", "max_buffer_size=1M must override the default and decline at 1 MiB") - -tr = Test.AddTestRun("2 MiB body declined at the 1 MiB override") -tr.MakeCurlCommand( - '-sS -D - -o /dev/null -x 127.0.0.1:{0} -H "Accept: image/webp" http://127.0.0.1:{1}/two_mib.jpg'.format( - ts.Variables.port, server.Variables.Port), - ts=ts) -tr.Processes.Default.StartBefore(server) -tr.Processes.Default.StartBefore(ts) -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Streams.stdout = Testers.ContainsExpression("HTTP/1.1 200", "Declined response is a 200 passthrough") -tr.Processes.Default.Streams.stdout += Testers.ContainsExpression( - "[Cc]ontent-[Tt]ype: image/jpeg", "Declined response keeps its original image/jpeg type") - -# ---- Instance 2: malformed values are rejected and keep the default ---- -ts_bad = Test.MakeATSProcess("ts_bad", enable_cache=False) -ts_bad.Disk.records_config.update({ - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'webp_transform', -}) -# Negative (sign guard), bad suffix, and a value that fits in u64 but overflows -# size_t when multiplied by the G suffix (multiply-overflow guard). All must be -# rejected; none may install a cap, so the 16 MiB default must survive. -ts_bad.Disk.plugin_config.AddLine( - 'webp_transform.so convert_to_webp max_buffer_size=-1 max_buffer_size=8X max_buffer_size=20000000000G') -ts_bad.Disk.remap_config.AddLine('map http://127.0.0.1:{0}/ http://127.0.0.1:{0}/'.format(server.Variables.Port)) - -# TSError() writes these to diags.log at ERROR level. Asserting them here both -# confirms parse_size rejected each value and tells autest the ERROR lines are -# expected (the default check fails on any ERROR in diags.log). -ts_bad.Disk.diags_log.Content = Testers.ContainsExpression( - "invalid max_buffer_size=-1, keeping default 16777216", "Negative value must be rejected") -ts_bad.Disk.diags_log.Content += Testers.ContainsExpression( - "invalid max_buffer_size=8X, keeping default 16777216", "Bad suffix must be rejected") -ts_bad.Disk.diags_log.Content += Testers.ContainsExpression( - "invalid max_buffer_size=20000000000G, keeping default 16777216", "Suffix-multiply overflow must be rejected") - -tr_bad = Test.AddTestRun("malformed max_buffer_size values are rejected, default retained") -tr_bad.MakeCurlCommand( - '-sS -o /dev/null -x 127.0.0.1:{0} http://127.0.0.1:{1}/two_mib.jpg'.format(ts_bad.Variables.port, server.Variables.Port), - ts=ts_bad) -tr_bad.Processes.Default.StartBefore(ts_bad) -tr_bad.Processes.Default.ReturnCode = 0 diff --git a/tests/gold_tests/pluginTest/webp_transform/webp_transform_size_cap.test.py b/tests/gold_tests/pluginTest/webp_transform/webp_transform_size_cap.test.py deleted file mode 100644 index d0f2fd52628..00000000000 --- a/tests/gold_tests/pluginTest/webp_transform/webp_transform_size_cap.test.py +++ /dev/null @@ -1,121 +0,0 @@ -''' -webp_transform must not buffer unbounded response bodies. - -ImageTransform buffered the entire origin response in memory before handing it -to ImageMagick, so a large or malicious image response could exhaust proxy -memory, and a decode could throw an exception the narrow catch did not handle. - -The fix bounds this two ways: when the origin advertises a Content-Length over -the 16 MiB cap, the transform is declined up front and the original response -passes through untouched and keeps its original Content-Type; bodies without a -usable Content-Length are still bounded by a per-transaction cap inside the -transform. - -This test drives a 20 MiB image/jpeg with a Content-Length through ATS with -webp_transform loaded (convert_to_webp) over both HTTP/1.1 and HTTP/2, and -confirms the client gets a 200 with Content-Type image/jpeg (declined, not a -mislabeled image/webp) rather than ATS buffering the body or crashing. -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -Test.Summary = 'webp_transform must not buffer unbounded response bodies' - -Test.SkipUnless( - Condition.PluginExists('webp_transform.so'), - Condition.HasCurlFeature('http2'), -) - -Test.ContinueOnFail = True - -server = Test.MakeOriginServer("server") - -# A 20 MiB image/jpeg with a Content-Length over the 16 MiB cap. ImageMagick -# would reject these bytes, so the point is that the transform is declined up -# front and ATS never buffers or decodes them. -BIG = 20 * 1024 * 1024 -body = "A" * BIG -server.addResponse( - "sessionlog.json", { - "headers": "GET /huge.jpg HTTP/1.1\r\nHost: *\r\n\r\n", - "timestamp": "1", - "body": "" - }, { - "headers": "HTTP/1.1 200 OK\r\nContent-Type: image/jpeg\r\nContent-Length: {0}\r\n\r\n".format(BIG), - "timestamp": "1", - "body": body - }) - -ts = Test.MakeATSProcess("ts", enable_tls=True, enable_cache=False) -ts.addDefaultSSLFiles() - -ts.Disk.ssl_multicert_yaml.AddLines( - """ -ssl_multicert: - - dest_ip: "*" - ssl_cert_name: server.pem - ssl_key_name: server.key -""".split("\n")) - -ts.Disk.records_config.update( - { - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'webp_transform', - 'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir), - 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts.Variables.SSLDir), - }) - -ts.Disk.plugin_config.AddLine('webp_transform.so convert_to_webp') - -# Plugin debug goes to traffic.out. Asserting the decline message naming the -# default 16 MiB cap (16777216) proves the plugin actually engaged and declined, -# rather than the response merely passing through untouched. -ts.Disk.traffic_out.Content = Testers.ContainsExpression( - "exceeds cap 16777216", "Plugin must engage and decline at the default 16 MiB cap") - -# Identity rule for the HTTP/1.1 forward-proxy run, plus a catch-all so the -# HTTP/2 reverse-proxy run resolves to the same origin. -ts.Disk.remap_config.AddLine('map http://127.0.0.1:{0}/ http://127.0.0.1:{0}/'.format(server.Variables.Port)) -ts.Disk.remap_config.AddLine('map / http://127.0.0.1:{0}/'.format(server.Variables.Port)) - -# The buffering DoS is in the origin-response transform, so it is independent of -# the client protocol. Exercise both H1 and H2 to confirm the oversized body is -# declined and the client gets a truthful 200 image/jpeg either way. -tr = Test.AddTestRun("HTTP/1.1 client: oversized body declined, original type preserved") -tr.MakeCurlCommandMulti( - '{curl} -sS -D - -o /dev/null -x 127.0.0.1:TSPORT -H "Accept: image/webp" http://127.0.0.1:OPORT/huge.jpg'.replace( - 'TSPORT', str(ts.Variables.port)).replace('OPORT', str(server.Variables.Port)), - ts=ts) -tr.Processes.Default.StartBefore(ts) -tr.Processes.Default.StartBefore(server) -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Streams.stdout = Testers.ContainsExpression("HTTP/1.1 200", "H1 client must see 200 OK, not a crash") -# The response must keep its truthful image/jpeg type. A mislabeled image/webp -# would mean the original bytes were forwarded under a transformed Content-Type. -tr.Processes.Default.Streams.stdout += Testers.ContainsExpression( - "[Cc]ontent-[Tt]ype: image/jpeg", "Declined response must keep its original image/jpeg type") -tr.Processes.Default.Streams.stdout += Testers.ExcludesExpression( - "image/webp", "Declined response must not be mislabeled image/webp") - -tr2 = Test.AddTestRun("HTTP/2 client: oversized body declined, original type preserved") -tr2.MakeCurlCommand( - '--http2 -k -sS -D - -o /dev/null -H "Accept: image/webp" https://127.0.0.1:{0}/huge.jpg'.format(ts.Variables.ssl_port), ts=ts) -tr2.Processes.Default.ReturnCode = 0 -tr2.Processes.Default.Streams.stdout = Testers.ContainsExpression("HTTP/2 200", "H2 client must see 200 OK, not a crash") -tr2.Processes.Default.Streams.stdout += Testers.ContainsExpression( - "[Cc]ontent-[Tt]ype: image/jpeg", "Declined response must keep its original image/jpeg type") -tr2.Processes.Default.Streams.stdout += Testers.ExcludesExpression( - "image/webp", "Declined response must not be mislabeled image/webp") diff --git a/tests/gold_tests/pluginTest/xdebug/x_cache_info/x_cache_info.test.py b/tests/gold_tests/pluginTest/xdebug/x_cache_info/x_cache_info.test.py deleted file mode 100644 index c69c65936af..00000000000 --- a/tests/gold_tests/pluginTest/xdebug/x_cache_info/x_cache_info.test.py +++ /dev/null @@ -1,72 +0,0 @@ -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import sys - -Test.Summary = ''' -Test xdebug plugin X-Cache-Info header -''' - -server = Test.MakeOriginServer("server") -started = False - -request_header = {"headers": "GET /argh HTTP/1.1\r\nHost: doesnotmatter\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -server.addResponse("sessionlog.json", request_header, response_header) - -ts = Test.MakeATSProcess("ts") - -ts.Disk.records_config.update( - { - 'proxy.config.url_remap.remap_required': 0, - 'proxy.config.diags.debug.enabled': 0, - 'proxy.config.diags.debug.tags': 'http' - }) - -ts.Disk.plugin_config.AddLine('xdebug.so --enable=x-cache-info') - -ts.Disk.remap_config.AddLine("map http://one http://127.0.0.1:{0}".format(server.Variables.Port)) -ts.Disk.remap_config.AddLine("map http://two http://127.0.0.1:{0}".format(server.Variables.Port)) -ts.Disk.remap_config.AddLine("regex_map http://three[0-9]+ http://127.0.0.1:{0}".format(server.Variables.Port)) - -Test.Setup.Copy(f'{Test.Variables.AtsTestToolsDir}') - -files = ["none", "one", "two", "three"] - -for file in files: - Test.Setup.Copy(f'{Test.TestDirectory}/{file}.in') - - -def sendMsg(msgFile): - global started - tr = Test.AddTestRun() - tr.Processes.Default.Command = f"( {sys.executable} tools/tcp_client.py 127.0.0.1 {ts.Variables.port} {msgFile}.in ; echo '======' ) | sed 's/:{server.Variables.Port}/:SERVER_PORT/' >> out.log 2>&1" - tr.Processes.Default.ReturnCode = 0 - - if not started: - tr.Processes.Default.StartBefore(Test.Processes.ts) - tr.Processes.Default.StartBefore(Test.Processes.server) - started = True - - -for file in files: - sendMsg(file) - -tr = Test.AddTestRun() -tr.Processes.Default.Command = "echo test gold" -tr.Processes.Default.ReturnCode = 0 -f = tr.Disk.File("out.log") -f.Content = "out.gold" diff --git a/tests/gold_tests/pluginTest/xdebug/x_effective_url/x_effective_url.test.py b/tests/gold_tests/pluginTest/xdebug/x_effective_url/x_effective_url.test.py deleted file mode 100644 index 9db0aa28163..00000000000 --- a/tests/gold_tests/pluginTest/xdebug/x_effective_url/x_effective_url.test.py +++ /dev/null @@ -1,82 +0,0 @@ -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import sys - -Test.Summary = ''' -Test xdebug plugin X-Effective header -''' - -server = Test.MakeOriginServer("server") - -request_header = {"headers": "GET /argh HTTP/1.1\r\nHost: doesnotmatter\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -server.addResponse("sessionlog.json", request_header, response_header) - -request_header_two = {"headers": "GET /two/argh HTTP/1.1\r\nHost: doesnotmatter\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -server.addResponse("sessionlog.json", request_header_two, response_header) - -# request_header_three would be identical to request_header. - -request_header_four = { - "headers": "GET /four/argh,urgh HTTP/1.1\r\nHost: doesnotmatter\r\n\r\n", - "timestamp": "1469733493.993", - "body": "" -} -server.addResponse("sessionlog.json", request_header_four, response_header) - -ts = Test.MakeATSProcess("ts") - -ts.Disk.records_config.update({ - 'proxy.config.url_remap.remap_required': 0, - 'proxy.config.diags.debug.enabled': 0, -}) - -ts.Disk.plugin_config.AddLine('xdebug.so --enable=x-effective-url') - -ts.Disk.remap_config.AddLine("map http://one http://127.0.0.1:{0}".format(server.Variables.Port)) -ts.Disk.remap_config.AddLine("map http://two http://127.0.0.1:{0}".format(server.Variables.Port) + "/two") -ts.Disk.remap_config.AddLine("regex_map http://three[0-9]+ http://127.0.0.1:{0}".format(server.Variables.Port) # Host: three123 - ) -ts.Disk.remap_config.AddLine("regex_map http://four http://127.0.0.1:{0}".format(server.Variables.Port) + "/four") - -tr = Test.AddTestRun() -tr.Processes.Default.StartBefore(Test.Processes.ts) -tr.Processes.Default.StartBefore(Test.Processes.server) -tr.Processes.Default.Command = "cp {}/tcp_client.py {}/tcp_client.py".format(Test.Variables.AtsTestToolsDir, Test.RunDirectory) -tr.Processes.Default.ReturnCode = 0 - - -def sendMsg(msgFile): - - tr = Test.AddTestRun() - tr.Processes.Default.Command = ( - f"( {sys.executable} {Test.RunDirectory}/tcp_client.py 127.0.0.1 {ts.Variables.port} {Test.TestDirectory}/{msgFile}.in" - f" ; echo '======' ) | sed 's/:{server.Variables.Port}/:SERVER_PORT/' >> {Test.RunDirectory}/out.log 2>&1 ") - tr.Processes.Default.ReturnCode = 0 - - -sendMsg('none') -sendMsg('one') -sendMsg('two') -sendMsg('three') -sendMsg('four') - -tr = Test.AddTestRun() -tr.Processes.Default.Command = "echo test out.gold" -tr.Processes.Default.ReturnCode = 0 -f = tr.Disk.File("out.log") -f.Content = "out.gold" diff --git a/tests/gold_tests/pluginTest/xdebug/x_probe/x_probe.test.py b/tests/gold_tests/pluginTest/xdebug/x_probe/x_probe.test.py deleted file mode 100644 index b128cc5eaa8..00000000000 --- a/tests/gold_tests/pluginTest/xdebug/x_probe/x_probe.test.py +++ /dev/null @@ -1,81 +0,0 @@ -''' -Verify xdebug plugin probe functionality. -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -Test.Summary = 'Test xdebug plugin probe functionality' -Test.ContinueOnFail = True -Test.SkipUnless(Condition.PluginExists('xdebug.so')) - - -class XDebugProbeTest: - """ - Test the xdebug probe functionality which transforms the response body - to include request and response headers in a multipart format. - - The probe feature: - - Changes Content-Type to text/plain - - Injects request headers before the original body - - Injects response headers after the original body - - Uses multipart boundary separators - - Disables caching due to body modification - """ - - _replay_file: str = "x_probe.replay.yaml" - - def __init__(self) -> None: - tr = Test.AddTestRun("xdebug probe test") - self._setupOriginServer(tr) - self._setupTS(tr) - self._setupClient(tr) - - def _setupOriginServer(self, tr: 'TestRun') -> None: - """Configure the origin server using Proxy Verifier. - :param tr: TestRun to add the server to. - """ - self._server = tr.AddVerifierServerProcess("server", self._replay_file) - - def _setupTS(self, tr: 'TestRun') -> None: - """Configure ATS with xdebug plugin enabled for probe functionality. - :param tr: TestRun to add the ATS process to. - """ - self._ts = Test.MakeATSProcess("ts") - - self._ts.Disk.records_config.update({ - "proxy.config.diags.debug.enabled": 1, - "proxy.config.diags.debug.tags": "xdebug", - }) - - self._ts.Disk.plugin_config.AddLine('xdebug.so --enable=probe') - self._ts.Disk.remap_config.AddLine(f"map / http://127.0.0.1:{self._server.Variables.http_port}") - - def _setupClient(self, tr: 'TestRun') -> None: - """Test basic probe functionality with header injection. - :param tr: TestRun to add the test to. - """ - tr.Processes.Default.StartBefore(self._server) - tr.Processes.Default.StartBefore(self._ts) - tr.AddVerifierClientProcess("client", self._replay_file, http_ports=[self._ts.Variables.port]) - tr.Processes.Default.ReturnCode = 0 - tr.Processes.Default.Streams.stdout += Testers.ContainsExpression( - 'ATS xDebug Probe Injection Boundary', "ATS xDebug Probe Injection Boundary should be present") - tr.Processes.Default.Streams.stdout += Testers.ContainsExpression('xDebugProbeAt', "xDebugProbeAt should be present") - tr.Processes.Default.Streams.stdout += Testers.ContainsExpression("'x-no-value' : ''", "x-no-value should be present") - - -# Execute the test -XDebugProbeTest() diff --git a/tests/gold_tests/pluginTest/xdebug/x_probe_full_json/x_probe_full_json.test.py b/tests/gold_tests/pluginTest/xdebug/x_probe_full_json/x_probe_full_json.test.py deleted file mode 100644 index 4edac9c3df2..00000000000 --- a/tests/gold_tests/pluginTest/xdebug/x_probe_full_json/x_probe_full_json.test.py +++ /dev/null @@ -1,162 +0,0 @@ -''' -Verify xdebug plugin probe-full-json functionality. -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -Test.Summary = 'Test xdebug plugin probe-full-json functionality' -Test.ContinueOnFail = True -Test.SkipUnless(Condition.PluginExists('xdebug.so')) -Test.SkipUnless(Condition.HasProgram("jq", "jq is required to validate JSON output")) - - -class XDebugProbeFullJsonTest: - """ - Test the xdebug probe-full-json functionality which transforms the response body - to include request headers, response body, and response headers in a complete JSON format. - - The probe-full-json feature: - - Changes Content-Type to application/json - - Generates a complete JSON object containing all debug information - - Includes client/server request headers, response body, and client/server response headers - - Disables caching due to body modification - """ - - _replay_file: str = "x_probe_full_json.replay.yaml" - - def __init__(self) -> None: - self._servers_are_started: bool = False - self._setupOriginServer() - self._setupTS() - self._setupClient() - self._setupJqValidation() - - def _setupOriginServer(self) -> None: - """Configure the origin server using Proxy Verifier. - """ - self._server = Test.MakeVerifierServerProcess("server", self._replay_file) - - def _setupTS(self) -> None: - """Configure ATS with xdebug plugin enabled for probe-full-json functionality. - """ - self._ts = Test.MakeATSProcess("ts") - - self._ts.Disk.records_config.update({ - "proxy.config.diags.debug.enabled": 1, - "proxy.config.diags.debug.tags": "xdebug", - }) - - self._ts.Disk.plugin_config.AddLine('xdebug.so --enable=probe-full-json') - self._ts.Disk.remap_config.AddLine(f"map / http://127.0.0.1:{self._server.Variables.http_port}") - - def _startServersIfNeeded(self, tr: 'TestRun') -> None: - '''Start the servers if they are not already started. - :param tr: TestRun to add the test to. - ''' - if not self._servers_are_started: - tr.Processes.Default.StartBefore(self._server) - tr.Processes.Default.StartBefore(self._ts) - self._servers_are_started = True - - def _setupClient(self) -> None: - """Test basic probe-full-json functionality with JSON output. - """ - tr = Test.AddTestRun("Verify probe-full-json functionality") - self._startServersIfNeeded(tr) - tr.AddVerifierClientProcess("client", self._replay_file, http_ports=[self._ts.Variables.port]) - tr.Processes.Default.ReturnCode = 0 - tr.Processes.Default.Streams.stdout += Testers.ContainsExpression( - 'X-Original-Content-Type: text/html', "X-Original-Content-Type of text/html should be present") - tr.Processes.Default.Streams.stdout += Testers.ContainsExpression( - 'X-Original-Content-Type: application/octet-stream', - "X-Original-Content-Type of application/octet-stream should be present") - # CSP test should produce valid JSON (single quotes not escaped) - tr.Processes.Default.Streams.stdout += Testers.ContainsExpression( - "default-src 'self'; script-src 'self' 'unsafe-inline'", - "CSP header with single quotes should be present in valid JSON") - - def _setupJqValidation(self) -> None: - """Use curl to get the response body and pipe through jq to validate JSON. - """ - tr = Test.AddTestRun("Escaped text/html content.") - self._startServersIfNeeded(tr) - tr.MakeCurlCommand( - f'-s -H"uuid: 1" -H "Host: example.com" -H "X-Debug: probe-full-json" ' - f'http://127.0.0.1:{self._ts.Variables.port}/test | ' - "jq '.\"client-request\".\"uuid\",.\"server-body\",.\"proxy-response\".\"x-response\"'", - ts=self._ts) - tr.Processes.Default.ReturnCode = 0 - tr.Processes.Default.Streams.stdout = "gold/jq_escaped.gold" - - tr = Test.AddTestRun("Hex-encoded application/octet-stream content.") - self._startServersIfNeeded(tr) - tr.MakeCurlCommand( - f'-s -H"uuid: 2" -H "Host: example.com" -H "X-Debug: probe-full-json" ' - f'http://127.0.0.1:{self._ts.Variables.port}/binary | ' - "jq '.\"client-request\".\"uuid\",.\"server-body\",.\"proxy-response\".\"x-response\"'", - ts=self._ts) - tr.Processes.Default.ReturnCode = 0 - tr.Processes.Default.Streams.stdout += "gold/jq_hex.gold" - - tr = Test.AddTestRun("=hex") - self._startServersIfNeeded(tr) - tr.MakeCurlCommand( - f'-s -H"uuid: 1" -H "Host: example.com" -H "X-Debug: probe-full-json=hex" ' - f'http://127.0.0.1:{self._ts.Variables.port}/test | ' - "jq '.\"server-body\"'", - ts=self._ts) - tr.Processes.Default.ReturnCode = 0 - tr.Processes.Default.Streams.stdout += Testers.ContainsExpression( - '3c21444f43545950452068746d6c3e', "Should contain hex-encoded HTML content (forced hex override)") - - tr = Test.AddTestRun("=escape") - self._startServersIfNeeded(tr) - tr.MakeCurlCommand( - f'-s -H"uuid: 1" -H "Host: example.com" -H "X-Debug: probe-full-json=escape" ' - f'http://127.0.0.1:{self._ts.Variables.port}/test | ' - "jq '.\"client-request\".\"uuid\",.\"server-body\",.\"proxy-response\".\"x-response\"'", - ts=self._ts) - tr.Processes.Default.ReturnCode = 0 - tr.Processes.Default.Streams.stdout += "gold/jq_escaped.gold" - - tr = Test.AddTestRun("=nobody") - self._startServersIfNeeded(tr) - tr.MakeCurlCommand( - f'-s -H"uuid: 1" -H "Host: example.com" -H "X-Debug: probe-full-json=nobody" ' - f'http://127.0.0.1:{self._ts.Variables.port}/test | ' - "jq '.\"client-request\".\"uuid\",.\"server-body\",.\"proxy-response\".\"x-response\"'", - ts=self._ts) - tr.Processes.Default.ReturnCode = 0 - tr.Processes.Default.Streams.stdout += "gold/jq_nobody.gold" - - tr = Test.AddTestRun("CSP header with single quotes (valid JSON)") - self._startServersIfNeeded(tr) - tr.MakeCurlCommand( - f'-s -H"uuid: 3" -H "Host: example.com" -H "X-Debug: probe-full-json" ' - f'http://127.0.0.1:{self._ts.Variables.port}/csp-test | ' - "jq '.\"server-response\".\"content-security-policy\"'", - ts=self._ts) - tr.Processes.Default.ReturnCode = 0 - # Verify jq can parse it (would fail if single quotes were escaped as \') - # and that the CSP header contains single quotes - tr.Processes.Default.Streams.stdout += Testers.ContainsExpression( - "'self'", "CSP header should contain single quotes (not escaped)") - tr.Processes.Default.Streams.stdout += Testers.ContainsExpression( - "'unsafe-inline'", "CSP header should contain 'unsafe-inline' directive") - - -# Execute the test -XDebugProbeFullJsonTest() diff --git a/tests/gold_tests/pluginTest/xdebug/x_probe_full_json_slow_origin/x_probe_full_json_slow_origin.test.py b/tests/gold_tests/pluginTest/xdebug/x_probe_full_json_slow_origin/x_probe_full_json_slow_origin.test.py deleted file mode 100644 index 9046dc4b867..00000000000 --- a/tests/gold_tests/pluginTest/xdebug/x_probe_full_json_slow_origin/x_probe_full_json_slow_origin.test.py +++ /dev/null @@ -1,80 +0,0 @@ -''' -Verify xdebug plugin probe-full-json does not hang with slow origin servers. - -This test reproduces a bug where the transform would enter an infinite loop -when the origin server delays sending the response body. The bug occurs because -the transform would reenable and call back upstream even when no data was -available to consume. -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -Test.Summary = ''' -Test xdebug probe-full-json with slow origin to verify no infinite loop -''' - -Test.SkipUnless(Condition.PluginExists('xdebug.so')) -Test.SkipUnless(Condition.HasProgram("nc", "nc (netcat) is required for custom slow server")) - -# Configure ATS -ts = Test.MakeATSProcess("ts", enable_cache=False) - -ts.Disk.records_config.update( - { - "proxy.config.diags.debug.enabled": 1, - "proxy.config.diags.debug.tags": "xdebug_transform", - # Set reasonable timeouts - "proxy.config.http.transaction_no_activity_timeout_in": 10, - "proxy.config.http.transaction_no_activity_timeout_out": 10, - }) - -ts.Disk.plugin_config.AddLine('xdebug.so --enable=probe-full-json') - -# Reserve a port for the custom slow server -Test.GetTcpPort("server_port") - -ts.Disk.remap_config.AddLine(f"map / http://127.0.0.1:{Test.Variables.server_port}/") - -# Start the custom slow-body server -server = Test.Processes.Process( - "server", - f"bash -c '{Test.TestDirectory}/slow-body-server.sh {Test.Variables.server_port} {Test.RunDirectory}/server_request.txt'") - -# Test with probe-full-json=nobody (which triggers the bug most easily) -tr = Test.AddTestRun("Verify probe-full-json with slow body delivery") -tr.TimeOut = 15 # Should complete well under this; timeout indicates hang/loop - -# Make the request - use timeout to detect if the request hangs -tr.Processes.Default.Command = ( - f'timeout 8 curl -s -o /dev/null -w "%{{http_code}}" ' - f'-H "Host: example.com" ' - f'-H "X-Debug: probe-full-json=nobody" ' - f'http://127.0.0.1:{ts.Variables.port}/test') -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.StartBefore(server) -tr.Processes.Default.StartBefore(ts) - -# Should get 200, not timeout (which would cause non-zero return and 124 output) -tr.Processes.Default.Streams.stdout = Testers.ContainsExpression("200", "Should receive 200 OK, not timeout") - -# Verify no infinite loop by checking the logs -# The bug manifests as the transform being called with "bytes of body is expected" -# but no data consumed. Every "expected" should be followed by "consumed". -tr2 = Test.AddTestRun("Verify no infinite loop in transform") -tr2.Processes.Default.Command = f"bash {Test.TestDirectory}/verify_no_loop.sh {ts.Variables.LOGDIR}/traffic.out" -tr2.Processes.Default.ReturnCode = 0 -tr2.Processes.Default.Streams.stdout = Testers.ContainsExpression( - "PASS", "Verification script should pass - every 'expected' followed by 'consumed'") diff --git a/tests/gold_tests/pluginTest/xdebug/x_remap/x_remap.test.py b/tests/gold_tests/pluginTest/xdebug/x_remap/x_remap.test.py deleted file mode 100644 index 332f8601b45..00000000000 --- a/tests/gold_tests/pluginTest/xdebug/x_remap/x_remap.test.py +++ /dev/null @@ -1,82 +0,0 @@ -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import sys - -Test.Summary = ''' -Test xdebug plugin X-Remap, Probe and fwd headers -''' - -server = Test.MakeOriginServer("server", options={'--load': (Test.TestDirectory + '/x_remap-observer.py')}) - -request_header = {"headers": "GET /argh HTTP/1.1\r\nHost: doesnotmatter\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -server.addResponse("sessionlog.json", request_header, response_header) - -ts = Test.MakeATSProcess("ts") - -ts.Disk.records_config.update( - { - 'proxy.config.url_remap.remap_required': 0, - 'proxy.config.diags.debug.enabled': 0, - # 'proxy.config.diags.debug.tags': 'http|xdebug' - # 'proxy.config.diags.debug.tags': 'xdebug' - }) - -ts.Disk.plugin_config.AddLine('xdebug.so --enable=x-remap,probe') - -ts.Disk.remap_config.AddLine("map http://one http://127.0.0.1:{0}".format(server.Variables.Port)) -ts.Disk.remap_config.AddLine("map http://two http://127.0.0.1:{0}".format(server.Variables.Port)) -ts.Disk.remap_config.AddLine("regex_map http://three[0-9]+ http://127.0.0.1:{0}".format(server.Variables.Port)) - -tr = Test.AddTestRun() -tr.Processes.Default.StartBefore(Test.Processes.ts) -tr.Processes.Default.StartBefore(Test.Processes.server) -tr.Processes.Default.Command = "cp {}/tcp_client.py {}/tcp_client.py".format(Test.Variables.AtsTestToolsDir, Test.RunDirectory) -tr.Processes.Default.ReturnCode = 0 - - -def sendMsg(msgFile): - - tr = Test.AddTestRun() - tr.Processes.Default.Command = ( - f"( {sys.executable} {Test.RunDirectory}/tcp_client.py 127.0.0.1 {ts.Variables.port} {Test.TestDirectory}/{msgFile}.in" - f" ; echo '======' ) | sed 's/:{server.Variables.Port}/:SERVER_PORT/' >> {Test.RunDirectory}/out.log 2>&1 ") - tr.Processes.Default.ReturnCode = 0 - - -sendMsg('none') -sendMsg('one') -sendMsg('two') -sendMsg('three') -sendMsg('four') -sendMsg('fwd1') -sendMsg('fwd2') -sendMsg('fwd3') -sendMsg('fwd4') -sendMsg('fwd5') - -tr = Test.AddTestRun() -tr.Processes.Default.Command = "echo test out.gold" -tr.Processes.Default.ReturnCode = 0 -f = tr.Disk.File("out.log") -f.Content = "out.gold" - -tr = Test.AddTestRun() -tr.Processes.Default.Command = "echo test x_remap.gold" -tr.Processes.Default.ReturnCode = 0 -f = tr.Disk.File("x_remap.log") -f.Content = "x_remap.gold" diff --git a/tests/gold_tests/post/expect_tests.test.py b/tests/gold_tests/post/expect_tests.test.py deleted file mode 100644 index e6f85cd660c..00000000000 --- a/tests/gold_tests/post/expect_tests.test.py +++ /dev/null @@ -1,88 +0,0 @@ -''' -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import sys - - -class ExpectTest: - - _expect_client: str = 'expect_client.py' - _http_utils: str = 'http_utils.py' - _replay_file: str = 'replay/expect-continue.replay.yaml' - - def __init__(self): - tr = Test.AddTestRun('Verify Expect: 100-Continue handling.') - self._setup_dns(tr) - self._setup_origin(tr) - self._setup_trafficserver(tr) - self._setup_client(tr) - - def _setup_dns(self, tr: 'TestRun') -> None: - '''Set up the DNS server. - - :param tr: The TestRun to which to add the DNS server. - ''' - dns = tr.MakeDNServer('dns', default='127.0.0.1') - self._dns = dns - - def _setup_origin(self, tr: 'TestRun') -> None: - '''Set up the origin server. - - :param tr: The TestRun to which to add the origin server. - ''' - server = tr.AddVerifierServerProcess("server", replay_path=self._replay_file) - self._server = server - - def _setup_trafficserver(self, tr: 'TestRun') -> None: - '''Set up the traffic server. - - :param tr: The TestRun to which to add the traffic server. - ''' - ts = tr.MakeATSProcess("ts", enable_cache=False) - self._ts = ts - ts.Disk.remap_config.AddLine(f'map / http://backend.example.com:{self._server.Variables.http_port}') - ts.Disk.records_config.update( - { - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'http', - 'proxy.config.dns.nameservers': f"127.0.0.1:{self._dns.Variables.Port}", - 'proxy.config.dns.resolv_conf': 'NULL', - 'proxy.config.http.send_100_continue_response': 1, - }) - - def _setup_client(self, tr: 'TestRun') -> None: - '''Set up the client. - - :param tr: The TestRun to which to add the client. - ''' - tr.Setup.CopyAs(self._expect_client) - tr.Setup.CopyAs(self._http_utils) - tr.Processes.Default.Command = \ - f'{sys.executable} {self._expect_client} 127.0.0.1 {self._ts.Variables.port} -s example.com' - tr.Processes.Default.ReturnCode = 0 - tr.Processes.Default.StartBefore(self._dns) - tr.Processes.Default.StartBefore(self._server) - tr.Processes.Default.StartBefore(self._ts) - tr.Processes.Default.Streams.stdout += Testers.ContainsExpression( - 'HTTP/1.1 100', 'Verify the 100 Continue response was received.') - tr.Processes.Default.Streams.stdout += Testers.ContainsExpression( - 'HTTP/1.1 200', 'Verify the 200 OK response was received.') - - -Test.Summary = 'Verify Expect: 100-Continue handling.' -ExpectTest() diff --git a/tests/gold_tests/post/post-continue.test.py b/tests/gold_tests/post/post-continue.test.py deleted file mode 100644 index b170f0e374e..00000000000 --- a/tests/gold_tests/post/post-continue.test.py +++ /dev/null @@ -1,280 +0,0 @@ -''' -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import os - -# ---- -# Setup Test -# ---- -Test.Summary = ''' -Test the Expect header in post -''' -# Require HTTP/2 enabled Curl -Test.SkipIf(Condition.CurlUsingUnixDomainSocket()) -Test.SkipUnless(Condition.HasCurlFeature('http2'),) -Test.ContinueOnFail = True - -# ---- -# Setup httpbin Origin Server -# ---- -replay_file = "replay/post-continue.replay.yaml" -server = Test.MakeVerifierServerProcess("server", replay_file) - -# ---- -# Setup ATS -# ---- -ts = Test.MakeATSProcess("ts", enable_tls=True, enable_cache=False) - -ts2 = Test.MakeATSProcess("ts2", enable_tls=True, enable_cache=False) - -# add ssl materials like key, certificates for the server -ts.addDefaultSSLFiles() -ts2.addDefaultSSLFiles() - -ts.Disk.remap_config.AddLine('map / http://127.0.0.1:{0}'.format(server.Variables.http_port)) -ts.Disk.ssl_multicert_yaml.AddLines( - """ -ssl_multicert: - - dest_ip: "*" - ssl_cert_name: server.pem - ssl_key_name: server.key -""".split("\n")) -ts.Disk.records_config.update( - { - 'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir), - 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts.Variables.SSLDir), - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'http', - }) -ts2.Disk.remap_config.AddLine('map / http://127.0.0.1:{0}'.format(server.Variables.http_port)) -ts2.Disk.ssl_multicert_yaml.AddLines( - """ -ssl_multicert: - - dest_ip: "*" - ssl_cert_name: server.pem - ssl_key_name: server.key -""".split("\n")) -ts2.Disk.records_config.update( - { - 'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir), - 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts.Variables.SSLDir), - 'proxy.config.diags.debug.enabled': 0, - 'proxy.config.diags.debug.tags': 'http', - 'proxy.config.http.send_100_continue_response': 1 - }) - -big_post_body = "0123456789" * 131070 -big_post_body_file = open(os.path.join(Test.RunDirectory, "big_post_body"), "w") -big_post_body_file.write(big_post_body) -big_post_body_file.close() - -test_run = Test.AddTestRun("http1.1 POST small body with Expect header") -test_run.Processes.Default.StartBefore(server) -test_run.Processes.Default.StartBefore(ts) -test_run.MakeCurlCommand( - '-v -o /dev/null --http1.1 -H "uuid: post" -H "Expect: 100-continue" -d "small body" -k https://127.0.0.1:{0}/post'.format( - ts.Variables.ssl_port), - ts=ts) -test_run.Processes.Default.Streams.All = "gold/post-h1.gold" -test_run.Processes.Default.Streams.All += Testers.ContainsExpression("HTTP/1.1 100 continue", "Has Expect header") -test_run.Processes.Default.Streams.All += Testers.ContainsExpression("Expect: 100-continue", "Has Expect header") -test_run.StillRunningAfter = server -test_run.StillRunningAfter = ts -test_run.Processes.Default.ReturnCode = 0 - -test_run = Test.AddTestRun("http1.1 POST large body with Expect header") -test_run.MakeCurlCommand( - '-v -o /dev/null --http1.1 -H "uuid: post" -H "Expect: 100-continue" -d @big_post_body -k https://127.0.0.1:{0}/post'.format( - ts.Variables.ssl_port), - ts=ts) -test_run.Processes.Default.Streams.All = "gold/post-h1.gold" -test_run.Processes.Default.Streams.All += Testers.ContainsExpression("HTTP/1.1 100 continue", "Has Expect header") -test_run.Processes.Default.Streams.All += Testers.ContainsExpression("Expect: 100-continue", "Has Expect header") -test_run.StillRunningAfter = server -test_run.StillRunningAfter = ts -test_run.Processes.Default.ReturnCode = 0 - -test_run = Test.AddTestRun("http1.1 POST small body w/o Expect header") -test_run.MakeCurlCommand( - '-v -o /dev/null --http1.1 -H "uuid: post" -H "Expect:" -d "small body" -k https://127.0.0.1:{0}/post'.format( - ts.Variables.ssl_port), - ts=ts) -test_run.Processes.Default.Streams.All = "gold/post-h1.gold" -test_run.Processes.Default.Streams.All += Testers.ExcludesExpression("HTTP/1.1 100 continue", "Does not have Expect header") -test_run.Processes.Default.Streams.All += Testers.ExcludesExpression("Expect: 100-continue", "Does not have Expect header") -test_run.StillRunningAfter = server -test_run.StillRunningAfter = ts -test_run.Processes.Default.ReturnCode = 0 - -test_run = Test.AddTestRun("http1.1 POST large body w/o Expect header") -test_run.MakeCurlCommand( - '-v -o /dev/null --http1.1 -H "uuid: post" -H "Expect: " -d @big_post_body -k https://127.0.0.1:{0}/post'.format( - ts.Variables.ssl_port), - ts=ts) -test_run.Processes.Default.Streams.All = "gold/post-h1.gold" -test_run.Processes.Default.Streams.All += Testers.ExcludesExpression("HTTP/1.1 100 continue", "Does not have Expect header") -test_run.Processes.Default.Streams.All += Testers.ExcludesExpression("Expect: 100-continue", "Does not have Expect header") -test_run.StillRunningAfter = server -test_run.StillRunningAfter = ts -test_run.Processes.Default.ReturnCode = 0 - -test_run = Test.AddTestRun("http2 POST small body with Expect header") -test_run.MakeCurlCommand( - '-v -o /dev/null --http2 -H "uuid: post" -H "Expect: 100-continue" -d "small body" -k https://127.0.0.1:{0}/post'.format( - ts.Variables.ssl_port), - ts=ts) -test_run.Processes.Default.Streams.All = "gold/post-h2.gold" -test_run.Processes.Default.Streams.All += Testers.ContainsExpression("xpect: 100-continue", "Has expect header") -test_run.Processes.Default.Streams.All += Testers.ContainsExpression("HTTP/2 100", "Has Expect header") -test_run.StillRunningAfter = server -test_run.StillRunningAfter = ts -test_run.Processes.Default.ReturnCode = 0 - -test_run = Test.AddTestRun("http2 POST large body with Expect header") -test_run.MakeCurlCommand( - '-v -o /dev/null --http2 -H "uuid: post" -H "Expect: 100-continue" -d @big_post_body -k https://127.0.0.1:{0}/post'.format( - ts.Variables.ssl_port), - ts=ts) -test_run.Processes.Default.Streams.All = "gold/post-h2.gold" -test_run.Processes.Default.Streams.All += Testers.ContainsExpression("xpect: 100-continue", "Has expect header") -test_run.Processes.Default.Streams.All += Testers.ContainsExpression("HTTP/2 100", "Has Expect header") -test_run.StillRunningAfter = server -test_run.StillRunningAfter = ts -test_run.Processes.Default.ReturnCode = 0 - -test_run = Test.AddTestRun("http2 POST small body w/o Expect header") -test_run.MakeCurlCommand( - '-v -o /dev/null --http2 -H "uuid: post" -H "Expect: " -d "small body" -k https://127.0.0.1:{0}/post'.format( - ts.Variables.ssl_port), - ts=ts) -test_run.Processes.Default.Streams.All = "gold/post-h2.gold" -test_run.Processes.Default.Streams.All += Testers.ExcludesExpression("xpect: 100-continue", "Has expect header") -test_run.Processes.Default.Streams.All += Testers.ExcludesExpression("HTTP/2 100", "Has Expect header") -test_run.StillRunningAfter = server -test_run.StillRunningAfter = ts -test_run.Processes.Default.ReturnCode = 0 - -test_run = Test.AddTestRun("http2 POST large body w/o Expect header") -test_run.MakeCurlCommand( - '-v -o /dev/null --http2 -H "uuid: post" -H "Expect: " -d @big_post_body -k https://127.0.0.1:{0}/post'.format( - ts.Variables.ssl_port), - ts=ts) -test_run.Processes.Default.Streams.All = "gold/post-h2.gold" -test_run.Processes.Default.Streams.All += Testers.ExcludesExpression("xpect: 100-continue", "Has expect header") -test_run.Processes.Default.Streams.All += Testers.ExcludesExpression("HTTP/2 100", "Has Expect header") -test_run.StillRunningAfter = server -test_run.StillRunningAfter = ts -test_run.Processes.Default.ReturnCode = 0 - -# Do them all again against the TS that will return 100-continue immediately -test_run = Test.AddTestRun("http1.1 POST small body with Expect header, immediate") -test_run.Processes.Default.StartBefore(Test.Processes.ts2) -test_run.MakeCurlCommand( - '-v -o /dev/null --http1.1 -H "uuid: post" -H "Expect: 100-continue" -d "small body" -k https://127.0.0.1:{0}/post'.format( - ts2.Variables.ssl_port), - ts=ts2) -test_run.Processes.Default.Streams.All = "gold/post-h1.gold" -test_run.Processes.Default.Streams.All += Testers.ContainsExpression("HTTP/1.1 100 Continue", "Has Expect header") -test_run.Processes.Default.Streams.All += Testers.ContainsExpression("Expect: 100-continue", "Has Expect header") -test_run.StillRunningAfter = server -test_run.StillRunningAfter = ts2 -test_run.Processes.Default.ReturnCode = 0 - -test_run = Test.AddTestRun("http1.1 POST large body with Expect header, immediate") -test_run.MakeCurlCommand( - '-v -o /dev/null --http1.1 -H "uuid: post" -H "Expect: 100-continue" -d @big_post_body -k https://127.0.0.1:{0}/post'.format( - ts2.Variables.ssl_port), - ts=ts2) -test_run.Processes.Default.Streams.All = "gold/post-h1.gold" -test_run.Processes.Default.Streams.All += Testers.ContainsExpression("HTTP/1.1 100 Continue", "Has Expect header") -test_run.Processes.Default.Streams.All += Testers.ContainsExpression("Expect: 100-continue", "Has Expect header") -test_run.StillRunningAfter = server -test_run.StillRunningAfter = ts2 -test_run.Processes.Default.ReturnCode = 0 - -test_run = Test.AddTestRun("http1.1 POST small body w/o Expect header, immediate") -test_run.MakeCurlCommand( - '-v -o /dev/null --http1.1 -H "uuid: post" -H "Expect:" -d "small body" -k https://127.0.0.1:{0}/post'.format( - ts2.Variables.ssl_port), - ts=ts2) -test_run.Processes.Default.Streams.All = "gold/post-h1.gold" -test_run.Processes.Default.Streams.All += Testers.ExcludesExpression("HTTP/1.1 100 Continue", "Has Expect header") -test_run.Processes.Default.Streams.All += Testers.ExcludesExpression("Expect 100-continue", "Has Expect header") -test_run.StillRunningAfter = server -test_run.StillRunningAfter = ts2 -test_run.Processes.Default.ReturnCode = 0 - -test_run = Test.AddTestRun("http1.1 POST large body w/o Expect header, immediate") -test_run.MakeCurlCommand( - '-v -o /dev/null --http1.1 -H "uuid: post" -H "Expect: " -d @big_post_body -k https://127.0.0.1:{0}/post'.format( - ts2.Variables.ssl_port), - ts=ts2) -test_run.Processes.Default.Streams.All = "gold/post-h1.gold" -test_run.Processes.Default.Streams.All += Testers.ExcludesExpression("HTTP/1.1 100 Continue", "Has Expect header") -test_run.Processes.Default.Streams.All += Testers.ExcludesExpression("Expect 100-continue", "Has Expect header") -test_run.StillRunningAfter = server -test_run.StillRunningAfter = ts2 -test_run.Processes.Default.ReturnCode = 0 - -test_run = Test.AddTestRun("http2 POST small body with Expect header, immediate") -test_run.MakeCurlCommand( - '-v -o /dev/null --http2 -H "uuid: post" -H "Expect: 100-continue" -d "small body" -k https://127.0.0.1:{0}/post'.format( - ts2.Variables.ssl_port), - ts=ts2) -test_run.Processes.Default.Streams.All = "gold/post-h2.gold" -test_run.Processes.Default.Streams.All += Testers.ContainsExpression("xpect: 100-continue", "Has expect header") -test_run.Processes.Default.Streams.All += Testers.ContainsExpression("HTTP/2 100", "Has Expect header") -test_run.StillRunningAfter = server -test_run.StillRunningAfter = ts2 -test_run.Processes.Default.ReturnCode = 0 - -test_run = Test.AddTestRun("http2 POST large body with Expect header, immediate") -test_run.MakeCurlCommand( - '-v -o /dev/null --http2 -H "uuid: post" -H "Expect: 100-continue" -d @big_post_body -k https://127.0.0.1:{0}/post'.format( - ts2.Variables.ssl_port), - ts=ts2) -test_run.Processes.Default.Streams.All = "gold/post-h2.gold" -test_run.Processes.Default.Streams.All += Testers.ContainsExpression("xpect: 100-continue", "Has expect header") -test_run.Processes.Default.Streams.All += Testers.ContainsExpression("HTTP/2 100", "Has Expect header") -test_run.StillRunningAfter = server -test_run.StillRunningAfter = ts2 -test_run.Processes.Default.ReturnCode = 0 - -test_run = Test.AddTestRun("http2 POST small body w/o Expect header, immediate") -test_run.MakeCurlCommand( - '-v -o /dev/null --http2 -H "uuid: post" -H "Expect: " -d "small body" -k https://127.0.0.1:{0}/post'.format( - ts2.Variables.ssl_port), - ts=ts2) -test_run.Processes.Default.Streams.All = "gold/post-h2.gold" -test_run.Processes.Default.Streams.All += Testers.ExcludesExpression("xpect: 100-continue", "Has expect header") -test_run.Processes.Default.Streams.All += Testers.ExcludesExpression("HTTP/2 100", "Has Expect header") -test_run.StillRunningAfter = server -test_run.StillRunningAfter = ts2 -test_run.Processes.Default.ReturnCode = 0 - -test_run = Test.AddTestRun("http2 POST large body w/o Expect header, immediate") -test_run.MakeCurlCommand( - '-v -o /dev/null --http2 -H "uuid: post" -H "Expect: " -d @big_post_body -k https://127.0.0.1:{0}/post'.format( - ts2.Variables.ssl_port), - ts=ts2) -test_run.Processes.Default.Streams.All = "gold/post-h2.gold" -test_run.Processes.Default.Streams.All += Testers.ExcludesExpression("xpect: 100-continue", "Has expect header") -test_run.Processes.Default.Streams.All += Testers.ExcludesExpression("HTTP/2 100", "Has Expect header") -test_run.StillRunningAfter = server -test_run.StillRunningAfter = ts2 -test_run.Processes.Default.ReturnCode = 0 diff --git a/tests/gold_tests/post/post-early-return.test.py b/tests/gold_tests/post/post-early-return.test.py deleted file mode 100644 index 054fd08fb5a..00000000000 --- a/tests/gold_tests/post/post-early-return.test.py +++ /dev/null @@ -1,158 +0,0 @@ -''' -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import os - -# ---- -# Setup Test -# ---- -Test.Summary = ''' -Test a case when the origin returns before the post is completed -''' -Test.SkipIf(Condition.CurlUsingUnixDomainSocket()) -Test.ContinueOnFail = True - -Test.GetTcpPort("upstream_port1") -Test.GetTcpPort("upstream_port2") -Test.GetTcpPort("upstream_port3") -Test.GetTcpPort("upstream_port4") -Test.GetTcpPort("upstream_port5") -Test.GetTcpPort("upstream_port6") - -# ---- -# Setup ATS -# ---- -ts = Test.MakeATSProcess("ts", enable_tls=True, enable_cache=False) - -# add ssl materials like key, certificates for the server -ts.addDefaultSSLFiles() - -ts.Disk.remap_config.AddLines( - [ - 'map /one http://127.0.0.1:{0}'.format(Test.Variables.upstream_port1), - 'map /two http://127.0.0.1:{0}'.format(Test.Variables.upstream_port2), - 'map /three http://127.0.0.1:{0}'.format(Test.Variables.upstream_port3), - 'map /four http://127.0.0.1:{0}'.format(Test.Variables.upstream_port4), - 'map /five http://127.0.0.1:{0}'.format(Test.Variables.upstream_port5), - 'map /six http://127.0.0.1:{0}'.format(Test.Variables.upstream_port6), - ]) -ts.Disk.ssl_multicert_yaml.AddLines( - """ -ssl_multicert: - - dest_ip: "*" - ssl_cert_name: server.pem - ssl_key_name: server.key -""".split("\n")) -ts.Disk.records_config.update( - { - 'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir), - 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts.Variables.SSLDir), - 'proxy.config.diags.debug.enabled': 0, - # 'proxy.config.http2.initial_window_size_in': 2*16384, # Make a ludacrisly small window - 'proxy.config.diags.debug.tags': 'http', - }) - -mock_origin = os.path.join(Test.Variables.AtsTestToolsDir, 'mock_origin.py') -mock_origin_args = '--status 420 --reason "Be Calm"' - -server1 = Test.Processes.Process( - "server1", f"python3 {mock_origin} {Test.Variables.upstream_port1} {mock_origin_args} --output outserver1") -server2 = Test.Processes.Process( - "server2", f"python3 {mock_origin} {Test.Variables.upstream_port2} {mock_origin_args} --output outserver1") -server3 = Test.Processes.Process( - "server3", f"python3 {mock_origin} {Test.Variables.upstream_port3} {mock_origin_args} --output outserver1") -server4 = Test.Processes.Process( - "server4", f"python3 {mock_origin} {Test.Variables.upstream_port4} {mock_origin_args} --output outserver1") -server5 = Test.Processes.Process( - "server5", f"python3 {mock_origin} {Test.Variables.upstream_port5} {mock_origin_args} --output outserver1") -server6 = Test.Processes.Process( - "server6", f"python3 {mock_origin} {Test.Variables.upstream_port6} {mock_origin_args} --output outserver1") -server1.Ready = When.PortOpen(Test.Variables.upstream_port1) -server2.Ready = When.PortOpen(Test.Variables.upstream_port2) -server3.Ready = When.PortOpen(Test.Variables.upstream_port3) -server4.Ready = When.PortOpen(Test.Variables.upstream_port4) -server5.Ready = When.PortOpen(Test.Variables.upstream_port5) -server6.Ready = When.PortOpen(Test.Variables.upstream_port6) - -big_post_body = "0123456789" * 231070 -big_post_body_file = open(os.path.join(Test.RunDirectory, "big_post_body"), "w") -big_post_body_file.write(big_post_body) -big_post_body_file.close() - -# First two cases we are using curl with no means to delay the post body. Even for the large body case, it looks like ATS processes the entire body before getting the response header -# The third case has an explicit multi-second sleep which ensures the early response path is exercised -test_run = Test.AddTestRun("http1.1 Post with small body early return") -test_run.Processes.Default.StartBefore(Test.Processes.ts) -test_run.Processes.Default.StartBefore(server1) -test_run.MakeCurlCommand( - '-v -o /dev/null --http1.1 -d "small body" -k https://127.0.0.1:{}/one'.format(ts.Variables.ssl_port), ts=ts) -test_run.Processes.Default.Streams.All = Testers.ContainsExpression("HTTP/1.1 420 Be Calm", "Receive the early response") -test_run.StillRunningAfter = ts -test_run.Processes.Default.ReturnCode = 0 - -test_run = Test.AddTestRun("http1.1 Post with large body early return") -test_run.Processes.Default.StartBefore(server2) -test_run.MakeCurlCommand( - '-H "Expect:" -v -o /dev/null --http1.1 -d @big_post_body -k https://127.0.0.1:{}/two'.format(ts.Variables.ssl_port), ts=ts) -test_run.Processes.Default.Streams.All = Testers.ContainsExpression("HTTP/1.1 420 Be Calm", "Receive the early response") -test_run.StillRunningAfter = ts -test_run.Processes.Default.ReturnCode = 0 - -test_run = Test.AddTestRun("http2 Post with large body, small window and early return") -test_run.Processes.Default.StartBefore(server3) -test_run.MakeCurlCommand( - '-v -o /dev/null --http2 -d @big_post_body -k https://127.0.0.1:{}/three'.format(ts.Variables.ssl_port), ts=ts) -test_run.Processes.Default.Streams.All = Testers.ContainsExpression("HTTP/2 420", "Receive the early response") -test_run.StillRunningAfter = ts -test_run.Processes.Default.ReturnCode = 0 - -client_out = Test.Disk.File("clientout") -client_out.Content = Testers.ExcludesExpression("0123456789", "The delayed body is not sent") -client_out.Content += Testers.ContainsExpression("HTTP/1.1 420 Be Calm", "Receive the early response") -client_out.Content += Testers.ContainsExpression("Connection: close", "ATS marks the client connection to close") - -client_out2 = Test.Disk.File("clientout2") -client_out2.Content = Testers.ExcludesExpression("0123456789", "The delayed body is not sent") -client_out2.Content += Testers.ContainsExpression("HTTP/1.1 420 Be Calm", "Receive the early response") -client_out2.Content += Testers.ContainsExpression("Connection: close", "ATS marks the client connection to close") - -client_out3 = Test.Disk.File("clientout3") -client_out3.Content = Testers.ExcludesExpression("0123456789", "The delayed body is not sent") -client_out3.Content += Testers.ContainsExpression("HTTP/1.1 420 Be Calm", "Receive the early response") -client_out3.Content += Testers.ContainsExpression("Connection: close", "ATS marks the client connection to close") - -test_run = Test.AddTestRun("http1.1 Post with paused body") -test_run.Processes.Default.StartBefore(server4) -test_run.Setup.Copy("delay_client.sh") -test_run.Processes.Default.Command = "sh ./delay_client.sh {} clientout".format(ts.Variables.port) -test_run.StillRunningAfter = ts -test_run.Processes.Default.ReturnCode = 0 - -test_run = Test.AddTestRun("http1.1 Post with delayed and paused body") -test_run.Processes.Default.StartBefore(server5) -test_run.Setup.Copy("delay_client2.sh") -test_run.Processes.Default.Command = "sh ./delay_client2.sh {} clientout2".format(ts.Variables.port) -test_run.StillRunningAfter = ts -test_run.Processes.Default.ReturnCode = 0 - -test_run = Test.AddTestRun("http1.1 Post with paused body and no delay on server") -test_run.Processes.Default.StartBefore(server6) -test_run.Setup.Copy("delay_client3.sh") -test_run.Processes.Default.Command = "sh ./delay_client3.sh {} clientout3".format(ts.Variables.port) -test_run.StillRunningAfter = ts -test_run.Processes.Default.ReturnCode = 0 diff --git a/tests/gold_tests/post/simple-post-valid-buffer-check.test.py b/tests/gold_tests/post/simple-post-valid-buffer-check.test.py deleted file mode 100644 index 600a36d6b29..00000000000 --- a/tests/gold_tests/post/simple-post-valid-buffer-check.test.py +++ /dev/null @@ -1,51 +0,0 @@ -''' -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import os - -Test.Summary = ''' -Make sure we have a valid buffer to write on. This used to make ats crash. -''' -Test.ContinueOnFail = True - -server = Test.MakeHttpBinServer("server") -ts = Test.MakeATSProcess("ts") - -ts.Disk.remap_config.AddLine('map / http://127.0.0.1:{0}'.format(server.Variables.Port)) - -ts.Disk.records_config.update( - ''' -diags: - debug: - enabled: 1 - tags: http -http: - request_buffer_enabled: 1 - number_of_redirections: 1 - -''') - -test_run = Test.AddTestRun("post buffer test") -test_run.Processes.Default.StartBefore(server) -test_run.Processes.Default.StartBefore(ts) -test_run.MakeCurlCommand(f' -v -H "Expect: 100-continue" -d "abc" http://localhost:{ts.Variables.port}/post', ts=ts) -ts.Disk.traffic_out.Content += Testers.ContainsExpression("HTTP/1.1 100 Continue", "Has Expect header") -ts.Disk.traffic_out.Content += Testers.ContainsExpression("HTTP/1.1 200 OK", "200OK") -test_run.StillRunningAfter = server -test_run.StillRunningAfter = ts # TS should not crash -test_run.Processes.Default.ReturnCode = 0 diff --git a/tests/gold_tests/post_slow_server/post_slow_server.test.py b/tests/gold_tests/post_slow_server/post_slow_server.test.py deleted file mode 100644 index 1f4612b43dd..00000000000 --- a/tests/gold_tests/post_slow_server/post_slow_server.test.py +++ /dev/null @@ -1,71 +0,0 @@ -''' -Server receives POST, sent by client over HTTP/2, waits 2 minutes, then sends response with 200 KBytes of data -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -Test.Summary = ''' -Server receives POST, sent by client over HTTP/2, waits 2 minutes, then sends response with 200 KBytes of data -''' - -# Because of the 2 minute delay, we don't want to run this test in CI checks. Comment out this line to run it. -Test.SkipIf(Condition.true("Test takes too long to run it in CI.")) - -Test.SkipUnless(Condition.HasCurlFeature('http2')) - -ts = Test.MakeATSProcess("ts", enable_tls=True, enable_cache=False) - -ts.addDefaultSSLFiles() - -ts.Disk.records_config.update( - { - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'http', - 'proxy.config.proxy_name': 'Poxy_Proxy', # This will be the server name. - 'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir), - 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts.Variables.SSLDir), - 'proxy.config.http.transaction_no_activity_timeout_out': 150, - 'proxy.config.http2.no_activity_timeout_in': 150, - }) - -ts.Disk.ssl_multicert_yaml.AddLines( - """ -ssl_multicert: - - dest_ip: "*" - ssl_cert_name: server.pem - ssl_key_name: server.key -""".split("\n")) - -Test.GetTcpPort("server_port") - -ts.Disk.remap_config.AddLine('map https://localhost http://localhost:{}'.format(Test.Variables.server_port)) - -server = Test.Processes.Process("server", "bash -c '" + Test.TestDirectory + "/server.sh {}'".format(Test.Variables.server_port)) - -tr = Test.AddTestRun() -tr.MakeCurlCommand( - '--request POST --verbose --ipv4 --http2 --insecure --header "Content-Length: 0"' + - " --header 'Host: localhost' https://localhost:{}/xyz >curl.log 2>curl.err".format(ts.Variables.ssl_port), - ts=ts) -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.StartBefore(server) -tr.Processes.Default.StartBefore(ts) - -# Make sure the curl command received 200 KB of data. -# -tr = Test.AddTestRun() -tr.Processes.Default.Command = "bash -c '" + Test.TestDirectory + "/check.sh'" -tr.Processes.Default.ReturnCode = 0 diff --git a/tests/gold_tests/post_slow_server/post_slow_server_max_requests_in.test.py b/tests/gold_tests/post_slow_server/post_slow_server_max_requests_in.test.py deleted file mode 100644 index 5397e23d6a2..00000000000 --- a/tests/gold_tests/post_slow_server/post_slow_server_max_requests_in.test.py +++ /dev/null @@ -1,74 +0,0 @@ -''' -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -from enum import Enum - -Test.Summary = 'Exercise POST request with max_requests_in' -Test.ContinueOnFail = True - - -class PostAndMaxRequestsInTest: - """ - Cover #8273 - Make sure inbound side inactive timeout doesn't happens during outbound side TLS handshake - """ - - def __init__(self): - self.__setupOriginServer() - self.__setupTS() - - def __setupOriginServer(self): - Test.GetTcpPort("server_port") - self.origin_server = Test.Processes.Process( - "server", "bash -c '" + Test.TestDirectory + "/server.sh {}'".format(Test.Variables.server_port)) - - def __setupTS(self): - self.ts = Test.MakeATSProcess("ts") - - self.ts.Disk.records_config.update( - { - "proxy.config.http.server_ports": f"{self.ts.Variables.port} {self.ts.Variables.uds_path}", - "proxy.config.net.max_requests_in": 1000, - 'proxy.config.http.connect_attempts_timeout': 1, - "proxy.config.diags.debug.enabled": 1, - "proxy.config.diags.debug.tags": "http|socket|v_net_queue", - }) - - self.ts.Disk.remap_config.AddLines([ - f"map / https://127.0.0.1:{Test.Variables.server_port}/", - ]) - - def __testCase0(self): - """ - - POST request - - Outbound side TLS Handshake hits connect_attempts_timeout - - Client gets 502 - """ - tr = Test.AddTestRun() - tr.Processes.Default.StartBefore(self.origin_server) - tr.Processes.Default.StartBefore(self.ts) - tr.MakeCurlCommand(f"-X POST --http1.1 -vs http://127.0.0.1:{self.ts.Variables.port}/ --data key=value", ts=self.ts) - tr.Processes.Default.ReturnCode = 0 - tr.Processes.Default.Streams.stdout = "gold/post_slow_server_max_requests_in_0_stdout.gold" - tr.Processes.Default.Streams.stderr = "gold/post_slow_server_max_requests_in_0_stderr.gold" - tr.StillRunningAfter = self.ts - - def run(self): - self.__testCase0() - - -PostAndMaxRequestsInTest().run() diff --git a/tests/gold_tests/proxy_protocol/proxy_protocol.test.py b/tests/gold_tests/proxy_protocol/proxy_protocol.test.py deleted file mode 100644 index db4eede8a11..00000000000 --- a/tests/gold_tests/proxy_protocol/proxy_protocol.test.py +++ /dev/null @@ -1,336 +0,0 @@ -''' -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import os - -Test.Summary = 'Test PROXY Protocol' -Test.ContinueOnFail = True - - -class ProxyProtocolInTest: - """Test that ATS can receive Proxy Protocol.""" - - replay_file = "replay/proxy_protocol_in.replay.yaml" - - def __init__(self, name, enable_cp=False): - self.setupOriginServer(name) - self.setupTS(name, enable_cp) - self.name = name - - def setupOriginServer(self, name): - self.server = Test.MakeVerifierServerProcess(f"pp-in-server-{name}", self.replay_file) - - def setupTS(self, name, enable_cp): - self.ts = Test.MakeATSProcess( - f"ts_in_{name}", - enable_tls=True, - enable_cache=False, - enable_proxy_protocol=True, - enable_proxy_protocol_cp_src=enable_cp) - - self.ts.addDefaultSSLFiles() - self.ts.Disk.ssl_multicert_yaml.AddLines( - """ -ssl_multicert: - - dest_ip: "*" - ssl_cert_name: server.pem - ssl_key_name: server.key -""".split("\n")) - - self.ts.Disk.remap_config.AddLine(f"map / http://127.0.0.1:{self.server.Variables.http_port}/") - - self.ts.Disk.records_config.update( - { - "proxy.config.http.proxy_protocol_allowlist": "127.0.0.1", - "proxy.config.http.insert_forwarded": "for|by=ip|proto", - "proxy.config.http.insert_client_ip": 2, - "proxy.config.http.insert_squid_x_forwarded_for": 1, - "proxy.config.ssl.server.cert.path": self.ts.Variables.SSLDir, - "proxy.config.ssl.server.private_key.path": self.ts.Variables.SSLDir, - "proxy.config.diags.debug.enabled": 1, - "proxy.config.diags.debug.tags": "proxyprotocol", - }) - - self.ts.Disk.logging_yaml.AddLines( - ''' -logging: - formats: - - name: access - format: '% % %' - - logs: - - filename: access - format: access -'''.split("\n")) - - def runTraffic(self): - tr = Test.AddTestRun(f"Verify correct handling of incoming PROXY header. {self.name}") - tr.AddVerifierClientProcess( - f"pp-in-client-{self.name}", - self.replay_file, - http_ports=[self.ts.Variables.proxy_protocol_port], - https_ports=[self.ts.Variables.proxy_protocol_ssl_port]) - tr.Processes.Default.StartBefore(self.server) - tr.Processes.Default.StartBefore(self.ts) - tr.StillRunningAfter = self.server - tr.StillRunningAfter = self.ts - - def checkAccessLog(self): - """ - check access log - """ - log_file = os.path.join(self.ts.Variables.LOGDIR, 'access.log') - - # Transactions can finish on different event threads, so their access - # log records are not guaranteed to be written in replay order. - Test.AddAwaitFileContainsTestRun( - f'Await PROXY protocol access log lines. {self.name}', - log_file, - r'^(127\.0\.0\.1|198\.51\.100\.1) (0|127\.0\.0\.1|198\.51\.100\.1) 127\.0\.0\.1$', - 8, - ) - - tr = Test.AddTestRun(f'Verify PROXY protocol access log lines. {self.name}') - tr.Processes.Default.Command = f'LC_ALL=C sort < "{log_file}"' - tr.Processes.Default.Streams.stdout = f"gold/access-{self.name}.gold" - tr.Processes.Default.ReturnCode = 0 - - def run(self): - self.runTraffic() - self.checkAccessLog() - - -class ProxyProtocolAllowlistTest: - """Test that the PROXY Protocol allowlist applies only to PP-prefaced traffic.""" - - replay_file = "replay/proxy_protocol_allowlist.replay.yaml" - - def __init__(self): - self.setupOriginServer() - self.setupTS() - - def setupOriginServer(self): - self.server = Test.MakeVerifierServerProcess("pp-allowlist-server", self.replay_file) - - def setupTS(self): - self.ts = Test.MakeATSProcess("ts_pp_allowlist", enable_tls=True, enable_cache=False, enable_proxy_protocol=True) - - self.ts.addDefaultSSLFiles() - self.ts.Disk.ssl_multicert_yaml.AddLines( - """ -ssl_multicert: - - dest_ip: "*" - ssl_cert_name: server.pem - ssl_key_name: server.key -""".split("\n")) - - self.ts.Disk.remap_config.AddLine(f"map / http://127.0.0.1:{self.server.Variables.http_port}/") - - self.ts.Disk.records_config.update( - { - "proxy.config.http.proxy_protocol_allowlist": "192.0.2.1", - "proxy.config.ssl.server.cert.path": self.ts.Variables.SSLDir, - "proxy.config.ssl.server.private_key.path": self.ts.Variables.SSLDir, - "proxy.config.diags.debug.enabled": 1, - "proxy.config.diags.debug.tags": "proxyprotocol", - }) - - def addCurlRun(self, name, args, return_code=0, expect_status=None, start_processes=False): - tr = Test.AddTestRun(name) - tr.TimeOut = 10 - tr.MakeCurlCommand(args, ts=self.ts) - tr.Processes.Default.ReturnCode = return_code - - if expect_status is not None: - tr.Processes.Default.Streams.stdout = Testers.ContainsExpression(expect_status, f"Expected HTTP {expect_status}") - - if start_processes: - tr.Processes.Default.StartBefore(self.server) - tr.Processes.Default.StartBefore(self.ts) - - tr.StillRunningAfter = self.server - tr.StillRunningAfter = self.ts - - def run(self): - self.addCurlRun( - "Non-PP HTTP traffic bypasses proxy_protocol_allowlist", - f'-sS -o /dev/null -w "%{{http_code}}" -H "uuid: 1" http://127.0.0.1:{self.ts.Variables.proxy_protocol_port}/get', - expect_status="200", - start_processes=True) - self.addCurlRun( - "Non-PP TLS traffic bypasses proxy_protocol_allowlist", f'-k -sS -o /dev/null -w "%{{http_code}}" -H "uuid: 2" ' - f'https://127.0.0.1:{self.ts.Variables.proxy_protocol_ssl_port}/get', - expect_status="200") - self.addCurlRun( - "PP-prefaced HTTP traffic is rejected when peer is not allowlisted", - f'-sS -o /dev/null --max-time 5 --haproxy-protocol ' - f'http://127.0.0.1:{self.ts.Variables.proxy_protocol_port}/get', - return_code=Any(52, 56)) - self.addCurlRun( - "PP-prefaced TLS traffic is rejected when peer is not allowlisted", - f'-k -sS -o /dev/null --max-time 5 --haproxy-protocol ' - f'https://127.0.0.1:{self.ts.Variables.proxy_protocol_ssl_port}/get', - return_code=Any(35, 52, 56)) - - -class ProxyProtocolOutTest: - """Test that ATS can send Proxy Protocol.""" - - _dns_counter = 0 - _client_counter = 0 - _server_counter = 0 - _ts_counter = 0 - _pp_out_replay_file = "replay/proxy_protocol_out.replay.yaml" - - def __init__(self, pp_version: int, is_tunnel: bool, is_tls_to_origin: bool = False) -> None: - """Initialize a ProxyProtocolOutTest. - - :param pp_version: The Proxy Protocol version to use (1 or 2). - :param is_tunnel: Whether ATS should tunnel to the origin. - :param is_tls: Whether ATS should connect to the origin via TLS. - """ - - if pp_version not in (-1, 1, 2): - raise ValueError(f'Invalid Proxy Protocol version (not 1 or 2): {pp_version}') - self._pp_version = pp_version - self._is_tunnel = is_tunnel - self._is_tls_to_origin = is_tls_to_origin - - def setupOriginServer(self) -> None: - """Configure the origin server. - """ - self._server = Test.MakeVerifierServerProcess( - f"pp-out-server-{ProxyProtocolOutTest._server_counter}", self._pp_out_replay_file) - ProxyProtocolOutTest._server_counter += 1 - - def setupDNS(self, tr: 'TestRun') -> None: - """Configure the DNS server. - - :param tr: The TestRun to associate the DNS's Process with. - """ - self._dns = tr.MakeDNServer(f'dns-{ProxyProtocolOutTest._dns_counter}', default='127.0.0.1') - ProxyProtocolOutTest._dns_counter += 1 - - def setupTS(self, tr: 'TestRun') -> None: - """Configure Traffic Server.""" - process_name = f'ts-out-{ProxyProtocolOutTest._ts_counter}' - ProxyProtocolOutTest._ts_counter += 1 - self._ts = tr.MakeATSProcess(process_name, enable_tls=True, enable_cache=False) - - self._ts.addDefaultSSLFiles() - self._ts.Disk.ssl_multicert_yaml.AddLines( - """ -ssl_multicert: - - dest_ip: "*" - ssl_cert_name: server.pem - ssl_key_name: server.key -""".split("\n")) - scheme = 'https' if self._is_tls_to_origin else 'http' - server_port = self._server.Variables.https_port if self._is_tls_to_origin else self._server.Variables.http_port - self._ts.Disk.remap_config.AddLine(f"map / {scheme}://backend.pp.origin.com:{server_port}/") - - self._ts.Disk.records_config.update( - { - "proxy.config.ssl.server.cert.path": self._ts.Variables.SSLDir, - "proxy.config.ssl.server.private_key.path": self._ts.Variables.SSLDir, - "proxy.config.diags.debug.enabled": 1, - "proxy.config.diags.debug.tags": "http|proxyprotocol", - "proxy.config.http.proxy_protocol_out": self._pp_version, - "proxy.config.dns.nameservers": f"127.0.0.1:{self._dns.Variables.Port}", - "proxy.config.dns.resolv_conf": 'NULL', - "proxy.config.ssl.client.verify.server.policy": 'PERMISSIVE' - }) - - if self._is_tunnel: - self._ts.Disk.records_config.update({ - "proxy.config.http.connect_ports": f'{self._server.Variables.https_port}', - }) - self._ts.addPrivateConnectAllowYaml() - - self._ts.Disk.sni_yaml.AddLines( - [ - 'sni:', - '- fqdn: pp.origin.com', - f' tunnel_route: backend.pp.origin.com:{self._server.Variables.https_port}', - ]) - - def setLogExpectations(self, tr: 'TestRun') -> None: - - tr.Processes.Default.Streams.All += Testers.ContainsExpression("HTTP/1.1 200 OK", "Verify the client got a 200 response.") - - if self._pp_version in (1, 2): - expected_pp = ('PROXY TCP4 127.0.0.1 127.0.0.1 ' - rf'\d+ {self._ts.Variables.ssl_port}') - self._server.Streams.All += Testers.ContainsExpression( - expected_pp, "Verify the server got the expected Proxy Protocol string.") - - self._server.Streams.All += Testers.ContainsExpression( - f'Received PROXY header v{self._pp_version}', "Verify the server got the expected Proxy Protocol version.") - - if self._pp_version == -1: - self._server.Streams.All += Testers.ContainsExpression( - 'No valid PROXY header found', 'There should be no Proxy Protocol string.') - - if self._is_tunnel: - self._ts.Disk.traffic_out.Content += Testers.ContainsExpression( - 'CONNECT tunnel://backend.pp.origin.com', 'Verify ATS establishes a blind tunnel to the server.') - - def run(self) -> None: - """Run the test.""" - description = f'Proxy Protocol v{self._pp_version} ' - if self._is_tunnel: - description += "with blind tunneling" - else: - description += "without blind tunneling" - if self._is_tls_to_origin: - description += " on TLS connection to origin" - tr = Test.AddTestRun(description) - - self.setupDNS(tr) - self.setupOriginServer() - self.setupTS(tr) - - tr.AddVerifierClientProcess( - f"pp-out-client-{ProxyProtocolOutTest._client_counter}", - self._pp_out_replay_file, - http_ports=[self._ts.Variables.port], - https_ports=[self._ts.Variables.ssl_port]) - ProxyProtocolOutTest._client_counter += 1 - self._ts.StartBefore(self._server) - self._ts.StartBefore(self._dns) - tr.Processes.Default.StartBefore(self._ts) - tr.StillRunningAfter = self._server - - self.setLogExpectations(tr) - - -ProxyProtocolInTest("nocp", False).run() -ProxyProtocolInTest("cp", True).run() -ProxyProtocolAllowlistTest().run() - -# non-tunnling HTTP to origin -ProxyProtocolOutTest(pp_version=-1, is_tunnel=False, is_tls_to_origin=False).run() -ProxyProtocolOutTest(pp_version=1, is_tunnel=False, is_tls_to_origin=False).run() -ProxyProtocolOutTest(pp_version=2, is_tunnel=False, is_tls_to_origin=False).run() -# non-tunnling HTTPS to origin -ProxyProtocolOutTest(pp_version=-1, is_tunnel=False, is_tls_to_origin=True).run() -ProxyProtocolOutTest(pp_version=1, is_tunnel=False, is_tls_to_origin=True).run() -ProxyProtocolOutTest(pp_version=2, is_tunnel=False, is_tls_to_origin=True).run() -# tunneling -ProxyProtocolOutTest(pp_version=1, is_tunnel=True).run() -ProxyProtocolOutTest(pp_version=2, is_tunnel=True).run() diff --git a/tests/gold_tests/qmux/qmux_go_client.test.py b/tests/gold_tests/qmux/qmux_go_client.test.py deleted file mode 100644 index 8a16a8619e6..00000000000 --- a/tests/gold_tests/qmux/qmux_go_client.test.py +++ /dev/null @@ -1,128 +0,0 @@ -''' -Verify HTTP/3 over QMux interoperability with a Go client. -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import os - -Test.Summary = '''Verify that a Go QMux client can complete HTTP/3 transactions through ATS.''' - -Test.SkipUnless( - Condition.HasATSFeature('TS_USE_QMUX'), - Condition.HasGoVersion('1.26'), -) - - -class TestQMuxGoClient: - '''Configure a Go client interoperability test for HTTP/3 over QMux.''' - - replay_file: str = 'qmux.replay.yaml' - - def __init__(self) -> None: - '''Configure the test run.''' - tr = Test.AddTestRun('Go HTTP/3 over QMux client request') - self._configure_server(tr) - self._configure_traffic_server(tr) - self._configure_client(tr) - - def _configure_server(self, tr: 'TestRun') -> 'Process': - '''Configure the Proxy Verifier origin server. - - :param tr: The TestRun to add the server process to. - :return: The server process. - ''' - server = tr.AddVerifierServerProcess('server', self.replay_file, verbose=False) - self._server = server - return server - - def _configure_traffic_server(self, tr: 'TestRun') -> 'Process': - '''Configure Traffic Server. - - :param tr: The TestRun to add the Traffic Server process to. - :return: The Traffic Server process. - ''' - ts = tr.MakeATSProcess('ts', enable_tls=True, enable_cache=False) - self._ts = ts - - ts.StartupTimeout = 60 - ts.addDefaultSSLFiles() - ts.Disk.ssl_multicert_yaml.AddLines( - ''' -ssl_multicert: - - dest_ip: "*" - ssl_cert_name: server.pem - ssl_key_name: server.key -'''.split('\n')) - ts.Disk.records_config.update( - { - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'qmux|http3', - 'proxy.config.http.server_ports': (f'{ts.Variables.port} {ts.Variables.ssl_port}:ssl:proto=h3qx-01'), - 'proxy.config.ssl.server.cert.path': ts.Variables.SSLDir, - 'proxy.config.ssl.server.private_key.path': ts.Variables.SSLDir, - }) - ts.Disk.remap_config.AddLine(f'map / http://127.0.0.1:{self._server.Variables.http_port}') - ts.Disk.logging_yaml.AddLines( - ''' -logging: - formats: - - name: qmux_access - format: 'c_alpn=% client_version=% c_method=% c_url=%' - - logs: - - filename: qmux_access - format: qmux_access -'''.split('\n')) - - access_log = Test.Disk.File(os.path.join(ts.Variables.LOGDIR, 'qmux_access.log'), exists=True) - access_log.Content = Testers.ContainsExpression( - r'c_alpn=h3qx-01 client_version=http/3 c_method=GET ' - r'c_url=https://qmux\.example\.com:[0-9]+/qmux-get-empty', - 'ATS should log the empty QMux request as HTTP/3 over the h3qx-01 ALPN.') - access_log.Content += Testers.ContainsExpression( - r'c_alpn=h3qx-01 client_version=http/3 c_method=POST ' - r'c_url=https://qmux\.example\.com:[0-9]+/qmux-post-large', - 'ATS should log the large QMux request as HTTP/3 over the h3qx-01 ALPN.') - return ts - - def _configure_client(self, tr: 'TestRun') -> 'Process': - '''Configure the Go QMux client. - - :param tr: The TestRun to add the client process to. - :return: The client process. - ''' - tr.Setup.Copy('go_qmux_client') - client = tr.Processes.Default - client.Env['GOFLAGS'] = '-mod=readonly -modcacherw' - client.Env['GOCACHE'] = os.path.join(tr.RunDirectory, 'gocache') - client.Env['GOMODCACHE'] = os.path.join(tr.RunDirectory, 'gomodcache') - client.Env['GOTOOLCHAIN'] = 'local' - client.Command = ( - f'cd "{os.path.join(tr.RunDirectory, "go_qmux_client")}" && ' - f'go run . --addr 127.0.0.1:{self._ts.Variables.ssl_port} ' - f'--authority qmux.example.com:{self._ts.Variables.ssl_port} ' - '--server-name qmux.example.com') - client.ReturnCode = 0 - client.Streams.stdout = Testers.ContainsExpression( - 'completed 3 QMux HTTP/3 requests: alpn=h3qx-01', - 'The Go client should complete all HTTP/3 requests over one QMux session.') - client.StartBefore(self._server) - client.StartBefore(self._ts) - return client - - -TestQMuxGoClient() diff --git a/tests/gold_tests/records/records_config_to_yaml.test.py b/tests/gold_tests/records/records_config_to_yaml.test.py deleted file mode 100644 index 49836a9d75b..00000000000 --- a/tests/gold_tests/records/records_config_to_yaml.test.py +++ /dev/null @@ -1,67 +0,0 @@ -''' -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. -import os -import yaml - -Test.Summary = 'Testing ATS records to yaml script.' - -file_suffix = 1 - -# case 1 TLS with no data -tr = Test.AddTestRun("Test records to yaml convert script - full file") - -tr.Setup.Copy(os.path.join(Test.Variables.RepoDir, "tools/records/convert2yaml.py")) -tr.Setup.Copy('legacy_config/full_records.config') -tr.Processes.Default.Command = f'python3 convert2yaml.py -f full_records.config --output generated{file_suffix}.yaml --yaml --mute' -f = tr.Disk.File(f"generated{file_suffix}.yaml") -f.Content = "gold/full_records.yaml" - -file_suffix = file_suffix + 1 - -tr = Test.AddTestRun("Test records to yaml convert script -only renamed records.") -tr.Setup.Copy(os.path.join(Test.Variables.RepoDir, "tools/records/convert2yaml.py")) -tr.Setup.Copy('legacy_config/old_records.config') -tr.Processes.Default.Command = f'python3 convert2yaml.py -f old_records.config --output generated{file_suffix}.yaml --yaml' -tr.Processes.Default.Stream = 'gold/renamed_records.gold' -f = tr.Disk.File(f"generated{file_suffix}.yaml") -f.Content = "gold/renamed_records.yaml" - -tr = Test.AddTestRun("Test errors when trying to override values ") -tr.Setup.Copy(os.path.join(Test.Variables.RepoDir, "tools/records/convert2yaml.py")) -tr.Setup.Copy('legacy_config/override_value.config') -tr.Processes.Default.Command = f'python3 convert2yaml.py -f override_value.config --output generated{file_suffix}.yaml --yaml -m' -tr.Processes.Default.Streams.stdout += Testers.ContainsExpression( - "We cannot continue with 'proxy.config.ssl.client.verify.server.policy' at line '3' as a value node will be overridden", - "Error should be present") - -tr = Test.AddTestRun("Test errors when trying to override maps") -tr.Setup.Copy(os.path.join(Test.Variables.RepoDir, "tools/records/convert2yaml.py")) -tr.Setup.Copy('legacy_config/override_map.config') -tr.Processes.Default.Command = f'python3 convert2yaml.py -f override_map.config --output generated{file_suffix}.yaml --yaml -m' -tr.Processes.Default.Streams.stdout += Testers.ContainsExpression( - "We cannot continue with 'proxy.config.ssl.client.verify.server' at line '3' as an existing YAML map will be overridden.", - "Error should be present") - -file_suffix = file_suffix + 1 - -tr = Test.AddTestRun("Test config file without trailing newline") -tr.Setup.Copy(os.path.join(Test.Variables.RepoDir, "tools/records/convert2yaml.py")) -tr.Setup.Copy('legacy_config/no_newline.config') -tr.Processes.Default.Command = f'python3 convert2yaml.py -f no_newline.config --output generated{file_suffix}.yaml --yaml --mute' -f = tr.Disk.File(f"generated{file_suffix}.yaml") -f.Content = "gold/no_newline.yaml" diff --git a/tests/gold_tests/records/records_runroot_precedence.test.py b/tests/gold_tests/records/records_runroot_precedence.test.py deleted file mode 100644 index 49077c174cc..00000000000 --- a/tests/gold_tests/records/records_runroot_precedence.test.py +++ /dev/null @@ -1,163 +0,0 @@ -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import os - -Test.Summary = ''' -Verify that when runroot is active, path records from records.yaml are -overridden by the resolved Layout paths (precedence: env var > runroot > records.yaml). - -When --run-root (or TS_RUNROOT) is set and the PROXY_CONFIG_* environment -variables for path records are unset, RecConfigOverrideFromEnvironment() -returns the actual Layout path (e.g. Layout::bindir, Layout::logdir) which -was populated from runroot.yaml — effectively making runroot.yaml override -records.yaml for these path records. -''' -Test.ContinueOnFail = True -Test.SkipUnless( - Test.Variables.BINDIR.startswith(Test.Variables.PREFIX), "need to guarantee bin path starts with prefix for runroot") - -ts = Test.MakeATSProcess("ts") -ts_dir = os.path.join(Test.RunDirectory, "ts") - -# Set deliberately WRONG values in records.yaml for all 4 runroot-managed -# path records. If runroot override works, these values must NOT be used. -ts.Disk.records_config.append_to_document( - ''' - bin_path: wrong_bin_path - local_state_dir: wrong_runtime - log: - logfile_dir: wrong_log - plugin: - plugin_dir: wrong_plugin -''') - -# Test 3 setup: env var that must win over both runroot and records.yaml. -ts.Env['PROXY_CONFIG_DIAGS_DEBUG_TAGS'] = 'env_wins' -ts.Disk.records_config.update(''' - diags: - debug: - enabled: 0 - tags: config_value - ''') - -# Build the ATS command: -# - Unset the 4 path env vars (the test framework always sets them, -# which masks the runroot code path). -# - Set TS_RUNROOT to the sandbox dir so the runroot mechanism activates. -original_cmd = ts.Command -ts.Command = ( - "env" - " -u PROXY_CONFIG_BIN_PATH" - " -u PROXY_CONFIG_LOCAL_STATE_DIR" - " -u PROXY_CONFIG_LOG_LOGFILE_DIR" - " -u PROXY_CONFIG_PLUGIN_PLUGIN_DIR" - f" TS_RUNROOT={ts_dir}" - f" {original_cmd}") - -# --------------------------------------------------------------------------- -# Test 0: Create runroot.yaml that maps to the sandbox layout, then start ATS. -# -# The runroot.yaml must exist before ATS starts because TS_RUNROOT triggers -# Layout::runroot_setup() during initialization. We write a runroot.yaml -# whose paths match the sandbox structure the test framework already created -# (traffic_layout init would create a different FHS-style layout that does -# not match the sandbox, so we write it manually). -# --------------------------------------------------------------------------- -runroot_yaml = os.path.join(ts_dir, 'runroot.yaml') - -runroot_lines = [ - f"prefix: {ts_dir}", - f"bindir: {os.path.join(ts_dir, 'bin')}", - f"sbindir: {os.path.join(ts_dir, 'bin')}", - f"sysconfdir: {os.path.join(ts_dir, 'config')}", - f"logdir: {os.path.join(ts_dir, 'log')}", - f"libexecdir: {os.path.join(ts_dir, 'plugin')}", - f"localstatedir: {os.path.join(ts_dir, 'runtime')}", - f"runtimedir: {os.path.join(ts_dir, 'runtime')}", - f"cachedir: {os.path.join(ts_dir, 'cache')}", -] -runroot_content = "\\n".join(runroot_lines) + "\\n" - -tr = Test.AddTestRun("Create runroot.yaml") -tr.Processes.Default.Command = f"mkdir -p {ts_dir} && printf '{runroot_content}' > {runroot_yaml}" -tr.Processes.Default.ReturnCode = 0 - -# --------------------------------------------------------------------------- -# Test 1: Start ATS with runroot active -# --------------------------------------------------------------------------- -tr = Test.AddTestRun("Start ATS with runroot") -tr.Processes.Default.Command = 'echo start' -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.StartBefore(ts) -tr.StillRunningAfter = ts - -# ATS must not crash (the original nullptr bug) and must complete startup. -ts.Disk.traffic_out.Content = Testers.ExcludesExpression( - "basic_string", "must not crash with 'basic_string: construction from null'") -ts.Disk.traffic_out.Content += Testers.ContainsExpression("records parsing completed", "ATS should complete records parsing") - -# Verify the override log messages appear in traffic.out. -# The errata notes from RecYAMLDecoder are printed by RecCoreInit(). -ts.Disk.traffic_out.Content += Testers.ContainsExpression( - "'proxy.config.bin_path' overridden with .* by runroot", "bin_path override by runroot must be logged") -ts.Disk.traffic_out.Content += Testers.ContainsExpression( - "'proxy.config.local_state_dir' overridden with .* by runroot", "local_state_dir override by runroot must be logged") -ts.Disk.traffic_out.Content += Testers.ContainsExpression( - "'proxy.config.log.logfile_dir' overridden with .* by runroot", "logfile_dir override by runroot must be logged") -ts.Disk.traffic_out.Content += Testers.ContainsExpression( - "'proxy.config.plugin.plugin_dir' overridden with .* by runroot", "plugin_dir override by runroot must be logged") -ts.Disk.traffic_out.Content += Testers.ContainsExpression( - "'proxy.config.diags.debug.tags' overridden with 'env_wins' by environment variable", - "diags.debug.tags override by environment variable must be logged") - -# --------------------------------------------------------------------------- -# Test 2: Verify path records do NOT contain the records.yaml values. -# -# Because runroot is active and env vars are unset, the records should hold -# the resolved Layout paths from runroot.yaml, not the records.yaml values. -# --------------------------------------------------------------------------- -tr = Test.AddTestRun("Verify runroot overrides records.yaml for path records") -tr.Processes.Default.Command = ( - 'traffic_ctl config get' - ' proxy.config.bin_path' - ' proxy.config.local_state_dir' - ' proxy.config.log.logfile_dir' - ' proxy.config.plugin.plugin_dir') -tr.Processes.Default.Env = ts.Env -tr.Processes.Default.ReturnCode = 0 -tr.StillRunningAfter = ts - -# The deliberately wrong records.yaml values must NOT appear. -tr.Processes.Default.Streams.stdout = Testers.ExcludesExpression( - 'wrong_bin_path', 'bin_path must be overridden by runroot, not records.yaml') -tr.Processes.Default.Streams.stdout += Testers.ExcludesExpression( - 'wrong_runtime', 'local_state_dir must be overridden by runroot, not records.yaml') -tr.Processes.Default.Streams.stdout += Testers.ExcludesExpression( - 'wrong_log', 'logfile_dir must be overridden by runroot, not records.yaml') -tr.Processes.Default.Streams.stdout += Testers.ExcludesExpression( - 'wrong_plugin', 'plugin_dir must be overridden by runroot, not records.yaml') - -# --------------------------------------------------------------------------- -# Test 3: Verify env vars still take highest precedence over runroot. -# --------------------------------------------------------------------------- -tr = Test.AddTestRun("Verify env var overrides both runroot and records.yaml") -tr.Processes.Default.Command = 'traffic_ctl config get proxy.config.diags.debug.tags' -tr.Processes.Default.Env = ts.Env -tr.Processes.Default.ReturnCode = 0 -tr.StillRunningAfter = ts -tr.Processes.Default.Streams.stdout = Testers.ContainsExpression( - 'proxy.config.diags.debug.tags: env_wins', 'Env var must override both runroot and records.yaml') diff --git a/tests/gold_tests/records/records_yaml.test.py b/tests/gold_tests/records/records_yaml.test.py deleted file mode 100644 index 95fd59b4298..00000000000 --- a/tests/gold_tests/records/records_yaml.test.py +++ /dev/null @@ -1,201 +0,0 @@ -''' -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -from jsonrpc import Notification, Request, Response - -Test.Summary = 'Basic records test. Testing the new records.yaml logic and making sure it works as expected.' - -ts = Test.MakeATSProcess("ts") - -ts.Disk.records_config.update( - ''' - diags: - debug: - enabled: 1 - tags: rpc|rec - ssl: - client: - cert: - filename: null - filenamee: some.txt - filenam: some2.txt - dns: - nameservers: null - test: - not_registered: - - # we expect this to be parsed and added to the records but inform that - # the record field is not registered. - field1: !!int 1 - - # we expect this to fail and report while parsing as the field is not - # registered nor have a valid type - # field2: 0 # we cant test this now as traffic_layout will output the error and setup.cli.ext will fail - # to encode json - field2: 0 - - cache: - # test multipliers with and without tag - ram_cache: - size: 30G - ''') - -# 0 - We want to make sure that the unregistered records are still being detected. -tr = Test.AddTestRun("Load unregistered records") -tr.Processes.Default.Command = 'echo 1' -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.StartBefore(ts) - -var1 = 'proxy.config.test.not_registered.field1' -ts.Disk.diags_log.Content = Testers.ContainsExpression(f"Unrecognized configuration value '{var1}'", "Field should be ignored") -var2 = 'proxy.config.test.not_registered.field2' -ts.Disk.diags_log.Content += Testers.ContainsExpression(f"Unrecognized configuration value '{var2}", "Field should be ignored") - -ts.Disk.traffic_out.Content += Testers.ContainsExpression( - f"Ignoring field 'filenamee' \\[proxy.config.ssl.client.cert.filenamee\\]", "Field should be ignored") -ts.Disk.traffic_out.Content += Testers.ContainsExpression( - f"Ignoring field 'filenam' \\[proxy.config.ssl.client.cert.filenam\\]", "Field should be ignored") - -# 1 -tr = Test.AddTestRun("Query unregistered records.") - - -def check_response(resp: Response): - if resp.is_error(): - return (False, resp.error_as_str()) - - lst = resp.result['recordList'] - - if len(lst) != 2: - return (False, "Should be 2 records only.") - - for el in lst: - rec = el['record'] - if rec['registered'] != "false": - return (False, f"{rec['record_name']} should not be registered") - - return (True, "All good") - - -tr.AddJsonRPCClientRequest( - ts, Request.admin_lookup_records([{ - "record_name_regex": "proxy.config.test.not_registered.field", - "rec_types": ["1", "16"] - }])) - -# do our own check on the response. -tr.Processes.Default.Streams.stdout = Testers.CustomJSONRPCResponse(check_response) - -# 2 -tctl = Test.AddTestRun("Test field with multiplier") -tctl.Processes.Default.Command = 'traffic_ctl config get proxy.config.cache.ram_cache.size' -tctl.Processes.Default.Env = ts.Env -tctl.Processes.Default.ReturnCode = 0 -tctl.Processes.Default.Streams.stdout = Testers.ContainsExpression( - 'proxy.config.cache.ram_cache.size: 32212254720', 'Should hold the configured value.') - -# The whole idea is to test how ATS handles having multiple docs in the same records.yaml -# file. -# Every subsequent document will overwrite the previous one, this tests are meant to -# exercise that logic. - -ts2 = Test.MakeATSProcess("ts2") - -# We are adding a new doc at the end of the original one created by the unit test. -ts2.Disk.records_config.append_to_document(''' - diags: - debug: - enabled: 0 - tags: rpc|rec - ''') - -# This will append a new doc in the same file, this config value should overwrite -# the previous one. -ts2.Disk.records_config.append_to_document(''' - diags: - debug: - tags: filemanager - ''') - -# After all the loading completed we want to have enabled=0 and tags=filemanager - -tr = Test.AddTestRun("Start a new ATS instance.") -tr.Processes.Default.Command = 'echo 1' -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.StartBefore(ts2) -tr.Processes.StillRunningAfter = ts2 - -tr2 = Test.AddTestRun("Test multiple docs from the same file") -tr2.Processes.Default.Command = 'traffic_ctl config get proxy.config.diags.debug.enabled proxy.config.diags.debug.tags' -tr2.Processes.Default.Env = ts2.Env -tr2.Processes.Default.ReturnCode = 0 - -# Make sure it's what we want. -tr2.Processes.Default.Streams.stdout += Testers.ContainsExpression( - 'proxy.config.diags.debug.enabled: 0', 'Config should show debug disabled') -tr2.Processes.Default.Streams.stdout += Testers.ContainsExpression( - 'proxy.config.diags.debug.tags: filemanager', 'Config should show a different tag') - -ts2.Disk.records_config.append_to_document( - ''' - dns: - resolv_conf: NULL - nameservers: null - local_ipv6: Null - ssl: - client: - cert: - filename: ~ - ''') - -tr3 = Test.AddTestRun("test null string") -tr3.Processes.Default.Command = 'traffic_ctl config get proxy.config.dns.resolv_conf proxy.config.dns.local_ipv6 proxy.config.dns.nameservers proxy.config.ssl.client.cert.filename' -tr3.Processes.Default.Env = ts2.Env -tr3.Processes.Default.ReturnCode = 0 - -# Make sure it's what we want. -tr3.Processes.Default.Streams.stdout += Testers.ContainsExpression('proxy.config.dns.resolv_conf: null', 'should be set to null') -tr3.Processes.Default.Streams.stdout += Testers.ContainsExpression('proxy.config.dns.nameservers: null', 'should be set to null') -tr3.Processes.Default.Streams.stdout += Testers.ContainsExpression( - 'proxy.config.ssl.client.cert.filename: null', 'should be set to null') -tr3.Processes.Default.Streams.stdout += Testers.ContainsExpression('proxy.config.dns.local_ipv6: null', 'should be set to null') - -ts3 = Test.MakeATSProcess("ts3") -ts3.Disk.records_config.update(''' - some_invalid_field_should_not_block_further_docs_from_the_parser_logic: OK - ''') - -ts3.Disk.records_config.append_to_document(''' - diags: - debug: - tags: rpc|rec - ''') - -# We want to make sure that any error in any of the docs will not stop ATS from loading -# the rest of the nodes. -tr4 = Test.AddTestRun("test parsing multiple docs with invalid fields.") -tr4.Processes.Default.Command = 'traffic_ctl config get proxy.config.diags.debug.tags' -tr4.Processes.Default.Env = ts3.Env -tr.Processes.Default.StartBefore(ts3) -tr.Processes.StillRunningAfter = ts3 -tr4.Processes.Default.ReturnCode = 0 - -# This record should be parsed after the invalid field. Value should be the one -# configured. -tr4.Processes.Default.Streams.stdout += Testers.ContainsExpression( - 'proxy.config.diags.debug.tags: rpc|rec', 'should be the configured value') diff --git a/tests/gold_tests/records/traffic_ctl_cold_config.test.py b/tests/gold_tests/records/traffic_ctl_cold_config.test.py deleted file mode 100644 index c27925baf2b..00000000000 --- a/tests/gold_tests/records/traffic_ctl_cold_config.test.py +++ /dev/null @@ -1,94 +0,0 @@ -''' -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import os -from jsonrpc import Notification, Request, Response - -Test.Summary = 'Basic records test. Testing the new records.yaml logic and making sure it works as expected.' - -ts = Test.MakeATSProcess("ts") - -ts.Disk.records_config.update( - ''' - accept_threads: 1 - cache: - limits: - http: - max_alts: 5 - diags: - debug: - enabled: 0 - tags: http|dns - ''') - -# 0 - We want to make sure that the unregistered records are still being detected. -tr = Test.AddTestRun("Test Append value to existing records.yaml") - -tr.Processes.Default.Command = 'traffic_ctl config set proxy.config.diags.debug.tags rpc --cold' -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Env = ts.Env -tr.Processes.Default.StartBefore(ts) -ts.Disk.records_config.Content = 'gold/records.yaml.cold_test0.gold' - -# 1 -tr = Test.AddTestRun("Get value from latest added node.") -tr.Processes.Default.Command = 'traffic_ctl config get proxy.config.diags.debug.tags --cold' -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Env = ts.Env - -tr.Processes.Default.Streams.stdout += Testers.ContainsExpression( - 'proxy.config.diags.debug.tags: rpc', 'Config should show the right tags') - -# 2 -tr = Test.AddTestRun("Test modify latest yaml document from records.yaml") -tr.Processes.Default.Command = 'traffic_ctl config set proxy.config.diags.debug.tags http -u -c' -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Env = ts.Env -ts.Disk.records_config.Content = 'gold/records.yaml.cold_test2.gold' - -# 3 -tr = Test.AddTestRun("Get value from latest added node 1.") -tr.Processes.Default.Command = 'traffic_ctl config get proxy.config.diags.debug.tags --cold' -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Env = ts.Env - -tr.Processes.Default.Streams.stdout += Testers.ContainsExpression( - 'proxy.config.diags.debug.tags: http', 'Config should show the right tags') - -# 4 -tr = Test.AddTestRun("Append a new field node using a tag") -tr.Processes.Default.Command = 'traffic_ctl config set proxy.config.cache.limits.http.max_alts 1 -t int -c' -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Env = ts.Env -ts.Disk.records_config.Content = 'gold/records.yaml.cold_test4.gold' - -# 5 -file = os.path.join(ts.Variables.CONFIGDIR, "new_records.yaml") -tr = Test.AddTestRun("Adding a new node(with update flag set) to a non existing file") -tr.Processes.Default.Command = f'traffic_ctl config set proxy.config.cache.limits.http.max_alts 3 -u -c {file}' -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Env = ts.Env -tr.Disk.File(file).Content = 'gold/records.yaml.cold_test5.gold' - -# 5 -file = os.path.join(ts.Variables.CONFIGDIR, "new_records2.yaml") -tr = Test.AddTestRun("Adding a new node to a non existing file") -tr.Processes.Default.Command = f'traffic_ctl config set proxy.config.cache.limits.http.max_alts 3 -c {file}' -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Env = ts.Env -tr.Disk.File(file).Content = 'gold/records.yaml.cold_test5.gold' diff --git a/tests/gold_tests/records/ts_max_records_param.test.py b/tests/gold_tests/records/ts_max_records_param.test.py deleted file mode 100644 index 573b5e32837..00000000000 --- a/tests/gold_tests/records/ts_max_records_param.test.py +++ /dev/null @@ -1,56 +0,0 @@ -''' -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -from jsonrpc import Notification, Request, Response - -Test.Summary = 'Basic test for traffic_server --maxRecords behavior when setting different values.' - -maxRecords = 1000 - -# 0 - maxRecords below the default value(2048). Traffic server should warn about this and use the default value. -ts = Test.MakeATSProcess(f"ts{maxRecords}", command=f"traffic_server --maxRecords {maxRecords}") -tr = Test.AddTestRun(f"--maxRecords {maxRecords}") -tr.Processes.Default.Command = 'echo 1' -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.StartBefore(ts) -tr.StillRunningAfter = ts -ts.Streams.All = Testers.ContainsExpression( - f"Passed maxRecords value={maxRecords} is lower than the default value 2048. Default will be used.", - "It should use the default value") - -# 1 - maxRecords with just invalid number. Traffic server should warn about this and use the default value. -maxRecords = "abc" -ts = Test.MakeATSProcess(f"ts{maxRecords}", command=f"traffic_server --maxRecords {maxRecords}") -tr = Test.AddTestRun(f"--maxRecords {maxRecords}") -tr.Processes.Default.Command = 'echo 1' -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.StartBefore(ts) -tr.StillRunningAfter = ts -ts.Streams.All = Testers.ContainsExpression( - f"Invalid 0 value for maxRecords. Default 2048 will be used.", "It should use the default value") - -# 2 - maxRecords over the default value -maxRecords = 5000 -ts = Test.MakeATSProcess(f"ts{maxRecords}", command=f"traffic_server --maxRecords {maxRecords}") -tr = Test.AddTestRun(f"--maxRecords {maxRecords}") -tr.Processes.Default.Command = 'echo 1' -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.StartBefore(ts) -tr.StillRunningAfter = ts -# At least it should not crash. -ts.Disk.traffic_out.Content = Testers.ContainsExpression(f"NOTE: records parsing completed", "should all be good") diff --git a/tests/gold_tests/redirect/number_of_redirects.test.py b/tests/gold_tests/redirect/number_of_redirects.test.py deleted file mode 100644 index e948dc62060..00000000000 --- a/tests/gold_tests/redirect/number_of_redirects.test.py +++ /dev/null @@ -1,100 +0,0 @@ -''' -Specific test for number_of_redirections config. -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import os - -Test.Summary = ''' -Test redirection/location & number of redirects(number_of_redirections config) -''' - - -class NumberOfRedirectionsTest: - ''' - Handy class to test with number_of_redirections values. Three servers will be created and request - will flow between them. Depending on the configured number_of_redirections, some request may be - followed by ATS and some directly by the client(curl) - ''' - - def __init__(self, testName, numberOfRedirections): - self._numberOfRedirections = numberOfRedirections - self._tr = Test.AddTestRun(testName) - self.setup_ts(f'ts{self._numberOfRedirections}') - self.setup_dns() - self.setup_verifier_servers() - self.add_config() - - def setup_ts(self, name="ts"): - self._ts = Test.MakeATSProcess(name, enable_cache=False) - - def setup_verifier_servers(self): - self._srv3 = Test.MakeVerifierServerProcess(f"srv3_{self._numberOfRedirections}", "replay/redirect_srv3_replay.yaml") - self._srv2 = Test.MakeVerifierServerProcess( - f"srv2_{self._numberOfRedirections}", - "replay/redirect_srv2_replay.yaml", - context={"vs_http_port": self._srv3.Variables.http_port}) - self._srv1 = Test.MakeVerifierServerProcess( - f"srv1_{self._numberOfRedirections}", - "replay/redirect_srv1_replay.yaml", - context={"vs_http_port": self._srv2.Variables.http_port}) - - def setup_dns(self): - self._dns = Test.MakeDNServer(f"dns_{self._numberOfRedirections}") - self._dns.addRecords(records={"a.test": ["127.0.0.1"]}) - self._dns.addRecords(records={"b.test": ["127.0.0.1"]}) - self._dns.addRecords(records={"c.test": ["127.0.0.1"]}) - - def add_config(self): - self._ts.Disk.records_config.update( - { - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'http|dns|redirect|http_redirect', - 'proxy.config.http.number_of_redirections': self._numberOfRedirections, - 'proxy.config.dns.nameservers': f'127.0.0.1:{self._dns.Variables.Port}', - 'proxy.config.dns.resolv_conf': 'NULL', - 'proxy.config.url_remap.remap_required': 0, # need this so the domain gets a chance to be evaluated through DNS - 'proxy.config.http.redirect.actions': 'self:follow', # redirects to self are not followed by default - }) - self._ts.Disk.remap_config.AddLines( - [ - 'map a.test/ping http://a.test:{0}/'.format(self._srv1.Variables.http_port), - 'map b.test/pong http://b.test:{0}/'.format(self._srv2.Variables.http_port), - 'map c.test/pang http://c.test:{0}/'.format(self._srv3.Variables.http_port), - ]) - - def run(self): - self._tr.Processes.Default.StartBefore(self._srv1) - self._tr.Processes.Default.StartBefore(self._srv2) - self._tr.Processes.Default.StartBefore(self._srv3) - self._tr.Processes.Default.StartBefore(self._dns) - self._tr.Processes.Default.StartBefore(self._ts) - self._tr.MakeCurlCommand( - "-L -v a.test/ping --proxy 127.0.0.1:{0} -H 'uuid: redirect_test_1'".format(self._ts.Variables.port), ts=self._ts) - self._tr.Processes.Default.Streams.All = f"gold/number_of_redirections_{self._numberOfRedirections}.gold" - self._tr.ReturnCode = 0 - self._tr.StillRunningAfter = self._ts - - -# No redirect, curl will get 302 and do the rest of the redirection. -NumberOfRedirectionsTest("Test number_of_redirections=0", 0).run() -# Single redirect, ATS will follow 1 redirect and the client the last one. -NumberOfRedirectionsTest("Test number_of_redirections=1", 1).run() -# The client will just get 200OK and no redirect will be done by it, TS will follow the two -# 302 Redirect. -NumberOfRedirectionsTest("Test number_of_redirections=2", 2).run() -# If adding more, need to touch the server side as well. It won't be enough. diff --git a/tests/gold_tests/redirect/redirect.test.py b/tests/gold_tests/redirect/redirect.test.py deleted file mode 100644 index 1a14ed1d974..00000000000 --- a/tests/gold_tests/redirect/redirect.test.py +++ /dev/null @@ -1,179 +0,0 @@ -''' -Test redirection -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import os -import sys - -Test.Summary = ''' -Test redirection -''' - -Test.ContinueOnFail = True - -ts = Test.MakeATSProcess("ts", enable_cache=False) -redirect_serv = Test.MakeOriginServer("re_server") -dest_serv = Test.MakeOriginServer("dest_server") -dns = Test.MakeDNServer("dns") - -ts.Disk.records_config.update( - { - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'http|dns|redirect', - 'proxy.config.http.number_of_redirections': 1, - 'proxy.config.dns.nameservers': '127.0.0.1:{0}'.format(dns.Variables.Port), - 'proxy.config.dns.resolv_conf': 'NULL', - 'proxy.config.url_remap.remap_required': 0, # need this so the domain gets a chance to be evaluated through DNS - 'proxy.config.http.redirect.actions': 'self:follow', # redirects to self are not followed by default - }) - -ts.Disk.logging_yaml.AddLines( - ''' -logging: - formats: - - name: custom - format: "client_url=% cache_result: code=% subcode=%" - logs: - - filename: the_log - format: custom -'''.split("\n")) - -Test.Setup.Copy(os.path.join(Test.Variables.AtsTestToolsDir, 'tcp_client.py')) - -redirect_request_header = {"headers": "GET /redirect HTTP/1.1\r\nHost: *\r\n\r\n", "timestamp": "5678", "body": ""} -redirect_response_header = { - "headers": "HTTP/1.1 302 Found\r\nLocation: http://127.0.0.1:{0}/redirectDest\r\n\r\n".format(dest_serv.Variables.Port), - "timestamp": "5678", - "body": "" -} -redirect_serv.addResponse("sessionfile.log", redirect_request_header, redirect_response_header) - -dest_request_header = {"headers": "GET /redirectDest HTTP/1.1\r\nHost: *\r\n\r\n", "timestamp": "11", "body": ""} -dest_response_header = {"headers": "HTTP/1.1 204 No Content\r\nConnection: close\r\n\r\n", "timestamp": "22", "body": ""} -dest_serv.addResponse("sessionfile.log", dest_request_header, dest_response_header) - -dns.addRecords(records={"iwillredirect.test": ["127.0.0.1"]}) - -data_dirname = 'redirect_generated_test_data' -data_path = os.path.join(Test.TestDirectory, data_dirname) -os.makedirs(data_path, exist_ok=True) - -# Here and below: spaces are deliberately omitted from the test run names because autest creates directories using these names. -tr = Test.AddTestRun("FollowsRedirectWithAbsoluteLocationURI") -# Here and below: because autest's Copy does not behave like standard cp, it's easiest to write all of our files out and copy last. -command_path = os.path.join(data_path, tr.Name) -with open(command_path, 'w') as f: - f.write('GET /redirect HTTP/1.1\r\nHost: iwillredirect.test:{port}\r\n\r\n'.format(port=redirect_serv.Variables.Port)) -tr.Processes.Default.Command = f"{sys.executable} tcp_client.py 127.0.0.1 {ts.Variables.port} {command_path} | egrep -v '^(Date: |Server: ATS/)'" -tr.Processes.Default.StartBefore(ts) -tr.Processes.Default.StartBefore(redirect_serv) -tr.Processes.Default.StartBefore(dest_serv) -tr.Processes.Default.StartBefore(dns) -tr.Processes.Default.Streams.stdout = "gold/redirect.gold" -tr.Processes.Default.ReturnCode = 0 - -redirect_request_header = {"headers": "GET /redirect-relative-path HTTP/1.1\r\nHost: *\r\n\r\n", "timestamp": "5678", "body": ""} -redirect_response_header = {"headers": "HTTP/1.1 302 Found\r\nLocation: /redirect\r\n\r\n", "timestamp": "5678", "body": ""} -redirect_serv.addResponse("sessionfile.log", redirect_request_header, redirect_response_header) - -redirect_request_header = {"headers": "GET /redirect HTTP/1.1\r\nHost: *\r\n\r\n", "timestamp": "5678", "body": ""} -redirect_response_header = {"headers": "HTTP/1.1 204 No Content\r\nConnection: close\r\n\r\n", "timestamp": "22", "body": ""} -redirect_serv.addResponse("sessionfile.log", redirect_request_header, redirect_response_header) - -tr = Test.AddTestRun("FollowsRedirectWithRelativeLocationURI") -command_path = os.path.join(data_path, tr.Name) -with open(command_path, 'w') as f: - f.write( - 'GET /redirect-relative-path HTTP/1.1\r\nHost: iwillredirect.test:{port}\r\n\r\n'.format(port=redirect_serv.Variables.Port)) -tr.Processes.Default.Command = f"{sys.executable} tcp_client.py 127.0.0.1 {ts.Variables.port} {command_path} | egrep -v '^(Date: |Server: ATS/)'" -tr.StillRunningAfter = ts -tr.StillRunningAfter = redirect_serv -tr.StillRunningAfter = dest_serv -tr.StillRunningAfter = dns -tr.Processes.Default.Streams.stdout = "gold/redirect.gold" -tr.Processes.Default.ReturnCode = 0 - -redirect_request_header = { - "headers": "GET /redirect-relative-path-no-leading-slash HTTP/1.1\r\nHost: *\r\n\r\n", - "timestamp": "5678", - "body": "" -} -redirect_response_header = {"headers": "HTTP/1.1 302 Found\r\nLocation: redirect\r\n\r\n", "timestamp": "5678", "body": ""} -redirect_serv.addResponse("sessionfile.log", redirect_request_header, redirect_response_header) - -tr = Test.AddTestRun("FollowsRedirectWithRelativeLocationURIMissingLeadingSlash") -command_path = os.path.join(data_path, tr.Name) -with open(command_path, 'w') as f: - f.write( - 'GET /redirect-relative-path-no-leading-slash HTTP/1.1\r\nHost: iwillredirect.test:{port}\r\n\r\n'.format( - port=redirect_serv.Variables.Port)) -tr.Processes.Default.Command = f"{sys.executable} tcp_client.py 127.0.0.1 {ts.Variables.port} {command_path} | egrep -v '^(Date: |Server: ATS/)'" -tr.StillRunningAfter = ts -tr.StillRunningAfter = redirect_serv -tr.StillRunningAfter = dest_serv -tr.StillRunningAfter = dns -tr.Processes.Default.Streams.stdout = "gold/redirect.gold" -tr.Processes.Default.ReturnCode = 0 - -for status, phrase in sorted({ - 301: 'Moved Permanently', - 302: 'Found', - 303: 'See Other', - 305: 'Use Proxy', - 307: 'Temporary Redirect', - 308: 'Permanent Redirect', -}.items()): - - redirect_request_header = { - "headers": ("GET /redirect{0} HTTP/1.1\r\n" - "Host: *\r\n\r\n").format(status), - "timestamp": "5678", - "body": "" - } - redirect_response_header = { - "headers": ("HTTP/1.1 {0} {1}\r\n" - "Connection: close\r\n" - "Location: /redirect\r\n\r\n").format(status, phrase), - "timestamp": "5678", - "body": "" - } - redirect_serv.addResponse("sessionfile.log", redirect_request_header, redirect_response_header) - - tr = Test.AddTestRun("FollowsRedirect{0}".format(status)) - command_path = os.path.join(data_path, tr.Name) - with open(command_path, 'w') as f: - f.write( - ('GET /redirect{0} HTTP/1.1\r\n' - 'Host: iwillredirect.test:{1}\r\n\r\n').format(status, redirect_serv.Variables.Port)) - tr.Processes.Default.Command = f"{sys.executable} tcp_client.py 127.0.0.1 {ts.Variables.port} {command_path} | egrep -v '^(Date: |Server: ATS/)'" - tr.StillRunningAfter = ts - tr.StillRunningAfter = redirect_serv - tr.StillRunningAfter = dest_serv - tr.StillRunningAfter = dns - tr.Processes.Default.Streams.stdout = "gold/redirect.gold" - tr.Processes.Default.ReturnCode = 0 - -Test.Setup.Copy('wait_for_log.sh') - -tr = Test.AddTestRun("wait_for_log") -tr.Processes.Default.Command = ( - './wait_for_log.sh {} {}'.format(os.path.join(ts.Variables.LOGDIR, 'the_log.log'), redirect_serv.Variables.Port)) -tr.Processes.Default.Streams.stdout = "gold/redirect_log.gold" -tr.Processes.Default.ReturnCode = 0 - -Test.Setup.Copy(data_path) diff --git a/tests/gold_tests/redirect/redirect_actions.test.py b/tests/gold_tests/redirect/redirect_actions.test.py deleted file mode 100644 index beaeae1bebc..00000000000 --- a/tests/gold_tests/redirect/redirect_actions.test.py +++ /dev/null @@ -1,269 +0,0 @@ -''' -Test redirection behavior to invalid addresses -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -from enum import Enum -import re -import os -import socket -import sys - -Test.Summary = ''' -Test redirection behavior to invalid addresses -''' - -Test.ContinueOnFail = False - -Test.Setup.Copy(os.path.join(Test.Variables.AtsTestToolsDir, 'tcp_client.py')) - -dns = Test.MakeDNServer('dns') -# This record is used in each test case to get the initial redirect response from the origin that we will handle. -dnsRecords = {'iwillredirect.test': ['127.0.0.1']} - -host = socket.gethostname() -ipv4addrs = set() -try: - ipv4addrs = set([ip for (family, _, _, _, (ip, *_)) in socket.getaddrinfo(host, port=None) if socket.AF_INET == family]) -except socket.gaierror: - pass - -ipv6addrs = set() -try: - ipv6addrs = set( - [ - "[{0}]".format(ip.split('%')[0]) - for (family, _, _, _, (ip, *_)) in socket.getaddrinfo(host, port=None) - if socket.AF_INET6 == family and 'fe80' != ip[0:4] - ]) # Skip link-local addresses. -except socket.gaierror: - pass - -# If the hostname is set to a loopback address, use the ip command to obtain a non-loopback IP address. -if any(ip.startswith('127.') for ip in ipv4addrs): - import json - from subprocess import check_output - - data = json.loads(check_output(['ip', '-json', 'address', 'show'])) - - ipv4addrs = set( - [ - addr["local"] for iface in data if iface["link_type"] != "loopback" for addr in iface.get("addr_info", []) - if addr["family"] == "inet" - ]) - ipv6addrs = set( - [ - addr["local"] for iface in data if iface["link_type"] != "loopback" for addr in iface.get("addr_info", []) - if addr["family"] == "inet6" and addr["scope"] != "link" - ]) - -origin = Test.MakeOriginServer('origin', ip='0.0.0.0') -ArbitraryTimestamp = '12345678' - -# This is for cases when the content is actually fetched from the invalid address. -request_header = { - 'headers': ('GET / HTTP/1.1\r\n' - 'Host: *\r\n\r\n'), - 'timestamp': ArbitraryTimestamp, - 'body': '' -} -response_header = { - 'headers': ('HTTP/1.1 204 No Content\r\n' - 'Connection: close\r\n\r\n'), - 'timestamp': ArbitraryTimestamp, - 'body': '' -} -origin.addResponse('sessionfile.log', request_header, response_header) - -# Map scenarios to trafficserver processes. -trafficservers = {} - -data_dirname = 'redirect_actions_generated_test_data' -data_path = os.path.join(Test.TestDirectory, data_dirname) -os.makedirs(data_path, exist_ok=True) - - -def normalizeForAutest(value): - ''' - autest uses "test run" names to build file and directory names, so we must transform them in case there are incompatible or - annoying characters. - This means we can also use them in URLs. - ''' - if not value: - return None - return re.sub(r'[^a-z0-9-]', '_', value, flags=re.I) - - -def makeTestCase(redirectTarget, expectedAction, scenario): - ''' - Helper method that creates a "meta-test" from which autest generates a test case. - - :param redirectTarget: The target address of a redirect from origin to be handled. - :param scenario: Defines the ACL to configure and the addresses to test. - ''' - - config = ','.join(':'.join(t) for t in sorted((addr.name.lower(), action.name.lower()) for (addr, action) in scenario.items())) - - normRedirectTarget = normalizeForAutest(redirectTarget) - normConfig = normalizeForAutest(config) - tr = Test.AddTestRun('With_Config_{0}_Redirect_to_{1}'.format(normConfig, normRedirectTarget)) - - if trafficservers: - tr.StillRunningAfter = origin - tr.StillRunningAfter = dns - else: - tr.Processes.Default.StartBefore(origin) - tr.Processes.Default.StartBefore(dns) - - if config not in trafficservers: - trafficservers[config] = Test.MakeATSProcess('ts_{0}'.format(normConfig), enable_cache=False) - trafficservers[config].Disk.records_config.update( - { - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'http|dns|redirect', - 'proxy.config.http.number_of_redirections': 1, - 'proxy.config.dns.nameservers': '127.0.0.1:{0}'.format(dns.Variables.Port), - 'proxy.config.dns.resolv_conf': 'NULL', - 'proxy.config.url_remap.remap_required': 0, - 'proxy.config.http.redirect.actions': config, - 'proxy.config.http.connect_attempts_timeout': 5, - 'proxy.config.http.connect_attempts_max_retries': 0, - }) - tr.Processes.Default.StartBefore(trafficservers[config]) - else: - tr.StillRunningAfter = trafficservers[config] - - testDomain = 'testdomain{0}.test'.format(normRedirectTarget) - # The micro DNS server can't tell us whether it has a record of the domain already, so we use a dictionary to avoid duplicates. - # We remove any surrounding brackets that are common to IPv6 addresses. - if redirectTarget: - dnsRecords[testDomain] = [redirectTarget.strip('[]')] - - # A GET request parameterized on the config and on the target. - request_header = { - 'headers': ('GET /redirect?config={0}&target={1} HTTP/1.1\r\n' - 'Host: *\r\n\r\n').format(normConfig, normRedirectTarget), - 'timestamp': ArbitraryTimestamp, - 'body': '' - } - # Returns a redirect to the test domain for the given target & the port number for the TS of the given config. - response_header = { - 'headers': - ('HTTP/1.1 307 Temporary Redirect\r\n' - 'Location: http://{0}:{1}/\r\n' - 'Connection: close\r\n\r\n').format(testDomain, origin.Variables.Port), - 'timestamp': ArbitraryTimestamp, - 'body': '' - } - origin.addResponse('sessionfile.log', request_header, response_header) - - # Generate the request data file. - command_path = os.path.join(data_path, tr.Name) - with open(command_path, 'w') as f: - f.write( - ('GET /redirect?config={0}&target={1} HTTP/1.1\r\n' - 'Host: iwillredirect.test:{2}\r\n\r\n').format(normConfig, normRedirectTarget, origin.Variables.Port)) - # Set the command with the appropriate URL. - port = trafficservers[config].Variables.port - dir_path = os.path.join(data_dirname, tr.Name) - tr.Processes.Default.Command = \ - (f"bash -o pipefail -c '{sys.executable} tcp_client.py 127.0.0.1 {port} " - f"{dir_path} | head -n 1'") - tr.Processes.Default.ReturnCode = 0 - # Generate and set the 'gold file' to check stdout - goldFilePath = os.path.join(data_path, '{0}.gold'.format(tr.Name)) - with open(goldFilePath, 'w') as f: - f.write(expectedAction.value['expectedStatusLine']) - tr.Processes.Default.Streams.stdout = goldFilePath - - -class AddressE(Enum): - ''' - Classes of addresses are mapped to example addresses. - ''' - Private = ('10.0.0.1', '[fc00::1]') - Loopback = (['127.1.2.3']) # [::1] is omitted here because it is likely overwritten by Self, and there are no others in IPv6. - Multicast = ('224.1.2.3', '[ff42::]') - Linklocal = ('169.254.0.1', '[fe80::]') - Routable = ('72.30.35.10', '[2001:4998:58:1836::10]') # Do not Follow redirects to these in an automated test. - Self = ipv4addrs | ipv6addrs # Addresses of this host. - Default = None # All addresses apply, nothing in particular to test. - - -class ActionE(Enum): - # Title case because 'return' is a Python keyword. - Return = {'config': 'return', 'expectedStatusLine': 'HTTP/1.1 307 Temporary Redirect\r\n'} - Reject = {'config': 'reject', 'expectedStatusLine': 'HTTP/1.1 403 Forbidden\r\n'} - Follow = {'config': 'follow', 'expectedStatusLine': 'HTTP/1.1 204 No Content\r\n'} - - # Added to test failure modes. - Break = {'expectedStatusLine': 'HTTP/1.1 500 Cannot find server.\r\n'} - - -scenarios = [ - { - # Follow to loopback, but alternately reject/return others. - AddressE.Private: ActionE.Reject, - AddressE.Loopback: ActionE.Follow, - AddressE.Multicast: ActionE.Reject, - AddressE.Linklocal: ActionE.Return, - AddressE.Routable: ActionE.Reject, - AddressE.Self: ActionE.Return, - AddressE.Default: ActionE.Reject, - }, - { - # Follow to loopback, but alternately reject/return others, flipped from the previous scenario. - AddressE.Private: ActionE.Return, - AddressE.Loopback: ActionE.Follow, - AddressE.Multicast: ActionE.Return, - AddressE.Linklocal: ActionE.Reject, - AddressE.Routable: ActionE.Return, - AddressE.Self: ActionE.Reject, - AddressE.Default: ActionE.Return, - }, - { - # Return loopback, but reject everything else. - AddressE.Loopback: ActionE.Return, - AddressE.Default: ActionE.Reject, - }, - { - # Reject loopback, but return everything else. - AddressE.Loopback: ActionE.Reject, - AddressE.Default: ActionE.Return, - }, - { - # Return everything. - AddressE.Default: ActionE.Return, - }, -] - -for scenario in scenarios: - for addressClass in AddressE: - if not addressClass.value: - # Default has no particular addresses to test. - continue - for address in addressClass.value: - expectedAction = scenario[addressClass] if addressClass in scenario else scenario[AddressE.Default] - makeTestCase(redirectTarget=address, expectedAction=expectedAction, scenario=scenario) - - # Test redirects to names that cannot be resolved. - makeTestCase(redirectTarget=None, expectedAction=ActionE.Break, scenario=scenario) - -dns.addRecords(records=dnsRecords) - -# Make sure this runs only after local files have been created. -Test.Setup.Copy(data_path) diff --git a/tests/gold_tests/redirect/redirect_post.test.py b/tests/gold_tests/redirect/redirect_post.test.py deleted file mode 100644 index 8d1d3d545c3..00000000000 --- a/tests/gold_tests/redirect/redirect_post.test.py +++ /dev/null @@ -1,91 +0,0 @@ -''' -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -Test.Summary = ''' -Test basic post redirection -''' - -# TODO figure out how to use this -MAX_REDIRECT = 99 - -Test.SkipUnless(Condition.HasProgram("truncate", "truncate need to be installed on system for this test to work")) - -Test.ContinueOnFail = True - -ts = Test.MakeATSProcess("ts", enable_cache=False) -redirect_serv1 = Test.MakeOriginServer("re_server1") -redirect_serv2 = Test.MakeOriginServer("re_server2") -dest_serv = Test.MakeOriginServer("dest_server") - -ts.Disk.records_config.update( - { - 'proxy.config.http.number_of_redirections': MAX_REDIRECT, - 'proxy.config.http.post_copy_size': 919430601, - 'proxy.config.http.redirect.actions': 'self:follow', # redirects to self are not followed by default - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'http', - }) - -redirect_request_header = { - "headers": "POST /redirect1 HTTP/1.1\r\nHost: *\r\nContent-Length: 52428800\r\n\r\n", - "timestamp": "5678", - "body": "" -} -redirect_response_header = { - "headers": "HTTP/1.1 302 Found\r\nLocation: http://127.0.0.1:{0}/redirect2\r\n\r\n".format(redirect_serv2.Variables.Port), - "timestamp": "5678", - "body": "" -} - -redirect_request_header2 = { - "headers": "POST /redirect2 HTTP/1.1\r\nHost: *\r\nContent-Length: 52428800\r\n\r\n", - "timestamp": "5678", - "body": "" -} -redirect_response_header2 = { - "headers": "HTTP/1.1 302 Found\r\nLocation: http://127.0.0.1:{0}/redirectDest\r\n\r\n".format(dest_serv.Variables.Port), - "timestamp": "5678", - "body": "" -} - -dest_request_header = { - "headers": "POST /redirectDest HTTP/1.1\r\nHost: *\r\nContent-Length: 52428800\r\n\r\n", - "timestamp": "11", - "body": "" -} -dest_response_header = {"headers": "HTTP/1.1 204 No Content\r\n\r\n", "timestamp": "22", "body": ""} - -redirect_serv1.addResponse("sessionfile.log", redirect_request_header, redirect_response_header) -redirect_serv2.addResponse("sessionfile.log", redirect_request_header2, redirect_response_header2) -dest_serv.addResponse("sessionfile.log", dest_request_header, dest_response_header) - -ts.Disk.remap_config.AddLine( - 'map http://127.0.0.1:{0} http://127.0.0.1:{1}'.format(ts.Variables.port, redirect_serv1.Variables.Port)) - -tr = Test.AddTestRun() -tr.MakeCurlCommandMulti( - 'touch largefile.txt && truncate -s 50M largefile.txt && {{curl}} -H "Expect: " -i http://127.0.0.1:{0}/redirect1 -F "filename=@./largefile.txt" && rm -f largefile.txt' - .format(ts.Variables.port), - ts=ts) -tr.TimeOut = 10 -tr.Processes.Default.StartBefore(ts) -tr.Processes.Default.StartBefore(redirect_serv1) -tr.Processes.Default.StartBefore(redirect_serv2) -tr.Processes.Default.StartBefore(dest_serv) -tr.Processes.Default.Streams.stdout = "gold/redirect_post.gold" -tr.Processes.Default.ReturnCode = 0 diff --git a/tests/gold_tests/redirect/redirect_stale.test.py b/tests/gold_tests/redirect/redirect_stale.test.py deleted file mode 100644 index 7d9cae55378..00000000000 --- a/tests/gold_tests/redirect/redirect_stale.test.py +++ /dev/null @@ -1,97 +0,0 @@ -''' -Test that redirects will be followed when refreshing stale cache objects. -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -Test.Summary = ''' -Test that redirects will be followed when refreshing stale cache objects. -''' - -server = Test.MakeOriginServer("server") - -ArbitraryTimestamp = '12345678' - -request_header = { - 'headers': ('GET /obj HTTP/1.1\r\n' - 'Host: *\r\n\r\n'), - 'timestamp': ArbitraryTimestamp, - 'body': '' -} -response_header = { - 'headers': ('HTTP/1.1 302 Found\r\n' - 'Location: http://127.0.0.1:{}/obj2\r\n\r\n'.format(server.Variables.Port)), - 'timestamp': ArbitraryTimestamp, - 'body': '' -} -server.addResponse('sessionfile.log', request_header, response_header) - -request_header = { - 'headers': ('GET /obj2 HTTP/1.1\r\n' - 'Host: *\r\n\r\n'), - 'timestamp': ArbitraryTimestamp, - 'body': '' -} -response_header = { - 'headers': ('HTTP/1.1 200 OK\r\n' - 'X-Obj: obj2\r\n' - 'Cache-Control: max-age=2\r\n' - 'Content-Length: 0\r\n\r\n'), - 'timestamp': ArbitraryTimestamp, - 'body': '' -} -server.addResponse('sessionfile.log', request_header, response_header) - -ts = Test.MakeATSProcess("ts") - -ts.Disk.records_config.update( - { - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'http|dns|cache|redirect', - 'proxy.config.http.cache.required_headers': 0, # Only Content-Length header required for caching. - 'proxy.config.http.push_method_enabled': 1, - 'proxy.config.url_remap.remap_required': 0, - 'proxy.config.http.redirect.actions': 'routable:follow,loopback:follow,self:follow', - 'proxy.config.http.number_of_redirections': 1 - }) - -# Set up to check the output after the tests have run. -# -log_id = Test.Disk.File("log2.txt") -log_id.Content = "gold/redirect_stale.gold" - -tr = Test.AddTestRun() -tr.Processes.Default.StartBefore(ts) -tr.Processes.Default.StartBefore(server) -tr.Processes.Default.Command = ( - r"printf 'GET /obj HTTP/1.1\r\nHost: 127.0.0.1:{}\r\n\r\n' | nc localhost {} >> log.txt".format( - server.Variables.Port, ts.Variables.port)) -tr.Processes.Default.ReturnCode = 0 - -# Wait for the response in cache to become stale, then GET it again. -# -tr = Test.AddTestRun() -tr.Processes.Default.Command = ( - r"sleep 4 ; printf 'GET /obj HTTP/1.1\r\nHost: 127.0.0.1:{}\r\n\r\n' | nc localhost {} >> log.txt".format( - server.Variables.Port, ts.Variables.port)) -tr.Processes.Default.ReturnCode = 0 - -# Filter out inconsistent content in test output. -# -tr = Test.AddTestRun() -tr.Processes.Default.Command = ( - r"grep -v -e '^Date: ' -e '^Age: ' -e '^Connection: ' -e '^Server: ATS/' log.txt | tr -d '\r'> log2.txt") -tr.Processes.Default.ReturnCode = 0 diff --git a/tests/gold_tests/redirect/redirect_to_same_origin_on_cache.test.py b/tests/gold_tests/redirect/redirect_to_same_origin_on_cache.test.py deleted file mode 100644 index 9a809ce6414..00000000000 --- a/tests/gold_tests/redirect/redirect_to_same_origin_on_cache.test.py +++ /dev/null @@ -1,141 +0,0 @@ -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import functools -from typing import Any, Callable, Dict, Optional - -from ports import get_port - -Test.Summary = 'Tests SNI port-based routing' - -TestParams = Dict[str, Any] - - -class TestRedirectToSameOriginOnCache: - """Configure a test for reproducing #9275.""" - - replay_filepath_one: str = "replay/redirect_to_same_origin_on_cache.replay.yaml" - client_counter: int = 0 - server_counter: int = 0 - ts_counter: int = 0 - - def __init__(self, name: str, /, autorun) -> None: - """Initialize the test. - - :param name: The name of the test. - """ - self.name = name - self.autorun = autorun - - def _init_run(self) -> "TestRun": - """Initialize processes for the test run.""" - - tr = Test.AddTestRun(self.name) - server_one = TestRedirectToSameOriginOnCache.configure_server(tr, "oof.com") - self._configure_traffic_server(tr, server_one) - dns = TestRedirectToSameOriginOnCache.configure_dns(tr, server_one, self._dns_port) - - tr.Processes.Default.StartBefore(server_one) - tr.Processes.Default.StartBefore(dns) - tr.Processes.Default.StartBefore(self._ts) - - return { - "tr": tr, - "ts": self._ts, - "server_one": server_one, - "port_one": self._port_one, - } - - @classmethod - def runner(cls, name: str, autorun: bool = True) -> Optional[Callable]: - """Create a runner for a test case. - - :param autorun: Run the test case once it's set up. Default is True. - """ - test = cls(name, autorun=autorun)._prepare_test_case - return test - - def _prepare_test_case(self, func: Callable) -> Callable: - """Set up a test case and possibly run it. - - :param func: The test case to set up. - """ - functools.wraps(func) - test_params = self._init_run() - - def wrapper(*args, **kwargs) -> Any: - return func(test_params, *args, **kwargs) - - if self.autorun: - wrapper() - return wrapper - - @staticmethod - def configure_server(tr: "TestRun", domain: str): - server = tr.AddVerifierServerProcess( - f"server{TestRedirectToSameOriginOnCache.server_counter}.{domain}", - TestRedirectToSameOriginOnCache.replay_filepath_one, - other_args="--format \"{url}\"") - TestRedirectToSameOriginOnCache.server_counter += 1 - - return server - - @staticmethod - def configure_dns(tr: "TestRun", server_one: "Process", dns_port: int): - dns = tr.MakeDNServer("dns", port=dns_port, default="127.0.0.1") - return dns - - def _configure_traffic_server(self, tr: "TestRun", server_one: "Process"): - """Configure Traffic Server. - - :param tr: The TestRun object to associate the ts process with. - """ - ts = tr.MakeATSProcess( - f"ts-{TestRedirectToSameOriginOnCache.ts_counter}", select_ports=False, enable_tls=True, enable_cache=True) - TestRedirectToSameOriginOnCache.ts_counter += 1 - - self._port_one = get_port(ts, "PortOne") - self._dns_port = get_port(ts, "DNSPort") - ts.Disk.records_config.update( - { - 'proxy.config.http.server_ports': f"{self._port_one}", - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': "cache|dns|http|redirect|remap", - 'proxy.config.dns.nameservers': f"127.0.0.1:{self._dns_port}", - 'proxy.config.dns.resolv_conf': 'NULL', - 'proxy.config.http.redirect.actions': "self:follow", - 'proxy.config.http.number_of_redirections': 1, - }) - - ts.Disk.remap_config.AddLine(f"map oof.com http://oof.backend.com:{server_one.Variables.http_port}") - ts.Disk.plugin_config.AddLine("xdebug.so --enable=x-cache,x-cache-key,via") - - self._ts = ts - - -# Tests start. - - -@TestRedirectToSameOriginOnCache.runner("Redirect to same origin with cache") -def test1(params: TestParams) -> None: - client = params["tr"].AddVerifierClientProcess( - f"client0", - TestRedirectToSameOriginOnCache.replay_filepath_one, - http_ports=[params["port_one"]], - other_args="--format \"{url}\" --keys \"/a/path/resource\"") - - params["tr"].Processes.Default.ReturnCode = 0 - params["tr"].Processes.Default.Streams.stdout.Content += Testers.IncludesExpression("200 OK", "We should get the resource.") diff --git a/tests/gold_tests/remap/basic_conf_remap_yaml.test.py b/tests/gold_tests/remap/basic_conf_remap_yaml.test.py deleted file mode 100644 index 22b7e930c96..00000000000 --- a/tests/gold_tests/remap/basic_conf_remap_yaml.test.py +++ /dev/null @@ -1,193 +0,0 @@ -''' -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -Test.Summary = ''' -Test conf_remap using a yaml file. -''' - -Test.ContinueOnFail = True - - -class conf_remap_yaml_load_test: - """Test conf_remap using a yaml file.""" - - client_counter: int = 0 - ts_counter: int = 0 - server_counter: int = 0 - - def __init__(self, name: str, gold_file="", remap_filename="", remap_content=""): - """Initialize the test. - :param name: The name of the test. - :param gold_file: Gold file to be checked. - :param remap_filename: Remap yaml filename. - :param remap_content: remap yaml file content. - """ - self.name = name - self.gold_file = gold_file - self._remap_filename = remap_filename - self._remap_content = remap_content - - def _configure_server(self, tr: 'TestRun'): - """Configure the server. - - :param tr: The TestRun object to associate the server process with. - """ - server = Test.MakeOriginServer(f"server-{conf_remap_yaml_load_test.ts_counter}", lookup_key="{%Host}{PATH}") - request_header2 = { - "headers": "GET /test HTTP/1.1\r\nHost: www.testexample.com\r\n\r\n", - "timestamp": "1469733493.993", - "body": "" - } - response_header2 = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} - - server.addResponse("sessionfile.log", request_header2, response_header2) - conf_remap_yaml_load_test.server_counter += 1 - self._server = server - - def _configure_traffic_server(self, tr: 'TestRun'): - """Configure Traffic Server. - - :param tr: The TestRun object to associate the ts process with. - """ - ts = Test.MakeATSProcess(f"ts-{conf_remap_yaml_load_test.ts_counter}") - - conf_remap_yaml_load_test.ts_counter += 1 - ts.Disk.records_config.update( - ''' - diags: - debug: - enabled: 1 - tags: conf_remap - dns: - resolv_conf: NULL - http: - referer_filter: 1 - url_remap: - pristine_host_hdr: 0 # make sure is 0 - - ''') - self._ts = ts - - def run(self, diags_fail_exp="", ts_retcode=0): - """Run the test. - :param diags_fail_exp: Text to be included to validate the error. - :param ts_retcode: Expected return code from TS. - """ - tr = Test.AddTestRun(self.name) - self._configure_server(tr) - self._configure_traffic_server(tr) - - tr.Processes.Default.StartBefore(self._server) - tr.Processes.Default.StartBefore(self._ts) - - self._ts.ReturnCode = ts_retcode - - if ts_retcode > 0: # we could have errors logged and yet, we still want to move on. - self._ts.Ready = 0 - - if diags_fail_exp != "": - # some error logs will be written to the diags. - self._ts.Disk.diags_log.Content = Testers.IncludesExpression(diags_fail_exp, "Have a look.") - else: - tr.Processes.Default.ReturnCode = 0 - - if self.gold_file: - tr.Processes.Default.Streams.stderr = self.gold_file - - if self._remap_filename != "" and self._remap_content != "": - self._ts.Disk.MakeConfigFile(self._remap_filename).update(self._remap_content) - self._ts.Disk.remap_config.AddLine( - f'map http://www.testexample.com/ http://127.0.0.1:{self._server.Variables.Port} @plugin=conf_remap.so @pparam={self._remap_filename}' - ) - if Condition.CurlUsingUnixDomainSocket(): - tr.MakeCurlCommand( - '-H "Host: www.testexample.com" "http://127.0.0.1:{0}/test" --verbose'.format(self._ts.Variables.port), ts=self._ts) - else: - tr.MakeCurlCommand( - '--proxy 127.0.0.1:{0} "http://www.testexample.com/test" -H "Host: www.testexample.com" --verbose'.format( - self._ts.Variables.port), - ts=self._ts) - conf_remap_yaml_load_test.client_counter += 1 - - -gold_file = "gold/200OK_test.gold" -if Condition.CurlUsingUnixDomainSocket(): - gold_file = "gold/200OK_test_uds.gold" - -test0 = conf_remap_yaml_load_test( - "Test success", - gold_file=gold_file, - remap_filename="testexample_remap.yaml", - remap_content=''' - records: - url_remap: - pristine_host_hdr: 1 - ''') -test0.run() - -test1 = conf_remap_yaml_load_test( - "Test mismatch type", - remap_filename="mismatch_field_type_remap.yaml", - remap_content=''' - records: - url_remap: - pristine_host_hdr: !!float '1' - ''') -test1.run(diags_fail_exp="'proxy.config.url_remap.pristine_host_hdr' variable type mismatch", ts_retcode=33) - -test2 = conf_remap_yaml_load_test( - "Test invalid variable", - remap_filename="invalid1_field_type_remap.yaml", - remap_content=''' - records: - plugin: - dynamic_reload_mode: 1 - ''') - -test2.run( - diags_fail_exp="'proxy.config.plugin.dynamic_reload_mode' is not a configuration variable or cannot be overridden", - ts_retcode=33) - -# We let the conf_remap parse two fields, only one is valid, we expect ATS to start and the invalid fields ignored. -test3 = conf_remap_yaml_load_test( - "Test success", - gold_file=gold_file, - remap_filename="testexample2_remap.yaml", - remap_content=''' - records: - plugin: - dynamic_reload_mode: 1 - - url_remap: - pristine_host_hdr: 1 - ''') -test3.run(diags_fail_exp="'proxy.config.plugin.dynamic_reload_mode' is not a configuration variable or cannot be overridden") - -# Check null values -test4 = conf_remap_yaml_load_test( - "Test success - with NULL variable", - gold_file=gold_file, - remap_filename="testexample_remap.yaml", - remap_content=''' - records: - url_remap: - pristine_host_hdr: 1 - hostdb: - ip_resolve: "NULL" # We want to make sure this gets read as it should. "NULL" could be the value of this field. - ''') -test4.run() diff --git a/tests/gold_tests/remap/conf_remap_float.test.py b/tests/gold_tests/remap/conf_remap_float.test.py deleted file mode 100644 index 300ad964810..00000000000 --- a/tests/gold_tests/remap/conf_remap_float.test.py +++ /dev/null @@ -1,42 +0,0 @@ -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -Test.Summary = ''' -Test command: traffic_ctl config describe proxy.config.http.background_fill_completed_threshold (YTSATS-3309) -''' -Test.testName = 'Float in conf_remap Config Test' - -ts = Test.MakeATSProcess("ts") - -ts.Disk.MakeConfigFile('conf_remap.yaml').update(''' -records: - http: - background_fill_completed_threshold: !!float '0.5' -''') - -ts.Disk.remap_config.AddLine( - f"map http://cdn.example.com/ http://origin.example.com/ @plugin=conf_remap.so @pparam={Test.RunDirectory}/ts/config/conf_remap.yaml" -) - -tr = Test.AddTestRun("traffic_ctl command") -tr.Env = ts.Env -tr.TimeOut = 5 -tr.StillRunningAfter = ts - -p = tr.Processes.Default -p.Command = f"traffic_ctl config describe proxy.config.http.background_fill_completed_threshold" -p.ReturnCode = 0 -p.StartBefore(Test.Processes.ts) diff --git a/tests/gold_tests/remap/map_with_recv_port.test.py b/tests/gold_tests/remap/map_with_recv_port.test.py deleted file mode 100644 index fe5013a412c..00000000000 --- a/tests/gold_tests/remap/map_with_recv_port.test.py +++ /dev/null @@ -1,82 +0,0 @@ -''' -Verify correct behavior of regex_map in remap.config. -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -Test.Summary = ''' -Verify correct behavior of map_with_recv_port in remap.config. -''' - -Test.ContinueOnFail = True -ts = Test.MakeATSProcess("ts") -server = Test.MakeOriginServer("server") -dns = Test.MakeDNServer("dns", default='127.0.0.1') - -Test.testName = "" -request_header_ip = {"headers": "GET /ip HTTP/1.1\r\nHost: origin.example.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -response_header_ip = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": "ip"} -request_header_unix = { - "headers": "GET /unix HTTP/1.1\r\nHost: origin.example.com\r\n\r\n", - "timestamp": "1469733493.993", - "body": "" -} -response_header_unix = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": "unix"} -request_header_error = { - "headers": "GET /error HTTP/1.1\r\nHost: origin.example.com\r\n\r\n", - "timestamp": "1469733493.993", - "body": "" -} -response_header_error = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": "error"} -server.addResponse("sessionfile.log", request_header_ip, response_header_ip) -server.addResponse("sessionfile.log", request_header_unix, response_header_unix) -server.addResponse("sessionfile.log", request_header_error, response_header_error) - -ts.Disk.records_config.update( - { - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'http|dns', - 'proxy.config.dns.nameservers': '127.0.0.1:{0}'.format(dns.Variables.Port), - 'proxy.config.dns.resolv_conf': 'NULL' - }) - -# This map rule should not match -ts.Disk.remap_config.AddLine( - r'map ' - r'http://test.example.com ' - r'http://origin.example.com:{}/error'.format(server.Variables.Port)) -# Rule for IP interface -ts.Disk.remap_config.AddLine( - r'map_with_recv_port ' - r'http://test.example.com:{}/ ' - r'http://origin.example.com:{}/ip'.format(ts.Variables.port, server.Variables.Port)) -# Rule for Unix Domain Socket -ts.Disk.remap_config.AddLine( - r'map_with_recv_port ' - r'http+unix://test.example.com ' - r'http://origin.example.com:{}/unix'.format(server.Variables.Port)) - -tr = Test.AddTestRun() -tr.MakeCurlCommand('-H"Host: test.example.com" http://127.0.0.1:{0}/ --verbose'.format(ts.Variables.port), ts) -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.StartBefore(server) -tr.Processes.Default.StartBefore(dns) -tr.Processes.Default.StartBefore(Test.Processes.ts) -if Condition.CurlUsingUnixDomainSocket(): - tr.Processes.Default.Streams.stderr = "gold/map-with-recv-port-unix.gold" -else: - tr.Processes.Default.Streams.stderr = "gold/map-with-recv-port-ip.gold" -tr.StillRunningAfter = server diff --git a/tests/gold_tests/remap/regex_map.test.py b/tests/gold_tests/remap/regex_map.test.py deleted file mode 100644 index 64b70bbb159..00000000000 --- a/tests/gold_tests/remap/regex_map.test.py +++ /dev/null @@ -1,59 +0,0 @@ -''' -Verify correct behavior of regex_map in remap.config. -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -Test.Summary = ''' -Verify correct behavior of regex_map in remap.config. -''' - -Test.ContinueOnFail = True -ts = Test.MakeATSProcess("ts") -server = Test.MakeOriginServer("server") -dns = Test.MakeDNServer("dns", default='127.0.0.1') - -Test.testName = "" -request_header = {"headers": "GET / HTTP/1.1\r\nHost: zero.one.two.three.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -server.addResponse("sessionfile.log", request_header, response_header) - -ts.Disk.records_config.update( - { - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'http.*|dns|conf_remap', - 'proxy.config.http.referer_filter': 1, - 'proxy.config.dns.nameservers': '127.0.0.1:{0}'.format(dns.Variables.Port), - 'proxy.config.dns.resolv_conf': 'NULL' - }) - -ts.Disk.remap_config.AddLine( - r'regex_map ' - r'http://(.*)?one\.two\.three\.com/ ' - r'http://$1reactivate.four.five.six.com:{}/'.format(server.Variables.Port)) -ts.Disk.remap_config.AddLine( - r'regex_map ' - r'https://\b(?!(.*one|two|three|four|five|six)).+\b\.seven\.eight\.nine\.com/blah12345.html ' - r'https://www.example.com:{}/one/two/three/blah12345.html'.format(server.Variables.Port)) - -tr = Test.AddTestRun() -tr.MakeCurlCommand('-H"Host: zero.one.two.three.com" http://127.0.0.1:{0}/ --verbose'.format(ts.Variables.port), ts=ts) -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.StartBefore(server) -tr.Processes.Default.StartBefore(dns) -tr.Processes.Default.StartBefore(Test.Processes.ts) -tr.Processes.Default.Streams.stderr = "gold/remap-zero-200.gold" -tr.StillRunningAfter = server diff --git a/tests/gold_tests/remap/remap_acl.test.py b/tests/gold_tests/remap/remap_acl.test.py deleted file mode 100644 index 33fef92e3ce..00000000000 --- a/tests/gold_tests/remap/remap_acl.test.py +++ /dev/null @@ -1,628 +0,0 @@ -''' -Verify remap.config acl behavior. -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# # http://www.apache.org/licenses/LICENSE-2.0 # -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import atexit -import os -import io -import re -import inspect -import shutil -import tempfile -from yaml import load, dump -from yaml import CLoader as Loader -from typing import List, Tuple - -from ports import get_port - -# Temporary directory for generated replay files. Cleaned up at process exit -# so the files don't accumulate in /tmp across test runs. -_replay_tmpdir = tempfile.mkdtemp(prefix='autest_replay_') -atexit.register(shutil.rmtree, _replay_tmpdir, True) - -Test.Summary = ''' -Verify remap.config acl behavior. -''' - - -def update_config_file(path1: str, content1: str, path2: str, content2: str) -> None: - """Update two config files. - - This is used for some of the updates to the config files between test runs. - - :param path1: The path to the first config file. - :param content1: The content to write to the first config file. - :param path2: The path to the second config file. - :param content2: The content to write to the second config file. - """ - with open(path1, 'w') as f: - f.write(content1 + '\n') - with open(path2, 'w') as f: - f.write(content2 + '\n') - - -class Test_remap_acl: - """Configure a test to verify remap.config acl behavior.""" - - _ts: 'TestProcess' = None - _ts_reload_counter: int = 0 - _ts_is_started: bool = False - - _server_counter: int = 0 - _client_counter: int = 0 - - def __init__( - self, name: str, replay_file: str, ip_allow_content: str, deactivate_ip_allow: bool, acl_behavior_policy: int, - acl_configuration: str, named_acls: List[Tuple[str, str]], expected_responses: List[int], proxy_protocol: bool): - """Initialize the test. - - :param name: The name of the test. - :param replay_file: The replay file to be used. - :param ip_allow_content: The ip_allow configuration to be used. - :param deactivate_ip_allow: Whether to deactivate the ip_allow filter. - :param acl_configuration: The ACL configuration to be used. - :param named_acls: The set of named ACLs to configure and use. - :param expect_responses: The in-order expected responses from the proxy. - """ - self._replay_file = replay_file - self._ip_allow_lines = ip_allow_content.split("\n") - self._deactivate_ip_allow = deactivate_ip_allow - self._acl_behavior_policy = acl_behavior_policy - self._acl_configuration = acl_configuration - self._named_acls = named_acls - self._expected_responses = expected_responses - - # Usually we configure the server first and use the server port to - # configure ATS to remap to it. In this case, though, we want a - # long-lived ATS process that spans TestRuns. So we let ATS choose an - # arbitrary availble server port, and then tell the TestRun-specific - # server to use that port. - server_port = self._configure_traffic_server() - tr = Test.AddTestRun(name) - self._configure_server(tr, server_port) - self._configure_client(tr, proxy_protocol) - - def _configure_server(self, tr: 'TestRun', server_port: int) -> None: - """Configure the server. - """ - name = f"server-{Test_remap_acl._server_counter}" - server = tr.AddVerifierServerProcess(name, self._replay_file, http_ports=[server_port]) - Test_remap_acl._server_counter += 1 - self._server = server - - def _configure_traffic_server(self) -> int: - """Configure Traffic Server. - - :return: The listening port that the server should use. - """ - - call_reload: bool = False - if Test_remap_acl._ts is not None: - ts = Test_remap_acl._ts - call_reload = True - else: - ts = Test.MakeATSProcess("ts", enable_cache=False, enable_proxy_protocol=True, enable_uds=False) - Test_remap_acl._ts = ts - self._ts = ts - port_name = f'ServerPort-{Test_remap_acl._ts_reload_counter}' - server_port: int = get_port(ts, port_name) - - remap_config_lines = [] - if self._deactivate_ip_allow: - remap_config_lines.append('.deactivatefilter ip_allow') - - # First, define the name ACLs (filters). - for name, definition in self._named_acls: - remap_config_lines.append(f'.definefilter {name} {definition}') - # Now activate them. - for name, _ in self._named_acls: - remap_config_lines.append(f'.activatefilter {name}') - - remap_config_lines.append(f'map / http://127.0.0.1:{server_port} {self._acl_configuration}') - - if call_reload: - # - # Update the ATS configuration. - # - tr = Test.AddTestRun("Change the ATS configuration") - p = tr.Processes.Default - p.Command = ( - f'traffic_ctl config set proxy.config.http.connect_ports {server_port} && ' - f'traffic_ctl config set proxy.config.url_remap.acl_behavior_policy {self._acl_behavior_policy}') - - p.Env = ts.Env - tr.StillRunningAfter = ts - - remap_cfg_path = os.path.join(ts.Variables.CONFIGDIR, 'remap.config') - ip_allow_path = os.path.join(ts.Variables.CONFIGDIR, 'ip_allow.yaml') - p.Setup.Lambda( - lambda: update_config_file( - remap_cfg_path, '\n'.join(remap_config_lines), ip_allow_path, '\n'.join(self._ip_allow_lines))) - - # - # Kick off the ATS config reload and await completion. - # - Test_remap_acl._ts_reload_counter += 1 - tr = Test.AddConfigReload(ts, expect_tasks=["remap.config"], description="Reload the ATS configuration") - tr.StillRunningAfter = ts - - else: - record_config = { - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'http|url|remap|ip_allow|proxyprotocol', - 'proxy.config.http.push_method_enabled': 1, - 'proxy.config.http.connect_ports': server_port, - 'proxy.config.url_remap.acl_behavior_policy': self._acl_behavior_policy, - 'proxy.config.acl.subjects': 'PROXY,PEER', - } - - ts.Disk.records_config.update(record_config) - ts.Disk.remap_config.AddLines(remap_config_lines) - ts.Disk.ip_allow_yaml.AddLines(self._ip_allow_lines) - - return server_port - - def _configure_client(self, tr: 'TestRun', proxy_protocol: bool) -> None: - """Run the test. - - :param tr: The TestRun object to associate the client process with. - """ - - name = f"client-{Test_remap_acl._client_counter}" - ts = Test_remap_acl._ts - port = ts.Variables.port if proxy_protocol == False else ts.Variables.proxy_protocol_port - p = tr.AddVerifierClientProcess(name, self._replay_file, http_ports=[port]) - Test_remap_acl._client_counter += 1 - p.StartBefore(self._server) - if not Test_remap_acl._ts_is_started: - p.StartBefore(ts) - Test_remap_acl._ts_is_started = True - - if self._expected_responses == [None, None]: - # If there are no expected responses, expect the Warning about the rejected ip. - self._ts.Disk.diags_log.Content += Testers.ContainsExpression( - "client '127.0.0.1' prohibited by ip-allow policy", "Verify the client rejection warning message.") - - # Also, the client will complain about the broken connections. - p.ReturnCode = 1 - - else: - codes = [str(code) for code in self._expected_responses] - p.Streams.stdout += Testers.ContainsExpression( - '.*'.join(codes), "Verifying the expected order of responses", reflags=re.DOTALL | re.MULTILINE) - - -class Test_old_action: - _ts_counter: int = 0 - - def __init__(self, name: str, acl_filter: str, ip_allow_content: str) -> None: - '''Test that ATS fails with a FATAL message if an old action is used with modern ACL filter policy. - - :param name: The name of the test run. - :param acl_filter: The ACL filter to use. - :param ip_allow_content: The ip_allow configuration to use. - ''' - - tr = Test.AddTestRun(name) - ts = self._configure_traffic_server(tr, acl_filter, ip_allow_content) - - def _configure_traffic_server(self, tr: 'TestRun', acl_filter: str, ip_allow_content: str) -> 'Process': - '''Configure Traffic Server process - - :param tr: The TestRun object to associate the Traffic Server process with. - :param acl_filter: The ACL filter to configure in remap.config. - :param ip_allow_content: The ip_allow configuration to use. - :return: The Traffic Server process. - ''' - name = f"ts-old-action-{Test_old_action._ts_counter}" - Test_old_action._ts_counter += 1 - ts = tr.MakeATSProcess(name, enable_uds=False) - self._ts = ts - - ts.Disk.records_config.update( - { - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'http|url|remap|ip_allow', - 'proxy.config.url_remap.acl_behavior_policy': 1, - }) - - ts.Disk.remap_config.AddLine(f'map / http://127.0.0.1:8080 {acl_filter}') - if ip_allow_content: - ts.Disk.ip_allow_yaml.AddLines(ip_allow_content.split("\n")) - - if acl_filter != '': - expected_error = '"allow" and "deny" are no longer valid.' - else: - expected_error = 'Legacy action name of' - - # We have to wait upon TS to emit the expected log message, but it cannot be - # the ts Ready criteria because autest might detect the process going away - # before it detects the log message. So we add a separate process that waits - # upon the log message. - watcher = tr.Processes.Process("watcher") - watcher.Command = "sleep 10" - watcher.Ready = When.FileContains(ts.Disk.diags_log.Name, expected_error) - watcher.StartBefore(ts) - - tr.Processes.Default.Command = 'printf "Fatal Shutdown Test"' - tr.Processes.Default.ReturnCode = 0 - tr.Processes.Default.StartBefore(watcher) - - tr.Timeout = 5 - ts.ReturnCode = Any(33, 70) - ts.Ready = 0 - ts.Disk.diags_log.Content = Testers.IncludesExpression(expected_error, 'ATS should fatal with the old actions.') - - return ts - - -IP_ALLOW_OLD_ACTION = f''' -ip_categories: - - name: ACME_LOCAL - ip_addrs: 127.0.0.1 - - name: ACME_EXTERNAL - ip_addrs: 5.6.7.8 - -ip_allow: - - apply: in - ip_addrs: 0/0 - action: allow - methods: - - GET -''' - -IP_ALLOW_CONTENT = f''' -ip_categories: - - name: ACME_LOCAL - ip_addrs: 127.0.0.1 - - name: ACME_EXTERNAL - ip_addrs: 5.6.7.8 - -ip_allow: - - apply: in - ip_addrs: 0/0 - action: set_allow - methods: - - GET -''' - -Test_old_action("Verify allow is reject in modern policy", "@action=allow @method=GET", IP_ALLOW_CONTENT) -Test_old_action("Verify deny is reject in modern policy", "@action=deny @method=GET", IP_ALLOW_CONTENT) -Test_old_action("Verify deny is reject in modern policy", "", IP_ALLOW_OLD_ACTION) - -test_ip_allow_optional_methods = Test_remap_acl( - "Verify non-allowed methods are blocked.", - replay_file='remap_acl_get_post_allowed.replay.yaml', - ip_allow_content=IP_ALLOW_CONTENT, - deactivate_ip_allow=False, - acl_behavior_policy=1, - acl_configuration='@action=set_allow @src_ip=127.0.0.1 @method=GET @method=POST', - named_acls=[], - expected_responses=[200, 200, 403, 403, 403], - proxy_protocol=False) - -test_ip_allow_optional_methods_pp = Test_remap_acl( - "Verify non-allowed methods are blocked (PP).", - replay_file='remap_acl_get_post_allowed_pp.replay.yaml', - ip_allow_content=IP_ALLOW_CONTENT, - deactivate_ip_allow=False, - acl_behavior_policy=1, - acl_configuration='@action=set_allow @src_ip=1.2.3.4 @method=GET @method=POST', - named_acls=[], - expected_responses=[200, 200, 403, 403, 403], - proxy_protocol=True) - -test_ip_allow_optional_methods = Test_remap_acl( - "Verify add_allow adds an allowed method.", - replay_file='remap_acl_get_post_allowed.replay.yaml', - ip_allow_content=IP_ALLOW_CONTENT, - deactivate_ip_allow=False, - acl_behavior_policy=1, - acl_configuration='@action=add_allow @src_ip=127.0.0.1 @method=POST', - named_acls=[], - expected_responses=[200, 200, 403, 403, 403], - proxy_protocol=False) - -test_ip_allow_optional_methods = Test_remap_acl( - "Verify add_allow adds allowed methods.", - replay_file='remap_acl_get_post_allowed.replay.yaml', - ip_allow_content=IP_ALLOW_CONTENT, - deactivate_ip_allow=False, - acl_behavior_policy=1, - acl_configuration='@action=add_allow @src_ip=127.0.0.1 @method=GET @method=POST', - named_acls=[], - expected_responses=[200, 200, 403, 403, 403], - proxy_protocol=False) - -test_ip_allow_optional_methods = Test_remap_acl( - "Verify if no ACLs match, ip_allow.yaml is used.", - replay_file='remap_acl_get_allowed.replay.yaml', - ip_allow_content=IP_ALLOW_CONTENT, - deactivate_ip_allow=False, - acl_behavior_policy=1, - acl_configuration='@action=set_allow @src_ip=1.2.3.4 @method=GET @method=POST', - named_acls=[], - expected_responses=[200, 403, 403, 403, 403], - proxy_protocol=False) - -test_ip_allow_optional_methods = Test_remap_acl( - "Verify @src_ip=all works.", - replay_file='remap_acl_get_post_allowed.replay.yaml', - ip_allow_content=IP_ALLOW_CONTENT, - deactivate_ip_allow=False, - acl_behavior_policy=1, - acl_configuration='@action=set_allow @src_ip=all @method=GET @method=POST', - named_acls=[], - expected_responses=[200, 200, 403, 403, 403], - proxy_protocol=False) - -test_ip_allow_optional_methods = Test_remap_acl( - "Verify @src_ip_category works.", - replay_file='remap_acl_get_post_allowed.replay.yaml', - ip_allow_content=IP_ALLOW_CONTENT, - deactivate_ip_allow=False, - acl_behavior_policy=1, - acl_configuration='@action=set_allow @src_ip_category=ACME_LOCAL @method=GET @method=POST', - named_acls=[], - expected_responses=[200, 200, 403, 403, 403], - proxy_protocol=False) - -test_ip_allow_optional_methods = Test_remap_acl( - "Verify no @src_ip implies all IP addresses.", - replay_file='remap_acl_get_post_allowed.replay.yaml', - ip_allow_content=IP_ALLOW_CONTENT, - deactivate_ip_allow=False, - acl_behavior_policy=1, - acl_configuration='@action=set_allow @method=GET @method=POST', - named_acls=[], - expected_responses=[200, 200, 403, 403, 403], - proxy_protocol=False) - -test_ip_allow_optional_methods = Test_remap_acl( - "Verify denied methods are blocked.", - replay_file='remap_acl_get_post_denied.replay.yaml', - ip_allow_content=IP_ALLOW_CONTENT, - deactivate_ip_allow=False, - acl_behavior_policy=1, - acl_configuration='@action=set_deny @src_ip=127.0.0.1 @method=GET @method=POST', - named_acls=[], - expected_responses=[403, 403, 200, 200, 400], - proxy_protocol=False) - -test_ip_allow_optional_methods = Test_remap_acl( - "Verify add_deny adds blocked methods.", - replay_file='remap_acl_all_denied.replay.yaml', - ip_allow_content=IP_ALLOW_CONTENT, - deactivate_ip_allow=False, - acl_behavior_policy=1, - acl_configuration='@action=add_deny @src_ip=127.0.0.1 @method=GET', - named_acls=[], - expected_responses=[403, 403, 403, 403, 403], - proxy_protocol=False) - -test_ip_allow_optional_methods = Test_remap_acl( - "Verify a default deny filter rule works.", - replay_file='remap_acl_all_denied.replay.yaml', - ip_allow_content=IP_ALLOW_CONTENT, - deactivate_ip_allow=False, - acl_behavior_policy=1, - acl_configuration='@action=set_allow @src_ip=1.2.3.4 @method=GET @method=POST', - named_acls=[('deny', '@action=set_deny')], - expected_responses=[403, 403, 403, 403, 403], - proxy_protocol=False) - -test_ip_allow_optional_methods = Test_remap_acl( - "Verify inverting @src_ip works.", - replay_file='remap_acl_all_denied.replay.yaml', - ip_allow_content=IP_ALLOW_CONTENT, - deactivate_ip_allow=False, - acl_behavior_policy=1, - acl_configuration='@action=set_allow @src_ip=~127.0.0.1 @method=GET @method=POST', - named_acls=[('deny', '@action=set_deny')], - expected_responses=[403, 403, 403, 403, 403], - proxy_protocol=False) - -test_ip_allow_optional_methods = Test_remap_acl( - "Verify inverting @src_ip works with the rule matching.", - replay_file='remap_acl_get_post_allowed.replay.yaml', - ip_allow_content=IP_ALLOW_CONTENT, - deactivate_ip_allow=False, - acl_behavior_policy=1, - acl_configuration='@action=set_allow @src_ip=~3.4.5.6 @method=GET @method=POST', - named_acls=[('deny', '@action=set_deny')], - expected_responses=[200, 200, 403, 403, 403], - proxy_protocol=False) - -test_ip_allow_optional_methods = Test_remap_acl( - "Verify inverting @src_ip_category works.", - replay_file='remap_acl_all_denied.replay.yaml', - ip_allow_content=IP_ALLOW_CONTENT, - deactivate_ip_allow=False, - acl_behavior_policy=1, - acl_configuration='@action=set_allow @src_ip_category=~ACME_LOCAL @method=GET @method=POST', - named_acls=[('deny', '@action=set_deny')], - expected_responses=[403, 403, 403, 403, 403], - proxy_protocol=False) - -test_ip_allow_optional_methods = Test_remap_acl( - "Verify inverting @src_ip_category works with the rule matching.", - replay_file='remap_acl_get_post_allowed.replay.yaml', - ip_allow_content=IP_ALLOW_CONTENT, - deactivate_ip_allow=False, - acl_behavior_policy=1, - acl_configuration='@action=set_allow @src_ip_category=~ACME_EXTERNAL @method=GET @method=POST', - named_acls=[('deny', '@action=set_deny')], - expected_responses=[200, 200, 403, 403, 403], - proxy_protocol=False) - -test_ip_allow_optional_methods = Test_remap_acl( - "Verify @src_ip and @src_ip_category AND together.", - replay_file='remap_acl_all_denied.replay.yaml', - ip_allow_content=IP_ALLOW_CONTENT, - deactivate_ip_allow=False, - acl_behavior_policy=1, - # The rule will not match because, while @src_ip matches, @src_ip_category does not. - acl_configuration='@action=set_allow @src_ip=127.0.0.1 @src_ip_category=ACME_EXTERNAL @method=GET @method=POST', - # Therefore, this named deny filter will block. - named_acls=[('deny', '@action=set_deny')], - expected_responses=[403, 403, 403, 403, 403], - proxy_protocol=False) - -test_ip_allow_optional_methods = Test_remap_acl( - "Verify defined in-line ACLS are evaluated before named ones.", - replay_file='remap_acl_get_post_allowed.replay.yaml', - ip_allow_content=IP_ALLOW_CONTENT, - deactivate_ip_allow=False, - acl_behavior_policy=1, - acl_configuration='@action=set_allow @src_ip=127.0.0.1 @method=GET @method=POST', - named_acls=[('deny', '@action=set_deny')], - expected_responses=[200, 200, 403, 403, 403], - proxy_protocol=False) - -test_ip_allow_optional_methods = Test_remap_acl( - "Verify remap.config line overrides ip_allow rule.", - replay_file='remap_acl_get_post_allowed.replay.yaml', - ip_allow_content=IP_ALLOW_CONTENT, - deactivate_ip_allow=False, - acl_behavior_policy=1, - acl_configuration='@action=set_allow @src_ip=127.0.0.1 @method=GET @method=POST', - named_acls=[], - expected_responses=[200, 200, 403, 403, 403], - proxy_protocol=False) - -test_ip_allow_optional_methods = Test_remap_acl( - "Verify we can deactivate the ip_allow filter.", - replay_file='remap_acl_all_allowed.replay.yaml', - ip_allow_content=IP_ALLOW_CONTENT, - deactivate_ip_allow=True, - acl_behavior_policy=1, - # This won't match, so nothing will match since ip_allow.yaml is off. - acl_configuration='@action=set_allow @src_ip=1.2.3.4 @method=GET @method=POST', - named_acls=[], - # Nothing will block the request since ip_allow.yaml is off. - expected_responses=[200, 200, 200, 200, 400], - proxy_protocol=False) - -test_ip_allow_optional_methods = Test_remap_acl( - "Verify in_ip matches on IP as expected.", - replay_file='remap_acl_get_post_allowed.replay.yaml', - ip_allow_content=IP_ALLOW_CONTENT, - deactivate_ip_allow=False, - acl_behavior_policy=1, - acl_configuration='@action=set_allow @in_ip=127.0.0.1 @method=GET @method=POST', - named_acls=[], - expected_responses=[200, 200, 403, 403, 403], - proxy_protocol=False) - -test_ip_allow_optional_methods = Test_remap_acl( - "Verify in_ip rules do not match on other IPs.", - replay_file='remap_acl_get_allowed.replay.yaml', - ip_allow_content=IP_ALLOW_CONTENT, - deactivate_ip_allow=False, - acl_behavior_policy=1, - acl_configuration='@action=set_allow @in_ip=3.4.5.6 @method=GET @method=POST', - named_acls=[], - expected_responses=[200, 403, 403, 403, 403], - proxy_protocol=False) - -test_named_acl_deny = Test_remap_acl( - "Verify a named ACL is applied if an in-line ACL is absent.", - replay_file='deny_head_post.replay.yaml', - ip_allow_content=IP_ALLOW_CONTENT, - deactivate_ip_allow=False, - acl_behavior_policy=1, - acl_configuration='', - named_acls=[('deny', '@action=set_deny @method=HEAD @method=POST')], - expected_responses=[200, 403, 403, 403], - proxy_protocol=False) - - -def replay_proxy_response(filename, replay_file, get_proxy_response, post_proxy_response): - """ - replay_proxy_response writes the given replay file (which expects a single GET & POST client-request) - with the given proxy_response value. This is only used to support the tests in the combination table. - """ - - current_dir = os.path.dirname(inspect.getfile(inspect.currentframe())) - path = os.path.join(current_dir, filename) - data = None - with open(path) as f: - data = load(f, Loader=Loader) - for session in data["sessions"]: - for transaction in session["transactions"]: - method = transaction["client-request"]["method"] - if method == "GET": - transaction["proxy-response"]["status"] = 403 if get_proxy_response == None else get_proxy_response - elif method == "POST": - transaction["proxy-response"]["status"] = 403 if post_proxy_response == None else post_proxy_response - else: - raise Exception("Expected to find GET or POST request, found %s", method) - with open(replay_file, "w") as f: - f.write(dump(data)) - - -from deactivate_ip_allow import all_deactivate_ip_allow_tests -from all_acl_combinations import all_acl_combination_tests -""" -Test all acl combinations -""" -for idx, test in enumerate(all_acl_combination_tests): - (fd, replay_file_name) = tempfile.mkstemp(suffix="acl_table_test_{}.replay".format(idx), dir=_replay_tmpdir) - os.close(fd) - replay_proxy_response( - "base.replay.yaml", - replay_file_name, - test["GET response"], - test["POST response"], - ) - Test_remap_acl( - "allcombo-{0} {1} {2} {3}".format(idx, test["inline"], test["named_acl"], test["ip_allow"]), - replay_file=replay_file_name, - ip_allow_content=test["ip_allow"], - deactivate_ip_allow=False, - acl_behavior_policy=0 if test["policy"] == "legacy" else 1, - acl_configuration=test["inline"], - named_acls=[("acl", test["named_acl"])] if test["named_acl"] != "" else [], - expected_responses=[test["GET response"], test["POST response"]], - proxy_protocol=False, - ) -""" -Test all ACL combinations -""" -for idx, test in enumerate(all_deactivate_ip_allow_tests): - try: - test["deactivate_ip_allow"] - except: - print(test) - (fd, replay_file_name) = tempfile.mkstemp(suffix="deactivate_ip_allow_table_test_{}.replay".format(idx), dir=_replay_tmpdir) - os.close(fd) - replay_proxy_response( - "base.replay.yaml", - replay_file_name, - test["GET response"], - test["POST response"], - ) - Test_remap_acl( - "ipallow-{0} {1} {2} {3}".format(idx, test["inline"], test["named_acl"], test["ip_allow"]), - replay_file=replay_file_name, - ip_allow_content=test["ip_allow"], - deactivate_ip_allow=test["deactivate_ip_allow"], - acl_behavior_policy=0 if test["policy"] == "legacy" else 1, - acl_configuration=test["inline"], - named_acls=[("acl", test["named_acl"])] if test["named_acl"] != "" else [], - expected_responses=[test["GET response"], test["POST response"]], - proxy_protocol=False, - ) diff --git a/tests/gold_tests/remap/remap_http.test.py b/tests/gold_tests/remap/remap_http.test.py deleted file mode 100644 index 9129c4fcc7b..00000000000 --- a/tests/gold_tests/remap/remap_http.test.py +++ /dev/null @@ -1,158 +0,0 @@ -''' -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -Test.Summary = ''' -Test a basic remap of a http connection -''' - -Test.ContinueOnFail = True -# Define default ATS -ts = Test.MakeATSProcess("ts") -server = Test.MakeOriginServer("server") -server2 = Test.MakeOriginServer("server2", lookup_key="{%Host}{PATH}") -dns = Test.MakeDNServer("dns") - -Test.testName = "" -request_header = {"headers": "GET / HTTP/1.1\r\nHost: www.example.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -# expected response from the origin server -response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} - -request_header2 = {"headers": "GET /test HTTP/1.1\r\nHost: www.testexample.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -# expected response from the origin server -response_header2 = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} - -# add response to the server dictionary -server.addResponse("sessionfile.log", request_header, response_header) -server2.addResponse("sessionfile.log", request_header2, response_header2) -ts.Disk.records_config.update( - { - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'http.*|dns|conf_remap', - 'proxy.config.http.referer_filter': 1, - 'proxy.config.dns.nameservers': '127.0.0.1:{0}'.format(dns.Variables.Port), - 'proxy.config.dns.resolv_conf': 'NULL' - }) - -ts.Disk.remap_config.AddLine('map http://www.example.com http://127.0.0.1:{0}'.format(server.Variables.Port)) -ts.Disk.remap_config.AddLine( - 'map_with_recv_port http://www.example2.com:{0} http://127.0.0.1:{1}'.format(ts.Variables.port, server.Variables.Port)) -ts.Disk.remap_config.AddLine('map http://www.example.com:8080 http://127.0.0.1:{0}'.format(server.Variables.Port)) -ts.Disk.remap_config.AddLine('redirect http://test3.com http://httpbin.org'.format(server.Variables.Port)) -ts.Disk.remap_config.AddLine( - 'map_with_referer http://test4.com http://127.0.0.1:{0} http://httpbin.org (.*[.])?persia[.]com'.format(server.Variables.Port)) -ts.Disk.remap_config.AddLine('map http://testDNS.com http://audrey.hepburn.com:{0}'.format(server.Variables.Port)) - -ts.Disk.remap_config.AddLine( - 'map http://www.testexample.com http://127.0.0.1:{0} @plugin=conf_remap.so @pparam=proxy.config.url_remap.pristine_host_hdr=1' - .format(server2.Variables.Port)) - -dns.addRecords(records={"audrey.hepburn.com.": ["127.0.0.1"]}) -dns.addRecords(records={"whatever.com.": ["127.0.0.1"]}) - -# call localhost straight -tr = Test.AddTestRun() -tr.MakeCurlCommand('"http://127.0.0.1:{0}/" --verbose'.format(ts.Variables.port), ts=ts) -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.StartBefore(server) -tr.Processes.Default.StartBefore(dns) -tr.Processes.Default.StartBefore(Test.Processes.ts) -tr.Processes.Default.Streams.stderr = "gold/remap-hitATS-404.gold" -tr.StillRunningAfter = server - -# www.example.com host -tr = Test.AddTestRun() -tr.MakeCurlCommand( - '--proxy 127.0.0.1:{0} "http://www.example.com" -H "Proxy-Connection: keep-alive" --verbose'.format(ts.Variables.port), ts=ts) -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Streams.stderr = "gold/remap-200.gold" - -# www.example2.com host (match on receive port) -# map_with_recv_port doesn't work with UDS -if not Condition.CurlUsingUnixDomainSocket(): - tr = Test.AddTestRun() - tr.MakeCurlCommand( - '--proxy 127.0.0.1:{0} "http://www.example2.com" -H "Proxy-Connection: keep-alive" --verbose'.format(ts.Variables.port), - ts=ts) - tr.Processes.Default.ReturnCode = 0 - tr.Processes.Default.Streams.stderr = "gold/remap2-200.gold" - -# www.example.com:80 host -tr = Test.AddTestRun() -tr.MakeCurlCommand( - ' --proxy 127.0.0.1:{0} "http://www.example.com:80/" -H "Proxy-Connection: keep-alive" --verbose'.format(ts.Variables.port), - ts=ts) -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Streams.stderr = "gold/remap-200.gold" - -# www.example.com:8080 host -tr = Test.AddTestRun() -tr.MakeCurlCommand( - ' --proxy 127.0.0.1:{0} "http://www.example.com:8080" -H "Proxy-Connection: keep-alive" --verbose'.format(ts.Variables.port), - ts=ts) -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Streams.stderr = "gold/remap-200.gold" - -# no rule for this -tr = Test.AddTestRun() -tr.MakeCurlCommand( - ' --proxy 127.0.0.1:{0} "http://www.test.com/" -H "Proxy-Connection: keep-alive" --verbose'.format(ts.Variables.port), ts=ts) -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Streams.stderr = "gold/remap-404.gold" - -# redirect result -tr = Test.AddTestRun() -tr.MakeCurlCommand(' --proxy 127.0.0.1:{0} "http://test3.com" --verbose'.format(ts.Variables.port), ts=ts) -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Streams.stderr = "gold/remap-redirect.gold" - -# referer hit -tr = Test.AddTestRun() -tr.MakeCurlCommand( - ' --proxy 127.0.0.1:{0} "http://test4.com" --header "Referer: persia.com" --verbose'.format(ts.Variables.port), ts=ts) -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Streams.stderr = "gold/remap-referer-hit.gold" - -# referer miss -tr = Test.AddTestRun() -tr.MakeCurlCommand( - ' --proxy 127.0.0.1:{0} "http://test4.com" --header "Referer: monkey.com" --verbose'.format(ts.Variables.port), ts=ts) -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Streams.stderr = "gold/remap-referer-miss.gold" - -# referer hit -tr = Test.AddTestRun() -tr.MakeCurlCommand( - ' --proxy 127.0.0.1:{0} "http://test4.com" --header "Referer: www.persia.com" --verbose'.format(ts.Variables.port), ts=ts) -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Streams.stderr = "gold/remap-referer-hit.gold" - -# DNS test -tr = Test.AddTestRun() -tr.MakeCurlCommand(' --proxy 127.0.0.1:{0} "http://testDNS.com" --verbose'.format(ts.Variables.port), ts=ts) -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Streams.stderr = "gold/remap-DNS-200.gold" - -# microserver lookup test -tr = Test.AddTestRun() -tr.MakeCurlCommand( - '--proxy 127.0.0.1:{0} "http://www.testexample.com/test" -H "Host: www.testexample.com" --verbose'.format(ts.Variables.port), - ts=ts) -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.StartBefore(server2) -tr.Processes.Default.Streams.stderr = "gold/lookupTest.gold" -tr.StillRunningAfter = server2 diff --git a/tests/gold_tests/remap/remap_https.test.py b/tests/gold_tests/remap/remap_https.test.py deleted file mode 100644 index 0c53fa2f4c9..00000000000 --- a/tests/gold_tests/remap/remap_https.test.py +++ /dev/null @@ -1,125 +0,0 @@ -''' -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -Test.Summary = ''' -Test a basic remap of a http connection -''' - -Test.SkipIf(Condition.CurlUsingUnixDomainSocket()) -Test.ContinueOnFail = True -# Define default ATS -ts = Test.MakeATSProcess("ts", enable_tls=True) -server = Test.MakeOriginServer("server") -server2 = Test.MakeOriginServer("server2", ssl=True) - -# **testname is required** -testName = "" -request_header = {"headers": "GET / HTTP/1.1\r\nHost: www.example.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -# desired response form the origin server -response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -server.addResponse("sessionlog.json", request_header, response_header) -server2.addResponse("sessionlog.json", request_header, response_header) - -# add ssl materials like key, certificates for the server -ts.addDefaultSSLFiles() - -ts.Disk.records_config.update( - { - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'lm|ssl', - 'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir), - 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts.Variables.SSLDir), - # enable ssl port - 'proxy.config.http.server_ports': '{0} {1}:proto=http2;http:ssl'.format(ts.Variables.port, ts.Variables.ssl_port), - 'proxy.config.ssl.client.verify.server.policy': 'PERMISSIVE', - }) - -ts.Disk.remap_config.AddLine('map https://www.example.com http://127.0.0.1:{0}'.format(server.Variables.Port)) -ts.Disk.remap_config.AddLine( - 'map https://www.example.com:{1} http://127.0.0.1:{0}'.format(server.Variables.Port, ts.Variables.ssl_port)) -ts.Disk.remap_config.AddLine( - 'map_with_recv_port https://www.example3.com:{1} http://127.0.0.1:{0}'.format(server.Variables.Port, ts.Variables.ssl_port)) -ts.Disk.remap_config.AddLine( - 'map https://www.anotherexample.com https://127.0.0.1:{0}'.format(server2.Variables.SSL_Port, ts.Variables.ssl_port)) - -ts.Disk.ssl_multicert_yaml.AddLines( - """ -ssl_multicert: - - dest_ip: "*" - ssl_cert_name: server.pem - ssl_key_name: server.key -""".split("\n")) - -# call localhost straight -tr = Test.AddTestRun() -tr.MakeCurlCommand('--http1.1 -k https://127.0.0.1:{0} --verbose'.format(ts.Variables.ssl_port), ts=ts) -tr.Processes.Default.ReturnCode = 0 - -tr.Processes.Default.StartBefore(server) -tr.Processes.Default.StartBefore(server2) -tr.Processes.Default.StartBefore(Test.Processes.ts) -tr.Processes.Default.Streams.stderr = "gold/remap-hitATS-404.gold" -tr.StillRunningAfter = server -tr.StillRunningAfter = ts - -# www.example.com host -tr = Test.AddTestRun() -tr.MakeCurlCommand('--http1.1 -k https://127.0.0.1:{0} -H "Host: www.example.com" --verbose'.format(ts.Variables.ssl_port), ts=ts) -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Streams.stderr = "gold/remap-https-200.gold" - -# www.example.com:80 host -tr = Test.AddTestRun() -tr.MakeCurlCommand( - '--http1.1 -k https://127.0.0.1:{0} -H "Host: www.example.com:443" --verbose'.format(ts.Variables.ssl_port), ts=ts) -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Streams.stderr = "gold/remap-https-200.gold" - -# www.example.com:8080 host -tr = Test.AddTestRun() -tr.MakeCurlCommand( - '--http1.1 -k https://127.0.0.1:{0} -H "Host: www.example.com:{0}" --verbose'.format(ts.Variables.ssl_port), ts=ts) -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Streams.stderr = "gold/remap-https-200.gold" - -# www.example3.com (match on receive port) -tr = Test.AddTestRun() -tr.MakeCurlCommand('--http1.1 -k https://127.0.0.1:{0} -H "Host: www.example3.com" --verbose'.format(ts.Variables.ssl_port), ts=ts) -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Streams.stderr = "gold/remap-https-200_3.gold" - -# no rule for this -tr = Test.AddTestRun() -tr.MakeCurlCommand('--http1.1 -k https://127.0.0.1:{0} -H "Host: www.test.com" --verbose'.format(ts.Variables.ssl_port), ts=ts) -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Streams.stderr = "gold/remap-hitATS-404.gold" - -# bad port -tr = Test.AddTestRun() -tr.MakeCurlCommand( - '--http1.1 -k https://127.0.0.1:{0} -H "Host: www.example.com:1234" --verbose'.format(ts.Variables.ssl_port), ts=ts) -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Streams.stderr = "gold/remap-hitATS-404.gold" - -# map www.anotherexample.com to https://.com -tr = Test.AddTestRun() -tr.MakeCurlCommand( - '--http1.1 -k https://127.0.0.1:{0} -H "Host: www.anotherexample.com" --verbose'.format(ts.Variables.ssl_port), ts=ts) -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Streams.stderr = "gold/remap-https-200_2.gold" -tr.StillRunningAfter = server2 diff --git a/tests/gold_tests/remap/remap_ip_resolve.test.py b/tests/gold_tests/remap/remap_ip_resolve.test.py deleted file mode 100644 index a8e1a1c8188..00000000000 --- a/tests/gold_tests/remap/remap_ip_resolve.test.py +++ /dev/null @@ -1,73 +0,0 @@ -''' -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -Test.Summary = ''' -Test a basic ip_resolve override using an ipv6 server -''' - -Test.ContinueOnFail = True -# Define default ATS -ts = Test.MakeATSProcess("ts") -server = Test.MakeOriginServer("server") -server_v6 = Test.MakeOriginServer("server_v6", None, None, '::1', 0) - -dns = Test.MakeDNServer("dns") - -Test.testName = "" -request_header = {"headers": "GET / HTTP/1.1\r\nHost: www.example.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -# expected response from the origin server -response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} - -# add response to the server dictionary -server.addResponse("sessionfile.log", request_header, response_header) -server_v6.addResponse("sessionfile.log", request_header, response_header) -ts.Disk.records_config.update( - { - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'http.*|dns|conf_remap', - 'proxy.config.http.referer_filter': 1, - 'proxy.config.dns.nameservers': '127.0.0.1:{0}'.format(dns.Variables.Port), - 'proxy.config.dns.resolv_conf': 'NULL', - 'proxy.config.hostdb.ip_resolve': 'ipv4' - }) - -ts.Disk.remap_config.AddLine( - 'map http://testDNS.com http://test.ipv4.only.com:{0} @plugin=conf_remap.so @pparam=proxy.config.hostdb.ip_resolve=ipv6;ipv4;client' - .format(server.Variables.Port)) -ts.Disk.remap_config.AddLine( - 'map http://testDNS2.com http://test.ipv6.only.com:{0} @plugin=conf_remap.so @pparam=proxy.config.hostdb.ip_resolve=ipv6;only' - .format(server_v6.Variables.Port)) - -dns.addRecords(records={"test.ipv4.only.com.": ["127.0.0.1"]}) -dns.addRecords(records={"test.ipv6.only.com": ["127.0.0.1", "::1"]}) - -tr = Test.AddTestRun() -tr.MakeCurlCommand(' --proxy 127.0.0.1:{0} "http://testDNS.com" --verbose'.format(ts.Variables.port), ts=ts) -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.StartBefore(server) -tr.Processes.Default.StartBefore(dns) -tr.Processes.Default.StartBefore(Test.Processes.ts) -tr.Processes.Default.Streams.stderr = "gold/remap-DNS-200.gold" -tr.StillRunningAfter = server - -tr = Test.AddTestRun() -tr.MakeCurlCommand(' --proxy 127.0.0.1:{0} "http://testDNS2.com" --verbose'.format(ts.Variables.port), ts=ts) -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.StartBefore(server_v6) -tr.Processes.Default.Streams.stderr = "gold/remap-DNS-ipv6-200.gold" -tr.StillRunningAfter = server_v6 diff --git a/tests/gold_tests/remap/remap_load_empty_failure.test.py b/tests/gold_tests/remap/remap_load_empty_failure.test.py deleted file mode 100644 index 224011b01d6..00000000000 --- a/tests/gold_tests/remap/remap_load_empty_failure.test.py +++ /dev/null @@ -1,42 +0,0 @@ -''' -Verify correct behavior of regex_map in remap.config. -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -Test.Summary = ''' -Test minimum rules on load - fail on missing file. -''' -ts = Test.MakeATSProcess("ts") -ts.Disk.remap_config.AddLine(f"") # empty file -ts.Disk.records_config.update({'proxy.config.url_remap.min_rules_required': 1}) -ts.ReturnCode = 33 # expect to Emergency fail due to empty "remap.config". -ts.Ready = 0 - -tr = Test.AddTestRun("test") - -# We have to wait upon TS to emit the expected log message, but it cannot be -# the ts Ready criteria because autest might detect the process going away -# before it detects the log message. So we add a separate process that waits -# upon the log message. -watcher = Test.Processes.Process("watcher") -watcher.Command = "sleep 10" -watcher.Ready = When.FileContains(ts.Disk.diags_log.Name, "remap.config failed to load") -watcher.StartBefore(ts) - -tr.Processes.Default.Command = "echo howdy" -tr.TimeOut = 5 -tr.Processes.Default.StartBefore(watcher) diff --git a/tests/gold_tests/remap/remap_load_empty_success.test.py b/tests/gold_tests/remap/remap_load_empty_success.test.py deleted file mode 100644 index 17dce500c02..00000000000 --- a/tests/gold_tests/remap/remap_load_empty_success.test.py +++ /dev/null @@ -1,33 +0,0 @@ -''' -Verify correct behavior of regex_map in remap.config. -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -Test.Summary = ''' -Test minimum rules on load - succeed on empty file. -''' - -ts = Test.MakeATSProcess("ts") -ts.Disk.remap_config.AddLine(f"") # empty file -ts.Disk.records_config.update({f'proxy.config.url_remap.min_rules_required': 0}) - -tr = Test.AddTestRun("startup") -p = tr.Processes.Default -p.Command = "sh -c echo" -p.ReturnCode = 0 -p.StartBefore(Test.Processes.ts) -tr.StillRunningAfter = ts diff --git a/tests/gold_tests/remap/remap_load_missing_failure.test.py b/tests/gold_tests/remap/remap_load_missing_failure.test.py deleted file mode 100644 index 880d953c7f2..00000000000 --- a/tests/gold_tests/remap/remap_load_missing_failure.test.py +++ /dev/null @@ -1,29 +0,0 @@ -''' -Verify correct behavior of regex_map in remap.config. -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -Test.Summary = ''' -Test minimum rules on load - fail on missing file. -''' -ts = Test.MakeATSProcess("ts") -ts.Disk.records_config.update({'proxy.config.url_remap.min_rules_required': 1}) -ts.ReturnCode = 33 # expect to Emergency fail due to empty "remap.config". - -tr = Test.AddTestRun("test") -tr.Processes.Default.Command = "echo" -tr.Processes.Default.StartAfter(ts, ready=When.FileExists(ts.Disk.diags_log)) diff --git a/tests/gold_tests/remap/remap_load_missing_success.test.py b/tests/gold_tests/remap/remap_load_missing_success.test.py deleted file mode 100644 index 557806bf5ae..00000000000 --- a/tests/gold_tests/remap/remap_load_missing_success.test.py +++ /dev/null @@ -1,33 +0,0 @@ -''' -Verify correct behavior of regex_map in remap.config. -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -Test.Summary = ''' -Test minimum rules on load - succeed on missing file. -''' - -ts = Test.MakeATSProcess("ts") - -ts.Disk.records_config.update({f'proxy.config.url_remap.min_rules_required': 0}) - -tr = Test.AddTestRun("startup") -p = tr.Processes.Default -p.Command = "sh -c echo" -p.ReturnCode = 0 -p.StartBefore(Test.Processes.ts) -tr.StillRunningAfter = ts diff --git a/tests/gold_tests/remap/remap_reload.test.py b/tests/gold_tests/remap/remap_reload.test.py deleted file mode 100644 index b49a1ab4e90..00000000000 --- a/tests/gold_tests/remap/remap_reload.test.py +++ /dev/null @@ -1,109 +0,0 @@ -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. -import os - - -def update_remap_config(path: str, lines: list) -> None: - """Update the remap.config file. - - This is used to update the config file between test runs without - triggering framework warnings about overriding file objects. - - :param path: The path to the remap.config file. - :param lines: The list of lines to write to the file. - """ - with open(path, 'w') as f: - f.write('\n'.join(lines) + '\n') - - -Test.Summary = ''' -Test remap reloading -''' -Test.testName = 'remap_reload' - -replay_file_1 = "reload_1.replay.yaml" -replay_file_2 = "reload_2.replay.yaml" -replay_file_3 = "reload_3.replay.yaml" -replay_file_4 = "reload_4.replay.yaml" - -tm = Test.MakeATSProcess("ts") -tm.Disk.diags_log.Content = Testers.ContainsExpression("remap.config failed to load", "Remap should fail to load") -remap_cfg_path = os.path.join(tm.Variables.CONFIGDIR, 'remap.config') - -pv = Test.MakeVerifierServerProcess("pv", "reload_server.replay.yaml") -pv_port = pv.Variables.http_port -tm.Disk.remap_config.AddLines( - [ - f"map http://alpha.ex http://alpha.ex:{pv_port}", - f"map http://bravo.ex http://bravo.ex:{pv_port}", - f"map http://charlie.ex http://charlie.ex:{pv_port}", - f"map http://delta.ex http://delta.ex:{pv_port}", - ]) -tm.Disk.records_config.update({'proxy.config.url_remap.min_rules_required': 3}) - -nameserver = Test.MakeDNServer("dns", default='127.0.0.1') -tm.Disk.records_config.update( - { - 'proxy.config.dns.nameservers': f"127.0.0.1:{nameserver.Variables.Port}", - 'proxy.config.dns.resolv_conf': 'NULL', - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'remap|config|file|rpc', - }) - -tr = Test.AddTestRun("verify load") -tr.Processes.Default.StartBefore(pv) -tr.Processes.Default.StartBefore(nameserver) -tr.Processes.Default.StartBefore(tm) -tr.AddVerifierClientProcess("client", replay_file_1, http_ports=[tm.Variables.port]) - -tr = Test.AddTestRun("Change remap.config to have only two lines") -p = tr.Processes.Default -p.Env = tm.Env -p.Command = 'echo "Change remap.config, two lines"' -p.Setup.Lambda( - lambda: update_remap_config( - remap_cfg_path, [ - f"map http://alpha.ex http://alpha.ex:{pv_port}", - f"map http://bravo.ex http://bravo.ex:{pv_port}", - ])) - -tr = Test.AddConfigReload(tm, expect="fail", expect_tasks=["remap.config"], delay_start=2, description="remap_config reload, fails") - -tr = Test.AddTestRun("after first reload") -tr.AddVerifierClientProcess("client_2", replay_file_2, http_ports=[tm.Variables.port]) - -tr = Test.AddTestRun("Change remap.config to have more than three lines") -p = tr.Processes.Default -p.Env = tm.Env -p.Command = 'echo "Change remap.config, more than three lines"' -p.Setup.Lambda( - lambda: update_remap_config( - remap_cfg_path, [ - f"map http://echo.ex http://echo.ex:{pv_port}", - f"map http://foxtrot.ex http://foxtrot.ex:{pv_port}", - f"map http://golf.ex http://golf.ex:{pv_port}", - f"map http://hotel.ex http://hotel.ex:{pv_port}", - f"map http://india.ex http://india.ex:{pv_port}", - ])) - -tr = Test.AddConfigReload( - tm, expect="success", expect_tasks=["remap.config"], delay_start=2, description="remap_config reload, succeeds") - -tr = Test.AddTestRun("post update charlie") -tr.AddVerifierClientProcess("client_3", replay_file_3, http_ports=[tm.Variables.port]) - -tr = Test.AddTestRun("post update golf") -tr.AddVerifierClientProcess("client_4", replay_file_4, http_ports=[tm.Variables.port]) diff --git a/tests/gold_tests/remap/remap_ws.test.py b/tests/gold_tests/remap/remap_ws.test.py deleted file mode 100644 index 3e122446415..00000000000 --- a/tests/gold_tests/remap/remap_ws.test.py +++ /dev/null @@ -1,126 +0,0 @@ -''' -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -Test.Summary = ''' -Test a basic remap of a websocket connections -''' - -Test.ContinueOnFail = True - -ts = Test.MakeATSProcess("ts", enable_tls=True) -server = Test.MakeOriginServer("server") - -testName = "Test WebSocket Remaps" -request_header = { - "headers": "GET /chat HTTP/1.1\r\nHost: www.example.com\r\nUpgrade: websocket\r\nConnection: Upgrade\r\n\r\n", - "body": None -} -response_header = { - "headers": - "HTTP/1.1 101 OK\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=\r\n\r\n", - "body": None -} -server.addResponse("sessionlog.json", request_header, response_header) - -ts.addDefaultSSLFiles() - -ts.Disk.records_config.update( - { - 'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir), - 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts.Variables.SSLDir), - }) - -ts.Disk.remap_config.AddLines( - [ - 'map ws://www.example.com:{1} ws://127.0.0.1:{0}'.format(server.Variables.Port, ts.Variables.port), - 'map wss://www.example.com:{1} ws://127.0.0.1:{0}'.format(server.Variables.Port, ts.Variables.ssl_port), - ]) - -ts.Disk.ssl_multicert_yaml.AddLines( - """ -ssl_multicert: - - dest_ip: "*" - ssl_cert_name: server.pem - ssl_key_name: server.key -""".split("\n")) - -if not Condition.CurlUsingUnixDomainSocket(): - # wss mapping - tr = Test.AddTestRun() - tr.Processes.Default.StartBefore(server) - tr.Processes.Default.StartBefore(Test.Processes.ts, ready=1) - tr.MakeCurlCommand( - '--max-time 2 -v -s -q -H "Connection: Upgrade" -H "Upgrade: websocket" -H "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==" -H "Sec-WebSocket-Version: 13" --http1.1 --resolve www.example.com:{0}:127.0.0.1 -k https://www.example.com:{0}/chat' - .format(ts.Variables.ssl_port), - ts=ts) - tr.Processes.Default.ReturnCode = 28 - tr.Processes.Default.Streams.stderr = "gold/remap-ws-upgrade.gold" - tr.StillRunningAfter = server - tr.StillRunningAfter = ts - -# ws mapping -tr = Test.AddTestRun() -if Condition.CurlUsingUnixDomainSocket(): - tr.Processes.Default.StartBefore(server) - tr.Processes.Default.StartBefore(Test.Processes.ts, ready=1) -tr.MakeCurlCommand( - '--max-time 2 -v -s -q -H "Connection: Upgrade" -H "Upgrade: websocket" -H "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==" -H "Sec-WebSocket-Version: 13" --http1.1 --resolve www.example.com:{0}:127.0.0.1 -k http://www.example.com:{0}/chat' - .format(ts.Variables.port), - ts=ts) -tr.Processes.Default.ReturnCode = 28 -tr.Processes.Default.Streams.stderr = "gold/remap-ws-upgrade.gold" -tr.StillRunningAfter = server -tr.StillRunningAfter = ts - -# Missing required headers (should result in 400) -tr = Test.AddTestRun() -tr.MakeCurlCommand( - '--max-time 2 -v -s -q -H "Connection: Upgrade" -H "Upgrade: websocket" --http1.1 --resolve www.example.com:{0}:127.0.0.1 -k http://www.example.com:{0}/chat' - .format(ts.Variables.port), - ts=ts) -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Streams.stderr = "gold/remap-ws-upgrade-400.gold" -tr.StillRunningAfter = server -tr.StillRunningAfter = ts - -# Test metrics -tr = Test.AddTestRun() -if Condition.CurlUsingUnixDomainSocket(): - metrics_gold_file = 'remap-ws-metrics-uds.gold' -else: - metrics_gold_file = 'remap-ws-metrics.gold' -tr.Processes.Default.Command = ( - f"{Test.Variables.AtsTestToolsDir}/stdout_wait" + " 'traffic_ctl metric get" + - " proxy.process.http.total_incoming_connections" + " proxy.process.http.total_client_connections" + - " proxy.process.http.total_client_connections_ipv4" + " proxy.process.http.total_client_connections_ipv6" + - " proxy.process.http.total_server_connections" + " proxy.process.http2.total_client_connections" + - " proxy.process.http.connect_requests" + " proxy.process.tunnel.total_client_connections_blind_tcp" + - " proxy.process.tunnel.current_client_connections_blind_tcp" + " proxy.process.tunnel.total_server_connections_blind_tcp" + - " proxy.process.tunnel.current_server_connections_blind_tcp" + " proxy.process.tunnel.total_client_connections_tls_tunnel" + - " proxy.process.tunnel.current_client_connections_tls_tunnel" + " proxy.process.tunnel.total_client_connections_tls_forward" + - " proxy.process.tunnel.current_client_connections_tls_forward" + - " proxy.process.tunnel.total_client_connections_tls_partial_blind" + - " proxy.process.tunnel.current_client_connections_tls_partial_blind" + - " proxy.process.tunnel.total_client_connections_tls_http" + " proxy.process.tunnel.current_client_connections_tls_http" + - " proxy.process.tunnel.total_server_connections_tls" + " proxy.process.tunnel.current_server_connections_tls'" + - f" {Test.TestDirectory}/gold/{metrics_gold_file}") -# Need to copy over the environment so traffic_ctl knows where to find the unix domain socket -tr.Processes.Default.Env = ts.Env -tr.Processes.Default.ReturnCode = 0 -tr.StillRunningAfter = server -tr.StillRunningAfter = ts diff --git a/tests/gold_tests/remap_yaml/basic_conf_remap_yaml_yaml.test.py b/tests/gold_tests/remap_yaml/basic_conf_remap_yaml_yaml.test.py deleted file mode 100644 index 5cff98416c1..00000000000 --- a/tests/gold_tests/remap_yaml/basic_conf_remap_yaml_yaml.test.py +++ /dev/null @@ -1,203 +0,0 @@ -''' -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -Test.Summary = ''' -Test conf_remap using a yaml file. -''' - -Test.ContinueOnFail = True - - -class conf_remap_yaml_load_test: - """Test conf_remap using a yaml file.""" - - client_counter: int = 0 - ts_counter: int = 0 - server_counter: int = 0 - - def __init__(self, name: str, gold_file="", remap_filename="", remap_content=""): - """Initialize the test. - :param name: The name of the test. - :param gold_file: Gold file to be checked. - :param remap_filename: Remap yaml filename. - :param remap_content: remap yaml file content. - """ - self.name = name - self.gold_file = gold_file - self._remap_filename = remap_filename - self._remap_content = remap_content - - def _configure_server(self, tr: 'TestRun'): - """Configure the server. - - :param tr: The TestRun object to associate the server process with. - """ - server = Test.MakeOriginServer(f"server-{conf_remap_yaml_load_test.ts_counter}", lookup_key="{%Host}{PATH}") - request_header2 = { - "headers": "GET /test HTTP/1.1\r\nHost: www.testexample.com\r\n\r\n", - "timestamp": "1469733493.993", - "body": "" - } - response_header2 = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} - - server.addResponse("sessionfile.log", request_header2, response_header2) - conf_remap_yaml_load_test.server_counter += 1 - self._server = server - - def _configure_traffic_server(self, tr: 'TestRun'): - """Configure Traffic Server. - - :param tr: The TestRun object to associate the ts process with. - """ - ts = Test.MakeATSProcess(f"ts-{conf_remap_yaml_load_test.ts_counter}") - - conf_remap_yaml_load_test.ts_counter += 1 - ts.Disk.records_config.update( - ''' - diags: - debug: - enabled: 1 - tags: conf_remap - dns: - resolv_conf: NULL - http: - referer_filter: 1 - url_remap: - pristine_host_hdr: 0 # make sure is 0 - - ''') - self._ts = ts - - def run(self, diags_fail_exp="", ts_retcode=0): - """Run the test. - :param diags_fail_exp: Text to be included to validate the error. - :param ts_retcode: Expected return code from TS. - """ - tr = Test.AddTestRun(self.name) - self._configure_server(tr) - self._configure_traffic_server(tr) - - tr.Processes.Default.StartBefore(self._server) - tr.Processes.Default.StartBefore(self._ts) - - self._ts.ReturnCode = ts_retcode - - if ts_retcode > 0: # we could have errors logged and yet, we still want to move on. - self._ts.Ready = 0 - - if diags_fail_exp != "": - # some error logs will be written to the diags. - self._ts.Disk.diags_log.Content = Testers.IncludesExpression(diags_fail_exp, "Have a look.") - else: - tr.Processes.Default.ReturnCode = 0 - - if self.gold_file: - tr.Processes.Default.Streams.stderr = self.gold_file - - if self._remap_filename != "" and self._remap_content != "": - self._ts.Disk.MakeConfigFile(self._remap_filename).update(self._remap_content) - self._ts.Disk.remap_yaml.AddLines( - f''' -remap: - - type: map - from: - url: http://www.testexample.com/ - to: - url: http://127.0.0.1:{self._server.Variables.Port} - plugins: - - name: conf_remap.so - params: - - {self._remap_filename} - '''.split("\n")) - if Condition.CurlUsingUnixDomainSocket(): - tr.MakeCurlCommand( - '-H "Host: www.testexample.com" "http://127.0.0.1:{0}/test" --verbose'.format(self._ts.Variables.port), ts=self._ts) - else: - tr.MakeCurlCommand( - '--proxy 127.0.0.1:{0} "http://www.testexample.com/test" -H "Host: www.testexample.com" --verbose'.format( - self._ts.Variables.port), - ts=self._ts) - conf_remap_yaml_load_test.client_counter += 1 - - -gold_file = "gold/200OK_test.gold" -if Condition.CurlUsingUnixDomainSocket(): - gold_file = "gold/200OK_test_uds.gold" - -test0 = conf_remap_yaml_load_test( - "Test success", - gold_file=gold_file, - remap_filename="testexample_remap.yaml", - remap_content=''' - records: - url_remap: - pristine_host_hdr: 1 - ''') -test0.run() - -test1 = conf_remap_yaml_load_test( - "Test mismatch type", - remap_filename="mismatch_field_type_remap.yaml", - remap_content=''' - records: - url_remap: - pristine_host_hdr: !!float '1' - ''') -test1.run(diags_fail_exp="'proxy.config.url_remap.pristine_host_hdr' variable type mismatch", ts_retcode=33) - -test2 = conf_remap_yaml_load_test( - "Test invalid variable", - remap_filename="invalid1_field_type_remap.yaml", - remap_content=''' - records: - plugin: - dynamic_reload_mode: 1 - ''') - -test2.run( - diags_fail_exp="'proxy.config.plugin.dynamic_reload_mode' is not a configuration variable or cannot be overridden", - ts_retcode=33) - -# We let the conf_remap parse two fields, only one is valid, we expect ATS to start and the invalid fields ignored. -test3 = conf_remap_yaml_load_test( - "Test success", - gold_file=gold_file, - remap_filename="testexample2_remap.yaml", - remap_content=''' - records: - plugin: - dynamic_reload_mode: 1 - - url_remap: - pristine_host_hdr: 1 - ''') -test3.run(diags_fail_exp="'proxy.config.plugin.dynamic_reload_mode' is not a configuration variable or cannot be overridden") - -# Check null values -test4 = conf_remap_yaml_load_test( - "Test success - with NULL variable", - gold_file=gold_file, - remap_filename="testexample_remap.yaml", - remap_content=''' - records: - url_remap: - pristine_host_hdr: 1 - hostdb: - ip_resolve: "NULL" # We want to make sure this gets read as it should. "NULL" could be the value of this field. - ''') -test4.run() diff --git a/tests/gold_tests/remap_yaml/conf_remap_float_yaml.test.py b/tests/gold_tests/remap_yaml/conf_remap_float_yaml.test.py deleted file mode 100644 index bf0edbc3108..00000000000 --- a/tests/gold_tests/remap_yaml/conf_remap_float_yaml.test.py +++ /dev/null @@ -1,52 +0,0 @@ -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -Test.Summary = ''' -Test command: traffic_ctl config describe proxy.config.http.background_fill_completed_threshold (YTSATS-3309) -''' -Test.testName = 'Float in conf_remap Config Test' - -ts = Test.MakeATSProcess("ts") - -ts.Disk.MakeConfigFile('conf_remap.yaml').update(''' -records: - http: - background_fill_completed_threshold: !!float '0.5' -''') - -ts.Disk.remap_yaml.AddLines( - f''' -remap: - - type: map - from: - url: http://cdn.example.com/ - to: - url: http://origin.example.com/ - plugins: - - name: conf_remap.so - params: - - {Test.RunDirectory}/ts/config/conf_remap.yaml - '''.split("\n")) - -tr = Test.AddTestRun("traffic_ctl command") -tr.Env = ts.Env -tr.TimeOut = 5 -tr.StillRunningAfter = ts - -p = tr.Processes.Default -p.Command = f"traffic_ctl config describe proxy.config.http.background_fill_completed_threshold" -p.ReturnCode = 0 -p.StartBefore(Test.Processes.ts) diff --git a/tests/gold_tests/remap_yaml/map_with_recv_port_yaml.test.py b/tests/gold_tests/remap_yaml/map_with_recv_port_yaml.test.py deleted file mode 100644 index 5513533a1f2..00000000000 --- a/tests/gold_tests/remap_yaml/map_with_recv_port_yaml.test.py +++ /dev/null @@ -1,87 +0,0 @@ -''' -Verify correct behavior of regex_map in remap.config. -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -Test.Summary = ''' -Verify correct behavior of map_with_recv_port in remap.config. -''' - -Test.ContinueOnFail = True -ts = Test.MakeATSProcess("ts") -server = Test.MakeOriginServer("server") -dns = Test.MakeDNServer("dns", default='127.0.0.1') - -Test.testName = "" -request_header_ip = {"headers": "GET /ip HTTP/1.1\r\nHost: origin.example.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -response_header_ip = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": "ip"} -request_header_unix = { - "headers": "GET /unix HTTP/1.1\r\nHost: origin.example.com\r\n\r\n", - "timestamp": "1469733493.993", - "body": "" -} -response_header_unix = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": "unix"} -request_header_error = { - "headers": "GET /error HTTP/1.1\r\nHost: origin.example.com\r\n\r\n", - "timestamp": "1469733493.993", - "body": "" -} -response_header_error = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": "error"} -server.addResponse("sessionfile.log", request_header_ip, response_header_ip) -server.addResponse("sessionfile.log", request_header_unix, response_header_unix) -server.addResponse("sessionfile.log", request_header_error, response_header_error) - -ts.Disk.records_config.update( - { - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'http|dns', - 'proxy.config.dns.nameservers': '127.0.0.1:{0}'.format(dns.Variables.Port), - 'proxy.config.dns.resolv_conf': 'NULL' - }) - -# This map rule should not match -ts.Disk.remap_yaml.AddLines( - f''' -remap: - - type: map - from: - url: http://test.example.com - to: - url: http://origin.example.com:{server.Variables.Port}/error - - type: map_with_recv_port - from: - url: http://test.example.com:{ts.Variables.port}/ - to: - url: http://origin.example.com:{server.Variables.Port}/ip - - type: map_with_recv_port - from: - url: http+unix://test.example.com - to: - url: http://origin.example.com:{server.Variables.Port}/unix - '''.split("\n")) - -tr = Test.AddTestRun() -tr.MakeCurlCommand('-H"Host: test.example.com" http://127.0.0.1:{0}/ --verbose'.format(ts.Variables.port), ts) -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.StartBefore(server) -tr.Processes.Default.StartBefore(dns) -tr.Processes.Default.StartBefore(Test.Processes.ts) -if Condition.CurlUsingUnixDomainSocket(): - tr.Processes.Default.Streams.stderr = "gold/map-with-recv-port-unix.gold" -else: - tr.Processes.Default.Streams.stderr = "gold/map-with-recv-port-ip.gold" -tr.StillRunningAfter = server diff --git a/tests/gold_tests/remap_yaml/regex_map_yaml.test.py b/tests/gold_tests/remap_yaml/regex_map_yaml.test.py deleted file mode 100644 index abecde2ee3d..00000000000 --- a/tests/gold_tests/remap_yaml/regex_map_yaml.test.py +++ /dev/null @@ -1,65 +0,0 @@ -''' -Verify correct behavior of regex_map in remap.yaml. -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -Test.Summary = ''' -Verify correct behavior of regex_map in remap.config. -''' - -Test.ContinueOnFail = True -ts = Test.MakeATSProcess("ts") -server = Test.MakeOriginServer("server") -dns = Test.MakeDNServer("dns", default='127.0.0.1') - -Test.testName = "" -request_header = {"headers": "GET / HTTP/1.1\r\nHost: zero.one.two.three.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -server.addResponse("sessionfile.log", request_header, response_header) - -ts.Disk.records_config.update( - { - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'http.*|dns|conf_remap', - 'proxy.config.http.referer_filter': 1, - 'proxy.config.dns.nameservers': '127.0.0.1:{0}'.format(dns.Variables.Port), - 'proxy.config.dns.resolv_conf': 'NULL' - }) - -ts.Disk.remap_yaml.AddLines( - rf''' -remap: - - type: regex_map - from: - url: http://(.*)?one\.two\.three\.com/ - to: - url: http://$1reactivate.four.five.six.com:{server.Variables.Port}/ - - type: regex_map - from: - url: https://\b(?!(.*one|two|three|four|five|six)).+\b\.seven\.eight\.nine\.com/blah12345.html - to: - url: https://www.example.com:{server.Variables.Port}/one/two/three/blah12345.html - '''.split("\n")) - -tr = Test.AddTestRun() -tr.MakeCurlCommand('-H"Host: zero.one.two.three.com" http://127.0.0.1:{0}/ --verbose'.format(ts.Variables.port), ts=ts) -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.StartBefore(server) -tr.Processes.Default.StartBefore(dns) -tr.Processes.Default.StartBefore(Test.Processes.ts) -tr.Processes.Default.Streams.stderr = "gold/remap-zero-200.gold" -tr.StillRunningAfter = server diff --git a/tests/gold_tests/remap_yaml/remap_acl_yaml.test.py b/tests/gold_tests/remap_yaml/remap_acl_yaml.test.py deleted file mode 100644 index de79a17a583..00000000000 --- a/tests/gold_tests/remap_yaml/remap_acl_yaml.test.py +++ /dev/null @@ -1,646 +0,0 @@ -''' -Verify remap.yaml acl behavior. -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# # http://www.apache.org/licenses/LICENSE-2.0 # -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import os -import io -import re -import inspect -import tempfile -from yaml import load, dump -from yaml import CLoader as Loader -from typing import List, Tuple - -from ports import get_port - -Test.Summary = ''' -Verify remap.yaml acl behavior. -''' - - -def update_config_file(path1: str, content1: str, path2: str, content2: str) -> None: - """Update two config files. - - This is used for some of the updates to the config files between test runs. - - :param path1: The path to the first config file. - :param content1: The content to write to the first config file. - :param path2: The path to the second config file. - :param content2: The content to write to the second config file. - """ - with open(path1, 'w') as f: - f.write(content1 + '\n') - with open(path2, 'w') as f: - f.write(content2 + '\n') - - -class Test_remap_acl: - """Configure a test to verify remap.config acl behavior.""" - - _ts: 'TestProcess' = None - _ts_reload_counter: int = 0 - _ts_is_started: bool = False - - _server_counter: int = 0 - _client_counter: int = 0 - - def __init__( - self, name: str, replay_file: str, ip_allow_content: str, deactivate_ip_allow: bool, acl_behavior_policy: int, - acl_configuration: List[str], named_acls: List[Tuple[str, - List[str]]], expected_responses: List[int], proxy_protocol: bool): - """Initialize the test. - - :param name: The name of the test. - :param replay_file: The replay file to be used. - :param ip_allow_content: The ip_allow configuration to be used. - :param deactivate_ip_allow: Whether to deactivate the ip_allow filter. - :param acl_configuration: The ACL configuration to be used. - :param named_acls: The set of named ACLs to configure and use. - :param expect_responses: The in-order expected responses from the proxy. - """ - self._replay_file = replay_file - self._ip_allow_lines = ip_allow_content.split("\n") - self._deactivate_ip_allow = deactivate_ip_allow - self._acl_behavior_policy = acl_behavior_policy - self._acl_configuration = acl_configuration - self._named_acls = named_acls - self._expected_responses = expected_responses - - # Usually we configure the server first and use the server port to - # configure ATS to remap to it. In this case, though, we want a - # long-lived ATS process that spans TestRuns. So we let ATS choose an - # arbitrary availble server port, and then tell the TestRun-specific - # server to use that port. - server_port = self._configure_traffic_server() - tr = Test.AddTestRun(name) - self._configure_server(tr, server_port) - self._configure_client(tr, proxy_protocol) - - def _configure_server(self, tr: 'TestRun', server_port: int) -> None: - """Configure the server. - """ - name = f"server-{Test_remap_acl._server_counter}" - server = tr.AddVerifierServerProcess(name, self._replay_file, http_ports=[server_port]) - Test_remap_acl._server_counter += 1 - self._server = server - - def _configure_traffic_server(self) -> int: - """Configure Traffic Server. - - :return: The listening port that the server should use. - """ - - call_reload: bool = False - if Test_remap_acl._ts is not None: - ts = Test_remap_acl._ts - call_reload = True - else: - ts = Test.MakeATSProcess("ts", enable_cache=False, enable_proxy_protocol=True, enable_uds=False) - Test_remap_acl._ts = ts - self._ts = ts - port_name = f'ServerPort-{Test_remap_acl._ts_reload_counter}' - server_port: int = get_port(ts, port_name) - - remap_yaml_lines = ['remap:'] - if self._deactivate_ip_allow: - remap_yaml_lines.append('- deactivate_filter: ip_allow') - - # First, define the name ACLs (filters). - defined_filters = [] - for name, definition in self._named_acls: - if len(definition) > 0: - remap_yaml_lines.append('- define_filter:') - remap_yaml_lines.append(f' {name}:') - for d in definition: - remap_yaml_lines.append(f' {d}') - defined_filters.append(name) - # Now activate them. - for name in defined_filters: - remap_yaml_lines.append(f'- activate_filter: {name}') - - remap_yaml_lines.append('- type: map') - remap_yaml_lines.append(' from:') - remap_yaml_lines.append(' url: /') - remap_yaml_lines.append(' to:') - remap_yaml_lines.append(f' url: http://127.0.0.1:{server_port}') - - if len(self._acl_configuration) > 0: - remap_yaml_lines.append(' acl_filter:') - for f in self._acl_configuration: - remap_yaml_lines.append(f' {f}') - - if call_reload: - # - # Update the ATS configuration. - # - tr = Test.AddTestRun("Change the ATS configuration") - p = tr.Processes.Default - p.Command = ( - f'traffic_ctl config set proxy.config.http.connect_ports {server_port} && ' - f'traffic_ctl config set proxy.config.url_remap.acl_behavior_policy {self._acl_behavior_policy}') - - p.Env = ts.Env - tr.StillRunningAfter = ts - - remap_yaml_path = os.path.join(ts.Variables.CONFIGDIR, 'remap.yaml') - ip_allow_path = os.path.join(ts.Variables.CONFIGDIR, 'ip_allow.yaml') - p.Setup.Lambda( - lambda: update_config_file( - remap_yaml_path, '\n'.join(remap_yaml_lines), ip_allow_path, '\n'.join(self._ip_allow_lines))) - - # - # Kick off the ATS config reload and await completion. - # - Test_remap_acl._ts_reload_counter += 1 - Test.AddConfigReload(ts, expect_tasks=["remap.yaml"], description="Reload the ATS configuration") - - else: - record_config = { - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'http|url|remap|ip_allow|proxyprotocol', - 'proxy.config.http.push_method_enabled': 1, - 'proxy.config.http.connect_ports': server_port, - 'proxy.config.url_remap.acl_behavior_policy': self._acl_behavior_policy, - 'proxy.config.acl.subjects': 'PROXY,PEER', - } - - ts.Disk.records_config.update(record_config) - ts.Disk.remap_yaml.AddLines(remap_yaml_lines) - ts.Disk.ip_allow_yaml.AddLines(self._ip_allow_lines) - - return server_port - - def _configure_client(self, tr: 'TestRun', proxy_protocol: bool) -> None: - """Run the test. - - :param tr: The TestRun object to associate the client process with. - """ - - name = f"client-{Test_remap_acl._client_counter}" - ts = Test_remap_acl._ts - port = ts.Variables.port if proxy_protocol == False else ts.Variables.proxy_protocol_port - p = tr.AddVerifierClientProcess(name, self._replay_file, http_ports=[port]) - Test_remap_acl._client_counter += 1 - p.StartBefore(self._server) - if not Test_remap_acl._ts_is_started: - p.StartBefore(ts) - Test_remap_acl._ts_is_started = True - - if self._expected_responses == [None, None]: - # If there are no expected responses, expect the Warning about the rejected ip. - self._ts.Disk.diags_log.Content += Testers.ContainsExpression( - "client '127.0.0.1' prohibited by ip-allow policy", "Verify the client rejection warning message.") - - # Also, the client will complain about the broken connections. - p.ReturnCode = 1 - - else: - codes = [str(code) for code in self._expected_responses] - p.Streams.stdout += Testers.ContainsExpression( - '.*'.join(codes), "Verifying the expected order of responses", reflags=re.DOTALL | re.MULTILINE) - - -class Test_old_action: - _ts_counter: int = 0 - - def __init__(self, name: str, acl_filter: List[str], ip_allow_content: str) -> None: - '''Test that ATS fails with a FATAL message if an old action is used with modern ACL filter policy. - - :param name: The name of the test run. - :param acl_filter: The ACL filter to use. - :param ip_allow_content: The ip_allow configuration to use. - ''' - - tr = Test.AddTestRun(name) - ts = self._configure_traffic_server(tr, acl_filter, ip_allow_content) - - def _configure_traffic_server(self, tr: 'TestRun', acl_filter: List[str], ip_allow_content: str) -> 'Process': - '''Configure Traffic Server process - - :param tr: The TestRun object to associate the Traffic Server process with. - :param acl_filter: The ACL filter to configure in remap.config. - :param ip_allow_content: The ip_allow configuration to use. - :return: The Traffic Server process. - ''' - name = f"ts-old-action-{Test_old_action._ts_counter}" - Test_old_action._ts_counter += 1 - ts = tr.MakeATSProcess(name, enable_uds=False) - self._ts = ts - - ts.Disk.records_config.update( - { - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'http|url|remap|ip_allow', - 'proxy.config.url_remap.acl_behavior_policy': 1, - }) - - ts.Disk.remap_yaml.AddLines( - f''' -remap: - - type: map - from: - url: / - to: - url: http://127.0.0.1:8080 - acl_filter: '''.split("\n")) - - for f in acl_filter: - ts.Disk.remap_yaml.AddLine(f' {f}') - - if ip_allow_content: - ts.Disk.ip_allow_yaml.AddLines(ip_allow_content.split("\n")) - - if len(acl_filter) > 0: - expected_error = '"allow" and "deny" are no longer valid.' - else: - expected_error = 'Legacy action name of' - - # We have to wait upon TS to emit the expected log message, but it cannot be - # the ts Ready criteria because autest might detect the process going away - # before it detects the log message. So we add a separate process that waits - # upon the log message. - watcher = tr.Processes.Process("watcher") - watcher.Command = "sleep 10" - watcher.Ready = When.FileContains(ts.Disk.diags_log.Name, expected_error) - watcher.StartBefore(ts) - - tr.Processes.Default.Command = 'printf "Fatal Shutdown Test"' - tr.Processes.Default.ReturnCode = 0 - tr.Processes.Default.StartBefore(watcher) - - tr.Timeout = 5 - ts.ReturnCode = Any(33, 70) - ts.Ready = 0 - ts.Disk.diags_log.Content = Testers.IncludesExpression(expected_error, 'ATS should fatal with the old actions.') - - return ts - - -IP_ALLOW_OLD_ACTION = f''' -ip_categories: - - name: ACME_LOCAL - ip_addrs: 127.0.0.1 - - name: ACME_EXTERNAL - ip_addrs: 5.6.7.8 - -ip_allow: - - apply: in - ip_addrs: 0/0 - action: allow - methods: - - GET -''' - -IP_ALLOW_CONTENT = f''' -ip_categories: - - name: ACME_LOCAL - ip_addrs: 127.0.0.1 - - name: ACME_EXTERNAL - ip_addrs: 5.6.7.8 - -ip_allow: - - apply: in - ip_addrs: 0/0 - action: set_allow - methods: - - GET -''' - -Test_old_action("Verify allow is reject in modern policy", ['action: allow', 'method: GET'], IP_ALLOW_CONTENT) -Test_old_action("Verify deny is reject in modern policy", ['action: deny', 'method: GET'], IP_ALLOW_CONTENT) -Test_old_action("Verify deny is reject in modern policy", [], IP_ALLOW_OLD_ACTION) - -test_ip_allow_optional_methods = Test_remap_acl( - "Verify non-allowed methods are blocked.", - replay_file='remap_acl_get_post_allowed.replay.yaml', - ip_allow_content=IP_ALLOW_CONTENT, - deactivate_ip_allow=False, - acl_behavior_policy=1, - acl_configuration=['action: set_allow', 'src_ip: 127.0.0.1', 'method: [GET, POST]'], - named_acls=[], - expected_responses=[200, 200, 403, 403, 403], - proxy_protocol=False) - -test_ip_allow_optional_methods_pp = Test_remap_acl( - "Verify non-allowed methods are blocked (PP).", - replay_file='remap_acl_get_post_allowed_pp.replay.yaml', - ip_allow_content=IP_ALLOW_CONTENT, - deactivate_ip_allow=False, - acl_behavior_policy=1, - acl_configuration=['action: set_allow', 'src_ip: 1.2.3.4', 'method: [GET, POST]'], - named_acls=[], - expected_responses=[200, 200, 403, 403, 403], - proxy_protocol=True) - -test_ip_allow_optional_methods = Test_remap_acl( - "Verify add_allow adds an allowed method.", - replay_file='remap_acl_get_post_allowed.replay.yaml', - ip_allow_content=IP_ALLOW_CONTENT, - deactivate_ip_allow=False, - acl_behavior_policy=1, - acl_configuration=['action: add_allow', 'src_ip: 127.0.0.1', 'method: POST'], - named_acls=[], - expected_responses=[200, 200, 403, 403, 403], - proxy_protocol=False) - -test_ip_allow_optional_methods = Test_remap_acl( - "Verify add_allow adds allowed methods.", - replay_file='remap_acl_get_post_allowed.replay.yaml', - ip_allow_content=IP_ALLOW_CONTENT, - deactivate_ip_allow=False, - acl_behavior_policy=1, - acl_configuration=['action: add_allow', 'src_ip: 127.0.0.1', 'method: [GET, POST]'], - named_acls=[], - expected_responses=[200, 200, 403, 403, 403], - proxy_protocol=False) - -test_ip_allow_optional_methods = Test_remap_acl( - "Verify if no ACLs match, ip_allow.yaml is used.", - replay_file='remap_acl_get_allowed.replay.yaml', - ip_allow_content=IP_ALLOW_CONTENT, - deactivate_ip_allow=False, - acl_behavior_policy=1, - acl_configuration=['action: set_allow', 'src_ip: 1.2.3.4', 'method: [GET, POST]'], - named_acls=[], - expected_responses=[200, 403, 403, 403, 403], - proxy_protocol=False) - -test_ip_allow_optional_methods = Test_remap_acl( - "Verify @src_ip=all works.", - replay_file='remap_acl_get_post_allowed.replay.yaml', - ip_allow_content=IP_ALLOW_CONTENT, - deactivate_ip_allow=False, - acl_behavior_policy=1, - acl_configuration=['action: set_allow', 'src_ip: all', 'method: [GET, POST]'], - named_acls=[], - expected_responses=[200, 200, 403, 403, 403], - proxy_protocol=False) - -test_ip_allow_optional_methods = Test_remap_acl( - "Verify @src_ip_category works.", - replay_file='remap_acl_get_post_allowed.replay.yaml', - ip_allow_content=IP_ALLOW_CONTENT, - deactivate_ip_allow=False, - acl_behavior_policy=1, - acl_configuration=['action: set_allow', 'src_ip_category: ACME_LOCAL', 'method: [GET, POST]'], - named_acls=[], - expected_responses=[200, 200, 403, 403, 403], - proxy_protocol=False) - -test_ip_allow_optional_methods = Test_remap_acl( - "Verify no @src_ip implies all IP addresses.", - replay_file='remap_acl_get_post_allowed.replay.yaml', - ip_allow_content=IP_ALLOW_CONTENT, - deactivate_ip_allow=False, - acl_behavior_policy=1, - acl_configuration=['action: set_allow', 'method: [GET, POST]'], - named_acls=[], - expected_responses=[200, 200, 403, 403, 403], - proxy_protocol=False) - -test_ip_allow_optional_methods = Test_remap_acl( - "Verify denied methods are blocked.", - replay_file='remap_acl_get_post_denied.replay.yaml', - ip_allow_content=IP_ALLOW_CONTENT, - deactivate_ip_allow=False, - acl_behavior_policy=1, - acl_configuration=['action: set_deny', 'src_ip: 127.0.0.1', 'method: [GET, POST]'], - named_acls=[], - expected_responses=[403, 403, 200, 200, 400], - proxy_protocol=False) - -test_ip_allow_optional_methods = Test_remap_acl( - "Verify add_deny adds blocked methods.", - replay_file='remap_acl_all_denied.replay.yaml', - ip_allow_content=IP_ALLOW_CONTENT, - deactivate_ip_allow=False, - acl_behavior_policy=1, - acl_configuration=['action: add_deny', 'src_ip: 127.0.0.1', 'method: GET'], - named_acls=[], - expected_responses=[403, 403, 403, 403, 403], - proxy_protocol=False) - -test_ip_allow_optional_methods = Test_remap_acl( - "Verify a default deny filter rule works.", - replay_file='remap_acl_all_denied.replay.yaml', - ip_allow_content=IP_ALLOW_CONTENT, - deactivate_ip_allow=False, - acl_behavior_policy=1, - acl_configuration=['action: set_allow', 'src_ip: 1.2.3.4', 'method: [GET, POST]'], - named_acls=[('deny', ['action: set_deny'])], - expected_responses=[403, 403, 403, 403, 403], - proxy_protocol=False) - -test_ip_allow_optional_methods = Test_remap_acl( - "Verify inverting @src_ip works.", - replay_file='remap_acl_all_denied.replay.yaml', - ip_allow_content=IP_ALLOW_CONTENT, - deactivate_ip_allow=False, - acl_behavior_policy=1, - acl_configuration=['action: set_allow', 'src_ip_invert: 127.0.0.1', 'method: [GET, POST]'], - named_acls=[('deny', ['action: set_deny'])], - expected_responses=[403, 403, 403, 403, 403], - proxy_protocol=False) - -test_ip_allow_optional_methods = Test_remap_acl( - "Verify inverting @src_ip works with the rule matching.", - replay_file='remap_acl_get_post_allowed.replay.yaml', - ip_allow_content=IP_ALLOW_CONTENT, - deactivate_ip_allow=False, - acl_behavior_policy=1, - acl_configuration=['action: set_allow', 'src_ip_invert: 3.4.5.6', 'method: [GET, POST]'], - named_acls=[('deny', ['action: set_deny'])], - expected_responses=[200, 200, 403, 403, 403], - proxy_protocol=False) - -test_ip_allow_optional_methods = Test_remap_acl( - "Verify inverting @src_ip_category works.", - replay_file='remap_acl_all_denied.replay.yaml', - ip_allow_content=IP_ALLOW_CONTENT, - deactivate_ip_allow=False, - acl_behavior_policy=1, - acl_configuration=['action: set_allow', 'src_ip_category_invert: ACME_LOCAL', 'method: [GET, POST]'], - named_acls=[('deny', ['action: set_deny'])], - expected_responses=[403, 403, 403, 403, 403], - proxy_protocol=False) - -test_ip_allow_optional_methods = Test_remap_acl( - "Verify inverting @src_ip_category works with the rule matching.", - replay_file='remap_acl_get_post_allowed.replay.yaml', - ip_allow_content=IP_ALLOW_CONTENT, - deactivate_ip_allow=False, - acl_behavior_policy=1, - acl_configuration=['action: set_allow', 'src_ip_category_invert: ACME_EXTERNAL', 'method: [GET, POST]'], - named_acls=[('deny', ['action: set_deny'])], - expected_responses=[200, 200, 403, 403, 403], - proxy_protocol=False) - -test_ip_allow_optional_methods = Test_remap_acl( - "Verify @src_ip and @src_ip_category AND together.", - replay_file='remap_acl_all_denied.replay.yaml', - ip_allow_content=IP_ALLOW_CONTENT, - deactivate_ip_allow=False, - acl_behavior_policy=1, - # The rule will not match because, while @src_ip matches, @src_ip_category does not. - acl_configuration=['action: set_allow', 'src_ip: 127.0.0.1', 'src_ip_category: ACME_EXTERNAL', 'method: [GET, POST]'], - # Therefore, this named deny filter will block. - named_acls=[('deny', ['action: set_deny'])], - expected_responses=[403, 403, 403, 403, 403], - proxy_protocol=False) - -test_ip_allow_optional_methods = Test_remap_acl( - "Verify defined in-line ACLS are evaluated before named ones.", - replay_file='remap_acl_get_post_allowed.replay.yaml', - ip_allow_content=IP_ALLOW_CONTENT, - deactivate_ip_allow=False, - acl_behavior_policy=1, - acl_configuration=['action: set_allow', 'src_ip: 127.0.0.1', 'method: [GET, POST]'], - named_acls=[('deny', ['action: set_deny'])], - expected_responses=[200, 200, 403, 403, 403], - proxy_protocol=False) - -test_ip_allow_optional_methods = Test_remap_acl( - "Verify remap.config line overrides ip_allow rule.", - replay_file='remap_acl_get_post_allowed.replay.yaml', - ip_allow_content=IP_ALLOW_CONTENT, - deactivate_ip_allow=False, - acl_behavior_policy=1, - acl_configuration=['action: set_allow', 'src_ip: 127.0.0.1', 'method: [GET, POST]'], - named_acls=[], - expected_responses=[200, 200, 403, 403, 403], - proxy_protocol=False) - -test_ip_allow_optional_methods = Test_remap_acl( - "Verify we can deactivate the ip_allow filter.", - replay_file='remap_acl_all_allowed.replay.yaml', - ip_allow_content=IP_ALLOW_CONTENT, - deactivate_ip_allow=True, - acl_behavior_policy=1, - # This won't match, so nothing will match since ip_allow.yaml is off. - acl_configuration=['action: set_allow', 'src_ip: 1.2.3.4', 'method: [GET, POST]'], - named_acls=[], - # Nothing will block the request since ip_allow.yaml is off. - expected_responses=[200, 200, 200, 200, 400], - proxy_protocol=False) - -test_ip_allow_optional_methods = Test_remap_acl( - "Verify in_ip matches on IP as expected.", - replay_file='remap_acl_get_post_allowed.replay.yaml', - ip_allow_content=IP_ALLOW_CONTENT, - deactivate_ip_allow=False, - acl_behavior_policy=1, - acl_configuration=['action: set_allow', 'in_ip: 127.0.0.1', 'method: [GET, POST]'], - named_acls=[], - expected_responses=[200, 200, 403, 403, 403], - proxy_protocol=False) - -test_ip_allow_optional_methods = Test_remap_acl( - "Verify in_ip rules do not match on other IPs.", - replay_file='remap_acl_get_allowed.replay.yaml', - ip_allow_content=IP_ALLOW_CONTENT, - deactivate_ip_allow=False, - acl_behavior_policy=1, - acl_configuration=['action: set_allow', 'in_ip: 3.4.5.6', 'method: [GET, POST]'], - named_acls=[], - expected_responses=[200, 403, 403, 403, 403], - proxy_protocol=False) - -test_named_acl_deny = Test_remap_acl( - "Verify a named ACL is applied if an in-line ACL is absent.", - replay_file='deny_head_post.replay.yaml', - ip_allow_content=IP_ALLOW_CONTENT, - deactivate_ip_allow=False, - acl_behavior_policy=1, - acl_configuration=[], - named_acls=[('deny', ['action: set_deny', 'method: [HEAD, POST]'])], - expected_responses=[200, 403, 403, 403], - proxy_protocol=False) - - -def replay_proxy_response(filename, replay_file, get_proxy_response, post_proxy_response): - """ - replay_proxy_response writes the given replay file (which expects a single GET & POST client-request) - with the given proxy_response value. This is only used to support the tests in the combination table. - """ - - current_dir = os.path.dirname(inspect.getfile(inspect.currentframe())) - path = os.path.join(current_dir, filename) - data = None - with open(path) as f: - data = load(f, Loader=Loader) - for session in data["sessions"]: - for transaction in session["transactions"]: - method = transaction["client-request"]["method"] - if method == "GET": - transaction["proxy-response"]["status"] = 403 if get_proxy_response == None else get_proxy_response - elif method == "POST": - transaction["proxy-response"]["status"] = 403 if post_proxy_response == None else post_proxy_response - else: - raise Exception("Expected to find GET or POST request, found %s", method) - with open(replay_file, "w") as f: - f.write(dump(data)) - - -from deactivate_ip_allow_yaml import all_deactivate_ip_allow_tests_yaml -from all_acl_combinations_yaml import all_acl_combination_tests_yaml -""" -Test all acl combinations -""" -for idx, test in enumerate(all_acl_combination_tests_yaml): - (_, replay_file_name) = tempfile.mkstemp(suffix="acl_table_test_{}.replay".format(idx)) - replay_proxy_response( - "base.replay.yaml", - replay_file_name, - test["GET response"], - test["POST response"], - ) - Test_remap_acl( - "allcombo-{0} {1} {2} {3}".format(idx, test["inline"], test["named_acl"], test["ip_allow"]), - replay_file=replay_file_name, - ip_allow_content=test["ip_allow"], - deactivate_ip_allow=False, - acl_behavior_policy=0 if test["policy"] == "legacy" else 1, - acl_configuration=test["inline"], - named_acls=[("acl", test["named_acl"])] if test["named_acl"] != "" else [], - expected_responses=[test["GET response"], test["POST response"]], - proxy_protocol=False, - ) -""" -Test all ACL combinations -""" -for idx, test in enumerate(all_deactivate_ip_allow_tests_yaml): - try: - test["deactivate_ip_allow"] - except: - print(test) - (_, replay_file_name) = tempfile.mkstemp(suffix="deactivate_ip_allow_table_test_{}.replay".format(idx)) - replay_proxy_response( - "base.replay.yaml", - replay_file_name, - test["GET response"], - test["POST response"], - ) - Test_remap_acl( - "ipallow-{0} {1} {2} {3}".format(idx, test["inline"], test["named_acl"], test["ip_allow"]), - replay_file=replay_file_name, - ip_allow_content=test["ip_allow"], - deactivate_ip_allow=test["deactivate_ip_allow"], - acl_behavior_policy=0 if test["policy"] == "legacy" else 1, - acl_configuration=test["inline"], - named_acls=[("acl", test["named_acl"])] if test["named_acl"] != "" else [], - expected_responses=[test["GET response"], test["POST response"]], - proxy_protocol=False, - ) diff --git a/tests/gold_tests/remap_yaml/remap_http_yaml.test.py b/tests/gold_tests/remap_yaml/remap_http_yaml.test.py deleted file mode 100644 index 204246fae99..00000000000 --- a/tests/gold_tests/remap_yaml/remap_http_yaml.test.py +++ /dev/null @@ -1,193 +0,0 @@ -''' -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -Test.Summary = ''' -Test a basic remap of a http connection -''' - -Test.ContinueOnFail = True -# Define default ATS -ts = Test.MakeATSProcess("ts") -server = Test.MakeOriginServer("server") -server2 = Test.MakeOriginServer("server2", lookup_key="{%Host}{PATH}") -dns = Test.MakeDNServer("dns") - -Test.testName = "" -request_header = {"headers": "GET / HTTP/1.1\r\nHost: www.example.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -# expected response from the origin server -response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} - -request_header2 = {"headers": "GET /test HTTP/1.1\r\nHost: www.testexample.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -# expected response from the origin server -response_header2 = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} - -# add response to the server dictionary -server.addResponse("sessionfile.log", request_header, response_header) -server2.addResponse("sessionfile.log", request_header2, response_header2) -ts.Disk.records_config.update( - { - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'http.*|dns|conf_remap|remap_yaml', - 'proxy.config.http.referer_filter': 1, - 'proxy.config.dns.nameservers': '127.0.0.1:{0}'.format(dns.Variables.Port), - 'proxy.config.dns.resolv_conf': 'NULL' - }) - -ts.Disk.remap_yaml.AddLines( - f''' -remap: - - type: map - from: - url: http://www.example.com - to: - url: http://127.0.0.1:{server.Variables.Port} - - type: map_with_recv_port - from: - url: http://www.example2.com:{ts.Variables.port} - to: - url: http://127.0.0.1:{server.Variables.Port} - - type: map - from: - url: http://www.example.com:8080 - to: - url: http://127.0.0.1:{server.Variables.Port} - - type: redirect - from: - url: http://test3.com - to: - url: http://httpbin.org - - type: map_with_referer - from: - url: http://test4.com - to: - url: http://127.0.0.1:{server.Variables.Port} - redirect: - url: http://httpbin.org - regex: - - (.*[.])?persia[.]com - - type: map - from: - url: http://testDNS.com - to: - url: http://audrey.hepburn.com:{server.Variables.Port} - - type: map - from: - url: http://www.testexample.com - to: - url: http://127.0.0.1:{server2.Variables.Port} - plugins: - - name: conf_remap.so - params: - - proxy.config.url_remap.pristine_host_hdr=1 - '''.split("\n")) - -dns.addRecords(records={"audrey.hepburn.com.": ["127.0.0.1"]}) -dns.addRecords(records={"whatever.com.": ["127.0.0.1"]}) - -# call localhost straight -tr = Test.AddTestRun() -tr.MakeCurlCommand('"http://127.0.0.1:{0}/" --verbose'.format(ts.Variables.port), ts=ts) -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.StartBefore(server) -tr.Processes.Default.StartBefore(dns) -tr.Processes.Default.StartBefore(Test.Processes.ts) -tr.Processes.Default.Streams.stderr = "gold/remap-hitATS-404.gold" -tr.StillRunningAfter = server - -# www.example.com host -tr = Test.AddTestRun() -tr.MakeCurlCommand( - '--proxy 127.0.0.1:{0} "http://www.example.com" -H "Proxy-Connection: keep-alive" --verbose'.format(ts.Variables.port), ts=ts) -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Streams.stderr = "gold/remap-200.gold" - -# www.example2.com host (match on receive port) -# map_with_recv_port doesn't work with UDS -if not Condition.CurlUsingUnixDomainSocket(): - tr = Test.AddTestRun() - tr.MakeCurlCommand( - '--proxy 127.0.0.1:{0} "http://www.example2.com" -H "Proxy-Connection: keep-alive" --verbose'.format(ts.Variables.port), - ts=ts) - tr.Processes.Default.ReturnCode = 0 - tr.Processes.Default.Streams.stderr = "gold/remap2-200.gold" - -# www.example.com:80 host -tr = Test.AddTestRun() -tr.MakeCurlCommand( - ' --proxy 127.0.0.1:{0} "http://www.example.com:80/" -H "Proxy-Connection: keep-alive" --verbose'.format(ts.Variables.port), - ts=ts) -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Streams.stderr = "gold/remap-200.gold" - -# www.example.com:8080 host -tr = Test.AddTestRun() -tr.MakeCurlCommand( - ' --proxy 127.0.0.1:{0} "http://www.example.com:8080" -H "Proxy-Connection: keep-alive" --verbose'.format(ts.Variables.port), - ts=ts) -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Streams.stderr = "gold/remap-200.gold" - -# no rule for this -tr = Test.AddTestRun() -tr.MakeCurlCommand( - ' --proxy 127.0.0.1:{0} "http://www.test.com/" -H "Proxy-Connection: keep-alive" --verbose'.format(ts.Variables.port), ts=ts) -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Streams.stderr = "gold/remap-404.gold" - -# redirect result -tr = Test.AddTestRun() -tr.MakeCurlCommand(' --proxy 127.0.0.1:{0} "http://test3.com" --verbose'.format(ts.Variables.port), ts=ts) -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Streams.stderr = "gold/remap-redirect.gold" - -# referer hit -tr = Test.AddTestRun() -tr.MakeCurlCommand( - ' --proxy 127.0.0.1:{0} "http://test4.com" --header "Referer: persia.com" --verbose'.format(ts.Variables.port), ts=ts) -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Streams.stderr = "gold/remap-referer-hit.gold" - -# referer miss -tr = Test.AddTestRun() -tr.MakeCurlCommand( - ' --proxy 127.0.0.1:{0} "http://test4.com" --header "Referer: monkey.com" --verbose'.format(ts.Variables.port), ts=ts) -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Streams.stderr = "gold/remap-referer-miss.gold" - -# referer hit -tr = Test.AddTestRun() -tr.MakeCurlCommand( - ' --proxy 127.0.0.1:{0} "http://test4.com" --header "Referer: www.persia.com" --verbose'.format(ts.Variables.port), ts=ts) -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Streams.stderr = "gold/remap-referer-hit.gold" - -# DNS test -tr = Test.AddTestRun() -tr.MakeCurlCommand(' --proxy 127.0.0.1:{0} "http://testDNS.com" --verbose'.format(ts.Variables.port), ts=ts) -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Streams.stderr = "gold/remap-DNS-200.gold" - -# microserver lookup test -tr = Test.AddTestRun() -tr.MakeCurlCommand( - '--proxy 127.0.0.1:{0} "http://www.testexample.com/test" -H "Host: www.testexample.com" --verbose'.format(ts.Variables.port), - ts=ts) -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.StartBefore(server2) -tr.Processes.Default.Streams.stderr = "gold/lookupTest.gold" -tr.StillRunningAfter = server2 diff --git a/tests/gold_tests/remap_yaml/remap_https_yaml.test.py b/tests/gold_tests/remap_yaml/remap_https_yaml.test.py deleted file mode 100644 index 57a579aa5bb..00000000000 --- a/tests/gold_tests/remap_yaml/remap_https_yaml.test.py +++ /dev/null @@ -1,142 +0,0 @@ -''' -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -Test.Summary = ''' -Test a basic remap of a http connection -''' - -Test.SkipIf(Condition.CurlUsingUnixDomainSocket()) -Test.ContinueOnFail = True -# Define default ATS -ts = Test.MakeATSProcess("ts", enable_tls=True) -server = Test.MakeOriginServer("server") -server2 = Test.MakeOriginServer("server2", ssl=True) - -# **testname is required** -testName = "" -request_header = {"headers": "GET / HTTP/1.1\r\nHost: www.example.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -# desired response form the origin server -response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -server.addResponse("sessionlog.json", request_header, response_header) -server2.addResponse("sessionlog.json", request_header, response_header) - -# add ssl materials like key, certificates for the server -ts.addDefaultSSLFiles() - -ts.Disk.records_config.update( - { - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'lm|ssl', - 'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir), - 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts.Variables.SSLDir), - # enable ssl port - 'proxy.config.http.server_ports': '{0} {1}:proto=http2;http:ssl'.format(ts.Variables.port, ts.Variables.ssl_port), - 'proxy.config.ssl.client.verify.server.policy': 'PERMISSIVE', - }) - -ts.Disk.remap_yaml.AddLines( - f''' -remap: - - type: map - from: - url: https://www.example.com - to: - url: http://127.0.0.1:{server.Variables.Port} - - type: map - from: - url: https://www.example.com:{ts.Variables.ssl_port} - to: - url: http://127.0.0.1:{server.Variables.Port} - - type: map_with_recv_port - from: - url: https://www.example3.com:{ts.Variables.ssl_port} - to: - url: http://127.0.0.1:{server.Variables.Port} - - type: map - from: - url: https://www.anotherexample.com - to: - url: https://127.0.0.1:{server2.Variables.SSL_Port} - '''.split("\n")) - -ts.Disk.ssl_multicert_yaml.AddLines( - """ -ssl_multicert: - - dest_ip: "*" - ssl_cert_name: server.pem - ssl_key_name: server.key -""".split("\n")) - -# call localhost straight -tr = Test.AddTestRun() -tr.MakeCurlCommand('--http1.1 -k https://127.0.0.1:{0} --verbose'.format(ts.Variables.ssl_port), ts=ts) -tr.Processes.Default.ReturnCode = 0 - -tr.Processes.Default.StartBefore(server) -tr.Processes.Default.StartBefore(server2) -tr.Processes.Default.StartBefore(Test.Processes.ts) -tr.Processes.Default.Streams.stderr = "gold/remap-hitATS-404.gold" -tr.StillRunningAfter = server -tr.StillRunningAfter = ts - -# www.example.com host -tr = Test.AddTestRun() -tr.MakeCurlCommand('--http1.1 -k https://127.0.0.1:{0} -H "Host: www.example.com" --verbose'.format(ts.Variables.ssl_port), ts=ts) -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Streams.stderr = "gold/remap-https-200.gold" - -# www.example.com:80 host -tr = Test.AddTestRun() -tr.MakeCurlCommand( - '--http1.1 -k https://127.0.0.1:{0} -H "Host: www.example.com:443" --verbose'.format(ts.Variables.ssl_port), ts=ts) -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Streams.stderr = "gold/remap-https-200.gold" - -# www.example.com:8080 host -tr = Test.AddTestRun() -tr.MakeCurlCommand( - '--http1.1 -k https://127.0.0.1:{0} -H "Host: www.example.com:{0}" --verbose'.format(ts.Variables.ssl_port), ts=ts) -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Streams.stderr = "gold/remap-https-200.gold" - -# www.example3.com (match on receive port) -tr = Test.AddTestRun() -tr.MakeCurlCommand('--http1.1 -k https://127.0.0.1:{0} -H "Host: www.example3.com" --verbose'.format(ts.Variables.ssl_port), ts=ts) -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Streams.stderr = "gold/remap-https-200_3.gold" - -# no rule for this -tr = Test.AddTestRun() -tr.MakeCurlCommand('--http1.1 -k https://127.0.0.1:{0} -H "Host: www.test.com" --verbose'.format(ts.Variables.ssl_port), ts=ts) -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Streams.stderr = "gold/remap-hitATS-404.gold" - -# bad port -tr = Test.AddTestRun() -tr.MakeCurlCommand( - '--http1.1 -k https://127.0.0.1:{0} -H "Host: www.example.com:1234" --verbose'.format(ts.Variables.ssl_port), ts=ts) -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Streams.stderr = "gold/remap-hitATS-404.gold" - -# map www.anotherexample.com to https://.com -tr = Test.AddTestRun() -tr.MakeCurlCommand( - '--http1.1 -k https://127.0.0.1:{0} -H "Host: www.anotherexample.com" --verbose'.format(ts.Variables.ssl_port), ts=ts) -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Streams.stderr = "gold/remap-https-200_2.gold" -tr.StillRunningAfter = server2 diff --git a/tests/gold_tests/remap_yaml/remap_ip_resolve_yaml.test.py b/tests/gold_tests/remap_yaml/remap_ip_resolve_yaml.test.py deleted file mode 100644 index 4cdc3c0122d..00000000000 --- a/tests/gold_tests/remap_yaml/remap_ip_resolve_yaml.test.py +++ /dev/null @@ -1,89 +0,0 @@ -''' -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -Test.Summary = ''' -Test a basic ip_resolve override using an ipv6 server -''' - -Test.ContinueOnFail = True -# Define default ATS -ts = Test.MakeATSProcess("ts") -server = Test.MakeOriginServer("server") -server_v6 = Test.MakeOriginServer("server_v6", None, None, '::1', 0) - -dns = Test.MakeDNServer("dns") - -Test.testName = "" -request_header = {"headers": "GET / HTTP/1.1\r\nHost: www.example.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -# expected response from the origin server -response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} - -# add response to the server dictionary -server.addResponse("sessionfile.log", request_header, response_header) -server_v6.addResponse("sessionfile.log", request_header, response_header) -ts.Disk.records_config.update( - { - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'http.*|dns|conf_remap', - 'proxy.config.http.referer_filter': 1, - 'proxy.config.dns.nameservers': '127.0.0.1:{0}'.format(dns.Variables.Port), - 'proxy.config.dns.resolv_conf': 'NULL', - 'proxy.config.hostdb.ip_resolve': 'ipv4' - }) - -ts.Disk.remap_yaml.AddLines( - f''' -remap: - - type: map - from: - url: http://testDNS.com - to: - url: http://test.ipv4.only.com:{server.Variables.Port} - plugins: - - name: conf_remap.so - params: - - proxy.config.hostdb.ip_resolve=ipv6;ipv4;client - - type: map - from: - url: http://testDNS2.com - to: - url: http://test.ipv6.only.com:{server_v6.Variables.Port} - plugins: - - name: conf_remap.so - params: - - proxy.config.hostdb.ip_resolve=ipv6;only - '''.split("\n")) - -dns.addRecords(records={"test.ipv4.only.com.": ["127.0.0.1"]}) -dns.addRecords(records={"test.ipv6.only.com": ["127.0.0.1", "::1"]}) - -tr = Test.AddTestRun() -tr.MakeCurlCommand(' --proxy 127.0.0.1:{0} "http://testDNS.com" --verbose'.format(ts.Variables.port), ts=ts) -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.StartBefore(server) -tr.Processes.Default.StartBefore(dns) -tr.Processes.Default.StartBefore(Test.Processes.ts) -tr.Processes.Default.Streams.stderr = "gold/remap-DNS-200.gold" -tr.StillRunningAfter = server - -tr = Test.AddTestRun() -tr.MakeCurlCommand(' --proxy 127.0.0.1:{0} "http://testDNS2.com" --verbose'.format(ts.Variables.port), ts=ts) -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.StartBefore(server_v6) -tr.Processes.Default.Streams.stderr = "gold/remap-DNS-ipv6-200.gold" -tr.StillRunningAfter = server_v6 diff --git a/tests/gold_tests/remap_yaml/remap_load_empty_failure_yaml.test.py b/tests/gold_tests/remap_yaml/remap_load_empty_failure_yaml.test.py deleted file mode 100644 index 03991886426..00000000000 --- a/tests/gold_tests/remap_yaml/remap_load_empty_failure_yaml.test.py +++ /dev/null @@ -1,42 +0,0 @@ -''' -Verify correct behavior of regex_map in remap.yaml. -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -Test.Summary = ''' -Test minimum rules on load - fail on missing file. -''' -ts = Test.MakeATSProcess("ts") -ts.Disk.remap_yaml.AddLine(f"") # empty file -ts.Disk.records_config.update({'proxy.config.url_remap.min_rules_required': 1}) -ts.ReturnCode = 33 # expect to Emergency fail due to empty "remap.config". -ts.Ready = 0 - -tr = Test.AddTestRun("test") - -# We have to wait upon TS to emit the expected log message, but it cannot be -# the ts Ready criteria because autest might detect the process going away -# before it detects the log message. So we add a separate process that waits -# upon the log message. -watcher = Test.Processes.Process("watcher") -watcher.Command = "sleep 10" -watcher.Ready = When.FileContains(ts.Disk.diags_log.Name, "remap.yaml failed to load") -watcher.StartBefore(ts) - -tr.Processes.Default.Command = "echo howdy" -tr.TimeOut = 5 -tr.Processes.Default.StartBefore(watcher) diff --git a/tests/gold_tests/remap_yaml/remap_load_empty_success_yaml.test.py b/tests/gold_tests/remap_yaml/remap_load_empty_success_yaml.test.py deleted file mode 100644 index 596973bde61..00000000000 --- a/tests/gold_tests/remap_yaml/remap_load_empty_success_yaml.test.py +++ /dev/null @@ -1,33 +0,0 @@ -''' -Verify correct behavior of regex_map in remap.yaml. -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -Test.Summary = ''' -Test minimum rules on load - succeed on empty file. -''' - -ts = Test.MakeATSProcess("ts") -ts.Disk.remap_yaml.AddLine(f"") # empty file -ts.Disk.records_config.update({f'proxy.config.url_remap.min_rules_required': 0}) - -tr = Test.AddTestRun("startup") -p = tr.Processes.Default -p.Command = "sh -c echo" -p.ReturnCode = 0 -p.StartBefore(Test.Processes.ts) -tr.StillRunningAfter = ts diff --git a/tests/gold_tests/remap_yaml/remap_load_missing_failure_yaml.test.py b/tests/gold_tests/remap_yaml/remap_load_missing_failure_yaml.test.py deleted file mode 100644 index 8f38e7644e2..00000000000 --- a/tests/gold_tests/remap_yaml/remap_load_missing_failure_yaml.test.py +++ /dev/null @@ -1,29 +0,0 @@ -''' -Verify correct behavior of regex_map in remap.yaml. -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -Test.Summary = ''' -Test minimum rules on load - fail on missing file. -''' -ts = Test.MakeATSProcess("ts") -ts.Disk.records_config.update({'proxy.config.url_remap.min_rules_required': 1}) -ts.ReturnCode = 33 # expect to Emergency fail due to empty "remap.config". - -tr = Test.AddTestRun("test") -tr.Processes.Default.Command = "echo" -tr.Processes.Default.StartAfter(ts, ready=When.FileExists(ts.Disk.diags_log)) diff --git a/tests/gold_tests/remap_yaml/remap_load_missing_success_yaml.test.py b/tests/gold_tests/remap_yaml/remap_load_missing_success_yaml.test.py deleted file mode 100644 index 1b2b519c645..00000000000 --- a/tests/gold_tests/remap_yaml/remap_load_missing_success_yaml.test.py +++ /dev/null @@ -1,33 +0,0 @@ -''' -Verify correct behavior of regex_map in remap.yaml. -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -Test.Summary = ''' -Test minimum rules on load - succeed on missing file. -''' - -ts = Test.MakeATSProcess("ts") - -ts.Disk.records_config.update({f'proxy.config.url_remap.min_rules_required': 0}) - -tr = Test.AddTestRun("startup") -p = tr.Processes.Default -p.Command = "sh -c echo" -p.ReturnCode = 0 -p.StartBefore(Test.Processes.ts) -tr.StillRunningAfter = ts diff --git a/tests/gold_tests/remap_yaml/remap_reload_yaml.test.py b/tests/gold_tests/remap_yaml/remap_reload_yaml.test.py deleted file mode 100644 index af45fde8f96..00000000000 --- a/tests/gold_tests/remap_yaml/remap_reload_yaml.test.py +++ /dev/null @@ -1,155 +0,0 @@ -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. -import os - - -def update_remap_yaml(path: str, lines: list) -> None: - """Update the remap.yaml file. - - This is used to update the config file between test runs without - triggering framework warnings about overriding file objects. - - :param path: The path to the remap.config file. - :param lines: The list of lines to write to the file. - """ - with open(path, 'w') as f: - f.write('\n'.join(lines) + '\n') - - -Test.Summary = ''' -Test remap reloading -''' -Test.testName = 'remap_reload' - -replay_file_1 = "reload_1.replay.yaml" -replay_file_2 = "reload_2.replay.yaml" -replay_file_3 = "reload_3.replay.yaml" -replay_file_4 = "reload_4.replay.yaml" - -tm = Test.MakeATSProcess("ts", disable_log_checks=True) -remap_cfg_path = os.path.join(tm.Variables.CONFIGDIR, 'remap.yaml') - -pv = Test.MakeVerifierServerProcess("pv", "reload_server.replay.yaml") -pv_port = pv.Variables.http_port - -tm.Disk.remap_yaml.AddLines( - f''' -remap: - - type: map - from: - url: http://alpha.ex - to: - url: http://alpha.ex:{pv_port} - - type: map - from: - url: http://bravo.ex - to: - url: http://bravo.ex:{pv_port} - - type: map - from: - url: http://charlie.ex - to: - url: http://charlie.ex:{pv_port} - - type: map - from: - url: http://delta.ex - to: - url: http://delta.ex:{pv_port} - '''.split("\n")) - -tm.Disk.records_config.update({'proxy.config.url_remap.min_rules_required': 3}) - -nameserver = Test.MakeDNServer("dns", default='127.0.0.1') -tm.Disk.records_config.update( - { - 'proxy.config.dns.nameservers': f"127.0.0.1:{nameserver.Variables.Port}", - 'proxy.config.dns.resolv_conf': 'NULL' - }) - -tr = Test.AddTestRun("verify load") -tr.Processes.Default.StartBefore(pv) -tr.Processes.Default.StartBefore(nameserver) -tr.Processes.Default.StartBefore(tm) -tr.AddVerifierClientProcess("client", replay_file_1, http_ports=[tm.Variables.port]) - -tr = Test.AddTestRun("Change remap.yaml to have only two remap rules") -p = tr.Processes.Default -p.Env = tm.Env -p.Command = 'echo "Change remap.yaml, two lines"' -p.Setup.Lambda( - lambda: update_remap_yaml( - remap_cfg_path, f''' -remap: - - type: map - from: - url: http://alpha.ex - to: - url: http://alpha.ex:{pv_port} - - type: map - from: - url: http://bravo.ex - to: - url: http://bravo.ex:{pv_port} - '''.split("\n"))) - -tr = Test.AddConfigReload(tm, expect="fail", expect_tasks=["remap.yaml"], delay_start=2, description="remap_yaml reload, fails") - -tr = Test.AddTestRun("after first reload") -tr.AddVerifierClientProcess("client_2", replay_file_2, http_ports=[tm.Variables.port]) - -tr = Test.AddTestRun("Change remap.yaml to have more than three remap rules") -p = tr.Processes.Default -p.Env = tm.Env -p.Command = 'echo "Change remap.yaml, more than three lines"' -p.Setup.Lambda( - lambda: update_remap_yaml( - remap_cfg_path, f''' -remap: - - type: map - from: - url: http://echo.ex - to: - url: http://echo.ex:{pv_port} - - type: map - from: - url: http://foxtrot.ex - to: - url: http://foxtrot.ex:{pv_port} - - type: map - from: - url: http://golf.ex - to: - url: http://golf.ex:{pv_port} - - type: map - from: - url: http://hotel.ex - to: - url: http://hotel.ex:{pv_port} - - type: map - from: - url: http://india.ex - to: - url: http://india.ex:{pv_port} - '''.split("\n"))) - -tr = Test.AddConfigReload( - tm, expect="success", expect_tasks=["remap.yaml"], delay_start=2, description="remap_yaml reload, succeeds") - -tr = Test.AddTestRun("post update charlie") -tr.AddVerifierClientProcess("client_3", replay_file_3, http_ports=[tm.Variables.port]) - -tr = Test.AddTestRun("post update golf") -tr.AddVerifierClientProcess("client_4", replay_file_4, http_ports=[tm.Variables.port]) diff --git a/tests/gold_tests/remap_yaml/remap_ws_yaml.test.py b/tests/gold_tests/remap_yaml/remap_ws_yaml.test.py deleted file mode 100644 index c80312a062f..00000000000 --- a/tests/gold_tests/remap_yaml/remap_ws_yaml.test.py +++ /dev/null @@ -1,135 +0,0 @@ -''' -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -Test.Summary = ''' -Test a basic remap of a websocket connections -''' - -Test.ContinueOnFail = True - -ts = Test.MakeATSProcess("ts", enable_tls=True) -server = Test.MakeOriginServer("server") - -testName = "Test WebSocket Remaps" -request_header = { - "headers": "GET /chat HTTP/1.1\r\nHost: www.example.com\r\nUpgrade: websocket\r\nConnection: Upgrade\r\n\r\n", - "body": None -} -response_header = { - "headers": - "HTTP/1.1 101 OK\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=\r\n\r\n", - "body": None -} -server.addResponse("sessionlog.json", request_header, response_header) - -ts.addDefaultSSLFiles() - -ts.Disk.records_config.update( - { - 'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir), - 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts.Variables.SSLDir), - }) - -ts.Disk.remap_yaml.AddLines( - f''' -remap: - - type: map - from: - url: ws://www.example.com:{ts.Variables.port} - to: - url: ws://127.0.0.1:{server.Variables.Port} - - type: map - from: - url: wss://www.example.com:{ts.Variables.ssl_port} - to: - url: ws://127.0.0.1:{server.Variables.Port} - '''.split("\n")) - -ts.Disk.ssl_multicert_yaml.AddLines( - """ -ssl_multicert: - - dest_ip: "*" - ssl_cert_name: server.pem - ssl_key_name: server.key -""".split("\n")) - -if not Condition.CurlUsingUnixDomainSocket(): - # wss mapping - tr = Test.AddTestRun() - tr.Processes.Default.StartBefore(server) - tr.Processes.Default.StartBefore(Test.Processes.ts, ready=1) - tr.MakeCurlCommand( - '--max-time 2 -v -s -q -H "Connection: Upgrade" -H "Upgrade: websocket" -H "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==" -H "Sec-WebSocket-Version: 13" --http1.1 --resolve www.example.com:{0}:127.0.0.1 -k https://www.example.com:{0}/chat' - .format(ts.Variables.ssl_port), - ts=ts) - tr.Processes.Default.ReturnCode = 28 - tr.Processes.Default.Streams.stderr = "gold/remap-ws-upgrade.gold" - tr.StillRunningAfter = server - tr.StillRunningAfter = ts - -# ws mapping -tr = Test.AddTestRun() -if Condition.CurlUsingUnixDomainSocket(): - tr.Processes.Default.StartBefore(server) - tr.Processes.Default.StartBefore(Test.Processes.ts, ready=1) -tr.MakeCurlCommand( - '--max-time 2 -v -s -q -H "Connection: Upgrade" -H "Upgrade: websocket" -H "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==" -H "Sec-WebSocket-Version: 13" --http1.1 --resolve www.example.com:{0}:127.0.0.1 -k http://www.example.com:{0}/chat' - .format(ts.Variables.port), - ts=ts) -tr.Processes.Default.ReturnCode = 28 -tr.Processes.Default.Streams.stderr = "gold/remap-ws-upgrade.gold" -tr.StillRunningAfter = server -tr.StillRunningAfter = ts - -# Missing required headers (should result in 400) -tr = Test.AddTestRun() -tr.MakeCurlCommand( - '--max-time 2 -v -s -q -H "Connection: Upgrade" -H "Upgrade: websocket" --http1.1 --resolve www.example.com:{0}:127.0.0.1 -k http://www.example.com:{0}/chat' - .format(ts.Variables.port), - ts=ts) -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Streams.stderr = "gold/remap-ws-upgrade-400.gold" -tr.StillRunningAfter = server -tr.StillRunningAfter = ts - -# Test metrics -tr = Test.AddTestRun() -if Condition.CurlUsingUnixDomainSocket(): - metrics_gold_file = 'remap-ws-metrics-uds.gold' -else: - metrics_gold_file = 'remap-ws-metrics.gold' -tr.Processes.Default.Command = ( - f"{Test.Variables.AtsTestToolsDir}/stdout_wait" + " 'traffic_ctl metric get" + - " proxy.process.http.total_incoming_connections" + " proxy.process.http.total_client_connections" + - " proxy.process.http.total_client_connections_ipv4" + " proxy.process.http.total_client_connections_ipv6" + - " proxy.process.http.total_server_connections" + " proxy.process.http2.total_client_connections" + - " proxy.process.http.connect_requests" + " proxy.process.tunnel.total_client_connections_blind_tcp" + - " proxy.process.tunnel.current_client_connections_blind_tcp" + " proxy.process.tunnel.total_server_connections_blind_tcp" + - " proxy.process.tunnel.current_server_connections_blind_tcp" + " proxy.process.tunnel.total_client_connections_tls_tunnel" + - " proxy.process.tunnel.current_client_connections_tls_tunnel" + " proxy.process.tunnel.total_client_connections_tls_forward" + - " proxy.process.tunnel.current_client_connections_tls_forward" + - " proxy.process.tunnel.total_client_connections_tls_partial_blind" + - " proxy.process.tunnel.current_client_connections_tls_partial_blind" + - " proxy.process.tunnel.total_client_connections_tls_http" + " proxy.process.tunnel.current_client_connections_tls_http" + - " proxy.process.tunnel.total_server_connections_tls" + " proxy.process.tunnel.current_server_connections_tls'" + - f" {Test.TestDirectory}/gold/{metrics_gold_file}") -# Need to copy over the environment so traffic_ctl knows where to find the unix domain socket -tr.Processes.Default.Env = ts.Env -tr.Processes.Default.ReturnCode = 0 -tr.StillRunningAfter = server -tr.StillRunningAfter = ts diff --git a/tests/gold_tests/runroot/runroot_error.test.py b/tests/gold_tests/runroot/runroot_error.test.py deleted file mode 100644 index dc0a26b2cf1..00000000000 --- a/tests/gold_tests/runroot/runroot_error.test.py +++ /dev/null @@ -1,58 +0,0 @@ -''' -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import os - -Test.Summary = ''' -Test for expected error and failure of runroot from traffic_layout. -''' -Test.ContinueOnFail = True - -# create runroot -path = os.path.join(Test.RunDirectory, "runroot") -tr = Test.AddTestRun() -tr.Processes.Default.Command = "$ATS_BIN/traffic_layout init --path " + path -f = tr.Disk.File(os.path.join(path, "runroot.yaml")) -f.Exists = True - -# use existing runroot -tr = Test.AddTestRun("using existing runroot") -tr.Processes.Default.Command = "$ATS_BIN/traffic_layout init --path " + path -tr.Processes.Default.Streams.All = Testers.ContainsExpression("Using existing runroot", "init incorrect usage") -f = tr.Disk.File(os.path.join(path, "runroot.yaml")) -f.Exists = True - -# create runroot inside another -path_inside = os.path.join(path, "runroot") -tr = Test.AddTestRun("create runroot inside runroot") -tr.Processes.Default.Command = "$ATS_BIN/traffic_layout init --path " + path_inside -tr.Processes.Default.Streams.All = Testers.ContainsExpression( - "Cannot create runroot inside another runroot", "init incorrect usage") -f = tr.Disk.File(os.path.join(path_inside, "runroot.yaml")) -f.Exists = False - -# remove invalid runroot -path_invalid = os.path.join(Test.RunDirectory, "tmp") -tr = Test.AddTestRun("remove invalid runroot") -tr.Processes.Default.Command = "$ATS_BIN/traffic_layout remove --path " + path_invalid -tr.Processes.Default.Streams.All = Testers.ContainsExpression("Unable to read", "remove incorrect usage") - -# verify invalid runroot -tr = Test.AddTestRun("verify invalid runroot") -tr.Processes.Default.Command = "$ATS_BIN/traffic_layout verify --path " + path_invalid -tr.Processes.Default.Streams.All = Testers.ContainsExpression("Unable to read", "verify incorrect usage") diff --git a/tests/gold_tests/runroot/runroot_init.test.py b/tests/gold_tests/runroot/runroot_init.test.py deleted file mode 100644 index aed658d8883..00000000000 --- a/tests/gold_tests/runroot/runroot_init.test.py +++ /dev/null @@ -1,78 +0,0 @@ -''' -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import os - -Test.Summary = ''' -Test for init of runroot from traffic_layout. -''' -Test.ContinueOnFail = True -Test.SkipUnless( - Test.Variables.BINDIR.startswith(Test.Variables.PREFIX), "need to guarantee bin path starts with prefix for runroot") - -# init from pass in path -path1 = os.path.join(Test.RunDirectory, "runroot1") -tr = Test.AddTestRun("Test traffic_layout init #1") -tr.Processes.Default.Command = "$ATS_BIN/traffic_layout init --path " + path1 -tr.Processes.Default.ReturnCode = 0 -f = tr.Disk.File(os.path.join(path1, "runroot.yaml")) -f.Exists = True - -# init to relative directory -path2 = os.path.join(Test.RunDirectory, "runroot2") -tr = Test.AddTestRun("Test traffic_layout init #2") -tr.Processes.Default.Command = "cd " + Test.RunDirectory + ";$ATS_BIN/traffic_layout init --path runroot2" -tr.Processes.Default.ReturnCode = 0 -f = tr.Disk.File(os.path.join(path2, "runroot.yaml")) -f.Exists = True - -# init to cwd -path3 = os.path.join(Test.RunDirectory, "runroot3") -tr = Test.AddTestRun("Test traffic_layout init #3") -tr.Processes.Default.Command = "mkdir " + path3 + ";cd " + path3 + ";$ATS_BIN/traffic_layout init" -tr.Processes.Default.ReturnCode = 0 -f = tr.Disk.File(os.path.join(path3, "runroot.yaml")) -f.Exists = True - -# --force init to an non-empty directory -path4 = os.path.join(Test.RunDirectory, "runroot4") -tr = Test.AddTestRun("Test traffic_layout init #4") -randomfile = os.path.join(path4, "foo") -tr.Processes.Default.Command = "mkdir " + path4 + ";touch " + randomfile + ";$ATS_BIN/traffic_layout init --force --path " + path4 -tr.Processes.Default.ReturnCode = 0 -f = tr.Disk.File(os.path.join(path4, "runroot.yaml")) -f.Exists = True - -# create runroot with junk to guarantee only traffic server related files are copied -bin_path = Test.Variables.BINDIR[Test.Variables.BINDIR.find(Test.Variables.PREFIX) + len(Test.Variables.PREFIX) + 1:] -path5 = os.path.join(Test.RunDirectory, "runroot5") -exe_path = os.path.join(bin_path, "traffic_layout") -junk_file = os.path.join(bin_path, "junk") -junk = os.path.join(path1, junk_file) - -tr = Test.AddTestRun("Test traffic_layout init #5") -# create the junk files in runroot1 and create(init and copy) runroot5 from runroot 1 -tr.Processes.Default.Command = "touch " + junk + ";" + os.path.join(path1, exe_path) + " init --path " + path5 -tr.Processes.Default.ReturnCode = 0 -f = tr.Disk.File(os.path.join(path5, "runroot.yaml")) -f.Exists = True -# check if the junk file is created and not copied to the new runroot -f = tr.Disk.File(junk) -f.Exists = True -f = tr.Disk.File(os.path.join(path5, junk_file)) -f.Exists = False diff --git a/tests/gold_tests/runroot/runroot_remove.test.py b/tests/gold_tests/runroot/runroot_remove.test.py deleted file mode 100644 index 987836c8f58..00000000000 --- a/tests/gold_tests/runroot/runroot_remove.test.py +++ /dev/null @@ -1,70 +0,0 @@ -''' -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import os - -Test.Summary = ''' -Test for remove of runroot from traffic_layout. -''' -Test.ContinueOnFail = True - -# create three runroot for removing testing -path1 = os.path.join(Test.RunDirectory, "runroot1") -tr = Test.AddTestRun() -tr.Processes.Default.Command = "$ATS_BIN/traffic_layout init --path " + path1 -f = tr.Disk.File(os.path.join(path1, "runroot.yaml")) -f.Exists = True - -path2 = os.path.join(Test.RunDirectory, "runroot2") -tr = Test.AddTestRun() -tr.Processes.Default.Command = "$ATS_BIN/traffic_layout init --path " + path2 -f = tr.Disk.File(os.path.join(path2, "runroot.yaml")) -f.Exists = True - -path3 = os.path.join(Test.RunDirectory, "runroot3") -tr = Test.AddTestRun() -tr.Processes.Default.Command = "$ATS_BIN/traffic_layout init --path " + path3 -f = tr.Disk.File(os.path.join(path3, "runroot.yaml")) -f.Exists = True - -# normal remove from pass in path -tr = Test.AddTestRun("Test traffic_layout remove #1") -tr.Processes.Default.Command = "$ATS_BIN/traffic_layout remove --path " + path1 -tr.Processes.Default.ReturnCode = 0 -f = tr.Disk.File(os.path.join(path1, "runroot.yaml")) -f.Exists = False -d = tr.Disk.Directory(path1) -d.Exists = False - -# remove of relative path -tr = Test.AddTestRun("Test traffic_layout remove #2") -tr.Processes.Default.Command = "cd " + Test.RunDirectory + ";$ATS_BIN/traffic_layout remove --path runroot2" -tr.Processes.Default.ReturnCode = 0 -f = tr.Disk.File(os.path.join(path2, "runroot.yaml")) -f.Exists = False -d = tr.Disk.Directory(path2) -d.Exists = False - -# remove cwd -tr = Test.AddTestRun("Test traffic_layout remove #3") -tr.Processes.Default.Command = "mkdir " + path3 + ";cd " + path3 + ";$ATS_BIN/traffic_layout remove" -tr.Processes.Default.ReturnCode = 0 -f = tr.Disk.File(os.path.join(path3, "runroot.yaml")) -f.Exists = False -d = tr.Disk.Directory(path3) -d.Exists = True diff --git a/tests/gold_tests/runroot/runroot_use.test.py b/tests/gold_tests/runroot/runroot_use.test.py deleted file mode 100644 index cf87266ceca..00000000000 --- a/tests/gold_tests/runroot/runroot_use.test.py +++ /dev/null @@ -1,65 +0,0 @@ -''' -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import os - -Test.Summary = ''' -Test for using of runroot from traffic_layout. -''' -Test.ContinueOnFail = True -Test.SkipUnless( - Test.Variables.BINDIR.startswith(Test.Variables.PREFIX), "need to guarantee bin path starts with prefix for runroot") - -# create two runroot for testing -path = os.path.join(Test.RunDirectory, "runroot") -tr = Test.AddTestRun() -tr.Processes.Default.Command = "$ATS_BIN/traffic_layout init --path " + path -f = tr.Disk.File(os.path.join(path, "runroot.yaml")) -f.Exists = True - -path2 = os.path.join(Test.RunDirectory, "runroot2") -tr = Test.AddTestRun() -tr.Processes.Default.Command = "$ATS_BIN/traffic_layout init --path " + path2 -f = tr.Disk.File(os.path.join(path2, "runroot.yaml")) -f.Exists = True - -# 1. --run-root use path cmd -tr = Test.AddTestRun("use runroot via commandline") -tr.Processes.Default.Command = "$ATS_BIN/traffic_layout info --run-root=" + path -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Streams.All = Testers.ContainsExpression("PREFIX: " + path, "commandline runroot path") - -# 2. use cwd as runroot -tr = Test.AddTestRun("use runroot via cwd") -tr.Processes.Default.Command = "cd " + path + ";" + "$ATS_BIN/traffic_layout info" -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Streams.All = Testers.ContainsExpression("PREFIX: " + path, "cwd runroot path") - -# 4. use path directly bin -bin_path = Test.Variables.BINDIR[Test.Variables.BINDIR.find(Test.Variables.PREFIX) + len(Test.Variables.PREFIX) + 1:] -tr = Test.AddTestRun("use runroot via bin executable") -tr.Processes.Default.Command = os.path.join(path, os.path.join(bin_path, "traffic_layout") + " info") -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Streams.All = Testers.ContainsExpression("PREFIX: " + path, "bin path") - -# 3. TS_RUNROOT ENV variable -tr = Test.AddTestRun("use runroot via TS_RUNROOT") -tr.Processes.Default.Env["TS_RUNROOT"] = path2 -tr.Processes.Default.Command = "$ATS_BIN/traffic_layout info" -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Streams.All = Testers.ContainsExpression("PREFIX: " + path2, "$TS_RUNROOT Env path") diff --git a/tests/gold_tests/runroot/runroot_verify.test.py b/tests/gold_tests/runroot/runroot_verify.test.py deleted file mode 100644 index 11d254bf7de..00000000000 --- a/tests/gold_tests/runroot/runroot_verify.test.py +++ /dev/null @@ -1,65 +0,0 @@ -''' -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import os - -Test.Summary = ''' -Test for verify of runroot from traffic_layout. -''' - -prefix = Test.Variables["PREFIX"] -bindir = Test.Variables["BINDIR"] -logdir = Test.Variables["LOGDIR"] - -if bindir.startswith(prefix): - # get the bin directory based on removing the common prefix - binsuffix = bindir[len(prefix) + 1:] -else: - # given a custom setup this might work.. or it might not - binsuffix = bindir - -if logdir.startswith(prefix): - # get the bin directory based on removing the common prefix - logsuffix = bindir[len(prefix) + 1:] -else: - # given a custom setup this might work.. or it might not - logsuffix = logdir - -# create runroot -path = os.path.join(Test.RunDirectory, "runroot") -tr = Test.AddTestRun("Create runroot") -tr.Processes.Default.Command = "$ATS_BIN/traffic_layout init --path " + path -f = tr.Disk.File(os.path.join(path, "runroot.yaml")) -f.Exists = True - -# verify test #1 -tr = Test.AddTestRun("verify runroot test1") -tr.Processes.Default.Command = "$ATS_BIN/traffic_layout verify --path " + path - -tr.Processes.Default.Streams.All = Testers.ContainsExpression(os.path.join(path, binsuffix), "example bindir output") -tr.Processes.Default.Streams.All = Testers.ContainsExpression(os.path.join(path, logsuffix), "example logdir output") -tr.Processes.Default.Streams.All = Testers.ContainsExpression("PASSED", "contain passed message") - -# verify test #2 -bin_path = Test.Variables.BINDIR[Test.Variables.BINDIR.find(Test.Variables.PREFIX) + len(Test.Variables.PREFIX) + 1:] -tr = Test.AddTestRun("verify runroot test2") -tr.Processes.Default.Command = "cd " + path + ";" + os.path.join(bin_path, "traffic_layout") + " verify --path " + path - -tr.Processes.Default.Streams.All = Testers.ContainsExpression(os.path.join(path, binsuffix), "example bindir output") -tr.Processes.Default.Streams.All = Testers.ContainsExpression(os.path.join(path, logsuffix), "example logdir output") -tr.Processes.Default.Streams.All = Testers.ContainsExpression("PASSED", "contain passed message") diff --git a/tests/gold_tests/session_sharing/session_match.test.py b/tests/gold_tests/session_sharing/session_match.test.py deleted file mode 100644 index 5844502a5f5..00000000000 --- a/tests/gold_tests/session_sharing/session_match.test.py +++ /dev/null @@ -1,123 +0,0 @@ -''' -Test that a plugin can modify server session sharing. -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - - -class SessionMatchTest: - TestCounter = 0 - - def __init__(self, TestSummary, sharingMatchValue): - SessionMatchTest.TestCounter += 1 - self._MyTestCount = SessionMatchTest.TestCounter - Test.Summary = TestSummary - self._tr = Test.AddTestRun() - self._sharingMatchValue = sharingMatchValue - self.setupOriginServer() - self.setupTS() - - def setupOriginServer(self): - self._server = Test.MakeOriginServer("server{counter}".format(counter=self._MyTestCount)) - request_header = { - "headers": "GET /one HTTP/1.1\r\nHost: www.example.com\r\nContent-Length: 0\r\n\r\n", - "timestamp": "1469733493.993", - "body": "" - } - response_header = { - "headers": "HTTP/1.1 200 OK\r\nServer: microserver\r\n" - "Content-Length: 0\r\n\r\n", - "timestamp": "1469733493.993", - "body": "" - } - self._server.addResponse("sessionlog.json", request_header, response_header) - - request_header2 = { - "headers": "GET /two HTTP/1.1\r\nContent-Length: 0\r\n" - "Host: www.example.com\r\n\r\n", - "timestamp": "1469733493.993", - "body": "a\r\na\r\na\r\n\r\n" - } - response_header2 = { - "headers": "HTTP/1.1 200 OK\r\nServer: microserver\r\n" - "Content-Length: 0\r\n\r\n", - "timestamp": "1469733493.993", - "body": "" - } - self._server.addResponse("sessionlog.json", request_header2, response_header2) - - request_header3 = { - "headers": "GET /three HTTP/1.1\r\nContent-Length: 0\r\n" - "Host: www.example.com\r\nConnection: close\r\n\r\n", - "timestamp": "1469733493.993", - "body": "a\r\na\r\na\r\n\r\n" - } - response_header3 = { - "headers": "HTTP/1.1 200 OK\r\nServer: microserver\r\n" - "Connection: close\r\nContent-Length: 0\r\n\r\n", - "timestamp": "1469733493.993", - "body": "" - } - self._server.addResponse("sessionlog.json", request_header3, response_header3) - - def setupTS(self): - self._ts = Test.MakeATSProcess("ts{counter}".format(counter=self._MyTestCount)) - self._ts.Disk.remap_config.AddLine('map / http://127.0.0.1:{0}'.format(self._server.Variables.Port)) - self._ts.Disk.records_config.update( - { - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'http', - 'proxy.config.http.auth_server_session_private': 1, - 'proxy.config.http.server_session_sharing.pool': 'global', - 'proxy.config.http.server_session_sharing.match': self._sharingMatchValue, - }) - - def _runTraffic(self): - self._tr.MakeCurlCommandMulti( - '{{curl}} -v -H\'Host: www.example.com\' -H\'Connection: close\' http://127.0.0.1:{port}/one &&' - '{{curl}} -v -H\'Host: www.example.com\' -H\'Connection: close\' http://127.0.0.1:{port}/two &&' - '{{curl}} -v -H\'Host: www.example.com\' -H\'Connection: close\' http://127.0.0.1:{port}/three'.format( - port=self._ts.Variables.port), - ts=self._ts) - self._tr.Processes.Default.ReturnCode = 0 - self._tr.Processes.Default.StartBefore(self._server) - self._tr.Processes.Default.StartBefore(self._ts) - self._tr.Processes.Default.Streams.stderr = "gold/200.gold" - - def runAndExpectSharing(self): - self._runTraffic() - self._ts.Disk.traffic_out.Content = Testers.ContainsExpression( - "global pool search successful", "Verify that sessions got shared") - - def runAndExpectNoSharing(self): - self._runTraffic() - self._ts.Disk.traffic_out.Content = Testers.ExcludesExpression( - "global pool search successful", "Verify that sessions did not get shared") - - -sessionMatchTest = SessionMatchTest(TestSummary='Test that session sharing works with host matching', sharingMatchValue='host') -sessionMatchTest.runAndExpectSharing() - -sessionMatchTest = SessionMatchTest(TestSummary='Test that session sharing works with ip matching', sharingMatchValue='ip') -sessionMatchTest.runAndExpectSharing() - -sessionMatchTest = SessionMatchTest( - TestSummary='Test that session sharing works with matching both ip and host', sharingMatchValue='both') -sessionMatchTest.runAndExpectSharing() - -sessionMatchTest = SessionMatchTest( - TestSummary='Test that session sharing is disabled when matching is set to none', sharingMatchValue='none') -sessionMatchTest.runAndExpectNoSharing() diff --git a/tests/gold_tests/shutdown/crashlog_no_false_positive.test.py b/tests/gold_tests/shutdown/crashlog_no_false_positive.test.py deleted file mode 100644 index 0feb322f5ef..00000000000 --- a/tests/gold_tests/shutdown/crashlog_no_false_positive.test.py +++ /dev/null @@ -1,73 +0,0 @@ -''' -Verify that a normal (SIGTERM) shutdown does not spuriously fire traffic_crashlog. -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import os - -Test.Summary = ''' -Verify that gracefully stopping traffic_server (SIGTERM) does not cause the crash -log helper (traffic_crashlog) to emit a bogus crash log. - -Regression test: the crash log helper is parked with SIGSTOP and arms a -parent-death signal (PR_SET_PDEATHSIG=SIGCONT). When traffic_server exits -normally -- e.g. a routine SIGTERM shutdown -- the kernel resumes the helper via -that death signal. The helper used to decide "did I wake because of a real crash -or a plain exit?" with a racy getppid() check, and would lose the race and write -an empty crash log against the already-exiting process. The fix replaces that -guess with a handshake: crash_logger_invoke() writes the signal number to the -helper, and a plain exit closes the socket so the helper reads EOF and stays -quiet. - -Note: PR_SET_PDEATHSIG is a no-op on Darwin, so this false positive only occurs -on Linux. On other platforms this test still confirms that the helper is armed -and does not emit a crash log on a clean shutdown. -''' - -ts = Test.MakeATSProcess('ts') - -ts.Disk.records_config.update( - { - # Enable the "server" debug tag so traffic_server emits the "received exit signal, - # shutting down" DIAG line (the shutdown anchor checked below) to traffic.out. - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'server', - # Force the crash log helper on regardless of the build's TS_USE_REMOTE_UNWINDING - # default (it is NULL on platforms without remote unwinding, e.g. Darwin). - 'proxy.config.crash_log_helper': os.path.join(ts.Variables.BINDIR, 'traffic_crashlog'), - }) - -tr = Test.AddTestRun('Start traffic_server with the crash log helper armed, then let AuTest SIGTERM it') -tr.Processes.Default.Command = 'printf "crash log helper armed"' -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.StartBefore(ts) -tr.StillRunningAfter = ts - -# Anchor the scenario: confirm traffic_server actually ran the graceful (SIGTERM) shutdown -# path -- the exact trigger that woke the crash log helper in production. Without this the -# ExcludesExpression checks below could pass without ever exercising shutdown. (The crash -# log helper itself is forced on via proxy.config.crash_log_helper above, and debug/DIAG -# output is emitted to traffic.out.) -ts.Disk.traffic_out.Content = Testers.ContainsExpression( - 'received exit signal, shutting down', 'traffic_server should have run the graceful shutdown path') - -# The crash log helper shares traffic_server's stderr (bound to traffic.out). On a clean -# shutdown it must neither wake nor write a crash log. -ts.Disk.traffic_out.Content += Testers.ExcludesExpression( - 'crashlog started', 'the crash log helper must not wake on a clean SIGTERM shutdown') -ts.Disk.traffic_out.Content += Testers.ExcludesExpression( - 'wrote crash log', 'a clean SIGTERM shutdown must not produce a crash log') diff --git a/tests/gold_tests/shutdown/emergency.test.py b/tests/gold_tests/shutdown/emergency.test.py deleted file mode 100644 index 6a0dff5fa79..00000000000 --- a/tests/gold_tests/shutdown/emergency.test.py +++ /dev/null @@ -1,62 +0,0 @@ -''' -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import os - -Test.Summary = 'Test TSEmergency API' -Test.ContinueOnFail = True - -# Define default ATS -ts = Test.MakeATSProcess('ts') - -Test.testName = 'Emergency Shutdown Test' - -ts.Disk.records_config.update( - { - 'proxy.config.exec_thread.autoconfig.enabled': 0, - 'proxy.config.exec_thread.autoconfig.scale': 1.5, - 'proxy.config.exec_thread.limit': 16, - 'proxy.config.accept_threads': 1, - 'proxy.config.task_threads': 2, - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'TSEmergency_test' - }) - -# Load plugin -Test.PrepareTestPlugin(os.path.join(Test.Variables.AtsTestPluginsDir, 'emergency_shutdown.so'), ts) - -tr = Test.AddTestRun() - -# We have to wait upon TS to emit the expected log message, but it cannot be -# the ts Ready criteria because autest might detect the process going away -# before it detects the log message. So we add a separate process that waits -# upon the log message. -watcher = Test.Processes.Process("watcher") -watcher.Command = "sleep 10" -watcher.Ready = When.FileContains(ts.Disk.diags_log.Name, "testing emergency shutdown") -watcher.StartBefore(ts) - -tr.Processes.Default.Command = 'printf "Emergency Shutdown Test"' -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.StartBefore(watcher) - -tr.Timeout = 5 -ts.ReturnCode = 33 -ts.Ready = 0 -ts.Disk.traffic_out.Content = Testers.ExcludesExpression('failed to shutdown', 'should NOT contain "failed to shutdown"') -ts.Disk.diags_log.Content = Testers.IncludesExpression('testing emergency shutdown', 'should contain "testing emergency shutdown"') diff --git a/tests/gold_tests/shutdown/fatal.test.py b/tests/gold_tests/shutdown/fatal.test.py deleted file mode 100644 index cff3c9fefbf..00000000000 --- a/tests/gold_tests/shutdown/fatal.test.py +++ /dev/null @@ -1,62 +0,0 @@ -''' -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import os - -Test.Summary = 'Test TSFatal API' -Test.ContinueOnFail = True - -# Define default ATS -ts = Test.MakeATSProcess('ts') - -Test.testName = 'Fatal Shutdown Test' - -ts.Disk.records_config.update( - { - 'proxy.config.exec_thread.autoconfig.enabled': 0, - 'proxy.config.exec_thread.autoconfig.scale': 1.5, - 'proxy.config.exec_thread.limit': 16, - 'proxy.config.accept_threads': 1, - 'proxy.config.task_threads': 2, - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'TSFatal_test' - }) - -# Load plugin -Test.PrepareTestPlugin(os.path.join(Test.Variables.AtsTestPluginsDir, 'fatal_shutdown.so'), ts) - -tr = Test.AddTestRun() - -# We have to wait upon TS to emit the expected log message, but it cannot be -# the ts Ready criteria because autest might detect the process going away -# before it detects the log message. So we add a separate process that waits -# upon the log message. -watcher = Test.Processes.Process("watcher") -watcher.Command = "sleep 10" -watcher.Ready = When.FileContains(ts.Disk.diags_log.Name, "testing fatal shutdown") -watcher.StartBefore(ts) - -tr.Processes.Default.Command = 'printf "Fatal Shutdown Test"' -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.StartBefore(watcher) - -tr.Timeout = 5 -ts.ReturnCode = 70 -ts.Ready = 0 -ts.Disk.traffic_out.Content = Testers.ExcludesExpression('failed to shutdown', 'should NOT contain "failed to shutdown"') -ts.Disk.diags_log.Content = Testers.IncludesExpression('testing fatal shutdown', 'should contain "testing fatal shutdown"') diff --git a/tests/gold_tests/slow_post/quick_server.test.py b/tests/gold_tests/slow_post/quick_server.test.py deleted file mode 100644 index fe250407987..00000000000 --- a/tests/gold_tests/slow_post/quick_server.test.py +++ /dev/null @@ -1,134 +0,0 @@ -"""Verify ATS handles a server that replies before receiving a full request.""" - -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import os -from ports import get_port -import sys - -Test.Summary = __doc__ - - -class QuickServerTest: - """Verify that ATS doesn't delay responses behind slow posts.""" - - _init_file = '__init__.py' - _http_utils = 'http_utils.py' - _slow_post_client = 'slow_post_client.py' - _quick_server = 'quick_server.py' - - _dns_counter = 0 - _server_counter = 0 - _ts_counter = 0 - - def __init__(self, abort_request: bool, drain_request: bool, abort_response_headers: bool): - """Initialize the test. - - :param drain_request: Whether the server should drain the request body. - :param abort_request: Whether the client should abort the request body. - before disconnecting. - """ - self._should_drain_request = drain_request - self._should_abort_request = abort_request - self._should_abort_response_headers = abort_response_headers - - def _configure_dns(self, tr: 'TestRun') -> None: - """Configure the DNS. - - :param tr: The test run to associate with the DNS process with. - """ - self._dns = tr.MakeDNServer(f'dns-{QuickServerTest._dns_counter}', default='127.0.0.1') - QuickServerTest._dns_counter += 1 - - def _configure_server(self, tr: 'TestRun'): - """Configure the origin server. - - This server replies with a response immediately after receiving the - request headers. - - :param tr: The test run to associate with the server process with. - """ - server = tr.Processes.Process(f'server-{QuickServerTest._server_counter}') - QuickServerTest._server_counter += 1 - port = get_port(server, "http_port") - server.Command = \ - f'{sys.executable} {self._quick_server} 127.0.0.1 {port} ' - if self._should_drain_request: - server.Command += '--drain-request ' - if self._should_abort_response_headers: - server.Command += '--abort-response-headers ' - server.Ready = When.PortOpenv4(port) - self._server = server - - def _configure_traffic_server(self, tr: 'TestRun'): - """Configure ATS. - - :param tr: The test run to associate with the ATS process with. - """ - self._ts = tr.MakeATSProcess(f'ts-{QuickServerTest._ts_counter}') - QuickServerTest._ts_counter += 1 - self._ts.Disk.remap_config.AddLine(f'map / http://quick.server.com:{self._server.Variables.http_port}') - self._ts.Disk.records_config.update( - { - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'http|dns|hostdb', - 'proxy.config.dns.nameservers': f'127.0.0.1:{self._dns.Variables.Port}', - 'proxy.config.dns.resolv_conf': 'NULL', - }) - - def run(self): - """Run the test.""" - tr = Test.AddTestRun( - f'Aborting request: {self._should_abort_request}, ' - f'Draining request: {self._should_drain_request}, ' - f'Aborting response headers: {self._should_abort_response_headers}') - - self._configure_dns(tr) - self._configure_server(tr) - self._configure_traffic_server(tr) - - tools_dir = self._ts.Variables.AtsTestToolsDir - http_utils = os.path.join(tools_dir, 'http_utils.py') - tr.Setup.CopyAs(self._init_file, Test.RunDirectory) - tr.Setup.CopyAs(http_utils, Test.RunDirectory) - tr.Setup.CopyAs(self._slow_post_client, Test.RunDirectory) - tr.Setup.CopyAs(self._quick_server, Test.RunDirectory) - - client_command = (f'{sys.executable} {self._slow_post_client} ' - '127.0.0.1 ' - f'{self._ts.Variables.port} ') - if not self._should_abort_request: - client_command += '--finish-request ' - p = tr.Processes.Default - p.Command = client_command - if self._should_abort_request or self._should_abort_response_headers: - p.Streams.All += Testers.ExcludesExpression('HTTP/1.1 200 OK', 'Verify response was received') - else: - p.Streams.All += Testers.ContainsExpression('HTTP/1.1 200 OK', 'Verify response was received') - - p.ReturnCode = 0 - self._ts.StartBefore(self._dns) - self._ts.StartBefore(self._server) - p.StartBefore(self._ts) - tr.Timeout = 10 - - -for abort_request in [True, False]: - for drain_request in [True, False]: - for abort_response_headers in [True, False]: - test = QuickServerTest(abort_request, drain_request, abort_response_headers) - test.run() diff --git a/tests/gold_tests/slow_post/server_abort.test.py b/tests/gold_tests/slow_post/server_abort.test.py deleted file mode 100644 index 394e7b350e3..00000000000 --- a/tests/gold_tests/slow_post/server_abort.test.py +++ /dev/null @@ -1,57 +0,0 @@ -''' -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. -import re - -Test.Summary = ''' -AuTest with bad configuration of microserver to simulate server aborting the connection unexpectedly -''' -Test.SkipIf(Condition.CurlUsingUnixDomainSocket()) -ts = Test.MakeATSProcess("ts", enable_tls=True) -# note the microserver by default is not configured to use ssl -server = Test.MakeOriginServer("server") -ts.Disk.remap_config.AddLine( - # The following config tells ATS to do tls with the origin server on a - # non-tls port. This is misconfigured intentionally to trigger an exception - # on the origin server so that it aborts the connection upon receiving a - # request - 'map / https://127.0.0.1:{0}'.format(server.Variables.Port)) -ts.Disk.ssl_multicert_yaml.AddLines( - """ -ssl_multicert: - - dest_ip: "*" - ssl_cert_name: aaa-signed.pem - ssl_key_name: aaa-signed.key -""".split("\n")) -ts.Disk.records_config.update( - { - 'proxy.config.diags.debug.tags': 'http|dns', - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.ssl.server.cert.path': f'{Test.TestDirectory}/test_secrets', - 'proxy.config.ssl.server.private_key.path': f'{Test.TestDirectory}/test_secrets', - }) - -tr = Test.AddTestRun() -tr.Processes.Default.StartBefore(server) -tr.Processes.Default.StartBefore(ts) -tr.MakeCurlCommand("-v -k -H \")host: foo.com\" https://127.0.0.1:{0}".format(ts.Variables.ssl_port), ts=ts) -tr.ReturnCode = 0 -tr.StillRunningAfter = server -tr.StillRunningAfter = ts -server.Streams.stderr += Testers.ContainsExpression( - "(UnicodeDecodeError|IndexError: list index out of range)", - "Verify that the server raises an exception when processing the request.") diff --git a/tests/gold_tests/slow_post/slow_post.test.py b/tests/gold_tests/slow_post/slow_post.test.py deleted file mode 100644 index aa57dfd3b78..00000000000 --- a/tests/gold_tests/slow_post/slow_post.test.py +++ /dev/null @@ -1,79 +0,0 @@ -''' -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import sys - -Test.SkipUnless(Condition.PluginExists('request_buffer.so')) - - -class SlowPostAttack: - - def __init__(cls): - Test.Summary = 'Test how ATS handles the slow-post attack' - cls._origin_max_connections = 3 - cls._slow_post_client = 'slow_post_clients.py' - cls.setupOriginServer() - cls.setupTS() - cls._ts.Setup.CopyAs(cls._slow_post_client, Test.RunDirectory) - - def setupOriginServer(self): - self._server = Test.MakeOriginServer("server") - request_header = {"headers": "GET / HTTP/1.1\r\nHost: www.example.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} - response_header = { - "headers": "HTTP/1.1 200 OK\r\nServer: microserver\r\nConnection: close\r\n\r\n", - "timestamp": "1469733493.993", - "body": "" - } - self._server.addResponse("sessionlog.json", request_header, response_header) - request_header2 = { - "headers": "POST / HTTP/1.1\r\nTransfer-Encoding: chunked\r\nHost: www.example.com\r\nConnection: keep-alive\r\n\r\n", - "timestamp": "1469733493.993", - "body": "a\r\na\r\na\r\n\r\n" - } - response_header2 = { - "headers": "HTTP/1.1 200 OK\r\nServer: microserver\r\nConnection: close\r\n\r\n", - "timestamp": "1469733493.993", - "body": "" - } - self._server.addResponse("sessionlog.json", request_header2, response_header2) - - def setupTS(self): - self._ts = Test.MakeATSProcess("ts") - self._ts.Disk.remap_config.AddLine('map / http://127.0.0.1:{0}'.format(self._server.Variables.Port)) - # This plugin can enable request buffer for POST. - Test.PrepareInstalledPlugin('request_buffer.so', self._ts) - self._ts.Disk.records_config.update( - { - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'http', - 'proxy.config.http.per_server.connection.max': self._origin_max_connections, - }) - - def run(self): - tr = Test.AddTestRun() - tr.Processes.Default.Command = \ - f'{sys.executable} {self._slow_post_client} -p {self._ts.Variables.port} -c {self._origin_max_connections}' - tr.Processes.Default.ReturnCode = 0 - tr.Processes.Default.StartBefore(self._server) - tr.Processes.Default.StartBefore(Test.Processes.ts) - tr.Processes.Default.Streams.stdout = "gold/200.gold" - - -Test.Summary = 'Test how ATS handles the slow-post attack' -slowPostAttack = SlowPostAttack() -slowPostAttack.run() diff --git a/tests/gold_tests/statistics/metric_response_000.test.py b/tests/gold_tests/statistics/metric_response_000.test.py deleted file mode 100644 index 30af20a1851..00000000000 --- a/tests/gold_tests/statistics/metric_response_000.test.py +++ /dev/null @@ -1,107 +0,0 @@ -"""Verify the proxy.process.http.000_responses stat is incremented for client aborts.""" - -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import os -import sys - -Test.Summary = __doc__ - - -class MetricResponse000Test: - """Verify that the 000_responses stat is incremented when a client aborts.""" - - _abort_client = 'abort_client.py' - _server_counter = 0 - _ts_counter = 0 - - def __init__(self): - """Configure and run the test.""" - self._configure_server() - self._configure_traffic_server() - self._configure_abort_client() - self._configure_successful_request() - self._verify_000_metric() - - def _configure_server(self) -> None: - """Configure the origin server.""" - self._server = Test.MakeOriginServer(f'server-{MetricResponse000Test._server_counter}') - MetricResponse000Test._server_counter += 1 - - request_header = {"headers": "GET / HTTP/1.1\r\nHost: www.example.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} - response_header = { - "headers": "HTTP/1.1 200 OK\r\nConnection: close\r\nContent-Length: 0\r\n\r\n", - "timestamp": "1469733493.993", - "body": "" - } - self._server.addResponse("sessionlog.json", request_header, response_header) - - def _configure_traffic_server(self) -> None: - """Configure ATS.""" - self._ts = Test.MakeATSProcess(f'ts-{MetricResponse000Test._ts_counter}', enable_cache=False) - MetricResponse000Test._ts_counter += 1 - - self._ts.Disk.remap_config.AddLine(f'map / http://127.0.0.1:{self._server.Variables.Port}/') - self._ts.Disk.records_config.update({ - 'proxy.config.diags.debug.enabled': 0, - 'proxy.config.diags.debug.tags': 'http', - }) - - def _configure_abort_client(self) -> None: - """Configure a client to send a partial request and abort.""" - tr = Test.AddTestRun('Trigger a client abort with partial request') - - tr.Setup.CopyAs(os.path.join(Test.TestDirectory, self._abort_client), Test.RunDirectory) - - p = tr.Processes.Default - p.Command = f'{sys.executable} {self._abort_client} 127.0.0.1 {self._ts.Variables.port}' - p.ReturnCode = 0 - - self._ts.StartBefore(self._server) - p.StartBefore(self._ts) - - tr.StillRunningAfter = self._ts - tr.StillRunningAfter = self._server - - def _configure_successful_request(self) -> None: - """Send a successful request to verify it doesn't increment 000 stat.""" - tr = Test.AddTestRun('Send a successful request') - tr.Processes.Default.Command = f'curl -s -o /dev/null -w "%{{http_code}}" http://127.0.0.1:{self._ts.Variables.port}/' - tr.Processes.Default.ReturnCode = 0 - tr.Processes.Default.Streams.All = Testers.ContainsExpression('200', 'Expected 200 response') - tr.StillRunningAfter = self._ts - tr.StillRunningAfter = self._server - - def _verify_000_metric(self) -> None: - """Verify the 000_responses stat is incremented.""" - # Wait for stats to propagate. - tr = Test.AddTestRun('Wait for stats') - tr.Processes.Default.Command = 'sleep 2' - tr.Processes.Default.ReturnCode = 0 - tr.StillRunningAfter = self._ts - - # Verify the 000_responses stat is non-zero. - tr = Test.AddTestRun('Check 000_responses stat') - tr.Processes.Default.Command = 'traffic_ctl metric get proxy.process.http.000_responses' - tr.Processes.Default.Env = self._ts.Env - tr.Processes.Default.ReturnCode = 0 - tr.Processes.Default.Streams.All = Testers.ContainsExpression( - 'proxy.process.http.000_responses 1', 'The 000_responses stat should be 1') - tr.StillRunningAfter = self._ts - - -MetricResponse000Test() diff --git a/tests/gold_tests/thread_config/thread_config.test.py b/tests/gold_tests/thread_config/thread_config.test.py deleted file mode 100644 index d583ec301fa..00000000000 --- a/tests/gold_tests/thread_config/thread_config.test.py +++ /dev/null @@ -1,252 +0,0 @@ -''' -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import sys - -Test.Summary = 'Test that Trafficserver starts with different thread configurations.' -Test.ContinueOnFail = True -Test.SkipUnless(Condition.IsPlatform("linux")) - -ts = Test.MakeATSProcess('ts-1_exec-0_accept-1_task-1_aio') -ts.Disk.records_config.update( - { - 'proxy.config.exec_thread.autoconfig.enabled': 0, - 'proxy.config.exec_thread.autoconfig.scale': 1.5, - 'proxy.config.exec_thread.limit': 1, - 'proxy.config.accept_threads': 0, - 'proxy.config.task_threads': 1, - 'proxy.config.cache.threads_per_disk': 1, - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'iocore_thread_start|iocore_net_accept_start' - }) -ts.Setup.CopyAs('check_threads.py', Test.RunDirectory) - -tr = Test.AddTestRun() -TS_ROOT = ts.Env['TS_ROOT'] -tr.Processes.Default.Command = f'{sys.executable} check_threads.py -p {TS_ROOT} -e 1 -a 0 -t 1 -c 1' -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.StartBefore(ts) - -ts = Test.MakeATSProcess('ts-1_exec-1_accept-2_task-8_aio') -ts.Disk.records_config.update( - { - 'proxy.config.exec_thread.autoconfig.enabled': 0, - 'proxy.config.exec_thread.autoconfig.scale': 1.5, - 'proxy.config.exec_thread.limit': 1, - 'proxy.config.accept_threads': 1, - 'proxy.config.task_threads': 2, - 'proxy.config.cache.threads_per_disk': 8, - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'iocore_thread_start|iocore_net_accept_start' - }) - -tr = Test.AddTestRun() -TS_ROOT = ts.Env['TS_ROOT'] -tr.Processes.Default.Command = f'{sys.executable} check_threads.py -p {TS_ROOT} -e 1 -a 1 -t 2 -c 8' -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.StartBefore(ts) - -ts = Test.MakeATSProcess('ts-1_exec-10_accept-10_task-32_aio') -ts.Disk.records_config.update( - { - 'proxy.config.exec_thread.autoconfig.enabled': 0, - 'proxy.config.exec_thread.autoconfig.scale': 1.5, - 'proxy.config.exec_thread.limit': 1, - 'proxy.config.accept_threads': 10, - 'proxy.config.task_threads': 10, - 'proxy.config.cache.threads_per_disk': 32, - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'iocore_thread_start|iocore_net_accept_start' - }) - -tr = Test.AddTestRun() -TS_ROOT = ts.Env['TS_ROOT'] -tr.Processes.Default.Command = f'{sys.executable} check_threads.py -p {TS_ROOT} -e 1 -a 10 -t 10 -c 32' -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.StartBefore(ts) - -ts = Test.MakeATSProcess('ts-2_exec-0_accept-1_task-1_aio') -ts.Disk.records_config.update( - { - 'proxy.config.exec_thread.autoconfig.enabled': 0, - 'proxy.config.exec_thread.autoconfig.scale': 1.5, - 'proxy.config.exec_thread.limit': 2, - 'proxy.config.accept_threads': 0, - 'proxy.config.task_threads': 1, - 'proxy.config.cache.threads_per_disk': 1, - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'iocore_thread_start|iocore_net_accept_start' - }) - -tr = Test.AddTestRun() -TS_ROOT = ts.Env['TS_ROOT'] -tr.Processes.Default.Command = f'{sys.executable} check_threads.py -p {TS_ROOT} -e 2 -a 0 -t 1 -c 1' -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.StartBefore(ts) - -ts = Test.MakeATSProcess('ts-2_exec-1_accept-2_task-8_aio') -ts.Disk.records_config.update( - { - 'proxy.config.exec_thread.autoconfig.enabled': 0, - 'proxy.config.exec_thread.autoconfig.scale': 1.5, - 'proxy.config.exec_thread.limit': 2, - 'proxy.config.accept_threads': 1, - 'proxy.config.task_threads': 2, - 'proxy.config.cache.threads_per_disk': 8, - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'iocore_thread_start|iocore_net_accept_start' - }) - -tr = Test.AddTestRun() -TS_ROOT = ts.Env['TS_ROOT'] -tr.Processes.Default.Command = f'{sys.executable} check_threads.py -p {TS_ROOT} -e 2 -a 1 -t 2 -c 8' -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.StartBefore(ts) - -ts = Test.MakeATSProcess('ts-2_exec-10_accept-10_task-32_aio') -ts.Disk.records_config.update( - { - 'proxy.config.exec_thread.autoconfig.enabled': 0, - 'proxy.config.exec_thread.autoconfig.scale': 1.5, - 'proxy.config.exec_thread.limit': 2, - 'proxy.config.accept_threads': 10, - 'proxy.config.task_threads': 10, - 'proxy.config.cache.threads_per_disk': 32, - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'iocore_thread_start|iocore_net_accept_start' - }) - -tr = Test.AddTestRun() -TS_ROOT = ts.Env['TS_ROOT'] -tr.Processes.Default.Command = f'{sys.executable} check_threads.py -p {TS_ROOT} -e 2 -a 10 -t 10 -c 32' -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.StartBefore(ts) - -ts = Test.MakeATSProcess('ts-32_exec-0_accept-1_task-1_aio') -ts.Disk.records_config.update( - { - 'proxy.config.exec_thread.autoconfig.enabled': 0, - 'proxy.config.exec_thread.autoconfig.scale': 1.5, - 'proxy.config.exec_thread.limit': 32, - 'proxy.config.accept_threads': 0, - 'proxy.config.task_threads': 1, - 'proxy.config.cache.threads_per_disk': 1, - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'iocore_thread_start|iocore_net_accept_start' - }) - -tr = Test.AddTestRun() -TS_ROOT = ts.Env['TS_ROOT'] -tr.Processes.Default.Command = f'{sys.executable} check_threads.py -p {TS_ROOT} -e 32 -a 0 -t 1 -c 1' -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.StartBefore(ts) - -ts = Test.MakeATSProcess('ts-32_exec-1_accept-2_task-8_aio') -ts.Disk.records_config.update( - { - 'proxy.config.exec_thread.autoconfig.enabled': 0, - 'proxy.config.exec_thread.autoconfig.scale': 1.5, - 'proxy.config.exec_thread.limit': 32, - 'proxy.config.accept_threads': 1, - 'proxy.config.task_threads': 2, - 'proxy.config.cache.threads_per_disk': 8, - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'iocore_thread_start|iocore_net_accept_start' - }) - -tr = Test.AddTestRun() -TS_ROOT = ts.Env['TS_ROOT'] -tr.Processes.Default.Command = f'{sys.executable} check_threads.py -p {TS_ROOT} -e 32 -a 1 -t 2 -c 8' -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.StartBefore(ts) - -ts = Test.MakeATSProcess('ts-32_exec-10_accept-10_task-32_aio') -ts.Disk.records_config.update( - { - 'proxy.config.exec_thread.autoconfig.enabled': 0, - 'proxy.config.exec_thread.autoconfig.scale': 1.5, - 'proxy.config.exec_thread.limit': 32, - 'proxy.config.accept_threads': 10, - 'proxy.config.task_threads': 10, - 'proxy.config.cache.threads_per_disk': 32, - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'iocore_thread_start|iocore_net_accept_start' - }) - -tr = Test.AddTestRun() -TS_ROOT = ts.Env['TS_ROOT'] -tr.Processes.Default.Command = f'{sys.executable} check_threads.py -p {TS_ROOT} -e 32 -a 10 -t 10 -c 32' -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.StartBefore(ts) - -ts = Test.MakeATSProcess('ts-100_exec-0_accept-1_task-1_aio') -ts.Disk.records_config.update( - { - 'proxy.config.exec_thread.autoconfig.enabled': 0, - 'proxy.config.exec_thread.autoconfig.scale': 1.5, - 'proxy.config.exec_thread.limit': 100, - 'proxy.config.accept_threads': 0, - 'proxy.config.task_threads': 1, - 'proxy.config.cache.threads_per_disk': 1, - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'iocore_thread_start|iocore_net_accept_start' - }) - -tr = Test.AddTestRun() -TS_ROOT = ts.Env['TS_ROOT'] -tr.Processes.Default.Command = f'{sys.executable} check_threads.py -p {TS_ROOT} -e 100 -a 0 -t 1 -c 1' -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.StartBefore(ts) - -ts = Test.MakeATSProcess('ts-100_exec-1_accept-2_task-8_aio') -ts.Disk.records_config.update( - { - 'proxy.config.exec_thread.autoconfig.enabled': 0, - 'proxy.config.exec_thread.autoconfig.scale': 1.5, - 'proxy.config.exec_thread.limit': 100, - 'proxy.config.accept_threads': 1, - 'proxy.config.task_threads': 2, - 'proxy.config.cache.threads_per_disk': 8, - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'iocore_thread_start|iocore_net_accept_start' - }) - -tr = Test.AddTestRun() -TS_ROOT = ts.Env['TS_ROOT'] -tr.Processes.Default.Command = f'{sys.executable} check_threads.py -p {TS_ROOT} -e 100 -a 1 -t 2 -c 8' -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.StartBefore(ts) - -ts = Test.MakeATSProcess('ts-100_exec-10_accept-10_task-32_aio') -ts.Disk.records_config.update( - { - 'proxy.config.exec_thread.autoconfig.enabled': 0, - 'proxy.config.exec_thread.autoconfig.scale': 1.5, - 'proxy.config.exec_thread.limit': 100, - 'proxy.config.accept_threads': 10, - 'proxy.config.task_threads': 10, - 'proxy.config.cache.threads_per_disk': 32, - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'iocore_thread_start|iocore_net_accept_start' - }) - -tr = Test.AddTestRun() -TS_ROOT = ts.Env['TS_ROOT'] -tr.Processes.Default.Command = f'{sys.executable} check_threads.py -p {TS_ROOT} -e 100 -a 10 -t 10 -c 32' -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.StartBefore(ts) diff --git a/tests/gold_tests/timeout/accept_timeout.test.py b/tests/gold_tests/timeout/accept_timeout.test.py deleted file mode 100644 index 80a7d44dcce..00000000000 --- a/tests/gold_tests/timeout/accept_timeout.test.py +++ /dev/null @@ -1,74 +0,0 @@ -''' -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -Test.Summary = 'Testing ATS inactivity timeout' - -Test.SkipUnless( - Condition.HasCurlFeature('http2'), Condition.HasProgram("telnet", "Need telnet to shutdown when server shuts down tcp"), - Condition.HasProgram("nc", "Need nc to send data to server")) -Test.SkipIf(Condition.CurlUsingUnixDomainSocket()) - -ts = Test.MakeATSProcess("ts", enable_tls=True) - -ts.addSSLfile("../tls/ssl/server.pem") -ts.addSSLfile("../tls/ssl/server.key") - -ts.Disk.records_config.update( - { - 'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir), - 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts.Variables.SSLDir), - 'proxy.config.http.transaction_no_activity_timeout_in': 6, - 'proxy.config.http.accept_no_activity_timeout': 2, - 'proxy.config.net.default_inactivity_timeout': 10, - 'proxy.config.net.defer_accept': 0 # Must turn off defer accept to test the raw TCP case - }) - -ts.Disk.ssl_multicert_yaml.AddLines( - """ -ssl_multicert: - - dest_ip: "*" - ssl_cert_name: server.pem - ssl_key_name: server.key -""".split("\n")) - -# case 1 TLS with no data -tr = Test.AddTestRun("tr") -tr.Setup.Copy("time_client.sh") -tr.Processes.Default.StartBefore(ts) -tr.Processes.Default.Command = 'sh ./time_client.sh \'openssl s_client -ign_eof -connect 127.0.0.1:{0}\''.format( - ts.Variables.ssl_port) -tr.Processes.Default.Streams.stdout = Testers.ContainsExpression("Accept timeout", "Request should fail from the accept timeout") - -# case 2 TLS with incomplete request header -tr2 = Test.AddTestRun("tr") -tr2.Processes.Default.Command = 'echo "GET /.html HTTP/1.1" | sh ./time_client.sh \'openssl s_client -ign_eof -connect 127.0.0.1:{0}\''.format( - ts.Variables.ssl_port) -tr2.Processes.Default.Streams.stdout = Testers.ContainsExpression( - "Transaction inactivity timeout", "Request should fail with transaction inactivity timeout") - -# case 3 TCP with no data -tr3 = Test.AddTestRun("tr") -tr3.Processes.Default.Command = 'sh ./time_client.sh \'telnet 127.0.0.1 {0}\''.format(ts.Variables.port) -tr3.Processes.Default.Streams.stdout = Testers.ContainsExpression("Accept timeout", "Request should fail from the accept timeout") - -# case 4 TCP with incomplete request header -tr4 = Test.AddTestRun("tr") -tr4.Setup.Copy("create_request.sh") -tr4.Processes.Default.Command = 'sh ./time_client.sh \'nc -c ./create_request.sh 127.0.0.1 {0}\''.format(ts.Variables.port) -tr4.Processes.Default.Streams.stdout = Testers.ContainsExpression( - "Transaction inactivity timeout", "Request should fail with transaction inactivity timeout") diff --git a/tests/gold_tests/timeout/active_timeout.test.py b/tests/gold_tests/timeout/active_timeout.test.py deleted file mode 100644 index f180340165a..00000000000 --- a/tests/gold_tests/timeout/active_timeout.test.py +++ /dev/null @@ -1,73 +0,0 @@ -''' -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -Test.Summary = 'Testing ATS active timeout' - -Test.SkipUnless(Condition.HasCurlFeature('http2')) - -if Condition.HasATSFeature('TS_USE_QUIC') and Condition.HasCurlFeature('http3'): - ts = Test.MakeATSProcess("ts", enable_tls=True, enable_quic=True) -else: - ts = Test.MakeATSProcess("ts", enable_tls=True) -server = Test.MakeOriginServer("server", delay=8) - -request_header = {"headers": "GET /file HTTP/1.1\r\nHost: *\r\n\r\n", "timestamp": "5678", "body": ""} -response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "5678", "body": ""} - -server.addResponse("sessionfile.log", request_header, response_header) - -ts.addDefaultSSLFiles() - -ts.Disk.records_config.update( - { - 'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir), - 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts.Variables.SSLDir), - 'proxy.config.url_remap.remap_required': 1, - 'proxy.config.http.transaction_active_timeout_out': 2, - }) - -ts.Disk.remap_config.AddLine('map / http://127.0.0.1:{0}/'.format(server.Variables.Port)) - -ts.Disk.ssl_multicert_yaml.AddLines( - """ -ssl_multicert: - - dest_ip: "*" - ssl_cert_name: server.pem - ssl_key_name: server.key -""".split("\n")) - -tr = Test.AddTestRun("tr") -tr.Processes.Default.StartBefore(server) -tr.Processes.Default.StartBefore(ts) -tr.MakeCurlCommand('-i http://127.0.0.1:{0}/file'.format(ts.Variables.port), ts=ts) -tr.Processes.Default.Streams.stdout = Testers.ContainsExpression("Activity Timeout", "Request should fail with active timeout") - -if not Condition.CurlUsingUnixDomainSocket(): - tr2 = Test.AddTestRun("tr") - tr2.MakeCurlCommand('-k -i --http1.1 https://127.0.0.1:{0}/file'.format(ts.Variables.ssl_port), ts=ts) - tr2.Processes.Default.Streams.stdout = Testers.ContainsExpression("Activity Timeout", "Request should fail with active timeout") - - tr3 = Test.AddTestRun("tr") - tr3.MakeCurlCommand('-k -i --http2 https://127.0.0.1:{0}/file'.format(ts.Variables.ssl_port), ts=ts) - tr3.Processes.Default.Streams.stdout = Testers.ContainsExpression("Activity Timeout", "Request should fail with active timeout") - - if Condition.HasATSFeature('TS_USE_QUIC') and Condition.HasCurlFeature('http3'): - tr4 = Test.AddTestRun("tr") - tr4.MakeCurlCommand('-k -i --http3 https://localhost:{0}/file'.format(ts.Variables.ssl_port), ts=ts) - tr4.Processes.Default.Streams.stdout = Testers.ContainsExpression( - "Activity Timeout", "Request should fail with active timeout") diff --git a/tests/gold_tests/timeout/conn_timeout.test.py b/tests/gold_tests/timeout/conn_timeout.test.py deleted file mode 100644 index 125b06465b6..00000000000 --- a/tests/gold_tests/timeout/conn_timeout.test.py +++ /dev/null @@ -1,99 +0,0 @@ -''' -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -Test.Summary = 'Testing ATS TCP handshake timeout' - -# Skipping this in the normal CI because it requires privilege. -# Comment out to run in your privileged environment -Test.SkipIf(Condition.true("Test requires privilege")) - -ts = Test.MakeATSProcess("ts") - -Test.ContinueOnFail = True -Test.GetTcpPort("blocked_upstream_port") -Test.GetTcpPort("upstream_port") - -server4 = Test.MakeOriginServer("server4") - -ts.Disk.records_config.update( - { - 'proxy.config.url_remap.remap_required': 1, - 'proxy.config.http.connect_attempts_timeout': 2, - 'proxy.config.http.connect_attempts_max_retries': 0, - 'proxy.config.http.transaction_no_activity_timeout_out': 5, - 'proxy.config.diags.debug.enabled': 0, - 'proxy.config.diags.debug.tags': 'http', - }) - -ts.Disk.remap_config.AddLine('map /blocked http://10.1.1.1:{0}'.format(Test.Variables.blocked_upstream_port)) -ts.Disk.remap_config.AddLine('map /not-blocked http://10.1.1.1:{0}'.format(Test.Variables.upstream_port)) - -ts.Disk.logging_yaml.AddLines( - ''' -logging: - formats: - - name: testformat - format: '% % % %' - logs: - - mode: ascii - format: testformat - filename: squid -'''.split("\n")) - -# Set up the network name space. Requires privilege -tr = Test.AddTestRun("tr-ns-setup") -tr.Processes.Default.StartBefore(ts) -tr.Processes.Default.TimeOut = 2 -tr.Setup.Copy('setupnetns.sh') -tr.Processes.Default.Command = 'echo start; sudo sh -x ./setupnetns.sh {0} {1}'.format( - Test.Variables.blocked_upstream_port, Test.Variables.upstream_port) - -# Request to the port that is blocked in the network ns. The SYN should never be responded to -# and the connect timeout should trigger with a 50x return. If the SYN handshake occurs, the -# no activity timeout would trigger, but not before the test timeout expires -tr = Test.AddTestRun("tr-blocking") -tr.MakeCurlCommand('-i http://127.0.0.1:{0}/blocked {0}'.format(ts.Variables.port), ts=ts) -tr.Processes.Default.TimeOut = 4 -tr.Processes.Default.Streams.All = Testers.ContainsExpression( - "HTTP/1.1 502 internal error - server connection terminated", "Connect failed") - -tr = Test.AddTestRun("tr-blocking-post") -tr.MakeCurlCommand('-d "stuff" -i http://127.0.0.1:{0}/blocked {0}'.format(ts.Variables.port), ts=ts) -tr.Processes.Default.TimeOut = 4 -tr.Processes.Default.Streams.All = Testers.ContainsExpression( - "HTTP/1.1 502 internal error - server connection terminated", "Connect failed") - -# Should not catch the connect timeout. Even though the first bytes are not sent until after the 2 second connect timeout -# But before the no-activity timeout -tr = Test.AddTestRun("tr-delayed") -tr.Setup.Copy('delay-server.sh') -tr.Setup.Copy('case1.sh') -tr.Processes.Default.Command = 'sh ./case1.sh {0} {1}'.format(ts.Variables.port, ts.Variables.upstream_port) -tr.Processes.Default.TimeOut = 7 -tr.Processes.Default.Streams.All = Testers.ContainsExpression("HTTP/1.1 200", "Connect succeeded") - -# cleanup the network namespace and virtual network -tr = Test.AddTestRun("tr-cleanup") -tr.Processes.Default.Command = 'sudo ip netns del testserver; sudo ip link del veth0 type veth peer name veth1' -tr.Processes.Default.TimeOut = 4 - -tr = Test.AddTestRun("Wait for the access log to write out") -tr.Processes.Default.StartBefore(server4, ready=When.FileExists(ts.Disk.squid_log)) -tr.StillRunningAfter = ts -tr.Processes.Default.Command = 'echo "log file exists"' -tr.Processes.Default.ReturnCode = 0 diff --git a/tests/gold_tests/timeout/default_inactivity_timeout.test.py b/tests/gold_tests/timeout/default_inactivity_timeout.test.py deleted file mode 100644 index a56bdd604a1..00000000000 --- a/tests/gold_tests/timeout/default_inactivity_timeout.test.py +++ /dev/null @@ -1,103 +0,0 @@ -''' -Verify correct behavior for default_inactivity_timeout. -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -Test.Summary = __doc__ - - -class TestDefaultInactivityTimeout: - """Configure a test for default_inactivity_test.""" - - replay_file = "replay/default_inactivity_timeout.replay.yaml" - client_gold_file = 'gold/client_default_inactivity_timeout.gold' - client_counter: int = 0 - ts_counter: int = 0 - server_counter: int = 0 - - def __init__(self, name: str, use_override: bool): - """Initialize the test. - - :param name: The name of the test. - :param use_override: Whether to use the override rather than the global - configuration. - """ - self.name = name - self.use_override = use_override - - def _configure_server(self, tr: 'TestRun') -> None: - """Configure the server. - - :param tr: The TestRun object to associate the server process with. - """ - server = tr.AddVerifierServerProcess(f"server_{TestDefaultInactivityTimeout.server_counter}", self.replay_file) - TestDefaultInactivityTimeout.server_counter += 1 - self._server = server - - def _configure_traffic_server(self, tr: 'TestRun') -> None: - """Configure Traffic Server. - - :param tr: The TestRun object to associate the ts process with. - :return: A Traffic Server Test process. - """ - ts = tr.MakeATSProcess(f"ts-{TestDefaultInactivityTimeout.ts_counter}") - TestDefaultInactivityTimeout.ts_counter += 1 - self._ts = ts - - debug_tags = 'http|cache|socket|net_queue|inactivity_cop|conf_remap' - ts.Disk.records_config.update({ - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': debug_tags, - }) - - origin_port = self._server.Variables.http_port - remap_line = f'map / http://127.0.0.1:{origin_port}' - - if self.use_override: - remap_line += (' @plugin=conf_remap.so ' - '@pparam=proxy.config.net.default_inactivity_timeout=2') - else: - ts.Disk.records_config.update({ - 'proxy.config.net.default_inactivity_timeout': 2, - }) - - ts.Disk.remap_config.AddLine(remap_line) - ts.Disk.traffic_out.Content += Testers.ContainsExpression( - 'timed out due to default inactivity timeout', 'Verify that the default inactivity timeout was triggered.') - - def run(self) -> None: - """Run the test.""" - tr = Test.AddTestRun(self.name) - self._configure_server(tr) - self._configure_traffic_server(tr) - - tr.Processes.Default.StartBefore(self._server) - tr.Processes.Default.StartBefore(self._ts) - tr.AddVerifierClientProcess( - f'client-{TestDefaultInactivityTimeout.client_counter}', self.replay_file, http_ports=[self._ts.Variables.port]) - TestDefaultInactivityTimeout.client_counter += 1 - - # Set up expectectations for the timeout closing the connection. - tr.Processes.Default.ReturnCode = 1 - tr.Processes.Default.Streams.all = self.client_gold_file - - -test = TestDefaultInactivityTimeout("global config", use_override=False) -test.run() - -test = TestDefaultInactivityTimeout("override config", use_override=True) -test.run() diff --git a/tests/gold_tests/timeout/http2_incomplete_header_timeout.test.py b/tests/gold_tests/timeout/http2_incomplete_header_timeout.test.py deleted file mode 100644 index aad0449282e..00000000000 --- a/tests/gold_tests/timeout/http2_incomplete_header_timeout.test.py +++ /dev/null @@ -1,207 +0,0 @@ -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import sys -from typing import Optional - -Test.Summary = 'Verify http2.incomplete_header_timeout_in' - -# VC_EVENT_ACTIVE_TIMEOUT. The incomplete header timeout is an active timeout on -# the stream, so ATS reports this event rather than VC_EVENT_INACTIVITY_TIMEOUT -# (105). -VC_EVENT_ACTIVE_TIMEOUT = 106 - -# Http2ErrorCode::HTTP2_ERROR_COMPRESSION_ERROR. ATS tears down the connection -# rather than just the stream because the undelivered CONTINUATION frame leaves -# its HPACK dynamic table out of sync. -HTTP2_ERROR_COMPRESSION_ERROR = 9 - - -class TestHttp2IncompleteHeaderTimeout: - """Configure a test for http2.incomplete_header_timeout_in. - - The client sends a HEADERS frame without the END_HEADERS flag, which leaves - ATS waiting for a CONTINUATION frame. incomplete_header_timeout_in bounds - how long ATS waits. - """ - - client_script: str = 'http2_incomplete_header_client.py' - replay_file: str = 'replay/http2_incomplete_header_timeout.replay.yaml' - counter: int = 0 - - def __init__( - self, - name: str, - incomplete_header_timeout_in: int, - path: str, - uuid: str, - end_headers: bool = False, - continuation_delay: Optional[float] = None, - min_elapsed: Optional[float] = None, - max_elapsed: Optional[float] = None, - expect_timeout: bool = False): - """Initialize the test. - - :param name: The name of the test run. - :param incomplete_header_timeout_in: The value to configure for - proxy.config.http2.incomplete_header_timeout_in. - :param path: The :path pseudo header the client requests. - :param uuid: The uuid of the replay file transaction to request. - :param end_headers: Whether the client sends the complete header block - with END_HEADERS in a single HEADERS frame. - :param continuation_delay: How long after the HEADERS frame the client - waits before sending the END_HEADERS CONTINUATION frame. None means - that the client never sends it. - :param min_elapsed: Fail if ATS ends the stream sooner than this. - :param max_elapsed: Fail if ATS ends the stream later than this. - :param expect_timeout: Whether ATS is expected to time out the stream. - """ - self._name = name - self._incomplete_header_timeout_in = incomplete_header_timeout_in - self._path = path - self._uuid = uuid - self._end_headers = end_headers - self._continuation_delay = continuation_delay - self._min_elapsed = min_elapsed - self._max_elapsed = max_elapsed - self._expect_timeout = expect_timeout - - def _configure_server(self, tr: 'TestRun') -> None: - """Configure the origin server. - - :param tr: The TestRun object to associate the server process with. - """ - self._server = tr.AddVerifierServerProcess(f'server-{self.counter}', self.replay_file) - - def _configure_traffic_server(self, tr: 'TestRun') -> None: - """Configure Traffic Server. - - :param tr: The TestRun object to associate the ts process with. - """ - ts = tr.MakeATSProcess(f'ts-{self.counter}', enable_tls=True, enable_cache=False) - self._ts = ts - - ts.addSSLfile('ssl/cert.crt') - ts.addSSLfile('ssl/private-key.key') - ts.Disk.ssl_multicert_yaml.AddLines( - f""" -ssl_multicert: - - dest_ip: "*" - ssl_cert_name: {ts.Variables.SSLDir}/cert.crt - ssl_key_name: {ts.Variables.SSLDir}/private-key.key -""".split('\n')) - - ts.Disk.records_config.update( - { - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'http2', - 'proxy.config.ssl.server.cert.path': ts.Variables.SSLDir, - 'proxy.config.ssl.server.private_key.path': ts.Variables.SSLDir, - 'proxy.config.http2.incomplete_header_timeout_in': self._incomplete_header_timeout_in, - }) - - ts.Disk.remap_config.AddLine(f'map / http://127.0.0.1:{self._server.Variables.http_port}') - - def _configure_client(self, tr: 'TestRun') -> None: - """Configure the ad hoc HTTP/2 client. - - :param tr: The TestRun object to associate the client process with. - """ - tr.Setup.Copy(self.client_script) - command = ( - f'{sys.executable} {self.client_script} {self._ts.Variables.ssl_port} ' - f'--path {self._path} --uuid {self._uuid}') - if self._end_headers: - command += ' --end-headers' - if self._continuation_delay is not None: - command += f' --continuation-delay {self._continuation_delay}' - if self._min_elapsed is not None: - command += f' --min-elapsed {self._min_elapsed}' - if self._max_elapsed is not None: - command += f' --max-elapsed {self._max_elapsed}' - - tr.Processes.Default.Command = command - tr.Processes.Default.ReturnCode = 0 - - def _configure_expectations(self, tr: 'TestRun') -> None: - """Configure the testers for the test run. - - :param tr: The TestRun object to associate the testers with. - """ - timeout_error = 'ERROR: HTTP/2 stream error timeout' - if self._expect_timeout: - tr.Processes.Default.Streams.stdout += Testers.ContainsExpression( - f'GOAWAY error_code={HTTP2_ERROR_COMPRESSION_ERROR} last_stream_id=1', - 'ATS should close the connection with COMPRESSION_ERROR because the HPACK table is out of sync.') - self._ts.Disk.traffic_out.Content += Testers.ContainsExpression( - f'timeout event={VC_EVENT_ACTIVE_TIMEOUT}', - 'ATS should time out the incomplete header with the stream active timeout.') - # The default diags.log check rejects any ERROR:, but this test - # expects the incomplete header timeout to be reported. - self._ts.Disk.diags_log.Content = Testers.ContainsExpression( - timeout_error, 'ATS should log the incomplete header timeout.') - else: - tr.Processes.Default.Streams.stdout += Testers.ContainsExpression( - 'stream 1: status=200', 'ATS should proxy the request rather than time out the stream.') - tr.Processes.Default.Streams.stdout += Testers.ExcludesExpression('GOAWAY', 'ATS should not close the connection.') - self._ts.Disk.diags_log.Content += Testers.ExcludesExpression( - timeout_error, 'ATS should not report an incomplete header timeout.') - - def run(self) -> None: - """Run the test.""" - tr = Test.AddTestRun(self._name) - self._configure_server(tr) - self._configure_traffic_server(tr) - self._configure_client(tr) - self._configure_expectations(tr) - - tr.Processes.Default.StartBefore(self._server) - tr.Processes.Default.StartBefore(self._ts) - TestHttp2IncompleteHeaderTimeout.counter += 1 - - -# A client that never completes the header block should be timed out at the -# configured timeout, well before the 10 second default. -TestHttp2IncompleteHeaderTimeout( - 'Incomplete header block times out', - incomplete_header_timeout_in=3, - path='/incomplete', - uuid='incomplete_header', - continuation_delay=None, - min_elapsed=2.5, - max_elapsed=7, - expect_timeout=True).run() - -# A client that completes the header block before the timeout expires should be -# proxied normally. -TestHttp2IncompleteHeaderTimeout( - 'CONTINUATION frame arrives before the timeout', - incomplete_header_timeout_in=5, - path='/incomplete', - uuid='incomplete_header', - continuation_delay=1, - expect_timeout=False).run() - -# The timeout is canceled when the transaction starts, so a slow origin -# response must not be mistaken for an incomplete header block. -TestHttp2IncompleteHeaderTimeout( - 'Timeout is canceled once the transaction starts', - incomplete_header_timeout_in=2, - path='/delayed', - uuid='delayed_response', - end_headers=True, - min_elapsed=4.5, - expect_timeout=False).run() diff --git a/tests/gold_tests/timeout/http2_no_activity_timeout.test.py b/tests/gold_tests/timeout/http2_no_activity_timeout.test.py deleted file mode 100644 index ab910e0732c..00000000000 --- a/tests/gold_tests/timeout/http2_no_activity_timeout.test.py +++ /dev/null @@ -1,157 +0,0 @@ -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -from typing import Optional -import re - -Test.Summary = 'Verify http2.no_activity_timeout_(in|out)' - - -class Test_http2_no_activity_timeout: - """Configure a test for http2 no_activity_timeout_(in|out).""" - - replay_file = "replay/http2_no_activity_timeout.replay.yaml" - client_counter: int = 0 - ts_counter: int = 0 - server_counter: int = 0 - - def __init__( - self, - name: str, - replay_keys: Optional[str] = None, - no_activity_timeout_in: Optional[int] = None, - expect_in_timeout=False, - no_activity_timeout_out: Optional[int] = None, - expect_out_timeout=False): - """Initialize the test. - - :param name: The name of the test. - :param replay_keys: The set of keys to replay from the replay file. If - not provided, all keys will be replayed. - :param no_activity_timeout_in: Configuration value for - ts.http2.no_activity_timeout_in. None means that the default value will - be used. - :param expect_in_timeout: True if there is an expected inbound timeout. - :param no_activity_timeout_out: Configuration value for - ts.http2.no_activity_timeout_out. None means that the default value will - be used. - :param expect_out_timeout: True if there is an expected outbound timeout. - """ - self._name = name - self._replay_keys = replay_keys - self._no_activity_timeout_in = no_activity_timeout_in - self._expect_in_timeout = expect_in_timeout - self._no_activity_timeout_out = no_activity_timeout_out - self._expect_out_timeout = expect_out_timeout - - def _configure_server(self, tr: 'TestRun'): - """Configure the server. - - :param tr: The TestRun object to associate the server process with. - """ - self._server = tr.AddVerifierServerProcess(f"server_{Test_http2_no_activity_timeout.server_counter}", self.replay_file) - Test_http2_no_activity_timeout.server_counter += 1 - - def _configure_traffic_server(self, tr: 'TestRun'): - """Configure Traffic Server. - - :param tr: The TestRun object to associate the ts process with. - """ - self._ts = tr.MakeATSProcess(f"ts-{Test_http2_no_activity_timeout.ts_counter}", enable_tls=True, enable_cache=False) - Test_http2_no_activity_timeout.ts_counter += 1 - - self._ts.addSSLfile("ssl/cert.crt") - self._ts.addSSLfile("ssl/private-key.key") - self._ts.Disk.ssl_multicert_yaml.AddLines( - f""" -ssl_multicert: - - dest_ip: "*" - ssl_cert_name: {self._ts.Variables.SSLDir}/cert.crt - ssl_key_name: {self._ts.Variables.SSLDir}/private-key.key -""".split("\n")) - - self._ts.Disk.records_config.update( - { - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'http|socket|inactivity_cop', - 'proxy.config.ssl.server.cert.path': self._ts.Variables.SSLDir, - 'proxy.config.ssl.server.private_key.path': self._ts.Variables.SSLDir, - "proxy.config.ssl.client.verify.server.policy": 'PERMISSIVE', - 'proxy.config.ssl.client.alpn_protocols': 'h2,http/1.1', - }) - - if self._no_activity_timeout_in is not None: - self._ts.Disk.records_config.update({ - 'proxy.config.http2.no_activity_timeout_in': self._no_activity_timeout_in, - }) - if self._no_activity_timeout_out is not None: - self._ts.Disk.records_config.update({ - 'proxy.config.http2.no_activity_timeout_out': self._no_activity_timeout_out, - }) - - self._ts.Disk.remap_config.AddLine(f'map / https://127.0.0.1:{self._server.Variables.https_port}') - - def run(self): - """Run the test.""" - tr = Test.AddTestRun(self._name) - self._configure_server(tr) - self._configure_traffic_server(tr) - - tr.Processes.Default.StartBefore(self._server) - tr.Processes.Default.StartBefore(self._ts) - - tr.AddVerifierClientProcess( - f'client-{Test_http2_no_activity_timeout.client_counter}', - self.replay_file, - https_ports=[self._ts.Variables.ssl_port], - keys=self._replay_keys) - Test_http2_no_activity_timeout.client_counter += 1 - - if self._expect_in_timeout: - tr.Processes.Default.Streams.All += Testers.IncludesExpression( - "SSL_read error", "The client should have a read error due to an ATS timeout.") - self._ts.Disk.traffic_out.Content += Testers.IncludesExpression( - "http2_cs.*Closing event:.*TIMEOUT", "We should detect a client side timeout.") - elif self._expect_out_timeout: - # There should be two origin connections: - # 1. For the first no delay transaction. - # 2. Another after a timeout, closing the first. - self._server.Streams.All += Testers.IncludesExpression( - "Negotiated ALPN from client ALPN.*Negotiated ALPN from client ALPN", - "A second server side connection should be needed after the first times out.", - reflags=re.MULTILINE | re.DOTALL) - self._ts.Disk.traffic_out.Content += Testers.IncludesExpression( - "http2_cs.*Closing event:.*TIMEOUT", "We should detect a server side timeout.") - - -test0 = Test_http2_no_activity_timeout("Default no activity timeout", expect_in_timeout=False, expect_out_timeout=False) -test0.run() - -test1 = Test_http2_no_activity_timeout( - "Client (in) side inactivity timeout", - no_activity_timeout_in=1, - replay_keys="no_delay 3_second_request_delay", - expect_in_timeout=True, - expect_out_timeout=False) -test1.run() - -test2 = Test_http2_no_activity_timeout( - "Server (out) side inactivity timeout", - no_activity_timeout_out=1, - replay_keys="no_delay 3_second_request_delay", - expect_in_timeout=False, - expect_out_timeout=True) -test2.run() diff --git a/tests/gold_tests/timeout/inactive_client_timeout.test.py b/tests/gold_tests/timeout/inactive_client_timeout.test.py deleted file mode 100644 index 84d3de4adf6..00000000000 --- a/tests/gold_tests/timeout/inactive_client_timeout.test.py +++ /dev/null @@ -1,78 +0,0 @@ -''' -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -Test.Summary = 'Testing ATS client inactivity timeout' - -ts = Test.MakeATSProcess("ts", enable_tls=True) -if Condition.CurlUsingUnixDomainSocket(): - replay_file = "slow_server_uds.yaml" -else: - replay_file = "slow_server.yaml" -server = Test.MakeVerifierServerProcess("server", replay_file) - -Test.ContinueOnFail = True - -ts.addDefaultSSLFiles() - -ts.Disk.records_config.update( - { - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'http', - 'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir), - 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts.Variables.SSLDir), - 'proxy.config.url_remap.remap_required': 1, - 'proxy.config.http.transaction_no_activity_timeout_in': 2, - 'proxy.config.ssl.client.verify.server.policy': 'PERMISSIVE', - }) - -ts.Disk.remap_config.AddLines( - [ - 'map https://www.tls.com/ https://127.0.0.1:{0}'.format(server.Variables.https_port), - 'map / http://127.0.0.1:{0}'.format(server.Variables.http_port), - ]) - -ts.Disk.ssl_multicert_yaml.AddLines( - """ -ssl_multicert: - - dest_ip: "*" - ssl_cert_name: server.pem - ssl_key_name: server.key -""".split("\n")) - -# -# Test 1: Verify that server delay does not trigger client activity timeout. -# - -# The Proxy Verifier server will delay for 3 seconds before returning a response. This is more than -# the 2 second proxy.config.http.transaction_no_activity_timeout_in (the client inactivity timeout), -# but less than the default 30 second proxy.config.http.transaction_no_activity_timeout_out (server -# inactivity timeout). These tests therefore exercise that the client inactivity timeout does not -# get applied after the request is sent. In other words, a slow to respond server should not -# trigger the client inactivity timeout. -tr = Test.AddTestRun("Verify that server delay does not trigger client activity timeout.") -client = tr.AddVerifierClientProcess("client", replay_file, http_ports=[ts.Variables.port], https_ports=[ts.Variables.ssl_port]) -tr.Processes.Default.StartBefore(ts) -tr.Processes.Default.StartBefore(server) - -client.Streams.All += Testers.ContainsExpression('x-response: 1', 'Verify that the first response is received') -client.Streams.All += Testers.ContainsExpression('x-response: 2', 'Verify that the second response is received') -if not Condition.CurlUsingUnixDomainSocket(): - client.Streams.All += Testers.ContainsExpression('x-response: 3', 'Verify that the third response is received') - client.Streams.All += Testers.ContainsExpression('x-response: 4', 'Verify that the fourth response is received') - client.Streams.All += Testers.ContainsExpression('x-response: 5', 'Verify that the fifth response is received') - client.Streams.All += Testers.ContainsExpression('x-response: 6', 'Verify that the sixth response is received') diff --git a/tests/gold_tests/timeout/inactive_timeout.test.py b/tests/gold_tests/timeout/inactive_timeout.test.py deleted file mode 100644 index 2265763eff7..00000000000 --- a/tests/gold_tests/timeout/inactive_timeout.test.py +++ /dev/null @@ -1,67 +0,0 @@ -''' -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -Test.Summary = 'Testing ATS inactivity timeout' - -Test.SkipUnless(Condition.HasCurlFeature('http2')) - -ts = Test.MakeATSProcess("ts", enable_tls=True) -server = Test.MakeOriginServer("server", delay=8) - -request_header = {"headers": "GET /file HTTP/1.1\r\nHost: *\r\n\r\n", "timestamp": "5678", "body": ""} -response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "5678", "body": ""} - -server.addResponse("sessionfile.log", request_header, response_header) - -ts.addDefaultSSLFiles() - -ts.Disk.records_config.update( - { - 'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir), - 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts.Variables.SSLDir), - 'proxy.config.url_remap.remap_required': 1, - 'proxy.config.http.transaction_no_activity_timeout_out': 2, - }) - -ts.Disk.remap_config.AddLine('map / http://127.0.0.1:{0}/'.format(server.Variables.Port)) - -ts.Disk.ssl_multicert_yaml.AddLines( - """ -ssl_multicert: - - dest_ip: "*" - ssl_cert_name: server.pem - ssl_key_name: server.key -""".split("\n")) - -tr = Test.AddTestRun("tr") -tr.Processes.Default.StartBefore(server) -tr.Processes.Default.StartBefore(ts) -tr.MakeCurlCommand('-i http://127.0.0.1:{0}/file'.format(ts.Variables.port), ts=ts) -tr.Processes.Default.Streams.stdout = Testers.ContainsExpression( - "Inactivity Timeout", "Request should fail with inactivity timeout") - -if not Condition.CurlUsingUnixDomainSocket(): - tr2 = Test.AddTestRun("tr") - tr2.MakeCurlCommand('-k -i --http1.1 https://127.0.0.1:{0}/file'.format(ts.Variables.ssl_port), ts=ts) - tr2.Processes.Default.Streams.stdout = Testers.ContainsExpression( - "Inactivity Timeout", "Request should fail with inactivity timeout") - - tr3 = Test.AddTestRun("tr") - tr3.MakeCurlCommand('-k -i --http2 https://127.0.0.1:{0}/file'.format(ts.Variables.ssl_port), ts=ts) - tr3.Processes.Default.Streams.stdout = Testers.ContainsExpression( - "Inactivity Timeout", "Request should fail with inactivity timeout") diff --git a/tests/gold_tests/timeout/quic_no_activity_timeout.test.py b/tests/gold_tests/timeout/quic_no_activity_timeout.test.py deleted file mode 100644 index 19b330097e2..00000000000 --- a/tests/gold_tests/timeout/quic_no_activity_timeout.test.py +++ /dev/null @@ -1,135 +0,0 @@ -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -Test.Summary = 'Basic checks on QUIC max_idle_timeout set by ts.quic.no_activity_timeout_in' - -Test.SkipUnless(Condition.HasATSFeature('TS_USE_QUIC')) - - -class Test_quic_no_activity_timeout: - """Configure a test for QUIC no_activity_timeout_in.""" - - replay_file = "replay/quic_no_activity_timeout.replay.yaml" - client_counter: int = 0 - ts_counter: int = 0 - server_counter: int = 0 - - def __init__(self, name: str, no_activity_timeout_in=1000, gold_file="", replay_keys="", extra_recs=None): - """Initialize the test. - - :param name: The name of the test. - :param no_activity_timeout_in: Configuration value for ts.quic.no_activity_timeout_in - :param gold_file: Gold file to be checked. - :param replay_keys: Keys to be used by pv - :param extra_recs: Any additional records to be set, either a yaml string or a dict. - """ - self.name = name - self.no_activity_timeout_in = no_activity_timeout_in - self.gold_file = gold_file - self.replay_keys = replay_keys - self.extra_recs = extra_recs - - def _configure_server(self, tr: 'TestRun'): - """Configure the server. - - :param tr: The TestRun object to associate the server process with. - """ - server = tr.AddVerifierServerProcess(f"server_{Test_quic_no_activity_timeout.server_counter}", self.replay_file) - Test_quic_no_activity_timeout.server_counter += 1 - self._server = server - - def _configure_traffic_server(self, tr: 'TestRun'): - """Configure Traffic Server. - - :param tr: The TestRun object to associate the ts process with. - """ - ts = tr.MakeATSProcess(f"ts-{Test_quic_no_activity_timeout.ts_counter}", enable_quic=True, enable_tls=True) - - Test_quic_no_activity_timeout.ts_counter += 1 - self._ts = ts - self._ts.addSSLfile("ssl/cert.crt") - self._ts.addSSLfile("ssl/private-key.key") - self._ts.Disk.records_config.update( - { - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'ssl|net|v_quic|quic|http|socket|inactivity_cop', - 'proxy.config.quic.no_activity_timeout_in': self.no_activity_timeout_in, - 'proxy.config.quic.qlog.file_base': f'log/qlog_{Test_quic_no_activity_timeout.server_counter}', - }) - - if self.extra_recs: - self._ts.Disk.records_config.update(self.extra_recs) - - self._ts.Disk.remap_config.AddLine(f'map / http://127.0.0.1:{self._server.Variables.http_port}') - self._ts.Disk.ssl_multicert_yaml.AddLines( - f""" -ssl_multicert: - - dest_ip: "*" - ssl_cert_name: {self._ts.Variables.SSLDir}/cert.crt - ssl_key_name: {self._ts.Variables.SSLDir}/private-key.key -""".split("\n")) - - def run(self, check_for_max_idle_timeout=False): - """Run the test.""" - tr = Test.AddTestRun(self.name) - self._configure_server(tr) - self._configure_traffic_server(tr) - - tr.Processes.Default.StartBefore(self._server) - tr.Processes.Default.StartBefore(self._ts) - - tr.AddVerifierClientProcess( - f'client-{Test_quic_no_activity_timeout.client_counter}', - self.replay_file, - http3_ports=[self._ts.Variables.ssl_port], - keys=self.replay_keys) - Test_quic_no_activity_timeout.client_counter += 1 - - if check_for_max_idle_timeout: - tr.Processes.Default.ReturnCode = 1 # timeout - self._ts.Disk.traffic_out.Content += Testers.IncludesExpression( - "QUIC Idle timeout detected", "We should detect the timeout.") - else: - tr.Processes.Default.ReturnCode = 0 - - if self.gold_file: - tr.Processes.Default.Streams.all = self.gold_file - - -# Tests start. - -test0 = Test_quic_no_activity_timeout( - "Test ts.quic.no_activity_timeout_in(quic max_idle_timeout), no delays", - no_activity_timeout_in=0, # no timeout `max_idle_timeout` - replay_keys="nodelays") -test0.run() - -if Condition.HasATSFeature('TS_HAS_QUICHE'): - test1 = Test_quic_no_activity_timeout( - "Test ts.quic.no_activity_timeout_in(quic max_idle_timeout) with a 5s delay", - no_activity_timeout_in=3000, # 3s `max_idle_timeout` - replay_keys="delay5s", - gold_file="gold/quic_no_activity_timeout.gold") - test1.run(check_for_max_idle_timeout=True) - - # QUIC Ignores the default_inactivity_timeout config, so the ts.quic.no_activity_timeout_in - # should be honored - test2 = Test_quic_no_activity_timeout( - "Ignoring default_inactivity_timeout and use the ts.quic.no_activity_timeout_in instead", - replay_keys="delay5s", - no_activity_timeout_in=3000, - extra_recs={'proxy.config.net.default_inactivity_timeout': 1}) - test2.run(check_for_max_idle_timeout=True) diff --git a/tests/gold_tests/timeout/quic_poll_timeout.test.py b/tests/gold_tests/timeout/quic_poll_timeout.test.py deleted file mode 100644 index 79386d30df3..00000000000 --- a/tests/gold_tests/timeout/quic_poll_timeout.test.py +++ /dev/null @@ -1,77 +0,0 @@ -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -from typing import Optional - -Test.Summary = 'Checks records.proxy.config.udp.poll_timeout' - -Test.SkipUnless(Condition.HasATSFeature('TS_HAS_QUICHE')) - - -class TestPollTimeout: - """Configure a test for poll_timeout.""" - - ts_counter: int = 0 - - def __init__(self, name: str, udp_poll_timeout_in: Optional[int] = None) -> None: - """Initialize the test. - - :param name: The name of the test. - :param udp_poll_timeout_in: Configuration value for proxy.config.udp.poll_timeout - """ - self.name = name - self.udp_poll_timeout_in = udp_poll_timeout_in - self.expected_udp_poll_timeout = 100 - if udp_poll_timeout_in is not None: - self.expected_udp_poll_timeout = udp_poll_timeout_in - - def _configure_traffic_server(self, tr: 'TestRun'): - """Configure Traffic Server. - - :param tr: The TestRun object to associate the ts process with. - """ - ts = tr.MakeATSProcess(f"ts-{TestPollTimeout.ts_counter}", enable_quic=True, enable_tls=True) - - TestPollTimeout.ts_counter += 1 - self._ts = ts - self._ts.Disk.records_config.update( - { - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'net|v_quic|quic|socket|inactivity_cop|v_iocore_net_poll', - }) - - if self.udp_poll_timeout_in is not None: - self._ts.Disk.records_config.update({'proxy.config.udp.poll_timeout': self.udp_poll_timeout_in}) - - def run(self): - """Run the test.""" - tr = Test.AddTestRun(self.name) - self._configure_traffic_server(tr) - - tr.Processes.Default.Command = "echo 'testing records.proxy.config.udp.poll_timeout'" - tr.Processes.Default.StartBefore(self._ts) - - self._ts.Disk.traffic_out.Content += Testers.IncludesExpression( - f"ET_UDP.*timeout: {self.expected_udp_poll_timeout},", "Verify UDP poll timeout.") - - -# Tests start. - -test0 = TestPollTimeout("Test records.proxy.config.udp.poll_timeout with default value.") -test0.run() - -test1 = TestPollTimeout("Test records.proxy.config.udp.poll_timeout with value of 10.", udp_poll_timeout_in=10) -test1.run() diff --git a/tests/gold_tests/timeout/tls_conn_timeout.test.py b/tests/gold_tests/timeout/tls_conn_timeout.test.py deleted file mode 100644 index 4342f768e37..00000000000 --- a/tests/gold_tests/timeout/tls_conn_timeout.test.py +++ /dev/null @@ -1,117 +0,0 @@ -''' -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import os - -Test.Summary = 'Testing ATS TLS handshake timeout' - -ts = Test.MakeATSProcess("ts") - -Test.Setup.Copy(os.path.join(Test.Variables.AtsBuildGoldTestsDir, 'timeout', 'ssl-delay-server')) - -Test.ContinueOnFail = True -Test.GetTcpPort("block_connect_port") -Test.GetTcpPort("block_ttfb_port") -Test.GetTcpPort("get_block_connect_port") -Test.GetTcpPort("get_block_ttfb_port") - -delay_post_connect = Test.Processes.Process( - "delay post connect", './ssl-delay-server {0} 3 0 server.pem'.format(Test.Variables.block_connect_port)) -delay_post_ttfb = Test.Processes.Process( - "delay post ttfb", './ssl-delay-server {0} 0 6 server.pem'.format(Test.Variables.block_ttfb_port)) - -delay_get_connect = Test.Processes.Process( - "delay get connect", './ssl-delay-server {0} 3 0 server.pem'.format(Test.Variables.get_block_connect_port)) -delay_get_ttfb = Test.Processes.Process( - "delay get ttfb", './ssl-delay-server {0} 0 6 server.pem'.format(Test.Variables.get_block_ttfb_port)) - -ts.Disk.records_config.update( - { - 'proxy.config.url_remap.remap_required': 1, - 'proxy.config.http.connect_attempts_timeout': 1, - 'proxy.config.http.connect_attempts_max_retries': 1, - 'proxy.config.http.transaction_no_activity_timeout_out': 4, - 'proxy.config.diags.debug.enabled': 0, - 'proxy.config.diags.debug.tags': 'http|ssl', - 'proxy.config.ssl.client.verify.server.policy': 'PERMISSIVE', - }) - -ts.Disk.remap_config.AddLine('map /connect_blocked https://127.0.0.1:{0}'.format(Test.Variables.block_connect_port)) -ts.Disk.remap_config.AddLine('map /ttfb_blocked https://127.0.0.1:{0}'.format(Test.Variables.block_ttfb_port)) -ts.Disk.remap_config.AddLine('map /get_connect_blocked https://127.0.0.1:{0}'.format(Test.Variables.get_block_connect_port)) -ts.Disk.remap_config.AddLine('map /get_ttfb_blocked https://127.0.0.1:{0}'.format(Test.Variables.get_block_ttfb_port)) - -# Commenting out the per test case timeouts. In the CI, there is too big of a risk that we hit those timeouts. Had hoped to use -# The test case timeouts to differentiate between a good origin timeout and a too long origin timeout - -# Run the connection and TTFB timeout tests first with POST. - -# Request the port that should timeout in the handshake -# Should retry once -tr = Test.AddTestRun("tr-blocking-post") -tr.Setup.Copy(os.path.join(Test.Variables.AtsTestToolsDir, "ssl", "server.pem")) -tr.Processes.Default.StartBefore(Test.Processes.ts) -tr.Processes.Default.StartBefore(delay_post_connect, ready=When.PortOpen(Test.Variables.block_connect_port)) -tr.MakeCurlCommand( - '-H"Connection:close" -d "bob" -i http://127.0.0.1:{0}/connect_blocked --tlsv1.2'.format(ts.Variables.port), ts=ts) -tr.Processes.Default.Streams.All = Testers.ContainsExpression("HTTP/1.1 502 Connection timed out", "Connect failed") -tr.Processes.Default.ReturnCode = 0 -tr.StillRunningAfter = delay_post_connect -tr.StillRunningAfter = Test.Processes.ts - -# Should not catch the connect timeout. Even though the first bytes are not sent until after the 2 second connect timeout -# Should not retry the connection -tr = Test.AddTestRun("tr-delayed-post") -tr.Processes.Default.StartBefore(delay_post_ttfb, ready=When.PortOpen(Test.Variables.block_ttfb_port)) -tr.MakeCurlCommand('-H"Connection:close" -d "bob" -i http://127.0.0.1:{0}/ttfb_blocked --tlsv1.2'.format(ts.Variables.port), ts=ts) -tr.Processes.Default.Streams.All = Testers.ContainsExpression("504 Connection Timed Out", "Connect timeout") -tr.Processes.Default.ReturnCode = 0 -tr.StillRunningAfter = delay_post_ttfb - -# Run the connection and TTFB timeout tests again with GET - -# Request the port that should timeout in the handshake -# Should retry once -tr = Test.AddTestRun("tr-blocking-get") -tr.Processes.Default.StartBefore(delay_get_connect, ready=When.PortOpen(Test.Variables.get_block_connect_port)) -tr.MakeCurlCommand('-H"Connection:close" -i http://127.0.0.1:{0}/get_connect_blocked --tlsv1.2'.format(ts.Variables.port), ts=ts) -tr.Processes.Default.Streams.All = Testers.ContainsExpression("HTTP/1.1 502 Connection timed out", "Connect failed") -tr.Processes.Default.ReturnCode = 0 -tr.StillRunningAfter = delay_get_connect - -# Should not catch the connect timeout. Even though the first bytes are not sent until after the 2 second connect timeout -# Since get is idempotent, It will try to connect again even though the GET request had been sent -tr = Test.AddTestRun("tr-delayed-get") -tr.Processes.Default.StartBefore(delay_get_ttfb, ready=When.PortOpen(Test.Variables.get_block_ttfb_port)) -tr.MakeCurlCommand('-H"Connection:close" -i http://127.0.0.1:{0}/get_ttfb_blocked --tlsv1.2'.format(ts.Variables.port), ts=ts) -tr.Processes.Default.Streams.All = Testers.ContainsExpression("504 Connection Timed Out", "Connect timeout") -tr.Processes.Default.ReturnCode = 0 -tr.StillRunningAfter = delay_get_ttfb - -delay_post_connect.Streams.All = Testers.ContainsExpression( - "Accept try", "Should appear at least two times (may be an extra one due to port ready test)") -delay_post_connect.Streams.All += Testers.ExcludesExpression("TTFB delay", "Should not reach the TTFB delay logic") -delay_post_ttfb.Streams.All = Testers.ContainsExpression("Accept try", "Should appear one time") -delay_post_ttfb.Streams.All += Testers.ContainsExpression("TTFB delay", "Should reach the TTFB delay logic") - -delay_get_connect.Streams.All = Testers.ContainsExpression( - "Accept try", "Should appear at least two times (may be an extra one due to port ready test)") -delay_get_connect.Streams.All += Testers.ExcludesExpression("TTFB delay", "Should not reach the TTFB delay logic") -delay_get_ttfb.Streams.All = Testers.ContainsExpression( - "Accept try", "Should appear at least two times (may be an extra one due to the port ready test)") -delay_get_ttfb.Streams.All += Testers.ContainsExpression("TTFB delay", "Should reach the TTFB delay logic") diff --git a/tests/gold_tests/timeout/tunnel_active_timeout.test.py b/tests/gold_tests/timeout/tunnel_active_timeout.test.py deleted file mode 100644 index 2d9ecdd7426..00000000000 --- a/tests/gold_tests/timeout/tunnel_active_timeout.test.py +++ /dev/null @@ -1,100 +0,0 @@ -''' -Verify that tunnel active timeout produces ERR_TUN_ACTIVE_TIMEOUT squid code. -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import os -import sys - -Test.Summary = ''' -Verify that tunnel active timeout produces ERR_TUN_ACTIVE_TIMEOUT squid code. -''' - -ts = Test.MakeATSProcess("ts", enable_tls=True) -server = Test.MakeOriginServer("server", ssl=True) - -# Simple response from origin -request_header = {"headers": "GET / HTTP/1.1\r\nHost: server\r\n\r\n", "timestamp": "1234", "body": ""} -response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1234", "body": "hello"} -server.addResponse("sessionlog.json", request_header, response_header) - -ts.addDefaultSSLFiles() - -ts.Disk.ssl_multicert_yaml.AddLines( - f""" -ssl_multicert: - - dest_ip: "*" - ssl_cert_name: server.pem - ssl_key_name: server.key -""".split('\n')) - -ts.Disk.records_config.update( - { - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'http|ssl|tunnel', - 'proxy.config.ssl.server.cert.path': ts.Variables.SSLDir, - 'proxy.config.ssl.server.private_key.path': ts.Variables.SSLDir, - 'proxy.config.ssl.client.verify.server.policy': 'PERMISSIVE', - 'proxy.config.http.connect_ports': f'{server.Variables.SSL_Port}', - # Set a short active timeout for tunnels (2 seconds) - 'proxy.config.http.transaction_active_timeout_in': 2, - # Force log flush every second for test reliability - 'proxy.config.log.max_secs_per_buffer': 1, - }) - -ts.Disk.remap_config.AddLine(f'map / https://127.0.0.1:{server.Variables.SSL_Port}') -ts.addPrivateConnectAllowYaml() - -# Configure custom log format to capture squid code -ts.Disk.logging_yaml.AddLines( - ''' -logging: - formats: - - name: custom - format: '% % %' - logs: - - filename: squid.log - format: custom -'''.split("\n")) - -# Test: Perform a CONNECT request that will time out -tr = Test.AddTestRun("Tunnel active timeout test") -tr.Processes.Default.StartBefore(server) -tr.Processes.Default.StartBefore(ts) - -# Use the tunnel_timeout_client.py script to establish a CONNECT tunnel and then -# just hold the connection until ATS times it out -tr.Setup.Copy('tunnel_timeout_client.py') - -# Connect, establish tunnel, then sleep to trigger active timeout -tr.Processes.Default.Command = ( - f'{sys.executable} tunnel_timeout_client.py 127.0.0.1 {ts.Variables.port} ' - f'127.0.0.1 {server.Variables.SSL_Port} 5') -# The connection will be closed by ATS due to timeout -tr.Processes.Default.ReturnCode = 0 -tr.StillRunningAfter = ts - -# Wait for the access log to be written -tr = Test.AddTestRun("Wait for the access log to write out") -tr.DelayStart = 3 -tr.StillRunningAfter = ts -tr.Processes.Default.Command = 'echo "waiting for log flush"' -tr.Processes.Default.ReturnCode = 0 - -# Verify the squid code in the access log -ts.Disk.File(os.path.join(ts.Variables.LOGDIR, 'squid.log')).Content = Testers.ContainsExpression( - 'ERR_TUN_ACTIVE_TIMEOUT.*CONNECT', 'Verify the tunnel timeout squid code is logged') diff --git a/tests/gold_tests/tls/allow-plain.test.py b/tests/gold_tests/tls/allow-plain.test.py deleted file mode 100644 index 0e2802b868d..00000000000 --- a/tests/gold_tests/tls/allow-plain.test.py +++ /dev/null @@ -1,106 +0,0 @@ -''' -Test the allow-plain attribute of the ssl port -Clients sending non-tls request to ssl port should get passed to -non-tls processing in ATS if the allow-plain attibute is present -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import os - -Test.Summary = ''' -Test allow-plain attributed -''' - -Test.ContinueOnFail = False - -# Define default ATS -ts = Test.MakeATSProcess("ts", enable_tls=True) - -# ---- -# Setup Origin Server -# ---- -replay_file = "replay/allow-plain.replay.yaml" -server = Test.MakeVerifierServerProcess("server", replay_file) - -ts.addDefaultSSLFiles() - -ts.Disk.records_config.update( - { - 'proxy.config.http.server_ports': '{0}:ssl:allow-plain'.format(ts.Variables.ssl_port), - 'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir), - 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts.Variables.SSLDir), - 'proxy.config.diags.debug.enabled': 0, - 'proxy.config.diags.debug.tags': 'ssl|http', - }) - -ts.Disk.remap_config.AddLines( - [ - 'map / http://127.0.0.1:{0}'.format(server.Variables.http_port), - 'map /post http://127.0.0.1:{0}/post'.format(server.Variables.http_port), - ]) - -ts.Disk.ssl_multicert_yaml.AddLines( - """ -ssl_multicert: - - dest_ip: "*" - ssl_cert_name: server.pem - ssl_key_name: server.key -""".split("\n")) - -big_post_body = "0123456789" * 50000 -big_post_body_file = open(os.path.join(Test.RunDirectory, "big_post_body"), "w") -big_post_body_file.write(big_post_body) -big_post_body_file.close() - -# TLS curl should work of course -tr = Test.AddTestRun() -# Wait for the micro server -tr.Processes.Default.StartBefore(server, ready=When.PortOpen(server.Variables.http_port)) -# Delay on readiness of our ssl ports -tr.Processes.Default.StartBefore(Test.Processes.ts) - -tr.MakeCurlCommand( - '-o /dev/null -k --verbose -H "uuid: get" --ipv4 --http1.1 --resolve www.example.com:{}:127.0.0.1 https://www.example.com:{}/' - .format(ts.Variables.ssl_port, ts.Variables.ssl_port), - ts=ts) -tr.Processes.Default.ReturnCode = 0 -tr.StillRunningAfter = server -tr.StillRunningAfter = ts -tr.Processes.Default.Streams.all = Testers.ContainsExpression("TLS", "Should negiotiate TLS") - -# non-TLS curl should also work to the same port -tr2 = Test.AddTestRun() -tr2.MakeCurlCommand( - '--verbose --ipv4 --http1.1 -H "uuid: get" --resolve www.example.com:{}:127.0.0.1 http://www.example.com:{}'.format( - ts.Variables.ssl_port, ts.Variables.ssl_port), - ts=ts) -tr2.Processes.Default.ReturnCode = 0 -tr2.StillRunningAfter = server -tr2.StillRunningAfter = ts -tr2.Processes.Default.Streams.all = Testers.ExcludesExpression("TLS", "Should not negiotiate TLS") - -# Make sure a post > 32K works. Early version forgot to free a reader which caused a stall once the initial buffer filled -# Seems like we needed to make a second resquest to trigger the issue -tr3 = Test.AddTestRun() -tr3.MakeCurlCommand( - '--verbose -d @big_post_body -H "uuid: post" --ipv4 --http1.1 --resolve www.example.com:{}:127.0.0.1 http://www.example.com:{}/post http://www.example.com:{}/post' - .format(ts.Variables.ssl_port, ts.Variables.ssl_port, ts.Variables.ssl_port), - ts=ts) -tr3.Processes.Default.ReturnCode = 0 -tr3.StillRunningAfter = server -tr3.StillRunningAfter = ts -tr3.Processes.Default.Streams.all = Testers.ExcludesExpression("TLS", "Should not negiotiate TLS") diff --git a/tests/gold_tests/tls/exit_on_cert_load_fail.test.py b/tests/gold_tests/tls/exit_on_cert_load_fail.test.py deleted file mode 100644 index cbac56e6672..00000000000 --- a/tests/gold_tests/tls/exit_on_cert_load_fail.test.py +++ /dev/null @@ -1,125 +0,0 @@ -''' -Test the exit_on_load_fail behavior for the SSL cert loading. -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import os - -Test.Summary = ''' -Test the exit_on_load_fail behavior for the SSL cert loading. -''' - - -class Test_exit_on_cert_load_fail: - """Configure a test to verify behavior of SSL cert load failures.""" - - ts_counter: int = 0 - - def __init__(self, name: str, is_testing_server_side: bool, enable_exit_on_load=False): - """ - Initialize the test. - :param name: The name of the test. - :param is_testing_server_side: Whether to test the server side or client side cert loading. - enable_exit_on_load: Whether to enable the exit_on_load_fail config. - """ - self.name = name - self.is_testing_server_side = is_testing_server_side - self.enable_exit_on_load = enable_exit_on_load - - def _configure_traffic_server(self, tr: 'TestRun'): - """Configure Traffic Server. - - :param tr: The TestRun object to associate the ts process with. - """ - ts = tr.MakeATSProcess(f"ts-{Test_exit_on_cert_load_fail.ts_counter}", enable_tls=True) - Test_exit_on_cert_load_fail.ts_counter += 1 - self._ts = ts - client_cert_path = 'NULL' - enable_exit_on_server_cert_load_failure = 1 if self.enable_exit_on_load and self.is_testing_server_side else 0 - enable_exit_on_client_cert_load_failure = 1 if self.enable_exit_on_load and not self.is_testing_server_side else 0 - - if not self.is_testing_server_side: - # Point to a non-existent cert to force a load failure. - client_cert_path = f'{ts.Variables.SSLDir}/non-existent-cert.pem' - # Also setup the server certs so that issues are limited to client - # cert loading. - self._ts.addDefaultSSLFiles() - self._ts.Disk.ssl_multicert_yaml.AddLines( - """ -ssl_multicert: - - dest_ip: "*" - ssl_cert_name: server.pem - ssl_key_name: server.key -""".split("\n")) - self._ts.Disk.records_config.update( - { - 'proxy.config.ssl.server.cert.path': f'{ts.Variables.SSLDir}', - 'proxy.config.ssl.server.private_key.path': f'{ts.Variables.SSLDir}', - 'proxy.config.ssl.client.cert.filename': client_cert_path, - 'proxy.config.ssl.server.multicert.exit_on_load_fail': enable_exit_on_server_cert_load_failure, - 'proxy.config.ssl.client.cert.exit_on_load_fail': enable_exit_on_client_cert_load_failure, - }) - # The cert loading happen on startup, so the remap rule is not - # triggered. - RANDOM_PORT = 12345 - self._ts.Disk.remap_config.AddLine(f'map / https://127.0.0.1:{RANDOM_PORT}/') - - def run(self): - """Run the test.""" - tr = Test.AddTestRun(self.name) - self._configure_traffic_server(tr) - - # The client process can be anything. - tr.Processes.Default.Command = "echo" - tr.Processes.Default.StartAfter(self._ts, ready=When.FileExists(self._ts.Disk.diags_log)) - - # Override the default exclusion of error log. - self._ts.Disk.diags_log.Content = Testers.ContainsExpression("ERROR:", "These tests should have error logs.") - - if self.enable_exit_on_load: - self._ts.ReturnCode = 33 - self._ts.Disk.diags_log.Content += Testers.ContainsExpression( - "EMERGENCY: ", "Failure loading the certs results in an emergency error.") - self._ts.Disk.diags_log.Content += Testers.ExcludesExpression( - "Traffic Server is fully initialized", "Traffic Server should exit upon the load failure.") - else: - self._ts.ReturnCode = 0 - self._ts.Disk.diags_log.Content += Testers.ContainsExpression( - "Traffic Server is fully initialized", "Traffic Server should start up successfully.") - - if self.is_testing_server_side: - self._ts.Disk.diags_log.Content += Testers.ContainsExpression( - "ERROR:.*failed to load", "Verify that there is a cert loading issue.") - else: - self._ts.Disk.diags_log.Content += Testers.ContainsExpression( - "ERROR: failed to access cert", "Verify that there is a cert loading issue.") - self._ts.Disk.diags_log.Content += Testers.ContainsExpression( - "Can't initialize the SSL client, HTTPS in remap rules will not function", - "There should be an error loading the cert.") - - -# Test server cert loading. -Test_exit_on_cert_load_fail( - "load server cert with exit on load disabled", is_testing_server_side=True, enable_exit_on_load=False).run() -Test_exit_on_cert_load_fail( - "load server cert with exit on load enabled", is_testing_server_side=True, enable_exit_on_load=True).run() - -# Test client cert loading. -Test_exit_on_cert_load_fail( - "load client cert with exit on load disabled", is_testing_server_side=False, enable_exit_on_load=False).run() -Test_exit_on_cert_load_fail( - "load client cert with exit on load enabled", is_testing_server_side=False, enable_exit_on_load=True).run() diff --git a/tests/gold_tests/tls/proxy_protocol_addressless.test.py b/tests/gold_tests/tls/proxy_protocol_addressless.test.py deleted file mode 100644 index 7372d00feee..00000000000 --- a/tests/gold_tests/tls/proxy_protocol_addressless.test.py +++ /dev/null @@ -1,87 +0,0 @@ -''' -Verify handling of addressless PROXY protocol headers. -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import sys - -Test.Summary = ''' -Verify listeners handle addressless PROXY protocol headers. -''' - -host = 'addressless.proxy.protocol.test' - - -def add_addressless_proxy_protocol_run(protocol_version: int, description: str, use_tls: bool) -> None: - """Add a test run for an addressless PROXY header.""" - mode = 'tls' if use_tls else 'http' - ts = Test.MakeATSProcess(f'ts_{mode}_v{protocol_version}', enable_tls=use_tls, enable_cache=False, enable_proxy_protocol=True) - server = Test.MakeOriginServer(f'server_{mode}_v{protocol_version}') - server.ReturnCode = 0 - - request_header = { - "headers": f"GET /proxy_protocol HTTP/1.1\r\nHost: {host}\r\nConnection: close\r\n\r\n", - "timestamp": "1469733493.993", - "body": "" - } - response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": "ok"} - server.addResponse("sessionlog.json", request_header, response_header) - - if use_tls: - ts.addDefaultSSLFiles() - ts.Disk.ssl_multicert_yaml.AddLines( - """ -ssl_multicert: - - dest_ip: "*" - ssl_cert_name: server.pem - ssl_key_name: server.key -""".split("\n")) - - ts.Disk.remap_config.AddLine(f'map / http://127.0.0.1:{server.Variables.Port}/') - records_config = { - 'proxy.config.http.proxy_protocol_allowlist': '127.0.0.1', - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'proxyprotocol', - } - if use_tls: - records_config.update( - { - 'proxy.config.ssl.server.cert.path': ts.Variables.SSLDir, - 'proxy.config.ssl.server.private_key.path': ts.Variables.SSLDir, - }) - ts.Disk.records_config.update(records_config) - - tr = Test.AddTestRun(description) - tr.TimeOut = 10 - tr.Setup.Copy('proxy_protocol_client.py') - port = ts.Variables.proxy_protocol_ssl_port if use_tls else ts.Variables.proxy_protocol_port - tr.Processes.Default.Command = ( - f'{sys.executable} proxy_protocol_client.py 127.0.0.1 {port} {host} ' - f'127.0.0.1 127.0.0.1 60123 {server.Variables.Port} ' - f'{protocol_version} --addressless') - if use_tls: - tr.Processes.Default.Command += ' --https' - tr.Processes.Default.StartBefore(server) - tr.Processes.Default.StartBefore(ts) - tr.ReturnCode = 0 - tr.Processes.Default.Streams.All += Testers.ContainsExpression("HTTP/1.1 200 OK", "Verify a successful response is received") - - -add_addressless_proxy_protocol_run(1, 'PROXY v1 UNKNOWN before HTTP/1', use_tls=False) -add_addressless_proxy_protocol_run(2, 'PROXY v2 LOCAL before HTTP/1', use_tls=False) -add_addressless_proxy_protocol_run(1, 'PROXY v1 UNKNOWN before TLS', use_tls=True) -add_addressless_proxy_protocol_run(2, 'PROXY v2 LOCAL before TLS', use_tls=True) diff --git a/tests/gold_tests/tls/ssl_key_dialog.test.py b/tests/gold_tests/tls/ssl_key_dialog.test.py deleted file mode 100644 index 9926bab88a6..00000000000 --- a/tests/gold_tests/tls/ssl_key_dialog.test.py +++ /dev/null @@ -1,99 +0,0 @@ -''' -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding right ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -Test.Summary = ''' -Test that Trafficserver starts with default configurations. -''' - -ts = Test.MakeATSProcess("ts", enable_tls=True) -server = Test.MakeOriginServer("server") - -ts.addSSLfile("ssl/passphrase.pem") -ts.addSSLfile("ssl/passphrase.key") - -ts.addSSLfile("ssl/passphrase2.pem") -ts.addSSLfile("ssl/passphrase2.key") - -ts.Disk.remap_config.AddLine(f"map https://passphrase:{ts.Variables.ssl_port}/ http://127.0.0.1:{server.Variables.Port}") -ts.Disk.remap_config.AddLine(f"map https://passphrase2:{ts.Variables.ssl_port}/ http://127.0.0.1:{server.Variables.Port}") -ts.Disk.records_config.update( - { - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'ssl_load|http', - 'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir), - 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts.Variables.SSLDir), - }) - -ts.Disk.ssl_multicert_yaml.AddLines( - """ -ssl_multicert: - - dest_ip: "*" - ssl_cert_name: passphrase.pem - ssl_key_name: passphrase.key - ssl_key_dialog: "exec:/bin/bash -c 'echo -n passphrase'" -""".split("\n")) - -request_header = {"headers": "GET / HTTP/1.1\r\nHost: bogus\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": "success!"} -server.addResponse("sessionlog.json", request_header, response_header) - -tr = Test.AddTestRun("use a key with passphrase") -tr.Setup.Copy("ssl/signer.pem") -tr.MakeCurlCommand( - f"-v --cacert ./signer.pem --resolve 'passphrase:{ts.Variables.ssl_port}:127.0.0.1' https://passphrase:{ts.Variables.ssl_port}/", - ts=ts) -tr.ReturnCode = 0 -tr.Processes.Default.StartBefore(server) -tr.Processes.Default.StartBefore(Test.Processes.ts) -tr.Processes.Default.Streams.stderr.Content = Testers.ContainsExpression("200", "expected 200 OK response") -tr.Processes.Default.Streams.stdout.Content = Testers.ContainsExpression("success!", "expected success") -tr.StillRunningAfter = server -tr.StillRunningAfter = ts - -tr2 = Test.AddTestRun("Update config files") -# Update the multicert config -sslcertpath = ts.Disk.ssl_multicert_yaml.AbsPath -tr2.Disk.File(sslcertpath, id="ssl_multicert_config", typename="ats:config"), -tr2.Disk.ssl_multicert_config.AddLines( - """ -ssl_multicert: - - dest_ip: "*" - ssl_cert_name: passphrase2.pem - ssl_key_name: passphrase2.key - ssl_key_dialog: "exec:/bin/bash -c 'echo -n passphrase'" -""".split("\n")) -tr2.StillRunningAfter = ts -tr2.StillRunningAfter = server -tr2.Processes.Default.Command = 'echo Updated configs' -# Need to copy over the environment so traffic_ctl knows where to find the unix domain socket -tr2.Processes.Default.Env = ts.Env -tr2.Processes.Default.ReturnCode = 0 - -tr2reload = Test.AddConfigReload(ts, expect_tasks=["ssl_multicert.yaml"], description="Reload config") -tr2reload.StillRunningAfter = server - -tr3 = Test.AddTestRun("use a key with passphrase") -tr3.Setup.Copy("ssl/signer.pem") -tr3.MakeCurlCommand( - f"-v --cacert ./signer.pem --resolve 'passphrase2:{ts.Variables.ssl_port}:127.0.0.1' https://passphrase2:{ts.Variables.ssl_port}/", - ts=ts) -tr3.ReturnCode = 0 -tr3.Processes.Default.Streams.stderr.Content = Testers.ContainsExpression("200", "expected 200 OK response") -tr3.Processes.Default.Streams.stdout.Content = Testers.ContainsExpression("success!", "expected success") -tr3.StillRunningAfter = server -tr3.StillRunningAfter = ts diff --git a/tests/gold_tests/tls/ssl_multicert_loader.test.py b/tests/gold_tests/tls/ssl_multicert_loader.test.py deleted file mode 100644 index 5a4fb1c1789..00000000000 --- a/tests/gold_tests/tls/ssl_multicert_loader.test.py +++ /dev/null @@ -1,163 +0,0 @@ -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -Test.Summary = ''' -Test reloading ssl_multicert.yaml with errors and keeping around the old ssl config structure -''' - -sni_domain = 'example.com' - -ts = Test.MakeATSProcess("ts", enable_tls=True, disable_log_checks=True) -server = Test.MakeOriginServer("server") -server2 = Test.MakeOriginServer("server2") -request_header = {"headers": f"GET / HTTP/1.1\r\nHost: {sni_domain}\r\n\r\n", "timestamp": "1469733493.993", "body": ""} - -response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -server.addResponse("sessionlog.json", request_header, response_header) - -ts.Disk.records_config.update( - { - 'proxy.config.ssl.server.cert.path': f'{ts.Variables.SSLDir}', - 'proxy.config.ssl.server.private_key.path': f'{ts.Variables.SSLDir}', - 'proxy.config.ssl.server.multicert.exit_on_load_fail': 0, - }) - -ts.addDefaultSSLFiles() - -ts.Disk.remap_config.AddLine(f'map / http://127.0.0.1:{server.Variables.Port}') - -ts.Disk.ssl_multicert_yaml.AddLines( - """ -ssl_multicert: - - dest_ip: "*" - ssl_cert_name: server.pem - ssl_key_name: server.key -""".split("\n")) - -tr = Test.AddTestRun("ensure we can connect for SNI $sni_domain") -tr.Processes.Default.StartBefore(Test.Processes.ts) -tr.Processes.Default.StartBefore(server) -tr.StillRunningAfter = ts -tr.StillRunningAfter = server -tr.MakeCurlCommand( - f"-q -s -v -k --resolve '{sni_domain}:{ts.Variables.ssl_port}:127.0.0.1' https://{sni_domain}:{ts.Variables.ssl_port}", ts=ts) -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Streams.stdout = Testers.ExcludesExpression("Could Not Connect", "Check response") -tr.Processes.Default.Streams.stderr = Testers.IncludesExpression(f"CN={sni_domain}", "Check response") - -tr2 = Test.AddTestRun("Update config files") -# Update the configs - overwrite the ssl_multicert.yaml file with an invalid config -sslcertpath = ts.Disk.ssl_multicert_yaml.AbsPath - -tr2.Disk.File(sslcertpath, id="ssl_multicert_update", typename="ats:config") -tr2.Disk.ssl_multicert_update.AddLines( - """ -ssl_multicert: - - ssl_cert_name: server_does_not_exist.pem - ssl_key_name: server_does_not_exist.key - - dest_ip: "*" - ssl_cert_name: server.pem_doesnotexist - ssl_key_name: server.key -""".split("\n")) -tr2.StillRunningAfter = ts -tr2.StillRunningAfter = server -tr2.Processes.Default.Command = 'echo Updated configs' -tr2.Processes.Default.Env = ts.Env -tr2.Processes.Default.ReturnCode = 0 - -tr2reload = Test.AddConfigReload(ts, expect="fail", expect_tasks=["ssl_multicert.yaml"], description="Reload config") -tr2reload.StillRunningAfter = server - -# Reload of ssl_multicert.yaml should fail, BUT the old config structure -# should be in place to successfully answer for the test domain -tr3 = Test.AddTestRun("Make request again for $sni_domain") -tr3.Processes.Default.StartBefore(server2) -tr3.StillRunningAfter = ts -tr3.StillRunningAfter = server -tr3.MakeCurlCommand( - f"-q -s -v -k --resolve '{sni_domain}:{ts.Variables.ssl_port}:127.0.0.1' https://{sni_domain}:{ts.Variables.ssl_port}", ts=ts) -tr3.Processes.Default.ReturnCode = 0 -tr3.Processes.Default.Streams.stdout = Testers.ExcludesExpression("Could Not Connect", "Check response") -tr3.Processes.Default.Streams.stderr = Testers.IncludesExpression(f"CN={sni_domain}", "Check response") -ts.Disk.diags_log.Content = Testers.ExcludesExpression( - r'\(quic\).*ssl_multicert', 'QUIC certificates should not load without a configured QUIC listener') - -########################################################################## -# Ensure ATS fails/exits when non-existent cert is specified -# Also, not explicitly setting proxy.config.ssl.server.multicert.exit_on_load_fail -# to catch if the current default (1) changes in the future - -ts2 = Test.MakeATSProcess("ts2", enable_tls=True) -ts2.Disk.ssl_multicert_yaml.AddLines( - """ -ssl_multicert: - - dest_ip: "*" - ssl_cert_name: server.pem_doesnotexist - ssl_key_name: server.key -""".split("\n")) - -tr4 = Test.AddTestRun() -tr4.Processes.Default.Command = 'echo Waiting' -tr4.Processes.Default.ReturnCode = 0 -tr4.Processes.Default.StartBefore(ts2) - -ts2.ReturnCode = 33 # ink_emergency will exit with UNRECOVERABLE_EXIT. -ts2.Ready = 0 # Need this to be 0 because we are testing shutdown, this is to make autest not think ats went away for a bad reason. -ts2.Disk.traffic_out.Content = Testers.ExcludesExpression( - 'Traffic Server is fully initialized', 'process should fail when invalid certificate specified') -ts2.Disk.diags_log.Content = Testers.IncludesExpression('EMERGENCY: failed to load SSL certificate file', 'check diags.log"') - -########################################################################## -# Verify parallel cert loading on startup (firstLoad uses hardware_concurrency, -# not the configured concurrency value, so the thread count is host-dependent) - -ts3 = Test.MakeATSProcess("ts3", enable_tls=True) -server3 = Test.MakeOriginServer("server3") -server3.addResponse("sessionlog.json", request_header, response_header) - -ts3.Disk.records_config.update( - { - 'proxy.config.ssl.server.cert.path': f'{ts3.Variables.SSLDir}', - 'proxy.config.ssl.server.private_key.path': f'{ts3.Variables.SSLDir}', - }) - -ts3.addDefaultSSLFiles() - -ts3.Disk.remap_config.AddLine(f'map / http://127.0.0.1:{server3.Variables.Port}') - -# Need at least 2 certs for multi-threading to kick in -ts3.Disk.ssl_multicert_yaml.AddLines( - """ -ssl_multicert: - - dest_ip: "*" - ssl_cert_name: server.pem - ssl_key_name: server.key - - ssl_cert_name: server.pem - ssl_key_name: server.key -""".split("\n")) - -tr5 = Test.AddTestRun("Verify parallel cert loading") -tr5.Processes.Default.StartBefore(ts3) -tr5.Processes.Default.StartBefore(server3) -tr5.StillRunningAfter = ts3 -tr5.StillRunningAfter = server3 -tr5.MakeCurlCommand( - f"-q -s -v -k --resolve '{sni_domain}:{ts3.Variables.ssl_port}:127.0.0.1' https://{sni_domain}:{ts3.Variables.ssl_port}", - ts=ts3) -tr5.Processes.Default.ReturnCode = 0 -tr5.Processes.Default.Streams.stdout = Testers.ExcludesExpression("Could Not Connect", "Check response") -tr5.Processes.Default.Streams.stderr = Testers.IncludesExpression(f"CN={sni_domain}", "Check response") -ts3.Disk.diags_log.Content = Testers.IncludesExpression('loaded 2 certs', 'verify certs were loaded successfully') diff --git a/tests/gold_tests/tls/tls.test.py b/tests/gold_tests/tls/tls.test.py deleted file mode 100644 index 5ac2951446d..00000000000 --- a/tests/gold_tests/tls/tls.test.py +++ /dev/null @@ -1,90 +0,0 @@ -''' -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import os - -Test.Summary = ''' -Test tls -''' - -# Define default ATS -ts = Test.MakeATSProcess("ts", enable_tls=True) -server = Test.MakeOriginServer("server") - -# ssl-post is built via `make`. Here we copy the built binary down to the test -# directory so that the test runs in this file can use it. -Test.Setup.Copy(os.path.join(Test.Variables.AtsBuildGoldTestsDir, 'tls', 'ssl-post')) - -requestLocation = "test2" -reHost = "www.example.com" - -testName = "" - -header_count = 378 - -header_string = "POST /post HTTP/1.1\r\nHost: www.example.com\r\nContent-Length:1000\r\n" - -for i in range(0, header_count): - header_string = "{1}header{0}:{0}\r\n".format(i, header_string) -header_string = "{0}\r\n".format(header_string) - -post_body = "" -for i in range(0, 1000): - post_body = "{0}0".format(post_body) - -# Add info the origin server responses -server.addResponse( - "sessionlog.json", { - "headers": header_string, - "timestamp": "1469733493.993", - "body": post_body - }, { - "headers": - "HTTP/1.1 200 OK\r\nServer: microserver\r\nConnection: close\r\nCache-Control: max-age=3600\r\nContent-Length: 2\r\n\r\n", - "timestamp": "1469733493.993", - "body": "ok" - }) - -# add ssl materials like key, certificates for the server -ts.addDefaultSSLFiles() - -ts.Disk.remap_config.AddLine('map / http://127.0.0.1:{0}'.format(server.Variables.Port)) - -ts.Disk.ssl_multicert_yaml.AddLines( - """ -ssl_multicert: - - dest_ip: "*" - ssl_cert_name: server.pem - ssl_key_name: server.key -""".split("\n")) -ts.Disk.records_config.update( - { - 'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir), - 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts.Variables.SSLDir), - 'proxy.config.exec_thread.autoconfig.scale': 1.0, - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'ssl', - }) - -tr = Test.AddTestRun("Run-Test") -tr.Command = './ssl-post 127.0.0.1 40 {0} {1}'.format(header_count, ts.Variables.ssl_port) -tr.ReturnCode = 0 -tr.Processes.Default.StartBefore(server) -tr.Processes.Default.StartBefore(Test.Processes.ts) -tr.Processes.Default.Streams.stdout = "gold/ssl-post.gold" -tr.StillRunningAfter = server diff --git a/tests/gold_tests/tls/tls_0rtt_server.test.py b/tests/gold_tests/tls/tls_0rtt_server.test.py deleted file mode 100644 index 49e6d5f6cd3..00000000000 --- a/tests/gold_tests/tls/tls_0rtt_server.test.py +++ /dev/null @@ -1,261 +0,0 @@ -''' -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import sys - -Test.Summary = ''' -Test ATS TLSv1.3 0-RTT support -''' - -# Checking only OpenSSL version allows you to run this test with BoringSSL (and it should pass). -Test.SkipUnless(Condition.HasOpenSSLVersion('1.1.1'),) - -ts1 = Test.MakeATSProcess('ts1', enable_tls=True) -ts2 = Test.MakeATSProcess('ts2', enable_tls=True) -server = Test.MakeOriginServer('server') - -request_header1 = {'headers': 'GET / HTTP/1.1\r\nHost: www.example.com\r\n\r\n', 'timestamp': '1469733493.993', 'body': ''} -response_header1 = {'headers': 'HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n', 'timestamp': '1469733493.993', 'body': 'curl test'} -request_header2 = {'headers': 'GET /early_get HTTP/1.1\r\nHost: www.example.com\r\n\r\n', 'timestamp': '1469733493.993', 'body': ''} -response_header2 = { - 'headers': 'HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n', - 'timestamp': '1469733493.993', - 'body': 'early data accepted' -} -request_header3 = { - 'headers': 'POST /early_post HTTP/1.1\r\nHost: www.example.com\r\nContent-Length: 11\r\n\r\n', - 'timestamp': '1469733493.993', - 'body': 'knock knock' -} -response_header3 = { - 'headers': 'HTTP/1.1 200 OK\r\nServer: uServer\r\nConnection: close\r\nTransfer-Encoding: chunked\r\n\r\n', - 'timestamp': '1415926535.898', - 'body': '' -} -request_header4 = { - 'headers': 'GET /early_multi_1 HTTP/1.1\r\nHost: www.example.com\r\n\r\n', - 'timestamp': '1469733493.993', - 'body': '' -} -response_header4 = { - 'headers': 'HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n', - 'timestamp': '1469733493.993', - 'body': 'early data accepted multi_1' -} -request_header5 = { - 'headers': 'GET /early_multi_2 HTTP/1.1\r\nHost: www.example.com\r\n\r\n', - 'timestamp': '1469733493.993', - 'body': '' -} -response_header5 = { - 'headers': 'HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n', - 'timestamp': '1469733493.993', - 'body': 'early data accepted multi_2' -} -request_header6 = { - 'headers': 'GET /early_multi_3 HTTP/1.1\r\nHost: www.example.com\r\n\r\n', - 'timestamp': '1469733493.993', - 'body': '' -} -response_header6 = { - 'headers': 'HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n', - 'timestamp': '1469733493.993', - 'body': 'early data accepted multi_3' -} -server.addResponse('sessionlog.json', request_header1, response_header1) -server.addResponse('sessionlog.json', request_header2, response_header2) -server.addResponse('sessionlog.json', request_header3, response_header3) -server.addResponse('sessionlog.json', request_header4, response_header4) -server.addResponse('sessionlog.json', request_header5, response_header5) -server.addResponse('sessionlog.json', request_header6, response_header6) - -ts1.addSSLfile('ssl/server.pem') -ts1.addSSLfile('ssl/server.key') - -ts1.Setup.Copy('test-0rtt-s_client.py') -ts1.Setup.Copy('h2_early_decode.py') -ts1.Setup.Copy('early_h1_get.txt') -ts1.Setup.Copy('early_h1_post.txt') -ts1.Setup.Copy('early_h2_get.txt') -ts1.Setup.Copy('early_h2_post.txt') -ts1.Setup.Copy('early_h2_multi1.txt') -ts1.Setup.Copy('early_h2_multi2.txt') - -ts1.Disk.records_config.update( - { - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'http|ssl_early_data|ssl', - 'proxy.config.exec_thread.autoconfig.enabled': 0, - 'proxy.config.exec_thread.limit': 8, - 'proxy.config.ssl.server.cert.path': '{0}'.format(ts1.Variables.SSLDir), - 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts1.Variables.SSLDir), - 'proxy.config.ssl.server.session_ticket.enable': 1, - 'proxy.config.ssl.server.max_early_data': 16384, - 'proxy.config.ssl.server.allow_early_data_params': 0, - 'proxy.config.ssl.server.cipher_suite': - 'ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-DSS-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-DSS-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA256:DHE-RSA-AES128-SHA256:DHE-DSS-AES128-SHA256:DHE-RSA-AES256-SHA:DHE-DSS-AES256-SHA:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!aECDH:!EDH-DSS-DES-CBC3-SHA:!EDH-RSA-DES-CBC3-SHA:!KRB5-DES-CBC3-SHA' - }) - -ts1.Disk.ssl_multicert_yaml.AddLines( - """ -ssl_multicert: - - dest_ip: "*" - ssl_cert_name: server.pem - ssl_key_name: server.key -""".split("\n")) - -ts1.Disk.remap_config.AddLine('map / http://127.0.0.1:{0}'.format(server.Variables.Port)) - -ts1.Disk.sni_yaml.AddLines([ - 'sni:', - '- fqdn: example-no.com', - ' server_max_early_data: 0', -]) - -ts2.Disk.records_config.update( - { - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'http|ssl_early_data|ssl', - 'proxy.config.exec_thread.autoconfig.enabled': 0, - 'proxy.config.exec_thread.limit': 8, - 'proxy.config.ssl.server.cert.path': '{0}'.format(ts1.Variables.SSLDir), - 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts1.Variables.SSLDir), - 'proxy.config.ssl.server.session_ticket.enable': 1, - 'proxy.config.ssl.server.max_early_data': 0, - 'proxy.config.ssl.server.allow_early_data_params': 0, - 'proxy.config.ssl.server.cipher_suite': - 'ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-DSS-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-DSS-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA256:DHE-RSA-AES128-SHA256:DHE-DSS-AES128-SHA256:DHE-RSA-AES256-SHA:DHE-DSS-AES256-SHA:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!aECDH:!EDH-DSS-DES-CBC3-SHA:!EDH-RSA-DES-CBC3-SHA:!KRB5-DES-CBC3-SHA' - }) - -ts2.Disk.ssl_multicert_yaml.AddLines( - """ -ssl_multicert: - - dest_ip: "*" - ssl_cert_name: server.pem - ssl_key_name: server.key -""".split("\n")) - -ts2.Disk.remap_config.AddLine('map / http://127.0.0.1:{0}'.format(server.Variables.Port)) - -ts2.Disk.sni_yaml.AddLines([ - 'sni:', - '- fqdn: example-yes.com', - ' server_max_early_data: 16384', -]) - -tr = Test.AddTestRun('Basic Curl Test') -tr.MakeCurlCommand('-k --resolve example.com:{0}:127.0.0.1 https://example.com:{0}'.format(ts1.Variables.ssl_port), ts=ts1) -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.StartBefore(server) -tr.Processes.Default.StartBefore(ts1) -tr.Processes.Default.Streams.All = Testers.ContainsExpression('curl test', 'Making sure the basics still work') -tr.Processes.Default.Streams.All += Testers.ExcludesExpression('early data accepted', '') -tr.StillRunningAfter = server -tr.StillRunningAfter += ts1 - -tr = Test.AddTestRun('TLSv1.3 0-RTT Support (HTTP/1.1 GET)') -tr.Processes.Default.Command = f'{sys.executable} {Test.RunDirectory}/test-0rtt-s_client.py -p {ts1.Variables.ssl_port} -v h1 -t get -r {Test.RunDirectory}' -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Streams.All = Testers.ContainsExpression('early data accepted', '') -tr.Processes.Default.Streams.All += Testers.ExcludesExpression('curl test', '') -tr.StillRunningAfter = server -tr.StillRunningAfter += ts1 - -tr = Test.AddTestRun('TLSv1.3 0-RTT Support (HTTP/1.1 POST)') -tr.Processes.Default.Command = f'{sys.executable} {Test.RunDirectory}/test-0rtt-s_client.py -p {ts1.Variables.ssl_port} -v h1 -t post -r {Test.RunDirectory}' -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Streams.All = Testers.ContainsExpression('HTTP/1.1 425 Too Early', '') -tr.Processes.Default.Streams.All += Testers.ExcludesExpression('curl test', '') -tr.Processes.Default.Streams.All += Testers.ExcludesExpression('early data accepted', '') -tr.StillRunningAfter = server -tr.StillRunningAfter += ts1 - -tr = Test.AddTestRun('TLSv1.3 0-RTT Support (HTTP/2 GET)') -tr.Processes.Default.Command = f'{sys.executable} {Test.RunDirectory}/test-0rtt-s_client.py -p {ts1.Variables.ssl_port} -v h2 -t get -r {Test.RunDirectory}' -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Streams.All = Testers.ContainsExpression('early data accepted', '') -tr.Processes.Default.Streams.All += Testers.ExcludesExpression('curl test', '') -tr.StillRunningAfter = server -tr.StillRunningAfter += ts1 - -tr = Test.AddTestRun('TLSv1.3 0-RTT Support (HTTP/2 POST)') -tr.Processes.Default.Command = f'{sys.executable} {Test.RunDirectory}/test-0rtt-s_client.py -p {ts1.Variables.ssl_port} -v h2 -t post -r {Test.RunDirectory}' -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Streams.All = Testers.ContainsExpression(':status 425', 'Only safe methods are allowed') -tr.Processes.Default.Streams.All += Testers.ExcludesExpression('curl test', '') -tr.Processes.Default.Streams.All += Testers.ExcludesExpression('early data accepted', '') -tr.StillRunningAfter = server -tr.StillRunningAfter += ts1 - -tr = Test.AddTestRun('TLSv1.3 0-RTT Support (HTTP/2 Multiplex)') -tr.Processes.Default.Command = f'{sys.executable} {Test.RunDirectory}/test-0rtt-s_client.py -p {ts1.Variables.ssl_port} -v h2 -t multi1 -r {Test.RunDirectory}' -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Streams.All = Testers.ContainsExpression('early data accepted multi_1', '') -tr.Processes.Default.Streams.All += Testers.ContainsExpression('early data accepted multi_2', '') -tr.Processes.Default.Streams.All += Testers.ContainsExpression('early data accepted multi_3', '') -tr.Processes.Default.Streams.All += Testers.ExcludesExpression('curl test', '') -tr.StillRunningAfter = server -tr.StillRunningAfter += ts1 - -tr = Test.AddTestRun('TLSv1.3 0-RTT Support (HTTP/2 Multiplex with POST)') -tr.Processes.Default.Command = f'{sys.executable} {Test.RunDirectory}/test-0rtt-s_client.py -p {ts1.Variables.ssl_port} -v h2 -t multi2 -r {Test.RunDirectory}' -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Streams.All = Testers.ContainsExpression('early data accepted multi_1', '') -tr.Processes.Default.Streams.All += Testers.ContainsExpression(':status 425', 'Only safe methods are allowed') -tr.Processes.Default.Streams.All += Testers.ContainsExpression('early data accepted multi_3', '') -tr.Processes.Default.Streams.All += Testers.ExcludesExpression('curl test', '') -tr.StillRunningAfter = server -tr.StillRunningAfter += ts1 - -tr = Test.AddTestRun('TLSv1.3 0-RTT Support (HTTP/1.1 GET) SNI Provided') -tr.Processes.Default.Command = f'{sys.executable} {Test.RunDirectory}/test-0rtt-s_client.py -p {ts1.Variables.ssl_port} -v h1 -t get -r {Test.RunDirectory} -s example.com' -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Streams.All = Testers.ContainsExpression('early data accepted', '') -tr.Processes.Default.Streams.All += Testers.ExcludesExpression('curl test', '') -tr.StillRunningAfter = server -tr.StillRunningAfter += ts1 - -tr = Test.AddTestRun('TLSv1.3 0-RTT Support (HTTP/1.1 GET) Disabled By SNI Config') -tr.Processes.Default.Command = f'{sys.executable} {Test.RunDirectory}/test-0rtt-s_client.py -p {ts1.Variables.ssl_port} -v h1 -t get -r {Test.RunDirectory} -s example-no.com' -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Streams.All = Testers.ExcludesExpression('early data accepted', '') -tr.Processes.Default.Streams.All += Testers.ExcludesExpression('curl test', '') -tr.StillRunningAfter = server - -tr = Test.AddTestRun('TLSv1.3 0-RTT Support (HTTP/1.1 GET) Disabled In General') -tr.Processes.Default.Command = f'{sys.executable} {Test.RunDirectory}/test-0rtt-s_client.py -p {ts2.Variables.ssl_port} -v h1 -t get -r {Test.RunDirectory}' -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.StartBefore(ts2) -tr.Processes.Default.Streams.All = Testers.ExcludesExpression('early data accepted', '') -tr.Processes.Default.Streams.All += Testers.ExcludesExpression('curl test', '') -tr.StillRunningAfter = server -tr.StillRunningAfter += ts2 - -tr = Test.AddTestRun('TLSv1.3 0-RTT Support (HTTP/1.1 GET) Disabled In General SNI Provided') -tr.Processes.Default.Command = f'{sys.executable} {Test.RunDirectory}/test-0rtt-s_client.py -p {ts2.Variables.ssl_port} -v h1 -t get -r {Test.RunDirectory} -s example.com' -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Streams.All = Testers.ExcludesExpression('early data accepted', '') -tr.Processes.Default.Streams.All += Testers.ExcludesExpression('curl test', '') -tr.StillRunningAfter = server -tr.StillRunningAfter += ts2 - -tr = Test.AddTestRun('TLSv1.3 0-RTT Support (HTTP/1.1 GET) Enabled By SNI Config') -tr.Processes.Default.Command = f'{sys.executable} {Test.RunDirectory}/test-0rtt-s_client.py -p {ts2.Variables.ssl_port} -v h1 -t get -r {Test.RunDirectory} -s example-yes.com' -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Streams.All = Testers.ContainsExpression('early data accepted', '') -tr.Processes.Default.Streams.All += Testers.ExcludesExpression('curl test', '') diff --git a/tests/gold_tests/tls/tls_accept_timeout_crash.test.py b/tests/gold_tests/tls/tls_accept_timeout_crash.test.py deleted file mode 100644 index 832fa06db35..00000000000 --- a/tests/gold_tests/tls/tls_accept_timeout_crash.test.py +++ /dev/null @@ -1,81 +0,0 @@ -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. -''' -Regression test: stale TLS accept-timeout events must not crash the session acceptors. - -With a pending TS_HTTP_SSN_START_HOOK whose callout defers, a stale TLS -handshake or accept-no-activity inactivity timer could fire after the read VIO -had been reassigned to a session acceptor but before the session took -ownership, triggering a release_assert in the acceptor's mainEvent and aborting -traffic_server. - -Reproduction: set the TLS handshake and accept-no-activity timeouts to 1 -second, load a plugin that delays the SSN_START hook by 3 seconds, then drive -TLS requests over HTTP/1.1 and HTTP/2. Before the fix, traffic_server aborts -during the delay window. After the fix, the client receives normal 200 -responses. -''' - -import os - -Test.Summary = 'TLS accept timeout must not crash HttpSessionAccept when SSN_START hook defers.' - -replay_file = "tls_accept_timeout_crash.replay.yaml" - -server = Test.MakeVerifierServerProcess("server", replay_file) - -ts = Test.MakeATSProcess("ts", enable_tls=True, enable_cache=False) -ts.addDefaultSSLFiles() - -ts.Disk.records_config.update( - { - 'proxy.config.ssl.server.cert.path': f'{ts.Variables.SSLDir}', - 'proxy.config.ssl.server.private_key.path': f'{ts.Variables.SSLDir}', - 'proxy.config.ssl.handshake_timeout_in': 1, - 'proxy.config.http.accept_no_activity_timeout': 1, - 'proxy.config.http2.accept_no_activity_timeout': 1, - 'proxy.config.url_remap.remap_required': 0, - }) - -ts.Disk.ssl_multicert_yaml.AddLines( - """ -ssl_multicert: - - dest_ip: "*" - ssl_cert_name: server.pem - ssl_key_name: server.key -""".split("\n")) - -ts.Disk.remap_config.AddLine(f'map / http://127.0.0.1:{server.Variables.http_port}/') - -# Delay TS_HTTP_SSN_START_HOOK reenable by 3s — longer than both -# handshake_timeout_in and accept_no_activity_timeout (1s each), shorter than -# the verifier's 5s read timeout. Either timer firing during the hook window -# lands a VC_EVENT_INACTIVITY_TIMEOUT on the session acceptor's VIO cont. -Test.PrepareTestPlugin(os.path.join(Test.Variables.AtsTestPluginsDir, 'hook_add_plugin.so'), ts, '-delay-ms=3000') - -# Assert traffic_server never hits the release_assert landmine in the session -# acceptors when an inactivity timer fires during the SSN_START hook window. -ts.Disk.traffic_out.Content += Testers.ExcludesExpression( - r'failed assertion `event == NET_EVENT_ACCEPT', 'session acceptors must not abort on stale inactivity timeouts') -ts.Disk.diags_log.Content += Testers.ExcludesExpression( - r'FATAL.*Assertion', 'traffic_server must not abort on a stale accept-path inactivity timeout') - -tr = Test.AddTestRun("TLS request through delayed SSN_START hook") -tr.Processes.Default.StartBefore(server) -tr.Processes.Default.StartBefore(ts) -tr.AddVerifierClientProcess("client", replay_file, https_ports=[ts.Variables.ssl_port]) -tr.StillRunningAfter = ts -tr.StillRunningAfter = server diff --git a/tests/gold_tests/tls/tls_async_handshake.test.py b/tests/gold_tests/tls/tls_async_handshake.test.py deleted file mode 100644 index 3adb786b671..00000000000 --- a/tests/gold_tests/tls/tls_async_handshake.test.py +++ /dev/null @@ -1,82 +0,0 @@ -''' -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import os - -Test.Summary = ''' -Test TLS handshakes through OpenSSL's async job interface. -''' - -async_handshake = os.path.join(Test.Variables.AtsTestPluginsDir, 'async_handshake.so') - -Test.SkipUnless( - Condition.HasOpenSSLVersion('1.1.1'), - Condition.IsOpenSSL(), - Condition(lambda: os.path.isfile(async_handshake), async_handshake + " not found."), -) - -ts = Test.MakeATSProcess("ts", enable_tls=True) -server = Test.MakeOriginServer("server") - -if os.path.isfile(async_handshake): - Test.PrepareTestPlugin(async_handshake, ts) - -server.addResponse( - "sessionlog.json", { - "headers": "GET / HTTP/1.1\r\nuuid: basic\r\n\r\n", - "timestamp": "1469733493.993", - "body": "" - }, { - "headers": - "HTTP/1.1 200 OK\r\nServer: microserver\r\nConnection: close\r\n" - "Cache-Control: max-age=3600\r\nContent-Length: 2\r\n\r\n", - "timestamp": "1469733493.993", - "body": "ok" - }) - -ts.addSSLfile("ssl/server.pem") -ts.addSSLfile("ssl/server.key") - -ts.Disk.remap_config.AddLine('map / http://127.0.0.1:{0}'.format(server.Variables.Port)) - -ts.Disk.ssl_multicert_yaml.AddLines( - """ -ssl_multicert: - - dest_ip: "*" - ssl_cert_name: server.pem - ssl_key_name: server.key -""".split("\n")) -ts.Disk.records_config.update( - { - 'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir), - 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts.Variables.SSLDir), - 'proxy.config.exec_thread.autoconfig.scale': 1.0, - 'proxy.config.ssl.async.handshake.enabled': 1, - 'proxy.config.diags.debug.enabled': 0, - 'proxy.config.diags.debug.tags': 'ssl|http' - }) - -tr = Test.AddTestRun("Run-Test") -tr.MakeCurlCommand("-k -v -H uuid:basic -H host:example.com https://127.0.0.1:{0}/".format(ts.Variables.ssl_port), ts=ts) -tr.ReturnCode = 0 -tr.Processes.Default.StartBefore(server) -tr.Processes.Default.StartBefore(Test.Processes.ts, ready=When.PortOpen(ts.Variables.ssl_port)) -tr.Processes.Default.Streams.All = Testers.ContainsExpression(r"HTTP/(2|1\.1) 200", "Request succeeds") -tr.StillRunningAfter = server - -ts.Disk.traffic_out.Content += Testers.ContainsExpression("resumed OpenSSL async job", "The OpenSSL async job resumes") diff --git a/tests/gold_tests/tls/tls_bad_alpn.test.py b/tests/gold_tests/tls/tls_bad_alpn.test.py deleted file mode 100644 index 513aba7e22f..00000000000 --- a/tests/gold_tests/tls/tls_bad_alpn.test.py +++ /dev/null @@ -1,81 +0,0 @@ -''' -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import os -import ports - -Test.Summary = ''' -Ensure that handshake fails if invalid alpn string is offered -''' - -# Only later versions of openssl support the `-alpn` option. -Test.SkipUnless(Condition.HasOpenSSLVersion('1.1.1')) - -# Define default ATS -ts = Test.MakeATSProcess("ts", enable_tls=True) - -# add ssl materials like key, certificates for the server -ts.addSSLfile("ssl/server.pem") -ts.addSSLfile("ssl/server.key") - -# Make sure the TS server certs are different from the origin certs -ts.Disk.ssl_multicert_yaml.AddLines( - """ -ssl_multicert: - - dest_ip: "*" - ssl_cert_name: server.pem - ssl_key_name: server.key -""".split("\n")) - -# Case 1, global config policy=permissive properties=signature -# override for foo.com policy=enforced properties=all -ts.Disk.records_config.update( - { - 'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir), - 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts.Variables.SSLDir), - }) - -tr = Test.AddTestRun("alpn banana") -tr.Processes.Default.Command = "openssl s_client -ign_eof -alpn=banana -connect 127.0.0.1:{}".format(ts.Variables.ssl_port) -tr.ReturnCode = 1 # Should fail -tr.Processes.Default.StartBefore(Test.Processes.ts) -tr.StillRunningAfter = ts -tr.Processes.Default.Streams.All += Testers.IncludesExpression("No ALPN negotiated", "Banana should not match a negotiation") - -tr = Test.AddTestRun("alpn http/1.1") -tr.Processes.Default.Command = "printf 'GET / HTTP/1.1\r\n\r\n' | openssl s_client -ign_eof -alpn=http/1.1 -connect 127.0.0.1:{}".format( - ts.Variables.ssl_port) -tr.ReturnCode = 0 -tr.StillRunningAfter = ts -tr.Processes.Default.Streams.All += Testers.IncludesExpression("ALPN protocol: http/1.1", "Successful ALPN") -tr.Processes.Default.Streams.All += Testers.IncludesExpression("HTTP/1.1 400 Host Header Required", "Processed the request") - -tr = Test.AddTestRun("no alpn") -tr.Processes.Default.Command = "printf 'GET / HTTP/1.1\r\n\r\n' | openssl s_client -ign_eof -connect 127.0.0.1:{}".format( - ts.Variables.ssl_port) -tr.ReturnCode = 0 -tr.StillRunningAfter = ts -tr.Processes.Default.Streams.All += Testers.IncludesExpression("No ALPN negotiated", "No ALPN offered, none negotiated") -tr.Processes.Default.Streams.All += Testers.IncludesExpression("HTTP/1.1 400 Host Header Required", "Processed the request") - -tr = Test.AddTestRun("alpn h2") -tr.MakeCurlCommand("-k --http2 -v -o /dev/null https://127.0.0.1:{}".format(ts.Variables.ssl_port), ts=ts) -tr.ReturnCode = 0 -tr.StillRunningAfter = ts -tr.Processes.Default.Streams.All += Testers.IncludesExpression("ALPN. server accepted.*h2", "negotiated h2") -tr.Processes.Default.Streams.All += Testers.IncludesExpression("HTTP/2 404 ", "Good response") diff --git a/tests/gold_tests/tls/tls_cert_comp.test.py b/tests/gold_tests/tls/tls_cert_comp.test.py deleted file mode 100644 index 030028d3d37..00000000000 --- a/tests/gold_tests/tls/tls_cert_comp.test.py +++ /dev/null @@ -1,176 +0,0 @@ -''' -Verify TLS Certificate Compression (RFC 8879) between two ATS processes. -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -Test.Summary = ''' -Verify TLS Certificate Compression (RFC 8879) works between two ATS -instances. An edge ATS (client) connects via HTTPS to a mid ATS (server) -with cert compression enabled. The test verifies compression and -decompression succeed by checking the ssl cert compression metrics. -''' - -Test.SkipUnless(Condition.HasATSFeature('TS_HAS_CERT_COMPRESSION_CALLBACKS')) - -REPLAY_FILE = 'replay/tls_cert_compression.replay.yaml' - - -class TestCertCompression: - server_counter: int = 0 - ts_counter: int = 0 - client_counter: int = 0 - - def __init__(self, algorithm: str) -> None: - self._algorithm = algorithm - self._server = self._configure_server() - self._ts_mid = self._configure_ts_mid() - self._ts_edge = self._configure_ts_edge() - - def _configure_server(self) -> 'Process': - name = f'server-{TestCertCompression.server_counter}' - TestCertCompression.server_counter += 1 - server = Test.MakeVerifierServerProcess(name, REPLAY_FILE) - return server - - def _configure_ts_mid(self) -> 'Process': - """Mid-tier ATS that terminates TLS and forwards to origin.""" - name = f'm{TestCertCompression.ts_counter}' - TestCertCompression.ts_counter += 1 - ts = Test.MakeATSProcess(name, enable_tls=True, enable_cache=False) - - ts.addDefaultSSLFiles() - ts.Disk.ssl_multicert_yaml.AddLines( - """ -ssl_multicert: - - dest_ip: "*" - ssl_cert_name: server.pem - ssl_key_name: server.key -""".split("\n")) - - ts.Disk.remap_config.AddLine(f'map / http://127.0.0.1:{self._server.Variables.http_port}/') - - ts.Disk.records_config.update( - { - 'proxy.config.ssl.server.cert.path': ts.Variables.SSLDir, - 'proxy.config.ssl.server.private_key.path': ts.Variables.SSLDir, - 'proxy.config.ssl.server.cert_compression.algorithms': self._algorithm, - 'proxy.config.ssl.server.cert_compression.cache': 0, - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'ssl_cert_compress', - }) - - return ts - - def _configure_ts_edge(self) -> 'Process': - """Edge ATS that connects to mid-tier via HTTPS.""" - name = f'e{TestCertCompression.ts_counter}' - TestCertCompression.ts_counter += 1 - ts = Test.MakeATSProcess(name, enable_tls=True, enable_cache=False) - - ts.addDefaultSSLFiles() - ts.Disk.ssl_multicert_yaml.AddLines( - """ -ssl_multicert: - - dest_ip: "*" - ssl_cert_name: server.pem - ssl_key_name: server.key -""".split("\n")) - - ts.Disk.remap_config.AddLine(f'map / https://127.0.0.1:{self._ts_mid.Variables.ssl_port}/') - - ts.Disk.records_config.update( - { - 'proxy.config.ssl.server.cert.path': ts.Variables.SSLDir, - 'proxy.config.ssl.server.private_key.path': ts.Variables.SSLDir, - 'proxy.config.ssl.client.verify.server.policy': 'PERMISSIVE', - 'proxy.config.ssl.client.cert_compression.algorithms': self._algorithm, - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'ssl_cert_compress', - }) - - return ts - - def run(self) -> None: - # Test run 1: Send traffic through the proxy chain. - tr = Test.AddTestRun(f'Send request through edge->mid with {self._algorithm} cert compression') - tr.Processes.Default.StartBefore(self._server) - tr.Processes.Default.StartBefore(self._ts_mid) - tr.Processes.Default.StartBefore(self._ts_edge) - - name = f'client-{TestCertCompression.client_counter}' - TestCertCompression.client_counter += 1 - tr.AddVerifierClientProcess(name, REPLAY_FILE, http_ports=[self._ts_edge.Variables.port]) - - # Test run 2: Check compression metric on the mid-tier (server side). - tr = Test.AddTestRun(f'Verify {self._algorithm} compression metric on mid-tier') - tr.Processes.Default.Command = (f'traffic_ctl metric get' - f' proxy.process.ssl.cert_compress.{self._algorithm}') - tr.Processes.Default.Env = self._ts_mid.Env - tr.Processes.Default.ReturnCode = 0 - tr.Processes.Default.Streams.All = Testers.ContainsExpression( - f'proxy.process.ssl.cert_compress.{self._algorithm} 1', - f'Certificate should have been compressed with {self._algorithm}') - tr.StillRunningAfter = self._ts_mid - tr.StillRunningAfter = self._ts_edge - tr.StillRunningAfter = self._server - - # Test run 3: Check decompression metric on the edge (client side). - tr = Test.AddTestRun(f'Verify {self._algorithm} decompression metric on edge') - tr.Processes.Default.Command = (f'traffic_ctl metric get' - f' proxy.process.ssl.cert_decompress.{self._algorithm}') - tr.Processes.Default.Env = self._ts_edge.Env - tr.Processes.Default.ReturnCode = 0 - tr.Processes.Default.Streams.All = Testers.ContainsExpression( - f'proxy.process.ssl.cert_decompress.{self._algorithm} 1', - f'Certificate should have been decompressed with {self._algorithm}') - tr.StillRunningAfter = self._ts_mid - tr.StillRunningAfter = self._ts_edge - tr.StillRunningAfter = self._server - - # Test run 4: Verify no failures on either side. - tr = Test.AddTestRun(f'Verify no {self._algorithm} compression failures on mid-tier') - tr.Processes.Default.Command = (f'traffic_ctl metric get' - f' proxy.process.ssl.cert_compress.{self._algorithm}_failure') - tr.Processes.Default.Env = self._ts_mid.Env - tr.Processes.Default.ReturnCode = 0 - tr.Processes.Default.Streams.All = Testers.ContainsExpression( - f'proxy.process.ssl.cert_compress.{self._algorithm}_failure 0', - f'There should be no {self._algorithm} compression failures') - tr.StillRunningAfter = self._ts_mid - tr.StillRunningAfter = self._ts_edge - tr.StillRunningAfter = self._server - - tr = Test.AddTestRun(f'Verify no {self._algorithm} decompression failures on edge') - tr.Processes.Default.Command = (f'traffic_ctl metric get' - f' proxy.process.ssl.cert_decompress.{self._algorithm}_failure') - tr.Processes.Default.Env = self._ts_edge.Env - tr.Processes.Default.ReturnCode = 0 - tr.Processes.Default.Streams.All = Testers.ContainsExpression( - f'proxy.process.ssl.cert_decompress.{self._algorithm}_failure 0', - f'There should be no {self._algorithm} decompression failures') - tr.StillRunningAfter = self._ts_mid - tr.StillRunningAfter = self._ts_edge - tr.StillRunningAfter = self._server - - -algorithms = ['zlib'] -if Condition.HasATSFeature('TS_HAS_BROTLI'): - algorithms.append('brotli') -if Condition.HasATSFeature('TS_HAS_ZSTD'): - algorithms.append('zstd') -for algorithm in algorithms: - TestCertCompression(algorithm).run() diff --git a/tests/gold_tests/tls/tls_cert_comp_cache.test.py b/tests/gold_tests/tls/tls_cert_comp_cache.test.py deleted file mode 100644 index 4d96385e8d2..00000000000 --- a/tests/gold_tests/tls/tls_cert_comp_cache.test.py +++ /dev/null @@ -1,183 +0,0 @@ -''' -Verify TLS Certificate Compression cache behavior. -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -Test.Summary = ''' -Verify that the certificate compression cache works correctly. When caching -is enabled, the compressed result is reused across handshakes. When disabled, -each handshake compresses independently and the cache_hit metric stays at 0. -''' - -Test.SkipUnless(Condition.HasATSFeature('TS_HAS_CERT_COMPRESSION_CALLBACKS')) - -REPLAY_FILE = 'replay/tls_cert_compression_cache.replay.yaml' - - -class TestCertCompressionCache: - server_counter: int = 0 - ts_counter: int = 0 - client_counter: int = 0 - - def __init__(self, cache_enabled: bool) -> None: - self._cache_enabled = cache_enabled - self._algorithm = 'zlib' - self._server = self._configure_server() - self._ts_mid = self._configure_ts_mid() - self._ts_edge = self._configure_ts_edge() - - def _configure_server(self) -> 'Process': - name = f'server-{TestCertCompressionCache.server_counter}' - TestCertCompressionCache.server_counter += 1 - server = Test.MakeVerifierServerProcess(name, REPLAY_FILE) - return server - - def _configure_ts_mid(self) -> 'Process': - """Mid-tier ATS that terminates TLS and forwards to origin.""" - name = f'm{TestCertCompressionCache.ts_counter}' - TestCertCompressionCache.ts_counter += 1 - ts = Test.MakeATSProcess(name, enable_tls=True, enable_cache=False) - - ts.addDefaultSSLFiles() - ts.Disk.ssl_multicert_yaml.AddLines( - """ -ssl_multicert: - - dest_ip: "*" - ssl_cert_name: server.pem - ssl_key_name: server.key -""".split("\n")) - - ts.Disk.remap_config.AddLine(f'map / http://127.0.0.1:{self._server.Variables.http_port}/') - - ts.Disk.records_config.update( - { - 'proxy.config.ssl.server.cert.path': ts.Variables.SSLDir, - 'proxy.config.ssl.server.private_key.path': ts.Variables.SSLDir, - 'proxy.config.ssl.server.cert_compression.algorithms': self._algorithm, - 'proxy.config.ssl.server.cert_compression.cache': 1 if self._cache_enabled else 0, - 'proxy.config.ssl.server.session_ticket.enable': 0, - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'ssl_cert_compress', - }) - - return ts - - def _configure_ts_edge(self) -> 'Process': - """Edge ATS that connects to mid-tier via HTTPS.""" - name = f'e{TestCertCompressionCache.ts_counter}' - TestCertCompressionCache.ts_counter += 1 - ts = Test.MakeATSProcess(name, enable_tls=True, enable_cache=False) - - ts.addDefaultSSLFiles() - ts.Disk.ssl_multicert_yaml.AddLines( - """ -ssl_multicert: - - dest_ip: "*" - ssl_cert_name: server.pem - ssl_key_name: server.key -""".split("\n")) - - ts.Disk.remap_config.AddLine(f'map / https://127.0.0.1:{self._ts_mid.Variables.ssl_port}/') - - ts.Disk.records_config.update( - { - 'proxy.config.ssl.server.cert.path': ts.Variables.SSLDir, - 'proxy.config.ssl.server.private_key.path': ts.Variables.SSLDir, - 'proxy.config.ssl.client.verify.server.policy': 'PERMISSIVE', - 'proxy.config.ssl.client.cert_compression.algorithms': self._algorithm, - 'proxy.config.http.keep_alive_enabled_out': 0, - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'ssl_cert_compress', - }) - - return ts - - def run(self) -> None: - cache_label = 'enabled' if self._cache_enabled else 'disabled' - - # Test run 1: Send 2 requests over 2 separate TLS connections. - tr = Test.AddTestRun(f'Send 2 requests with cache {cache_label}') - tr.Processes.Default.StartBefore(self._server) - tr.Processes.Default.StartBefore(self._ts_mid) - tr.Processes.Default.StartBefore(self._ts_edge) - - name = f'client-{TestCertCompressionCache.client_counter}' - TestCertCompressionCache.client_counter += 1 - tr.AddVerifierClientProcess(name, REPLAY_FILE, http_ports=[self._ts_edge.Variables.port]) - - # Test run 2: Check compression count on mid-tier — should be 2. - tr = Test.AddTestRun(f'Verify compression count with cache {cache_label}') - tr.Processes.Default.Command = (f'traffic_ctl metric get' - f' proxy.process.ssl.cert_compress.{self._algorithm}') - tr.Processes.Default.Env = self._ts_mid.Env - tr.Processes.Default.ReturnCode = 0 - tr.Processes.Default.Streams.All = Testers.ContainsExpression( - f'proxy.process.ssl.cert_compress.{self._algorithm} 2', f'Should have 2 {self._algorithm} compressions') - tr.StillRunningAfter = self._ts_mid - tr.StillRunningAfter = self._ts_edge - tr.StillRunningAfter = self._server - - # Test run 3: Check decompression count on edge — should be 2. - tr = Test.AddTestRun(f'Verify decompression count with cache {cache_label}') - tr.Processes.Default.Command = (f'traffic_ctl metric get' - f' proxy.process.ssl.cert_decompress.{self._algorithm}') - tr.Processes.Default.Env = self._ts_edge.Env - tr.Processes.Default.ReturnCode = 0 - tr.Processes.Default.Streams.All = Testers.ContainsExpression( - f'proxy.process.ssl.cert_decompress.{self._algorithm} 2', f'Should have 2 {self._algorithm} decompressions') - tr.StillRunningAfter = self._ts_mid - tr.StillRunningAfter = self._ts_edge - tr.StillRunningAfter = self._server - - # Test run 4: Verify no compression failures. - tr = Test.AddTestRun(f'Verify no compression failures with cache {cache_label}') - tr.Processes.Default.Command = (f'traffic_ctl metric get' - f' proxy.process.ssl.cert_compress.{self._algorithm}_failure') - tr.Processes.Default.Env = self._ts_mid.Env - tr.Processes.Default.ReturnCode = 0 - tr.Processes.Default.Streams.All = Testers.ContainsExpression( - f'proxy.process.ssl.cert_compress.{self._algorithm}_failure 0', - f'Should have no {self._algorithm} compression failures') - tr.StillRunningAfter = self._ts_mid - tr.StillRunningAfter = self._ts_edge - tr.StillRunningAfter = self._server - - # Test run 5: Check cache_hit metric. - if self._cache_enabled: - tr = Test.AddTestRun(f'Verify cache_hit is 1 with cache {cache_label}') - tr.Processes.Default.Command = 'traffic_ctl metric get proxy.process.ssl.cert_compress.cache_hit' - tr.Processes.Default.Env = self._ts_mid.Env - tr.Processes.Default.ReturnCode = 0 - tr.Processes.Default.Streams.All = Testers.ContainsExpression( - 'proxy.process.ssl.cert_compress.cache_hit 1', 'cache_hit should be 1 when caching is enabled (1 miss + 1 hit)') - tr.StillRunningAfter = self._ts_mid - tr.StillRunningAfter = self._ts_edge - tr.StillRunningAfter = self._server - else: - tr = Test.AddTestRun(f'Verify cache_hit is 0 with cache {cache_label}') - tr.Processes.Default.Command = 'traffic_ctl metric get proxy.process.ssl.cert_compress.cache_hit' - tr.Processes.Default.Env = self._ts_mid.Env - tr.Processes.Default.ReturnCode = 0 - tr.Processes.Default.Streams.All = Testers.ContainsExpression( - 'proxy.process.ssl.cert_compress.cache_hit 0', 'cache_hit should be 0 when caching is disabled') - tr.StillRunningAfter = self._ts_mid - tr.StillRunningAfter = self._ts_edge - tr.StillRunningAfter = self._server - - -TestCertCompressionCache(cache_enabled=True).run() -TestCertCompressionCache(cache_enabled=False).run() diff --git a/tests/gold_tests/tls/tls_check_cert_select_plugin.test.py b/tests/gold_tests/tls/tls_check_cert_select_plugin.test.py deleted file mode 100644 index 00eee2411a9..00000000000 --- a/tests/gold_tests/tls/tls_check_cert_select_plugin.test.py +++ /dev/null @@ -1,184 +0,0 @@ -''' -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import os - -Test.Summary = ''' -Test ATS offering different certificates based on SNI. Load via plugin -''' - -# The origin only has a response registered for a request with no Host header, so with -# pristine_host_hdr every request 404s at the origin. A 404 therefore proves the exchange -# happened. Match the response status line rather than a bare "404", which also matches the -# ephemeral port number curl prints (a port such as 62404 made this test fail at random). -# Covers both HTTP/1.1 ("HTTP/1.1 404 Not Found") and HTTP/2 ("HTTP/2 404"). -HTTP_404 = r"HTTP/[\d.]+ 404" - -# Define default ATS -ts = Test.MakeATSProcess("ts", enable_tls=True) -server = Test.MakeOriginServer("server", ssl=True) -dns = Test.MakeDNServer("dns") - -request_header = {"headers": "GET / HTTP/1.1\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -server.addResponse("sessionlog.json", request_header, response_header) - -# add ssl materials like key, certificates for the server -ts.addSSLfile("ssl/signed-foo.pem") -ts.addSSLfile("ssl/signed-foo.key") -ts.addSSLfile("ssl/signed-bar.pem") -ts.addSSLfile("ssl/signed2-bar.pem") -ts.addSSLfile("ssl/signed-bar.key") -ts.addSSLfile("ssl/server.pem") -ts.addSSLfile("ssl/server.key") -ts.addSSLfile("ssl/signer.pem") -ts.addSSLfile("ssl/signer.key") - -ts.Disk.remap_config.AddLine('map / https://foo.com:{1}'.format(ts.Variables.ssl_port, server.Variables.SSL_Port)) - -ts.Disk.ssl_multicert_yaml.AddLines( - """ -ssl_multicert: - - dest_ip: 127.0.0.1 - ssl_cert_name: signed-foo.pem - ssl_key_name: signed-foo.key - - ssl_cert_name: signed2-bar.pem - ssl_key_name: signed-bar.key - - dest_ip: "*" - ssl_cert_name: server.pem - ssl_key_name: server.key -""".split("\n")) - -Test.PrepareTestPlugin(os.path.join(Test.Variables.AtsTestPluginsDir, 'ssl_secret_load_test.so'), ts) - -# Case 1, global config policy=permissive properties=signature -# override for foo.com policy=enforced properties=all -ts.Disk.records_config.update( - { - 'proxy.config.diags.debug.tags': 'ssl_secret_load_test|ssl', - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.ssl.server.cert.path': '{0}/../'.format(ts.Variables.SSLDir), - 'proxy.config.ssl.server.private_key.path': '{0}/../'.format(ts.Variables.SSLDir), - 'proxy.config.ssl.server.cipher_suite': - 'ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-SHA384:AES128-GCM-SHA256:AES256-GCM-SHA384:ECDHE-RSA-RC4-SHA:ECDHE-RSA-AES128-SHA:ECDHE-RSA-AES256-SHA:RC4-SHA:RC4-MD5:AES128-SHA:AES256-SHA:DES-CBC3-SHA!SRP:!DSS:!PSK:!aNULL:!eNULL:!SSLv2', - 'proxy.config.url_remap.pristine_host_hdr': 1, - 'proxy.config.dns.nameservers': '127.0.0.1:{0}'.format(dns.Variables.Port), - 'proxy.config.exec_thread.autoconfig.scale': 1.0, - 'proxy.config.dns.resolv_conf': 'NULL', - 'proxy.config.ssl.client.verify.server.policy': 'PERMISSIVE', - }) - -dns.addRecords(records={"foo.com.": ["127.0.0.1"]}) -dns.addRecords(records={"bar.com.": ["127.0.0.1"]}) - -# Should receive a bar.com cert -tr = Test.AddTestRun("bar.com cert") -tr.Setup.Copy("ssl/signer.pem") -tr.Setup.Copy("ssl/signer2.pem") -tr.MakeCurlCommand( - "-v --cacert ./signer2.pem --resolve 'bar.com:{0}:127.0.0.1' https://bar.com:{0}".format(ts.Variables.ssl_port), ts=ts) -tr.ReturnCode = 0 -tr.Processes.Default.StartBefore(server) -tr.Processes.Default.StartBefore(dns) -tr.Processes.Default.StartBefore(Test.Processes.ts, ready=When.PortOpen(ts.Variables.ssl_port)) -tr.StillRunningAfter = server -tr.StillRunningAfter = ts -tr.Processes.Default.Streams.All = Testers.ExcludesExpression("Could Not Connect", "Curl attempt should have succeeded") -tr.Processes.Default.Streams.All += Testers.ContainsExpression("CN=bar.com", "Cert should contain bar.com") -tr.Processes.Default.Streams.All += Testers.ExcludesExpression("CN=foo.com", "Cert should not contain foo.com") -tr.Processes.Default.Streams.All += Testers.ContainsExpression(HTTP_404, "Should make an exchange") - -# Should receive a foo.com cert -tr2 = Test.AddTestRun("foo.com cert") -tr2.MakeCurlCommand( - "-v --cacert ./signer.pem --resolve 'foo.com:{0}:127.0.0.1' https://foo.com:{0}".format(ts.Variables.ssl_port), ts=ts) -tr2.ReturnCode = 0 -tr2.StillRunningAfter = server -tr2.StillRunningAfter = ts -tr2.Processes.Default.Streams.All = Testers.ExcludesExpression("Could Not Connect", "Curl attempt should have succeeded") -tr2.Processes.Default.Streams.All += Testers.ContainsExpression("CN=foo.com", "Cert should contain foo.com") -tr2.Processes.Default.Streams.All += Testers.ExcludesExpression("CN=bar.com", "Cert should not contain bar.com") -tr2.Processes.Default.Streams.All += Testers.ContainsExpression(HTTP_404, "Should make an exchange") - -# Should receive random.server.com -tr2 = Test.AddTestRun("random.server.com cert") -tr2.MakeCurlCommand( - "-v -k --resolve 'random.server.com:{0}:127.0.0.1' https://random.server.com:{0}".format(ts.Variables.ssl_port), ts=ts) -tr2.ReturnCode = 0 -tr2.StillRunningAfter = server -tr2.StillRunningAfter = ts -tr2.Processes.Default.Streams.All = Testers.ExcludesExpression("Could Not Connect", "Curl attempt should have succeeded") -tr2.Processes.Default.Streams.All += Testers.ContainsExpression("CN=random.server.com", "Cert should contain random.server.com") -tr2.Processes.Default.Streams.All += Testers.ExcludesExpression("CN=foo.com", "Cert should not contain foo.com") -tr2.Processes.Default.Streams.All += Testers.ExcludesExpression("CN=bar.com", "Cert should not contain bar.com") -tr2.Processes.Default.Streams.All += Testers.ContainsExpression(HTTP_404, "Should make an exchange") - -# No SNI match should match specific IP address, foo.com -# SNI name and returned cert name will not match, so must use -k to avoid cert verification -tr2 = Test.AddTestRun("Bad SNI") -tr2.MakeCurlCommand( - "-v -k --cacert ./signer.pem --resolve 'bad.sni.com:{0}:127.0.0.1' https://bad.sni.com:{0}".format(ts.Variables.ssl_port), - ts=ts) -tr2.ReturnCode = 0 -tr2.StillRunningAfter = server -tr2.StillRunningAfter = ts -tr2.Processes.Default.Streams.All = Testers.ExcludesExpression("Could Not Connect", "Curl attempt should have succeeded") -tr2.Processes.Default.Streams.All += Testers.ContainsExpression("CN=foo.com", "Cert should contain foo.com") -tr2.Processes.Default.Streams.All += Testers.ExcludesExpression("CN=bar.com", "Cert should not contain bar.com") -tr2.Processes.Default.Streams.All += Testers.ContainsExpression(HTTP_404, "Should make an exchange") - -# Copy in a new version of the bar.com cert. Replace it with the version -# signed by signer 1. Wait at least a second to sure the file update time -# differs -trupdate = Test.AddTestRun("Update server bar cert file in place") -trupdate.StillRunningAfter = ts -trupdate.StillRunningAfter = server -trupdate.Setup.CopyAs("ssl/signed-bar.pem", ".", "{0}/signed2-bar.pem".format(ts.Variables.SSLDir)) -# For some reason the Setup.CopyAs does not change the modification time, so we touch -trupdate.Processes.Default.Command = 'touch {0}/signed2-bar.pem'.format(ts.Variables.SSLDir) -# Need to copy over the environment so traffic_ctl knows where to find the unix domain socket -trupdate.Processes.Default.Env = ts.Env -trupdate.Processes.Default.ReturnCode = 0 - -# The plugin will pull every 3 seconds. So wait 4 seconds and test again. -# Request with CA=signer.pem should work. Request with CA=signer2.pem -# should fail -tr = Test.AddTestRun("Test new version of bar cert with good CA") -tr.DelayStart = 4 -tr.MakeCurlCommandMulti( - "date; {{curl}} -v --cacert ./signer.pem --resolve 'bar.com:{0}:127.0.0.1' https://bar.com:{0}".format(ts.Variables.ssl_port), - ts=ts) -tr.ReturnCode = 0 -tr.StillRunningAfter = server -tr.StillRunningAfter = ts -tr.Processes.Default.Streams.All = Testers.ExcludesExpression("Could Not Connect", "Curl attempt should have succeeded") -tr.Processes.Default.Streams.All += Testers.ContainsExpression("CN=bar.com", "Cert should contain bar.com") -tr.Processes.Default.Streams.All += Testers.ExcludesExpression("CN=foo.com", "Cert should not contain foo.com") -tr.Processes.Default.Streams.All += Testers.ContainsExpression(HTTP_404, "Should make an exchange") - -tr = Test.AddTestRun("Test new version of bar cert with bad CA") -tr.MakeCurlCommand( - "-v --cacert ./signer2.pem --resolve 'bar.com:{0}:127.0.0.1' https://bar.com:{0}".format(ts.Variables.ssl_port), ts=ts) -tr.ReturnCode = 60 -tr.StillRunningAfter = server -tr.StillRunningAfter = ts -tr.Processes.Default.Streams.All = Testers.ContainsExpression(r"curl: \(60\) SSL certificate", "Failed certificate verification") -# Older versions of curl do not print certificate subject details when -# certificate verification fails. -tr.Processes.Default.Streams.All += Testers.ExcludesExpression("CN=foo.com", "Cert should not contain foo.com") -tr.Processes.Default.Streams.All += Testers.ExcludesExpression(HTTP_404, "Should make an exchange") diff --git a/tests/gold_tests/tls/tls_check_cert_selection.test.py b/tests/gold_tests/tls/tls_check_cert_selection.test.py deleted file mode 100644 index bc3006193a6..00000000000 --- a/tests/gold_tests/tls/tls_check_cert_selection.test.py +++ /dev/null @@ -1,126 +0,0 @@ -''' -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -Test.Summary = ''' -Test ATS offering different certificates based on SNI -''' - -# Define default ATS -ts = Test.MakeATSProcess("ts", enable_tls=True) -server = Test.MakeOriginServer("server", ssl=True) -dns = Test.MakeDNServer("dns") - -request_header = {"headers": "GET / HTTP/1.1\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -server.addResponse("sessionlog.json", request_header, response_header) - -# add ssl materials like key, certificates for the server -ts.addSSLfile("ssl/signed-foo.pem") -ts.addSSLfile("ssl/signed-foo.key") -ts.addSSLfile("ssl/signed-bar.pem") -ts.addSSLfile("ssl/signed2-bar.pem") -ts.addSSLfile("ssl/signed-bar.key") -ts.addSSLfile("ssl/signer.pem") -ts.addSSLfile("ssl/signer.key") -ts.addSSLfile("ssl/combo.pem") - -ts.Disk.remap_config.AddLine('map / https://foo.com:{1}'.format(ts.Variables.ssl_port, server.Variables.SSL_Port)) - -ts.Disk.ssl_multicert_yaml.AddLines( - """ -ssl_multicert: - - dest_ip: "127.0.0.1" - ssl_cert_name: signed-foo.pem - ssl_key_name: signed-foo.key - - ssl_cert_name: signed2-bar.pem - ssl_key_name: signed-bar.key - - dest_ip: "*" - ssl_cert_name: combo.pem -""".split("\n")) - -# Case 1, global config policy=permissive properties=signature -# override for foo.com policy=enforced properties=all -ts.Disk.records_config.update( - { - 'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir), - 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts.Variables.SSLDir), - 'proxy.config.url_remap.pristine_host_hdr': 1, - 'proxy.config.dns.nameservers': '127.0.0.1:{0}'.format(dns.Variables.Port), - 'proxy.config.exec_thread.autoconfig.scale': 1.0, - 'proxy.config.dns.resolv_conf': 'NULL', - 'proxy.config.ssl.client.verify.server.policy': 'PERMISSIVE', - }) - -dns.addRecords(records={"foo.com.": ["127.0.0.1"]}) -dns.addRecords(records={"bar.com.": ["127.0.0.1"]}) - -# Should receive a bar.com cert -tr = Test.AddTestRun("bar.com cert") -tr.Setup.Copy("ssl/signer.pem") -tr.Setup.Copy("ssl/signer2.pem") -tr.MakeCurlCommand( - "-v --cacert ./signer2.pem --resolve 'bar.com:{0}:127.0.0.1' https://bar.com:{0}".format(ts.Variables.ssl_port), ts=ts) -tr.ReturnCode = 0 -tr.Processes.Default.StartBefore(server) -tr.Processes.Default.StartBefore(dns) -tr.Processes.Default.StartBefore(Test.Processes.ts) -tr.StillRunningAfter = server -tr.StillRunningAfter = ts -tr.Processes.Default.Streams.All = Testers.ExcludesExpression("Could Not Connect", "Curl attempt should have succeeded") -tr.Processes.Default.Streams.All += Testers.ContainsExpression("CN=bar.com", "Cert should contain bar.com") -tr.Processes.Default.Streams.All += Testers.ExcludesExpression("CN=foo.com", "Cert should not contain foo.com") -tr.Processes.Default.Streams.All += Testers.ContainsExpression("404", "Should make an exchange") - -# Should receive a foo.com cert -tr2 = Test.AddTestRun("foo.com cert") -tr2.MakeCurlCommand( - "-v --cacert ./signer.pem --resolve 'foo.com:{0}:127.0.0.1' https://foo.com:{0}".format(ts.Variables.ssl_port), ts=ts) -tr2.ReturnCode = 0 -tr2.StillRunningAfter = server -tr2.StillRunningAfter = ts -tr2.Processes.Default.Streams.All = Testers.ExcludesExpression("Could Not Connect", "Curl attempt should have succeeded") -tr2.Processes.Default.Streams.All += Testers.ContainsExpression("CN=foo.com", "Cert should contain foo.com") -tr2.Processes.Default.Streams.All += Testers.ExcludesExpression("CN=bar.com", "Cert should not contain bar.com") -tr.Processes.Default.Streams.All += Testers.ContainsExpression("404", "Should make an exchange") - -# Should receive random.server.com -tr2 = Test.AddTestRun("random.server.com cert") -tr2.MakeCurlCommand( - "-v -k --resolve 'random.server.com:{0}:127.0.0.1' https://random.server.com:{0}".format(ts.Variables.ssl_port), ts=ts) -tr2.ReturnCode = 0 -tr2.StillRunningAfter = server -tr2.StillRunningAfter = ts -tr2.Processes.Default.Streams.All = Testers.ExcludesExpression("Could Not Connect", "Curl attempt should have succeeded") -tr2.Processes.Default.Streams.All += Testers.ContainsExpression("CN=random.server.com", "Cert should contain random.server.com") -tr2.Processes.Default.Streams.All += Testers.ExcludesExpression("CN=foo.com", "Cert should not contain foo.com") -tr2.Processes.Default.Streams.All += Testers.ExcludesExpression("CN=bar.com", "Cert should not contain bar.com") -tr.Processes.Default.Streams.All += Testers.ContainsExpression("404", "Should make an exchange") - -# No SNI match should match specific IP address, foo.com -# SNI name and returned cert name will not match, so must use -k to avoid cert verification -tr2 = Test.AddTestRun("Bad SNI") -tr2.MakeCurlCommand( - "-v -k --cacert ./signer.pem --resolve 'bad.sni.com:{0}:127.0.0.1' https://bad.sni.com:{0}".format(ts.Variables.ssl_port), - ts=ts) -tr2.ReturnCode = 0 -tr2.StillRunningAfter = server -tr2.StillRunningAfter = ts -tr2.Processes.Default.Streams.All = Testers.ExcludesExpression("Could Not Connect", "Curl attempt should have succeeded") -tr2.Processes.Default.Streams.All += Testers.ContainsExpression("CN=foo.com", "Cert should contain foo.com") -tr2.Processes.Default.Streams.All += Testers.ExcludesExpression("CN=bar.com", "Cert should not contain bar.com") -tr.Processes.Default.Streams.All += Testers.ContainsExpression("404", "Should make an exchange") diff --git a/tests/gold_tests/tls/tls_check_cert_selection_reload.test.py b/tests/gold_tests/tls/tls_check_cert_selection_reload.test.py deleted file mode 100644 index 327b61f300d..00000000000 --- a/tests/gold_tests/tls/tls_check_cert_selection_reload.test.py +++ /dev/null @@ -1,124 +0,0 @@ -''' -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -Test.Summary = ''' -Test ATS offering different certificates based on SNI -''' - -# Define default ATS -ts = Test.MakeATSProcess("ts", enable_tls=True) -server = Test.MakeOriginServer("server", ssl=True) -server3 = Test.MakeOriginServer("server3", ssl=True) - -request_header = {"headers": "GET / HTTP/1.1\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -server.addResponse("sessionlog.json", request_header, response_header) - -# add ssl materials like key, certificates for the server -ts.addSSLfile("ssl/signed-bar.pem") -ts.addSSLfile("ssl/signed2-bar.pem") -ts.addSSLfile("ssl/signed-bar.key") -ts.addSSLfile("ssl/signer.pem") -ts.addSSLfile("ssl/signer.key") -ts.addSSLfile("ssl/combo.pem") - -ts.Disk.remap_config.AddLine('map /stuff https://foo.com:{1}'.format(ts.Variables.ssl_port, server.Variables.SSL_Port)) - -ts.Disk.ssl_multicert_yaml.AddLines( - """ -ssl_multicert: - - ssl_cert_name: signed-bar.pem - ssl_key_name: signed-bar.key - - dest_ip: "*" - ssl_cert_name: combo.pem -""".split("\n")) - -# Case 1, global config policy=permissive properties=signature -# override for foo.com policy=enforced properties=all -ts.Disk.records_config.update( - { - 'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir), - 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts.Variables.SSLDir), - 'proxy.config.url_remap.pristine_host_hdr': 1, - 'proxy.config.exec_thread.autoconfig.scale': 1.0, - 'proxy.config.ssl.client.verify.server.policy': 'PERMISSIVE', - 'proxy.config.diags.debug.tags': 'ssl|http|lm', - 'proxy.config.diags.debug.enabled': 1 - }) - -# Should receive a bar.com cert issued by first signer -tr = Test.AddTestRun("bar.com cert signer1") -tr.Setup.Copy("ssl/signer.pem") -tr.Setup.Copy("ssl/signer2.pem") -tr.MakeCurlCommand( - "-v --cacert ./signer.pem --resolve 'bar.com:{0}:127.0.0.1' https://bar.com:{0}/random".format(ts.Variables.ssl_port), ts=ts) -tr.ReturnCode = 0 -tr.Processes.Default.StartBefore(server) -tr.Processes.Default.StartBefore(Test.Processes.ts) -tr.StillRunningAfter = server -tr.StillRunningAfter = ts -tr.Processes.Default.Streams.All = Testers.ExcludesExpression("Could Not Connect", "Curl attempt should have succeeded") -tr.Processes.Default.Streams.All += Testers.ContainsExpression("CN=bar.com", "Cert should contain bar.com") -tr.Processes.Default.Streams.All += Testers.ExcludesExpression("CN=foo.com", "Cert should not contain foo.com") -tr.Processes.Default.Streams.All += Testers.ContainsExpression("404", "Should make an exchange") - -tr = Test.AddTestRun("bar.com cert signer2") -tr.MakeCurlCommand( - "-v --cacert ./signer2.pem --resolve 'bar.com:{0}:127.0.0.1' https://bar.com:{0}/random".format(ts.Variables.ssl_port), ts=ts) -tr.ReturnCode = 60 -tr.StillRunningAfter = server -tr.StillRunningAfter = ts -tr.Processes.Default.Streams.All = Testers.ContainsExpression( - "unable to get local issuer certificate", "Server certificate not issued by expected signer") - -# Pause a little to ensure mtime will be updated -tr = Test.AddTestRun("Pause a little to ensure mtime will be different") -tr.Processes.Default.Command = "sleep 2" -tr.Processes.Default.ReturnCode = 0 - -tr = Test.AddTestRun("Update the bar cert to the signed 2 version") -tr.Setup.CopyAs("ssl/signed2-bar.pem", ".", "{0}/signed-bar.pem".format(ts.Variables.SSLDir)) -# For some reason the Setup.CopyAs does not change the modification time, so we touch -tr.Processes.Default.Env = ts.Env -tr.Processes.Default.Command = 'touch {0}/signed-bar.pem'.format(ts.Variables.SSLDir) -# Need to copy over the environment so traffic_ctl knows where to find the unix domain socket -tr.Processes.Default.ReturnCode = 0 - -tr = Test.AddConfigReload(ts, expect_tasks=["ssl_multicert.yaml"], description="Reload config") -tr.StillRunningAfter = server - -tr = Test.AddTestRun("Try with signer 1 again") -tr.Processes.Default.StartBefore(server3) -tr.StillRunningAfter = ts -tr.StillRunningAfter = server -tr.MakeCurlCommand( - "-v --cacert ./signer.pem --resolve 'bar.com:{0}:127.0.0.1' https://bar.com:{0}/random".format(ts.Variables.ssl_port), ts=ts) -tr.ReturnCode = 60 -tr.Processes.Default.Streams.All = Testers.ContainsExpression( - "unable to get local issuer certificate", "Server certificate not issued by expected signer") - -tr = Test.AddTestRun("Try with signer 2 again") -tr.MakeCurlCommand( - "-v --cacert ./signer2.pem --resolve 'bar.com:{0}:127.0.0.1' https://bar.com:{0}/random".format(ts.Variables.ssl_port), ts=ts) -tr.ReturnCode = 0 -tr.StillRunningAfter = server -tr.StillRunningAfter = ts -tr.Processes.Default.Streams.All = Testers.ExcludesExpression("Could Not Connect", "Curl attempt should have succeeded") -tr.Processes.Default.Streams.All += Testers.ContainsExpression("CN=bar.com", "Cert should contain bar.com") -tr.Processes.Default.Streams.All += Testers.ExcludesExpression("CN=foo.com", "Cert should not contain foo.com") -tr.Processes.Default.Streams.All += Testers.ContainsExpression("404", "Should make an exchange") diff --git a/tests/gold_tests/tls/tls_check_dual_cert_selection.test.py b/tests/gold_tests/tls/tls_check_dual_cert_selection.test.py deleted file mode 100644 index 5fd3c2ad71c..00000000000 --- a/tests/gold_tests/tls/tls_check_dual_cert_selection.test.py +++ /dev/null @@ -1,162 +0,0 @@ -''' -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import os -import re - -Test.Summary = ''' -Test ATS offering both RSA and EC certificates -''' - -# Define default ATS -ts = Test.MakeATSProcess("ts", enable_tls=True) -server = Test.MakeOriginServer("server", ssl=True) -dns = Test.MakeDNServer("dns") - -request_header = {"headers": "GET / HTTP/1.1\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -server.addResponse("sessionlog.json", request_header, response_header) - -# add ssl materials like key, certificates for the server -ts.addSSLfile("ssl/signed-foo.pem") -ts.addSSLfile("ssl/signed-foo.key") -ts.addSSLfile("ssl/signed-foo-ec.pem") -ts.addSSLfile("ssl/signed-foo-ec.key") -ts.addSSLfile("ssl/signed-san.pem") -ts.addSSLfile("ssl/signed-san.key") -ts.addSSLfile("ssl/signed-san-ec.pem") -ts.addSSLfile("ssl/signed-san-ec.key") -ts.addSSLfile("ssl/signer.pem") -ts.addSSLfile("ssl/signer.key") -ts.addSSLfile("ssl/server.pem") -ts.addSSLfile("ssl/server.key") - -ts.Disk.remap_config.AddLine('map / https://foo.com:{1}'.format(ts.Variables.ssl_port, server.Variables.SSL_Port)) - -ts.Disk.ssl_multicert_yaml.AddLines( - """ -ssl_multicert: - - ssl_cert_name: signed-foo-ec.pem,signed-foo.pem - ssl_key_name: signed-foo-ec.key,signed-foo.key - - ssl_cert_name: signed-san-ec.pem,signed-san.pem - ssl_key_name: signed-san-ec.key,signed-san.key - - dest_ip: "*" - ssl_cert_name: server.pem - ssl_key_name: server.key -""".split("\n")) - -# Case 1, global config policy=permissive properties=signature -# override for foo.com policy=enforced properties=all -ts.Disk.records_config.update( - { - 'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir), - 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts.Variables.SSLDir), - 'proxy.config.ssl.server.cipher_suite': 'ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256', - 'proxy.config.url_remap.pristine_host_hdr': 1, - 'proxy.config.dns.nameservers': '127.0.0.1:{0}'.format(dns.Variables.Port), - 'proxy.config.exec_thread.autoconfig.scale': 1.0, - 'proxy.config.dns.resolv_conf': 'NULL', - 'proxy.config.diags.debug.tags': 'ssl', - 'proxy.config.diags.debug.enabled': 1 - }) - -dns.addRecords(records={"foo.com.": ["127.0.0.1"]}) -dns.addRecords(records={"bar.com.": ["127.0.0.1"]}) - -foo_ec_string = "" -foo_rsa_string = "" -san_ec_string = "" -san_rsa_string = "" -with open(os.path.join(Test.TestDirectory, 'ssl', 'signed-foo-ec.pem'), 'r') as myfile: - file_string = myfile.read() - cert_end = file_string.find("END CERTIFICATE-----") - foo_ec_string = re.escape(file_string[0:cert_end]) -with open(os.path.join(Test.TestDirectory, 'ssl', 'signed-foo.pem'), 'r') as myfile: - file_string = myfile.read() - cert_end = file_string.find("END CERTIFICATE-----") - foo_rsa_string = re.escape(file_string[0:cert_end]) -with open(os.path.join(Test.TestDirectory, 'ssl', 'signed-san-ec.pem'), 'r') as myfile: - file_string = myfile.read() - cert_end = file_string.find("END CERTIFICATE-----") - san_ec_string = re.escape(file_string[0:cert_end]) -with open(os.path.join(Test.TestDirectory, 'ssl', 'signed-san.pem'), 'r') as myfile: - file_string = myfile.read() - cert_end = file_string.find("END CERTIFICATE-----") - san_rsa_string = re.escape(file_string[0:cert_end]) - -# Should receive a EC cert since ATS cipher list prefers EC -tr = Test.AddTestRun("Default for foo should return EC cert") -tr.Setup.Copy("ssl/signer.pem") -tr.Processes.Default.Command = "echo foo | openssl s_client -tls1_2 -servername foo.com -connect 127.0.0.1:{0}".format( - ts.Variables.ssl_port, foo_ec_string) -tr.ReturnCode = 0 -tr.Processes.Default.StartBefore(server) -tr.Processes.Default.StartBefore(dns) -tr.Processes.Default.StartBefore(Test.Processes.ts) -tr.StillRunningAfter = server -tr.StillRunningAfter = ts -tr.Processes.Default.Streams.All += Testers.ContainsExpression(foo_ec_string, "Should select EC cert", reflags=re.S | re.M) - -# Should receive a RSA cert -tr = Test.AddTestRun("Only offer RSA ciphers, should receive RSA cert") -tr.Processes.Default.Command = "echo foo | openssl s_client -tls1_2 -servername foo.com -cipher 'ECDHE-RSA-AES128-GCM-SHA256' -connect 127.0.0.1:{0}".format( - ts.Variables.ssl_port) -tr.ReturnCode = 0 -tr.StillRunningAfter = server -tr.StillRunningAfter = ts -tr.Processes.Default.Streams.All += Testers.ContainsExpression(foo_rsa_string, "Should select RSA cert", reflags=re.S | re.M) - -# Should receive a EC cert -tr = Test.AddTestRun("Default for two.com should return EC cert") -tr.Processes.Default.Command = "echo foo | openssl s_client -tls1_2 -servername two.com -connect 127.0.0.1:{0}".format( - ts.Variables.ssl_port) -tr.ReturnCode = 0 -tr.StillRunningAfter = server -tr.StillRunningAfter = ts -tr.Processes.Default.Streams.All += Testers.ContainsExpression(san_ec_string, "Should select EC cert", reflags=re.S | re.M) -tr.Processes.Default.Streams.All += Testers.ContainsExpression("CN ?= ?group.com", "Should select a group SAN") - -# Should receive a RSA cert -tr = Test.AddTestRun("Only offer RSA ciphers, should receive RSA cert") -tr.Processes.Default.Command = "echo foo | openssl s_client -tls1_2 -servername two.com -cipher 'ECDHE-RSA-AES128-GCM-SHA256' -connect 127.0.0.1:{0}".format( - ts.Variables.ssl_port) -tr.ReturnCode = 0 -tr.StillRunningAfter = server -tr.StillRunningAfter = ts -tr.Processes.Default.Streams.All += Testers.ContainsExpression(san_rsa_string, "Should select RSA cert", reflags=re.S | re.M) -tr.Processes.Default.Streams.All += Testers.ContainsExpression("CN ?= ?group.com", "Should select a group SAN") - -# Should receive a RSA cert -tr = Test.AddTestRun("rsa.com only in rsa cert") -tr.Processes.Default.Command = "echo foo | openssl s_client -tls1_2 -servername rsa.com -connect 127.0.0.1:{0}".format( - ts.Variables.ssl_port) -tr.ReturnCode = 0 -tr.StillRunningAfter = server -tr.StillRunningAfter = ts -tr.Processes.Default.Streams.All += Testers.ContainsExpression(san_rsa_string, "Should select RSA cert", reflags=re.S | re.M) -tr.Processes.Default.Streams.All += Testers.ContainsExpression("CN ?= ?group.com", "Should select a group SAN") - -# Should receive a EC cert -tr = Test.AddTestRun("ec.com only in ec cert") -tr.Processes.Default.Command = "echo foo | openssl s_client -tls1_2 -servername ec.com -connect 127.0.0.1:{0}".format( - ts.Variables.ssl_port) -tr.ReturnCode = 0 -tr.StillRunningAfter = server -tr.StillRunningAfter = ts -tr.Processes.Default.Streams.All += Testers.ContainsExpression(san_ec_string, "Should select EC cert", reflags=re.S | re.M) -tr.Processes.Default.Streams.All += Testers.ContainsExpression("CN ?= ?group.com", "Should select a group SAN") diff --git a/tests/gold_tests/tls/tls_check_dual_cert_selection2.test.py b/tests/gold_tests/tls/tls_check_dual_cert_selection2.test.py deleted file mode 100644 index b443564c0c2..00000000000 --- a/tests/gold_tests/tls/tls_check_dual_cert_selection2.test.py +++ /dev/null @@ -1,190 +0,0 @@ -''' -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import os -import re - -Test.Summary = ''' -Test ATS offering both RSA and EC certificates -Combined key and cert files. Faulty key path -''' - -# Define default ATS -ts = Test.MakeATSProcess("ts", enable_tls=True) -server = Test.MakeOriginServer("server", ssl=True) -dns = Test.MakeDNServer("dns") - -request_header = {"headers": "GET / HTTP/1.1\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -server.addResponse("sessionlog.json", request_header, response_header) - -# add ssl materials like key, certificates for the server -ts.addSSLfile("ssl/signed-foo.pem") -ts.addSSLfile("ssl/signed-foo.key") -ts.addSSLfile("ssl/signed-foo-ec.pem") -ts.addSSLfile("ssl/signed-foo-ec.key") -ts.addSSLfile("ssl/signed-san.pem") -ts.addSSLfile("ssl/signed-san.key") -ts.addSSLfile("ssl/signed-san-ec.pem") -ts.addSSLfile("ssl/signed-san-ec.key") -ts.addSSLfile("ssl/combined-ec.pem") -ts.addSSLfile("ssl/combined.pem") -ts.addSSLfile("ssl/signer.pem") -ts.addSSLfile("ssl/signer.key") - -ts.Disk.remap_config.AddLine('map / https://foo.com:{1}'.format(ts.Variables.ssl_port, server.Variables.SSL_Port)) - -ts.Disk.ssl_multicert_yaml.AddLines( - """ -ssl_multicert: - - ssl_cert_name: combined-ec.pem,combined.pem - - ssl_cert_name: signed-foo-ec.pem,signed-foo.pem - - dest_ip: "*" - ssl_cert_name: signed-san-ec.pem,signed-san.pem -""".split("\n")) - -# Case 1, global config policy=permissive properties=signature -# override for foo.com policy=enforced properties=all -ts.Disk.records_config.update( - { - 'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir), - 'proxy.config.ssl.server.private_key.path': '/tmp', # Faulty key path should not matter, since there are no key files - 'proxy.config.ssl.server.cipher_suite': 'ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256', - 'proxy.config.url_remap.pristine_host_hdr': 1, - 'proxy.config.dns.nameservers': '127.0.0.1:{0}'.format(dns.Variables.Port), - 'proxy.config.exec_thread.autoconfig.scale': 1.0, - 'proxy.config.dns.resolv_conf': 'NULL', - 'proxy.config.diags.debug.tags': 'ssl', - 'proxy.config.diags.debug.enabled': 0 - }) - -dns.addRecords(records={"foo.com.": ["127.0.0.1"]}) -dns.addRecords(records={"bar.com.": ["127.0.0.1"]}) - -foo_ec_string = "" -foo_rsa_string = "" -san_ec_string = "" -san_rsa_string = "" -combo_ec_string = "" -combo_rsa_string = "" -with open(os.path.join(Test.TestDirectory, 'ssl', 'signed-foo-ec.pem'), 'r') as myfile: - file_string = myfile.read() - cert_end = file_string.find("END CERTIFICATE-----") - foo_ec_string = re.escape(file_string[0:cert_end]) -with open(os.path.join(Test.TestDirectory, 'ssl', 'signed-foo.pem'), 'r') as myfile: - file_string = myfile.read() - cert_end = file_string.find("END CERTIFICATE-----") - foo_rsa_string = re.escape(file_string[0:cert_end]) -with open(os.path.join(Test.TestDirectory, 'ssl', 'signed-san-ec.pem'), 'r') as myfile: - file_string = myfile.read() - cert_end = file_string.find("END CERTIFICATE-----") - san_ec_string = re.escape(file_string[0:cert_end]) -with open(os.path.join(Test.TestDirectory, 'ssl', 'signed-san.pem'), 'r') as myfile: - file_string = myfile.read() - cert_end = file_string.find("END CERTIFICATE-----") - san_rsa_string = re.escape(file_string[0:cert_end]) -with open(os.path.join(Test.TestDirectory, 'ssl', 'combined-ec.pem'), 'r') as myfile: - file_string = myfile.read() - cert_end = file_string.find("END CERTIFICATE-----") - combo_ec_string = re.escape(file_string[0:cert_end]) -with open(os.path.join(Test.TestDirectory, 'ssl', 'combined.pem'), 'r') as myfile: - file_string = myfile.read() - cert_end = file_string.find("END CERTIFICATE-----") - combo_rsa_string = re.escape(file_string[0:cert_end]) - -# Should receive a EC cert since ATS cipher list prefers EC -tr = Test.AddTestRun("Default for foo should return EC cert") -tr.Setup.Copy("ssl/signer.pem") -tr.Processes.Default.Command = "echo foo | openssl s_client -tls1_2 -servername foo.com -connect 127.0.0.1:{0}".format( - ts.Variables.ssl_port, foo_ec_string) -tr.ReturnCode = 0 -tr.Processes.Default.StartBefore(server) -tr.Processes.Default.StartBefore(dns) -tr.Processes.Default.StartBefore(Test.Processes.ts, ready=When.PortOpen(ts.Variables.ssl_port)) -tr.StillRunningAfter = server -tr.StillRunningAfter = ts -tr.Processes.Default.Streams.All += Testers.ContainsExpression(foo_ec_string, "Should select EC cert", reflags=re.S | re.M) - -# Should receive a RSA cert -tr = Test.AddTestRun("Only offer RSA ciphers, should receive RSA cert") -tr.Processes.Default.Command = "echo foo | openssl s_client -tls1_2 -servername foo.com -cipher 'ECDHE-RSA-AES128-GCM-SHA256' -connect 127.0.0.1:{0}".format( - ts.Variables.ssl_port) -tr.ReturnCode = 0 -tr.StillRunningAfter = server -tr.StillRunningAfter = ts -tr.Processes.Default.Streams.All += Testers.ContainsExpression(foo_rsa_string, "Should select RSA cert", reflags=re.S | re.M) - -# Should receive a EC cert -tr = Test.AddTestRun("Default for two.com should return EC cert") -tr.Processes.Default.Command = "echo foo | openssl s_client -tls1_2 -servername two.com -connect 127.0.0.1:{0}".format( - ts.Variables.ssl_port) -tr.ReturnCode = 0 -tr.StillRunningAfter = server -tr.StillRunningAfter = ts -tr.Processes.Default.Streams.All += Testers.ContainsExpression(san_ec_string, "Should select EC cert", reflags=re.S | re.M) -tr.Processes.Default.Streams.All += Testers.ContainsExpression("CN ?= ?group.com", "Should select a group SAN") - -# Should receive a RSA cert -tr = Test.AddTestRun("Only offer RSA ciphers, should receive RSA cert") -tr.Processes.Default.Command = "echo foo | openssl s_client -tls1_2 -servername two.com -cipher 'ECDHE-RSA-AES128-GCM-SHA256' -connect 127.0.0.1:{0}".format( - ts.Variables.ssl_port) -tr.ReturnCode = 0 -tr.StillRunningAfter = server -tr.StillRunningAfter = ts -tr.Processes.Default.Streams.All += Testers.ContainsExpression(san_rsa_string, "Should select RSA cert", reflags=re.S | re.M) -tr.Processes.Default.Streams.All += Testers.ContainsExpression("CN ?= ?group.com", "Should select a group SAN") - -# Should receive a RSA cert -tr = Test.AddTestRun("rsa.com only in rsa cert") -tr.Processes.Default.Command = "echo foo | openssl s_client -tls1_2 -servername rsa.com -connect 127.0.0.1:{0}".format( - ts.Variables.ssl_port) -tr.ReturnCode = 0 -tr.StillRunningAfter = server -tr.StillRunningAfter = ts -tr.Processes.Default.Streams.All += Testers.ContainsExpression(san_rsa_string, "Should select RSA cert", reflags=re.S | re.M) -tr.Processes.Default.Streams.All += Testers.ContainsExpression("CN ?= ?group.com", "Should select a group SAN") - -# Should receive a EC cert -tr = Test.AddTestRun("ec.com only in ec cert") -tr.Processes.Default.Command = "echo foo | openssl s_client -tls1_2 -servername ec.com -connect 127.0.0.1:{0}".format( - ts.Variables.ssl_port) -tr.ReturnCode = 0 -tr.StillRunningAfter = server -tr.StillRunningAfter = ts -tr.Processes.Default.Streams.All += Testers.ContainsExpression(san_ec_string, "Should select EC cert", reflags=re.S | re.M) -tr.Processes.Default.Streams.All += Testers.ContainsExpression("CN ?= ?group.com", "Should select a group SAN") - -# Should receive a EC cert -tr = Test.AddTestRun("Default for combined.com should return EC cert") -tr.Processes.Default.Command = "echo foo | openssl s_client -tls1_2 -servername combined.com -connect 127.0.0.1:{0}".format( - ts.Variables.ssl_port) -tr.ReturnCode = 0 -tr.StillRunningAfter = server -tr.StillRunningAfter = ts -tr.Processes.Default.Streams.All += Testers.ContainsExpression(combo_ec_string, "Should select EC cert", reflags=re.S | re.M) -tr.Processes.Default.Streams.All += Testers.ContainsExpression("CN ?= ?combined.com", "Should select combined pem") - -# Should receive a RSA cert -tr = Test.AddTestRun("Only offer RSA ciphers, should receive RSA cert") -tr.Processes.Default.Command = "echo foo | openssl s_client -tls1_2 -servername combined.com -cipher 'ECDHE-RSA-AES128-GCM-SHA256' -connect 127.0.0.1:{0}".format( - ts.Variables.ssl_port) -tr.ReturnCode = 0 -tr.StillRunningAfter = server -tr.StillRunningAfter = ts -tr.Processes.Default.Streams.All += Testers.ContainsExpression(combo_rsa_string, "Should select RSA cert", reflags=re.S | re.M) -tr.Processes.Default.Streams.All += Testers.ContainsExpression("CN ?= ?combined.com", "Should select combined pem") diff --git a/tests/gold_tests/tls/tls_check_dual_cert_selection_plugin.test.py b/tests/gold_tests/tls/tls_check_dual_cert_selection_plugin.test.py deleted file mode 100644 index fbc1dea836e..00000000000 --- a/tests/gold_tests/tls/tls_check_dual_cert_selection_plugin.test.py +++ /dev/null @@ -1,200 +0,0 @@ -''' -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import os - -Test.Summary = ''' -Test ATS offering both RSA and EC certificates loaded via plugin -''' - -Test.SkipUnless(Condition.HasOpenSSLVersion('1.1.1')) - -# Define default ATS -ts = Test.MakeATSProcess("ts", enable_tls=True) -server = Test.MakeOriginServer("server", ssl=True) -dns = Test.MakeDNServer("dns") - -request_header = {"headers": "GET / HTTP/1.1\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -server.addResponse("sessionlog.json", request_header, response_header) - -# add ssl materials like key, certificates for the server -ts.addSSLfile("ssl/signed-foo.pem") -ts.addSSLfile("ssl/signed2-foo.pem") -ts.addSSLfile("ssl/signed-foo.key") -ts.addSSLfile("ssl/signed-foo-ec.pem") -ts.addSSLfile("ssl/signed-foo-ec.key") -ts.addSSLfile("ssl/signed-san.pem") -ts.addSSLfile("ssl/signed-san.key") -ts.addSSLfile("ssl/signed-san-ec.pem") -ts.addSSLfile("ssl/signed-san-ec.key") -ts.addSSLfile("ssl/signer.pem") -ts.addSSLfile("ssl/signer.key") -ts.addSSLfile("ssl/server.pem") -ts.addSSLfile("ssl/server.key") - -ts.Disk.remap_config.AddLine('map / https://foo.com:{1}'.format(ts.Variables.ssl_port, server.Variables.SSL_Port)) - -ts.Disk.ssl_multicert_yaml.AddLines( - """ -ssl_multicert: - - ssl_cert_name: signed-foo-ec.pem,signed-foo.pem - ssl_key_name: signed-foo-ec.key,signed-foo.key - - ssl_cert_name: signed-san-ec.pem,signed-san.pem - ssl_key_name: signed-san-ec.key,signed-san.key - - dest_ip: "*" - ssl_cert_name: server.pem - ssl_key_name: server.key -""".split("\n")) - -Test.PrepareTestPlugin(os.path.join(Test.Variables.AtsTestPluginsDir, 'ssl_secret_load_test.so'), ts) - -# Case 1, global config policy=permissive properties=signature -# override for foo.com policy=enforced properties=all -ts.Disk.records_config.update( - { - 'proxy.config.ssl.server.cert.path': '{0}/../'.format(ts.Variables.SSLDir), - 'proxy.config.ssl.server.private_key.path': '{0}/../'.format(ts.Variables.SSLDir), - 'proxy.config.ssl.server.cipher_suite': - 'ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-SHA384:AES128-GCM-SHA256:AES256-GCM-SHA384:ECDHE-RSA-RC4-SHA:ECDHE-RSA-AES128-SHA:ECDHE-RSA-AES256-SHA:RC4-SHA:RC4-MD5:AES128-SHA:AES256-SHA:DES-CBC3-SHA!SRP:!DSS:!PSK:!aNULL:!eNULL:!SSLv2', - 'proxy.config.url_remap.pristine_host_hdr': 1, - 'proxy.config.dns.nameservers': '127.0.0.1:{0}'.format(dns.Variables.Port), - 'proxy.config.exec_thread.autoconfig.scale': 1.0, - 'proxy.config.dns.resolv_conf': 'NULL', - 'proxy.config.diags.debug.tags': 'ssl_secret_load_test', - 'proxy.config.diags.debug.enabled': 1, - }) - -dns.addRecords(records={"foo.com.": ["127.0.0.1"]}) -dns.addRecords(records={"bar.com.": ["127.0.0.1"]}) - -# Should receive a EC cert -tr = Test.AddTestRun("Default for foo should return EC cert") -tr.Setup.Copy("ssl/signer.pem") -tr.Setup.Copy("ssl/signer2.pem") -tr.Processes.Default.Command = "echo foo | openssl s_client -CAfile signer.pem -servername foo.com -connect 127.0.0.1:{0}".format( - ts.Variables.ssl_port) -tr.ReturnCode = 0 -tr.Processes.Default.StartBefore(server) -tr.Processes.Default.StartBefore(dns) -tr.Processes.Default.StartBefore(Test.Processes.ts, ready=When.PortOpen(ts.Variables.ssl_port)) -tr.StillRunningAfter = server -tr.StillRunningAfter = ts -tr.Processes.Default.Streams.All += Testers.ContainsExpression("Peer signature type: (ECDSA|ecdsa_)", "Should select EC cert") -tr.Processes.Default.Streams.All += Testers.ExcludesExpression("unable to verify the first certificate", "Correct signer") - -# Should receive a RSA cert -tr = Test.AddTestRun("Only offer RSA ciphers, should receive RSA cert") -tr.Processes.Default.Command = "echo foo | openssl s_client -CAfile signer.pem -servername foo.com -sigalgs 'RSA-PSS+SHA256' -connect 127.0.0.1:{0}".format( - ts.Variables.ssl_port) -tr.ReturnCode = 0 -tr.StillRunningAfter = server -tr.StillRunningAfter = ts -tr.Processes.Default.Streams.All += Testers.ContainsExpression("Peer signature type: (RSA-PSS|rsa_pss_)", "Should select RSA cert") -tr.Processes.Default.Streams.All += Testers.ExcludesExpression("unable to verify the first certificate", "Correct signer") - -# Should receive a EC cert -tr = Test.AddTestRun("Default for one.com should return EC cert") -tr.Processes.Default.Command = "echo foo | openssl s_client -CAfile signer.pem -servername one.com -connect 127.0.0.1:{0}".format( - ts.Variables.ssl_port) -tr.ReturnCode = 0 -tr.StillRunningAfter = server -tr.StillRunningAfter = ts -tr.Processes.Default.Streams.All += Testers.ContainsExpression("Peer signature type: (ECDSA|ecdsa_)", "Should select EC cert") -tr.Processes.Default.Streams.All += Testers.ContainsExpression("CN ?= ?group.com", "Should select a group SAN") -tr.Processes.Default.Streams.All += Testers.ExcludesExpression("unable to verify the first certificate", "Correct signer") - -# Should receive a RSA cert -tr = Test.AddTestRun("Only offer RSA ciphers, should receive RSA cert") -tr.Processes.Default.Command = "echo foo | openssl s_client -CAfile signer.pem -servername one.com -sigalgs 'RSA-PSS+SHA256' -connect 127.0.0.1:{0}".format( - ts.Variables.ssl_port) -tr.ReturnCode = 0 -tr.StillRunningAfter = server -tr.StillRunningAfter = ts -tr.Processes.Default.Streams.All += Testers.ContainsExpression("Peer signature type: (RSA-PSS|rsa_pss_)", "Should select RSA cert") -tr.Processes.Default.Streams.All += Testers.ContainsExpression("CN ?= ?group.com", "Should select a group SAN") -tr.Processes.Default.Streams.All += Testers.ExcludesExpression("unable to verify the first certificate", "Correct signer") - -# Should receive a RSA cert -tr = Test.AddTestRun("rsa.com only in rsa cert") -tr.Processes.Default.Command = "echo foo | openssl s_client -CAfile signer.pem -servername rsa.com -connect 127.0.0.1:{0}".format( - ts.Variables.ssl_port) -tr.ReturnCode = 0 -tr.StillRunningAfter = server -tr.StillRunningAfter = ts -tr.Processes.Default.Streams.All += Testers.ContainsExpression("Peer signature type: (RSA-PSS|rsa_pss_)", "Should select RSA cert") -tr.Processes.Default.Streams.All += Testers.ContainsExpression("CN ?= ?group.com", "Should select a group SAN") -tr.Processes.Default.Streams.All += Testers.ExcludesExpression("unable to verify the first certificate", "Correct signer") - -# Should receive a EC cert -tr = Test.AddTestRun("ec.com only in ec cert") -tr.Processes.Default.Command = "echo foo | openssl s_client -CAfile signer.pem -servername ec.com -connect 127.0.0.1:{0}".format( - ts.Variables.ssl_port) -tr.ReturnCode = 0 -tr.StillRunningAfter = server -tr.StillRunningAfter = ts -tr.Processes.Default.Streams.All += Testers.ContainsExpression("Peer signature type: (ECDSA|ecdsa_)", "Should select EC cert") -tr.Processes.Default.Streams.All += Testers.ContainsExpression("CN ?= ?group.com", "Should select a group SAN") -tr.Processes.Default.Streams.All += Testers.ExcludesExpression("unable to verify the first certificate", "Correct signer") - -# Copy in a new version of the foo.com cert. Replace it with the version -# signed by signer 2. Wait at least a second to sure the file update time -# differs -trupdate = Test.AddTestRun("Update server bar cert file in place") -trupdate.StillRunningAfter = ts -trupdate.StillRunningAfter = server -trupdate.Setup.CopyAs("ssl/signed2-foo.pem", ".", "{0}/signed-foo.pem".format(ts.Variables.SSLDir)) -# For some reason the Setup.CopyAs does not change the modification time, so we touch -trupdate.Processes.Default.Command = 'touch {0}/signed-foo.pem'.format(ts.Variables.SSLDir) -# Need to copy over the environment so traffic_ctl knows where to find the unix domain socket -trupdate.Processes.Default.Env = ts.Env -trupdate.Processes.Default.ReturnCode = 0 - -# The plugin will pull every 3 seconds. So wait 4 seconds and test again. Request with CA=signer2.pem should work. Request with CA=signer.pem should fail -# Should receive a RSA cert -tr = Test.AddTestRun("Only offer RSA ciphers, should receive RSA cert") -tr.Processes.Default.Command = "echo foo | openssl s_client -CAfile signer.pem -servername foo.com -sigalgs 'RSA-PSS+SHA256' -connect 127.0.0.1:{0}".format( - ts.Variables.ssl_port) -tr.DelayStart = 4 -tr.ReturnCode = 0 -tr.StillRunningAfter = server -tr.StillRunningAfter = ts -tr.Processes.Default.Streams.All += Testers.ContainsExpression("Peer signature type: (RSA-PSS|rsa_pss_)", "Should select RSA cert") -tr.Processes.Default.Streams.All += Testers.ContainsExpression("CN ?= ?foo.com", "Should select foo.com") -tr.Processes.Default.Streams.All += Testers.ContainsExpression("unable to verify the first certificate", "Different signer") - -tr = Test.AddTestRun("Only offer RSA ciphers, should receive RSA cert with correct CA") -tr.Processes.Default.Command = "echo foo | openssl s_client -CAfile signer2.pem -servername foo.com -sigalgs 'RSA-PSS+SHA256' -connect 127.0.0.1:{0}".format( - ts.Variables.ssl_port) -tr.ReturnCode = 0 -tr.StillRunningAfter = server -tr.StillRunningAfter = ts -tr.Processes.Default.Streams.All += Testers.ContainsExpression("Peer signature type: (RSA-PSS|rsa_pss_)", "Should select RSA cert") -tr.Processes.Default.Streams.All += Testers.ContainsExpression("CN ?= ?foo.com", "Should select foo.com") -tr.Processes.Default.Streams.All += Testers.ExcludesExpression("unable to verify the first certificate", "Correct signer") - -# The EC case should be unchanged -tr = Test.AddTestRun("Offer any cipher") -tr.Processes.Default.Command = "echo foo | openssl s_client -CAfile signer.pem -servername foo.com -connect 127.0.0.1:{0}".format( - ts.Variables.ssl_port) -tr.ReturnCode = 0 -tr.StillRunningAfter = server -tr.StillRunningAfter = ts -tr.Processes.Default.Streams.All += Testers.ContainsExpression("Peer signature type: (ECDSA|ecdsa_)", "Should select EC cert") -tr.Processes.Default.Streams.All += Testers.ContainsExpression("CN ?= ?foo.com", "Should select foo.com") -tr.Processes.Default.Streams.All += Testers.ExcludesExpression("unable to verify the first certificate", "Correct signer") diff --git a/tests/gold_tests/tls/tls_client_alpn_configuration.test.py b/tests/gold_tests/tls/tls_client_alpn_configuration.test.py deleted file mode 100644 index 1bd5cffcd26..00000000000 --- a/tests/gold_tests/tls/tls_client_alpn_configuration.test.py +++ /dev/null @@ -1,164 +0,0 @@ -"""Verify ALPN to origin functionality.""" - -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -from typing import Optional - -Test.Summary = __doc__ - - -class TestAlpnFunctionality: - """Define an object to test a set of ALPN functionality.""" - - _replay_file: str = 'tls_client_alpn_configuration.replay.yaml' - _server_counter: int = 0 - _ts_counter: int = 0 - _client_counter: int = 0 - - def __init__( - self, - records_config_alpn: Optional[str] = None, - conf_remap_alpn: Optional[str] = None, - alpn_is_malformed: bool = False): - """Declare the various test Processes. - - :param records_config_alpn: The string with which to configure the ATS - ALPN via proxy.config.ssl.client.alpn_protocols in the records.yaml. - If the paramenter is None, then no ALPN configuration will be - explicitly set and ATS will use the default value. - - :param conf_remap_alpn: The string with which to configure the Traffic - Server ALPN proxy.config.http.alpn_protocols configuration via - conf_remap. If the parameter is None, then no conf_remap configuration - will be set. - - :param alpn_is_malformed: If True, then the configured ALPN string in - the records.yaml will be malformed. The TestRun will be configured to - expect a warning and the server will be configured to receive no ALPN. - """ - self._alpn = records_config_alpn - self._alpn_conf_remap_alpn = conf_remap_alpn - self._alpn_is_malformed = alpn_is_malformed - - configured_alpn = records_config_alpn if conf_remap_alpn is None else conf_remap_alpn - if alpn_is_malformed: - configured_alpn = None - self._server = self._configure_server(configured_alpn) - - self._ts = self._configure_trafficserver(records_config_alpn, conf_remap_alpn, alpn_is_malformed) - - def _configure_server(self, expected_alpn: Optional[str] = None): - """Configure the test server. - - :param expected_alpn: The ALPN expected from the client. If this is - None, then the server will not expect an ALPN value. - """ - server = Test.MakeVerifierServerProcess(f'server-{TestAlpnFunctionality._server_counter}', self._replay_file) - TestAlpnFunctionality._server_counter += 1 - - if expected_alpn is None: - server.Streams.stdout = Testers.ContainsExpression('Negotiated ALPN: none', 'Verify that ATS sent no ALPN string.') - else: - protocols = expected_alpn.split(',') - for protocol in protocols: - server.Streams.stdout = Testers.ContainsExpression( - f'ALPN.*:.*{protocol}', 'Verify that the server parsed the configured ALPN string from ATS.') - return server - - def _configure_trafficserver( - self, - records_config_alpn: Optional[str] = None, - conf_remap_alpn: Optional[str] = None, - alpn_is_malformed: bool = False): - """Configure a Traffic Server process. - - :param records_config_alpn: See the description of this parameter in - TestAlpnFunctionality._init__. - """ - ts = Test.MakeATSProcess(f'ts-{TestAlpnFunctionality._ts_counter}', enable_tls=True, enable_cache=False) - TestAlpnFunctionality._ts_counter += 1 - - ts.addDefaultSSLFiles() - ts.Disk.records_config.update( - { - "proxy.config.ssl.server.cert.path": f'{ts.Variables.SSLDir}', - "proxy.config.ssl.server.private_key.path": f'{ts.Variables.SSLDir}', - "proxy.config.ssl.client.verify.server.policy": 'PERMISSIVE', - 'proxy.config.diags.debug.enabled': 3, - 'proxy.config.diags.debug.tags': 'ssl|http', - }) - - if records_config_alpn is not None: - ts.Disk.records_config.update({ - 'proxy.config.ssl.client.alpn_protocols': records_config_alpn, - }) - - ts.Disk.ssl_multicert_yaml.AddLines( - """ -ssl_multicert: - - dest_ip: "*" - ssl_cert_name: server.pem - ssl_key_name: server.key -""".split("\n")) - - conf_remap_specification = '' - if conf_remap_alpn is not None: - conf_remap_specification = ( - '@plugin=conf_remap.so ' - f'@pparam=proxy.config.ssl.client.alpn_protocols={conf_remap_alpn}') - - ts.Disk.remap_config.AddLine(f'map / https://127.0.0.1:{self._server.Variables.https_port} {conf_remap_specification}') - - if alpn_is_malformed: - ts.Disk.diags_log.Content = Testers.ContainsExpression("ERROR.*ALPN", "There should be no ALPN parse warnings.") - else: - ts.Disk.diags_log.Content += Testers.ExcludesExpression("ERROR.*ALPN", "There should be no ALPN parse warnings.") - - return ts - - def run(self): - """Configure the TestRun.""" - description = "default" if self._alpn is None else self._alpn - tr = Test.AddTestRun(f'ATS ALPN configuration: {description}') - tr.Processes.Default.StartBefore(self._server) - tr.Processes.Default.StartBefore(self._ts) - - tr.AddVerifierClientProcess( - f'client-{TestAlpnFunctionality._client_counter}', self._replay_file, https_ports=[self._ts.Variables.ssl_port]) - TestAlpnFunctionality._client_counter += 1 - - -# -# Test default configuration. -# -TestAlpnFunctionality().run() - -# -# Test various valid ALPN configurations. -# -TestAlpnFunctionality(records_config_alpn='http/1.1').run() -TestAlpnFunctionality(records_config_alpn='http/1.1,http/1.0').run() -TestAlpnFunctionality(records_config_alpn='http/1.1', conf_remap_alpn='http/1.1,http/1.0').run() -TestAlpnFunctionality(records_config_alpn='h2,http/1.1').run() -TestAlpnFunctionality(records_config_alpn='h2').run() - -# -# Test malformed ALPN configurations. -# -TestAlpnFunctionality(records_config_alpn='not_a_protocol', alpn_is_malformed=True).run() -# Note that HTTP/3 to origin is not currently supported. -TestAlpnFunctionality(records_config_alpn='h3', alpn_is_malformed=True).run() diff --git a/tests/gold_tests/tls/tls_client_cert.test.py b/tests/gold_tests/tls/tls_client_cert.test.py deleted file mode 100644 index eef28390680..00000000000 --- a/tests/gold_tests/tls/tls_client_cert.test.py +++ /dev/null @@ -1,294 +0,0 @@ -''' -Test offering client cert to origin -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -Test.Summary = ''' -Test different combinations of TLS handshake hooks to ensure they are applied consistently. -''' - -ts = Test.MakeATSProcess("ts") -cafile = "{0}/signer.pem".format(Test.RunDirectory) -cafile2 = "{0}/signer2.pem".format(Test.RunDirectory) -# --clientverify: "" empty string because microserver does store_true for argparse, but options is a dictionary -server = Test.MakeOriginServer( - "server", - ssl=True, - options={ - "--clientCA": cafile, - "--clientverify": "" - }, - clientcert="{0}/signed-foo.pem".format(Test.RunDirectory), - clientkey="{0}/signed-foo.key".format(Test.RunDirectory)) -server2 = Test.MakeOriginServer( - "server2", - ssl=True, - options={ - "--clientCA": cafile2, - "--clientverify": "" - }, - clientcert="{0}/signed2-bar.pem".format(Test.RunDirectory), - clientkey="{0}/signed-bar.key".format(Test.RunDirectory)) -server3 = Test.MakeOriginServer("server3") -server4 = Test.MakeOriginServer("server4") -server.Setup.Copy("ssl/signer.pem") -server.Setup.Copy("ssl/signer2.pem") -server.Setup.Copy("ssl/signed-foo.pem") -server.Setup.Copy("ssl/signed-foo.key") -server.Setup.Copy("ssl/signed2-foo.pem") -server.Setup.Copy("ssl/signed2-bar.pem") -server.Setup.Copy("ssl/signed-bar.key") -server2.Setup.Copy("ssl/signer.pem") -server2.Setup.Copy("ssl/signer2.pem") -server2.Setup.Copy("ssl/signed-foo.pem") -server2.Setup.Copy("ssl/signed-foo.key") -server2.Setup.Copy("ssl/signed2-foo.pem") -server2.Setup.Copy("ssl/signed2-bar.pem") -server2.Setup.Copy("ssl/signed-bar.key") - -request_header = {"headers": "GET / HTTP/1.1\r\nHost: example.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -server.addResponse("sessionlog.json", request_header, response_header) -request_header = {"headers": "GET / HTTP/1.1\r\nHost: bar.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -server.addResponse("sessionlog.json", request_header, response_header) - -ts.addSSLfile("ssl/server.pem") -ts.addSSLfile("ssl/server.key") -ts.addSSLfile("ssl/signed-foo.pem") -ts.addSSLfile("ssl/signed-foo.key") -ts.addSSLfile("ssl/signed2-foo.pem") -ts.addSSLfile("ssl/signed-bar.pem") -ts.addSSLfile("ssl/signed2-bar.pem") -ts.addSSLfile("ssl/signed-bar.key") - -ts.Disk.records_config.update( - { - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'ssl_verify_test', - 'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir), - 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts.Variables.SSLDir), - 'proxy.config.ssl.client.cert.path': '{0}'.format(ts.Variables.SSLDir), - 'proxy.config.ssl.client.cert.filename': 'signed-foo.pem', - 'proxy.config.ssl.client.private_key.path': '{0}'.format(ts.Variables.SSLDir), - 'proxy.config.ssl.client.private_key.filename': 'signed-foo.key', - 'proxy.config.exec_thread.autoconfig.scale': 1.0, - 'proxy.config.url_remap.pristine_host_hdr': 1, - 'proxy.config.ssl.client.verify.server.policy': 'PERMISSIVE', - }) - -ts.Disk.ssl_multicert_yaml.AddLines( - """ -ssl_multicert: - - dest_ip: "*" - ssl_cert_name: server.pem - ssl_key_name: server.key -""".split("\n")) - -ts.Disk.remap_config.AddLine('map /case1 https://127.0.0.1:{0}/'.format(server.Variables.SSL_Port)) -ts.Disk.remap_config.AddLine('map /case2 https://127.0.0.1:{0}/'.format(server2.Variables.SSL_Port)) - -ts.Disk.sni_yaml.AddLine('sni:') -ts.Disk.sni_yaml.AddLine('- fqdn: bar.com') -ts.Disk.sni_yaml.AddLine(' client_cert: {0}/signed2-bar.pem'.format(ts.Variables.SSLDir)) -ts.Disk.sni_yaml.AddLine(' client_key: {0}/signed-bar.key'.format(ts.Variables.SSLDir)) - -ts.Disk.logging_yaml.AddLines( - ''' -logging: - formats: - - name: testformat - format: '% % % %' - logs: - - mode: ascii - format: testformat - filename: squid -'''.split("\n")) - -# Should succeed -tr = Test.AddTestRun("Connect with first client cert to first server") -tr.Processes.Default.StartBefore(Test.Processes.ts) -tr.Processes.Default.StartBefore(server) -tr.Processes.Default.StartBefore(server2) -tr.StillRunningAfter = ts -tr.StillRunningAfter = server -tr.StillRunningAfter = server2 -tr.MakeCurlCommand("-H host:example.com http://127.0.0.1:{0}/case1".format(ts.Variables.port), ts=ts) -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Streams.stdout = Testers.ExcludesExpression("Could Not Connect", "Check response") - -# Should fail -trfail = Test.AddTestRun("Connect with first client cert to second server") -trfail.StillRunningAfter = ts -trfail.StillRunningAfter = server -trfail.StillRunningAfter = server2 -trfail.MakeCurlCommand('-H host:example.com http://127.0.0.1:{0}/case2'.format(ts.Variables.port), ts=ts) -trfail.Processes.Default.ReturnCode = 0 -trfail.Processes.Default.Streams.stdout = Testers.ContainsExpression("Could Not Connect", "Check response") - -# Should succeed -trbar = Test.AddTestRun("Connect with signed2 bar to second server") -trbar.StillRunningAfter = ts -trbar.StillRunningAfter = server -trbar.StillRunningAfter = server2 -trbar.MakeCurlCommand("-H host:bar.com http://127.0.0.1:{0}/case2".format(ts.Variables.port), ts=ts) -trbar.Processes.Default.ReturnCode = 0 -trbar.Processes.Default.Streams.stdout = Testers.ExcludesExpression("Could Not Connect", "Check response") - -# Should fail -trbarfail = Test.AddTestRun("Connect with signed2 bar cert to first server") -trbarfail.StillRunningAfter = ts -trbarfail.StillRunningAfter = server -trbarfail.StillRunningAfter = server2 -trbarfail.MakeCurlCommand('-H host:bar.com http://127.0.0.1:{0}/case1'.format(ts.Variables.port), ts=ts) -trbarfail.Processes.Default.ReturnCode = 0 -trbarfail.Processes.Default.Streams.stdout = Testers.ContainsExpression("Could Not Connect", "Check response") - -tr2 = Test.AddTestRun("Update config files") -# Update the SNI config -snipath = ts.Disk.sni_yaml.AbsPath -recordspath = ts.Disk.records_config.AbsPath -tr2.Disk.File(snipath, id="sni_yaml", typename="ats:config"), -tr2.Disk.sni_yaml.AddLine('sni:') -tr2.Disk.sni_yaml.AddLine('- fqdn: bar.com') -tr2.Disk.sni_yaml.AddLine(' client_cert: {0}/signed-bar.pem'.format(ts.Variables.SSLDir)) -tr2.Disk.sni_yaml.AddLine(' client_key: {0}/signed-bar.key'.format(ts.Variables.SSLDir)) -# recreate the records.yaml with the cert filename changed -tr2.Disk.File(recordspath, id="records_config", typename="ats:config:records"), -tr2.Disk.records_config.update( - { - 'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir), - 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts.Variables.SSLDir), - 'proxy.config.ssl.client.cert.path': '{0}'.format(ts.Variables.SSLDir), - 'proxy.config.ssl.client.cert.filename': 'signed2-foo.pem', - 'proxy.config.ssl.client.private_key.path': '{0}'.format(ts.Variables.SSLDir), - 'proxy.config.ssl.client.private_key.filename': 'signed-foo.key', - 'proxy.config.url_remap.pristine_host_hdr': 1, - 'proxy.config.ssl.client.verify.server.policy': 'PERMISSIVE', - }) -tr2.StillRunningAfter = ts -tr2.StillRunningAfter = server -tr2.StillRunningAfter = server2 -tr2.Processes.Default.Command = 'echo Updated configs' -# Need to copy over the environment so traffic_ctl knows where to find the unix domain socket -tr2.Processes.Default.Env = ts.Env -tr2.Processes.Default.ReturnCode = 0 - -tr2reload = Test.AddConfigReload(ts, expect_tasks=["sni.yaml"], description="Reload config") -tr2reload.StillRunningAfter = server -tr2reload.StillRunningAfter = server2 - -# Should succeed -tr3bar = Test.AddTestRun("Make request with other bar cert to first server") -tr3bar.Processes.Default.StartBefore(server3) -tr3bar.StillRunningAfter = ts -tr3bar.StillRunningAfter = server -tr3bar.StillRunningAfter = server2 -tr3bar.MakeCurlCommand(' -H host:bar.com http://127.0.0.1:{0}/case1'.format(ts.Variables.port), ts=ts) -tr3bar.Processes.Default.ReturnCode = 0 -tr3bar.Processes.Default.Streams.stdout = Testers.ExcludesExpression("Could Not Connect", "Check response") - -# Should fail -tr3barfail = Test.AddTestRun("Make request with other bar cert to second server") -tr3barfail.StillRunningAfter = ts -tr3barfail.StillRunningAfter = server -tr3barfail.StillRunningAfter = server2 -tr3barfail.MakeCurlCommand(' -H host:bar.com http://127.0.0.1:{0}/case2'.format(ts.Variables.port), ts=ts) -tr3barfail.Processes.Default.ReturnCode = 0 -tr3barfail.Processes.Default.Streams.stdout = Testers.ContainsExpression("Could Not Connect", "Check response") - -# Should succeed -tr3 = Test.AddTestRun("Make request with other cert to second server") -# Wait for the reload to complete -tr3.StillRunningAfter = ts -tr3.StillRunningAfter = server -tr3.StillRunningAfter = server2 -tr3.MakeCurlCommand(' -H host:example.com http://127.0.0.1:{0}/case2'.format(ts.Variables.port), ts=ts) -tr3.Processes.Default.ReturnCode = 0 -tr3.Processes.Default.Streams.stdout = Testers.ExcludesExpression("Could Not Connect", "Check response") - -# Should fail -tr3fail = Test.AddTestRun("Make request with other cert to first server") -tr3fail.StillRunningAfter = ts -tr3fail.StillRunningAfter = server -tr3fail.StillRunningAfter = server2 -tr3fail.MakeCurlCommand(' -H host:example.com http://127.0.0.1:{0}/case1'.format(ts.Variables.port), ts=ts) -tr3fail.Processes.Default.ReturnCode = 0 -tr3fail.Processes.Default.Streams.stdout = Testers.ContainsExpression("Could Not Connect", "Check response") - -# Test the case of updating certificate contents without changing file name. -trupdate = Test.AddTestRun("Update client cert file in place") -trupdate.StillRunningAfter = ts -trupdate.StillRunningAfter = server -trupdate.StillRunningAfter = server2 -# Make a meaningless config change on the path so the records.yaml reload logic will trigger -trupdate.Setup.CopyAs("ssl/signed2-bar.pem", ".", "{0}/signed-bar.pem".format(ts.Variables.SSLDir)) -# in the config/ssl directory for records.yaml -trupdate.Setup.CopyAs("ssl/signed-foo.pem", ".", "{0}/signed2-foo.pem".format(ts.Variables.SSLDir)) -trupdate.Processes.Default.Command = 'traffic_ctl config set proxy.config.ssl.client.cert.path {0}/; touch {1}'.format( - ts.Variables.SSLDir, snipath) -# Need to copy over the environment so traffic_ctl knows where to find the unix domain socket -trupdate.Processes.Default.Env = ts.Env -trupdate.Processes.Default.ReturnCode = 0 - -trreload = Test.AddConfigReload(ts, expect_tasks=["sni.yaml"], description="Reload config after renaming certs") -trreload.StillRunningAfter = server -trreload.StillRunningAfter = server2 - -# Should succeed -tr4bar = Test.AddTestRun("Make request with renamed bar cert to second server") -tr4bar.StillRunningAfter = ts -tr4bar.StillRunningAfter = server -tr4bar.StillRunningAfter = server2 -tr4bar.MakeCurlCommand(' -H host:bar.com http://127.0.0.1:{0}/case2'.format(ts.Variables.port), ts=ts) -tr4bar.Processes.Default.ReturnCode = 0 -tr4bar.Processes.Default.Streams.stdout = Testers.ExcludesExpression("Could Not Connect", "Check response") - -# Should fail -tr4barfail = Test.AddTestRun("Make request with renamed bar cert to first server") -tr4barfail.StillRunningAfter = ts -tr4barfail.StillRunningAfter = server -tr4barfail.StillRunningAfter = server2 -tr4barfail.MakeCurlCommand(' -H host:bar.com http://127.0.0.1:{0}/case1'.format(ts.Variables.port), ts=ts) -tr4barfail.Processes.Default.ReturnCode = 0 -tr4barfail.Processes.Default.Streams.stdout = Testers.ContainsExpression("Could Not Connect", "Check response") - -# Should succeed -tr4 = Test.AddTestRun("Make request with renamed foo cert to first server") -tr4.StillRunningAfter = ts -tr4.StillRunningAfter = server -tr4.StillRunningAfter = server2 -tr4.MakeCurlCommand(' -H host:example.com http://127.0.0.1:{0}/case1'.format(ts.Variables.port), ts=ts) -tr4.Processes.Default.ReturnCode = 0 -tr4.Processes.Default.Streams.stdout = Testers.ExcludesExpression("Could Not Connect", "Check response") - -# Should fail -tr4fail = Test.AddTestRun("Make request with renamed foo cert to second server") -tr4fail.StillRunningAfter = ts -tr4fail.StillRunningAfter = server -tr4fail.StillRunningAfter = server2 -tr4fail.MakeCurlCommand(' -H host:example.com http://127.0.0.1:{0}/case2'.format(ts.Variables.port), ts=ts) -tr4fail.Processes.Default.ReturnCode = 0 -tr4fail.Processes.Default.Streams.stdout = Testers.ContainsExpression("Could Not Connect", "Check response") - -tr = Test.AddTestRun("Wait for the access log to write out") -tr.Processes.Default.StartBefore(server4, ready=When.FileContains(ts.Disk.squid_log.Name, 'https', 12)) -tr.StillRunningAfter = ts -tr.Processes.Default.Command = 'echo "log file exists"' -tr.Processes.Default.ReturnCode = 0 - -ts.Disk.squid_log.Content = "gold/proxycert-accesslog.gold" diff --git a/tests/gold_tests/tls/tls_client_cert2.test.py b/tests/gold_tests/tls/tls_client_cert2.test.py deleted file mode 100644 index 4645c048483..00000000000 --- a/tests/gold_tests/tls/tls_client_cert2.test.py +++ /dev/null @@ -1,209 +0,0 @@ -''' -Test offering client cert to origin -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -Test.Summary = ''' -Test client certs to origin selected via wildcard names in sni -''' - -ts = Test.MakeATSProcess("ts") -cafile = "{0}/signer.pem".format(Test.RunDirectory) -cafile2 = "{0}/signer2.pem".format(Test.RunDirectory) -server = Test.MakeOriginServer( - "server", - ssl=True, - options={ - "--clientCA": cafile, - "--clientverify": "" - }, - clientcert="{0}/signed-foo.pem".format(Test.RunDirectory), - clientkey="{0}/signed-foo.key".format(Test.RunDirectory)) -server2 = Test.MakeOriginServer( - "server2", - ssl=True, - options={ - "--clientCA": cafile2, - "--clientverify": "" - }, - clientcert="{0}/signed2-bar.pem".format(Test.RunDirectory), - clientkey="{0}/signed-bar.key".format(Test.RunDirectory)) -server4 = Test.MakeOriginServer("server4") -server.Setup.Copy("ssl/signer.pem") -server.Setup.Copy("ssl/signer2.pem") -server.Setup.Copy("ssl/signed-foo.pem") -server.Setup.Copy("ssl/signed-foo.key") -server.Setup.Copy("ssl/signed2-foo.pem") -server.Setup.Copy("ssl/signed2-bar.pem") -server.Setup.Copy("ssl/signed-bar.key") -server2.Setup.Copy("ssl/signer.pem") -server2.Setup.Copy("ssl/signer2.pem") -server2.Setup.Copy("ssl/signed-foo.pem") -server2.Setup.Copy("ssl/signed-foo.key") -server2.Setup.Copy("ssl/signed2-foo.pem") -server2.Setup.Copy("ssl/signed2-bar.pem") -server2.Setup.Copy("ssl/signed-bar.key") - -request_header = {"headers": "GET / HTTP/1.1\r\nHost: example.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -server.addResponse("sessionlog.json", request_header, response_header) -request_header = {"headers": "GET / HTTP/1.1\r\nHost: bar.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -server.addResponse("sessionlog.json", request_header, response_header) - -ts.addSSLfile("ssl/server.pem") -ts.addSSLfile("ssl/server.key") -ts.addSSLfile("ssl/combo-signed-foo.pem") -ts.addSSLfile("ssl/signed-foo.pem") -ts.addSSLfile("ssl/signed-foo.key") -ts.addSSLfile("ssl/signed2-foo.pem") -ts.addSSLfile("ssl/signed-bar.pem") -ts.addSSLfile("ssl/signed2-bar.pem") -ts.addSSLfile("ssl/signed-bar.key") - -ts.Disk.records_config.update( - { - 'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir), - 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts.Variables.SSLDir), - 'proxy.config.ssl.client.cert.path': '{0}'.format(ts.Variables.SSLDir), - 'proxy.config.ssl.client.private_key.path': '{0}'.format(ts.Variables.SSLDir), - 'proxy.config.exec_thread.autoconfig.scale': 1.0, - 'proxy.config.url_remap.pristine_host_hdr': 1, - 'proxy.config.ssl.client.verify.server.policy': 'PERMISSIVE', - }) - -ts.Disk.ssl_multicert_yaml.AddLines( - """ -ssl_multicert: - - dest_ip: "*" - ssl_cert_name: server.pem - ssl_key_name: server.key -""".split("\n")) - -ts.Disk.remap_config.AddLine('map /case1 https://127.0.0.1:{0}/'.format(server.Variables.SSL_Port)) -ts.Disk.remap_config.AddLine('map /case2 https://127.0.0.1:{0}/'.format(server2.Variables.SSL_Port)) - -ts.Disk.sni_yaml.AddLines( - [ - 'sni:', - '- fqdn: bob.bar.com', - ' client_cert: signed-bar.pem', - ' client_key: signed-bar.key', - '- fqdn: "bob.foo.com"', - ' client_cert: {0}/combo-signed-foo.pem'.format(ts.Variables.SSLDir), - '- fqdn: "*.bar.com"', - ' client_cert: {0}/signed2-bar.pem'.format(ts.Variables.SSLDir), - ' client_key: {0}/signed-bar.key'.format(ts.Variables.SSLDir), - '- fqdn: "foo.com"', - ' client_cert: {0}/signed2-foo.pem'.format(ts.Variables.SSLDir), - ' client_key: {0}/signed-foo.key'.format(ts.Variables.SSLDir), - ]) - -ts.Disk.logging_yaml.AddLines( - ''' -logging: - formats: - - name: testformat - format: '% % % %' - logs: - - mode: ascii - format: testformat - filename: squid -'''.split("\n")) - -# Should succeed -tr = Test.AddTestRun("bob.bar.com to server 1") -tr.Processes.Default.StartBefore(Test.Processes.ts) -tr.Processes.Default.StartBefore(server) -tr.Processes.Default.StartBefore(server2) -tr.StillRunningAfter = ts -tr.StillRunningAfter = server -tr.StillRunningAfter = server2 -tr.MakeCurlCommand("-H host:bob.bar.com http://127.0.0.1:{0}/case1".format(ts.Variables.port), ts=ts) -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Streams.stdout = Testers.ExcludesExpression("Could Not Connect", "Check response") - -# Should fail -trfail = Test.AddTestRun("bob.bar.com to server 2") -trfail.StillRunningAfter = ts -trfail.StillRunningAfter = server -trfail.StillRunningAfter = server2 -trfail.MakeCurlCommand('-H host:bob.bar.com http://127.0.0.1:{0}/case2'.format(ts.Variables.port), ts=ts) -trfail.Processes.Default.ReturnCode = 0 -trfail.Processes.Default.Streams.stdout = Testers.ContainsExpression("Could Not Connect", "Check response") - -# Should succeed -tr = Test.AddTestRun("bob.foo.com to server 1") -tr.StillRunningAfter = ts -tr.StillRunningAfter = server -tr.StillRunningAfter = server2 -tr.MakeCurlCommand("-H host:bob.foo.com http://127.0.0.1:{0}/case1".format(ts.Variables.port), ts=ts) -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Streams.stdout = Testers.ExcludesExpression("Could Not Connect", "Check response") - -# Should fail -trfail = Test.AddTestRun("bob.foo.com to server 2") -trfail.StillRunningAfter = ts -trfail.StillRunningAfter = server -trfail.StillRunningAfter = server2 -trfail.MakeCurlCommand('-H host:bob.foo.com http://127.0.0.1:{0}/case2'.format(ts.Variables.port), ts=ts) -trfail.Processes.Default.ReturnCode = 0 -trfail.Processes.Default.Streams.stdout = Testers.ContainsExpression("Could Not Connect", "Check response") - -# Should succeed -tr = Test.AddTestRun("random.bar.com to server 2") -tr.StillRunningAfter = ts -tr.StillRunningAfter = server -tr.StillRunningAfter = server2 -tr.MakeCurlCommand("-H host:random.bar.com http://127.0.0.1:{0}/case2".format(ts.Variables.port), ts=ts) -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Streams.stdout = Testers.ExcludesExpression("Could Not Connect", "Check response") - -# Should fail -trfail = Test.AddTestRun("random.bar.com to server 1") -trfail.StillRunningAfter = ts -trfail.StillRunningAfter = server -trfail.StillRunningAfter = server2 -trfail.MakeCurlCommand('-H host:random.bar.com http://127.0.0.1:{0}/case1'.format(ts.Variables.port), ts=ts) -trfail.Processes.Default.ReturnCode = 0 -trfail.Processes.Default.Streams.stdout = Testers.ContainsExpression("Could Not Connect", "Check response") - -# Should fail -tr = Test.AddTestRun("random.foo.com to server 2") -tr.StillRunningAfter = ts -tr.StillRunningAfter = server -tr.StillRunningAfter = server2 -tr.MakeCurlCommand("-H host:random.foo.com http://127.0.0.1:{0}/case2".format(ts.Variables.port), ts=ts) -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Streams.stdout = Testers.ContainsExpression("Could Not Connect", "Check response") - -# Should fail -trfail = Test.AddTestRun("random.foo.com to server 1") -trfail.StillRunningAfter = ts -trfail.StillRunningAfter = server -trfail.StillRunningAfter = server2 -trfail.MakeCurlCommand('-H host:random.foo.com http://127.0.0.1:{0}/case1'.format(ts.Variables.port), ts=ts) -trfail.Processes.Default.ReturnCode = 0 -trfail.Processes.Default.Streams.stdout = Testers.ContainsExpression("Could Not Connect", "Check response") - -tr = Test.AddTestRun("Wait for the access log to write out") -tr.Processes.Default.StartBefore(server4, ready=When.FileExists(ts.Disk.squid_log)) -tr.StillRunningAfter = ts -tr.Processes.Default.Command = 'echo "Log file exists"' -tr.Processes.Default.ReturnCode = 0 - -ts.Disk.squid_log.Content = "gold/proxycert2-accesslog.gold" diff --git a/tests/gold_tests/tls/tls_client_cert2_plugin.test.py b/tests/gold_tests/tls/tls_client_cert2_plugin.test.py deleted file mode 100644 index c3dade36eb8..00000000000 --- a/tests/gold_tests/tls/tls_client_cert2_plugin.test.py +++ /dev/null @@ -1,198 +0,0 @@ -''' -Test offering client cert to origin, but using plugin for cert loading -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import subprocess -import os - -Test.Summary = ''' -Test offering client cert to origin, but using plugin for cert loading -''' - -ts = Test.MakeATSProcess("ts") -cafile = "{0}/signer.pem".format(Test.RunDirectory) -cafile2 = "{0}/signer2.pem".format(Test.RunDirectory) -server = Test.MakeOriginServer( - "server", - ssl=True, - options={ - "--clientCA": cafile, - "--clientverify": "" - }, - clientcert="{0}/signed-foo.pem".format(Test.RunDirectory), - clientkey="{0}/signed-foo.key".format(Test.RunDirectory)) -server2 = Test.MakeOriginServer( - "server2", - ssl=True, - options={ - "--clientCA": cafile2, - "--clientverify": "" - }, - clientcert="{0}/signed2-bar.pem".format(Test.RunDirectory), - clientkey="{0}/signed-bar.key".format(Test.RunDirectory)) -server.Setup.Copy("ssl/signer.pem") -server.Setup.Copy("ssl/signer2.pem") -server.Setup.Copy("ssl/signed-foo.pem") -server.Setup.Copy("ssl/signed-foo.key") -server.Setup.Copy("ssl/signed2-foo.pem") -server.Setup.Copy("ssl/signed2-bar.pem") -server.Setup.Copy("ssl/signed-bar.key") -server2.Setup.Copy("ssl/signer.pem") -server2.Setup.Copy("ssl/signer2.pem") -server2.Setup.Copy("ssl/signed-foo.pem") -server2.Setup.Copy("ssl/signed-foo.key") -server2.Setup.Copy("ssl/signed2-foo.pem") -server2.Setup.Copy("ssl/signed2-bar.pem") -server2.Setup.Copy("ssl/signed-bar.key") - -request_header = {"headers": "GET / HTTP/1.1\r\nHost: example.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -server.addResponse("sessionlog.json", request_header, response_header) -request_header = {"headers": "GET / HTTP/1.1\r\nHost: bar.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -server.addResponse("sessionlog.json", request_header, response_header) - -# -# Certs and keys loaded into the ts/ssl directory, but the paths in the -# configs omit the ssl subdirectory. The ssl_secret_load_test plugin adds this back in -ts.addSSLfile("ssl/server.pem") -ts.addSSLfile("ssl/server.key") -ts.addSSLfile("ssl/combo-signed-foo.pem") -ts.addSSLfile("ssl/signed-foo.pem") -ts.addSSLfile("ssl/signed-foo.key") -ts.addSSLfile("ssl/signed2-foo.pem") -ts.addSSLfile("ssl/signed-bar.pem") -ts.addSSLfile("ssl/signed2-bar.pem") -ts.addSSLfile("ssl/signed-bar.key") - -Test.PrepareTestPlugin(os.path.join(Test.Variables.AtsTestPluginsDir, 'ssl_secret_load_test.so'), ts) - -ts.Disk.records_config.update( - { - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'ssl_secret_load_test', - 'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir), - 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts.Variables.SSLDir), - 'proxy.config.ssl.client.cert.path': '{0}/../'.format(ts.Variables.SSLDir), - 'proxy.config.ssl.client.private_key.path': '{0}'.format(ts.Variables.SSLDir), - 'proxy.config.ssl.client.verify.server.policy': 'PERMISSIVE', - 'proxy.config.exec_thread.autoconfig.scale': 1.0, - 'proxy.config.url_remap.pristine_host_hdr': 1, - }) - -ts.Disk.ssl_multicert_yaml.AddLines( - """ -ssl_multicert: - - dest_ip: "*" - ssl_cert_name: server.pem - ssl_key_name: server.key -""".split("\n")) - -ts.Disk.remap_config.AddLine('map /case1 https://127.0.0.1:{0}/'.format(server.Variables.SSL_Port)) -ts.Disk.remap_config.AddLine('map /case2 https://127.0.0.1:{0}/'.format(server2.Variables.SSL_Port)) - -ts.Disk.sni_yaml.AddLines( - [ - 'sni:', - '- fqdn: bob.bar.com', - ' client_cert: {0}/../signed-bar.pem'.format(ts.Variables.SSLDir), - ' client_key: {0}/../signed-bar.key'.format(ts.Variables.SSLDir), - '- fqdn: "bob.foo.com"', - ' client_cert: {0}/../combo-signed-foo.pem'.format(ts.Variables.SSLDir), - '- fqdn: "*.bar.com"', - ' client_cert: {0}/../signed2-bar.pem'.format(ts.Variables.SSLDir), - ' client_key: {0}/../signed-bar.key'.format(ts.Variables.SSLDir), - '- fqdn: "foo.com"', - ' client_cert: {0}/../signed2-foo.pem'.format(ts.Variables.SSLDir), - ' client_key: {0}/../signed-foo.key'.format(ts.Variables.SSLDir), - ]) - -# Should succeed -tr = Test.AddTestRun("bob.bar.com to server 1") -tr.Processes.Default.StartBefore(Test.Processes.ts, ready=When.PortOpen(ts.Variables.port)) -tr.Processes.Default.StartBefore(server) -tr.Processes.Default.StartBefore(server2) -tr.StillRunningAfter = ts -tr.StillRunningAfter = server -tr.StillRunningAfter = server2 -tr.MakeCurlCommand("-H host:bob.bar.com http://127.0.0.1:{0}/case1".format(ts.Variables.port), ts=ts) -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Streams.stdout = Testers.ExcludesExpression("Could Not Connect", "Check response") - -# Should fail -trfail = Test.AddTestRun("bob.bar.com to server 2") -trfail.StillRunningAfter = ts -trfail.StillRunningAfter = server -trfail.StillRunningAfter = server2 -trfail.MakeCurlCommand('-H host:bob.bar.com http://127.0.0.1:{0}/case2'.format(ts.Variables.port), ts=ts) -trfail.Processes.Default.ReturnCode = 0 -trfail.Processes.Default.Streams.stdout = Testers.ContainsExpression("Could Not Connect", "Check response") - -# Should succeed -tr = Test.AddTestRun("bob.foo.com to server 1") -tr.StillRunningAfter = ts -tr.StillRunningAfter = server -tr.StillRunningAfter = server2 -tr.MakeCurlCommand("-H host:bob.foo.com http://127.0.0.1:{0}/case1".format(ts.Variables.port), ts=ts) -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Streams.stdout = Testers.ExcludesExpression("Could Not Connect", "Check response") - -# Should fail -trfail = Test.AddTestRun("bob.foo.com to server 2") -trfail.StillRunningAfter = ts -trfail.StillRunningAfter = server -trfail.StillRunningAfter = server2 -trfail.MakeCurlCommand('-H host:bob.foo.com http://127.0.0.1:{0}/case2'.format(ts.Variables.port), ts=ts) -trfail.Processes.Default.ReturnCode = 0 -trfail.Processes.Default.Streams.stdout = Testers.ContainsExpression("Could Not Connect", "Check response") - -# Should succeed -tr = Test.AddTestRun("random.bar.com to server 2") -tr.StillRunningAfter = ts -tr.StillRunningAfter = server -tr.StillRunningAfter = server2 -tr.MakeCurlCommand("-H host:random.bar.com http://127.0.0.1:{0}/case2".format(ts.Variables.port), ts=ts) -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Streams.stdout = Testers.ExcludesExpression("Could Not Connect", "Check response") - -# Should fail -trfail = Test.AddTestRun("random.bar.com to server 1") -trfail.StillRunningAfter = ts -trfail.StillRunningAfter = server -trfail.StillRunningAfter = server2 -trfail.MakeCurlCommand('-H host:random.bar.com http://127.0.0.1:{0}/case1'.format(ts.Variables.port), ts=ts) -trfail.Processes.Default.ReturnCode = 0 -trfail.Processes.Default.Streams.stdout = Testers.ContainsExpression("Could Not Connect", "Check response") - -# Should fail -tr = Test.AddTestRun("random.foo.com to server 2") -tr.StillRunningAfter = ts -tr.StillRunningAfter = server -tr.StillRunningAfter = server2 -tr.MakeCurlCommand("-H host:random.foo.com http://127.0.0.1:{0}/case2".format(ts.Variables.port), ts=ts) -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Streams.stdout = Testers.ContainsExpression("Could Not Connect", "Check response") - -# Should fail -trfail = Test.AddTestRun("random.foo.com to server 1") -trfail.StillRunningAfter = ts -trfail.StillRunningAfter = server -trfail.StillRunningAfter = server2 -trfail.MakeCurlCommand('-H host:random.foo.com http://127.0.0.1:{0}/case1'.format(ts.Variables.port), ts=ts) -trfail.Processes.Default.ReturnCode = 0 -trfail.Processes.Default.Streams.stdout = Testers.ContainsExpression("Could Not Connect", "Check response") diff --git a/tests/gold_tests/tls/tls_client_cert_override.test.py b/tests/gold_tests/tls/tls_client_cert_override.test.py deleted file mode 100644 index 466eaf9eba2..00000000000 --- a/tests/gold_tests/tls/tls_client_cert_override.test.py +++ /dev/null @@ -1,149 +0,0 @@ -''' -Test offering client cert to origin -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -Test.Summary = ''' -Test conf_remp to specify different client certificates to offer to the origin -''' - -ts = Test.MakeATSProcess("ts") -cafile = "{0}/signer.pem".format(Test.RunDirectory) -cafile2 = "{0}/signer2.pem".format(Test.RunDirectory) -server = Test.MakeOriginServer( - "server", - ssl=True, - options={ - "--clientCA": cafile, - "--clientverify": "" - }, - clientcert="{0}/signed-foo.pem".format(Test.RunDirectory), - clientkey="{0}/signed-foo.key".format(Test.RunDirectory)) -server2 = Test.MakeOriginServer( - "server2", - ssl=True, - options={ - "--clientCA": cafile2, - "--clientverify": "" - }, - clientcert="{0}/signed2-bar.pem".format(Test.RunDirectory), - clientkey="{0}/signed-bar.key".format(Test.RunDirectory)) -server.Setup.Copy("ssl/signer.pem") -server.Setup.Copy("ssl/signer2.pem") -server.Setup.Copy("ssl/signed-foo.pem") -server.Setup.Copy("ssl/signed-foo.key") -server.Setup.Copy("ssl/signed2-foo.pem") -server.Setup.Copy("ssl/signed2-bar.pem") -server.Setup.Copy("ssl/signed-bar.key") -server2.Setup.Copy("ssl/signer.pem") -server2.Setup.Copy("ssl/signer2.pem") -server2.Setup.Copy("ssl/signed-foo.pem") -server2.Setup.Copy("ssl/signed-foo.key") -server2.Setup.Copy("ssl/signed2-foo.pem") -server2.Setup.Copy("ssl/signed2-bar.pem") -server2.Setup.Copy("ssl/signed-bar.key") - -request_header = {"headers": "GET / HTTP/1.1\r\nHost: example.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -server.addResponse("sessionlog.json", request_header, response_header) -request_header = {"headers": "GET / HTTP/1.1\r\nHost: bar.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -server.addResponse("sessionlog.json", request_header, response_header) - -ts.addSSLfile("ssl/server.pem") -ts.addSSLfile("ssl/server.key") -ts.addSSLfile("ssl/signed-foo.pem") -ts.addSSLfile("ssl/signed-foo.key") -ts.addSSLfile("ssl/signed2-foo.pem") -ts.addSSLfile("ssl/signed-bar.pem") -ts.addSSLfile("ssl/signed2-bar.pem") -ts.addSSLfile("ssl/signed-bar.key") - -ts.Disk.records_config.update( - { - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'ssl', - 'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir), - 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts.Variables.SSLDir), - 'proxy.config.ssl.client.cert.path': '{0}'.format(ts.Variables.SSLDir), - 'proxy.config.ssl.client.cert.filename': 'signed-foo.pem', - 'proxy.config.ssl.client.private_key.path': '{0}'.format(ts.Variables.SSLDir), - 'proxy.config.ssl.client.private_key.filename': 'signed-foo.key', - 'proxy.config.exec_thread.autoconfig.scale': 1.0, - 'proxy.config.url_remap.pristine_host_hdr': 1, - 'proxy.config.ssl.client.verify.server.policy': 'PERMISSIVE', - }) - -ts.Disk.ssl_multicert_yaml.AddLines( - """ -ssl_multicert: - - dest_ip: "*" - ssl_cert_name: server.pem - ssl_key_name: server.key -""".split("\n")) - -ts.Disk.remap_config.AddLine( - 'map /case1 https://127.0.0.1:{0}/ @plugin=conf_remap.so @pparam=proxy.config.ssl.client.cert.filename={1} plugin=conf_remap.so @pparam=proxy.config.ssl.client.private_key.filename={2}' - .format(server.Variables.SSL_Port, "signed-foo.pem", "signed-foo.key")) -ts.Disk.remap_config.AddLine( - 'map /badcase1 https://127.0.0.1:{0}/ @plugin=conf_remap.so @pparam=proxy.config.ssl.client.cert.filename={1} plugin=conf_remap.so @pparam=proxy.config.ssl.client.private_key.filename={2}' - .format(server.Variables.SSL_Port, "signed2-foo.pem", "signed-foo.key")) -ts.Disk.remap_config.AddLine( - 'map /case2 https://127.0.0.1:{0}/ @plugin=conf_remap.so @pparam=proxy.config.ssl.client.cert.filename={1} plugin=conf_remap.so @pparam=proxy.config.ssl.client.private_key.filename={2}' - .format(server2.Variables.SSL_Port, "signed2-foo.pem", "signed-foo.key")) -ts.Disk.remap_config.AddLine( - 'map /badcase2 https://127.0.0.1:{0}/ @plugin=conf_remap.so @pparam=proxy.config.ssl.client.cert.filename={1} plugin=conf_remap.so @pparam=proxy.config.ssl.client.private_key.filename={2}' - .format(server2.Variables.SSL_Port, "signed-foo.pem", "signed-foo.key")) - -# Should succeed -tr = Test.AddTestRun("Connect with correct client cert to first server") -tr.Processes.Default.StartBefore(Test.Processes.ts) -tr.Processes.Default.StartBefore(server) -tr.Processes.Default.StartBefore(server2) -tr.StillRunningAfter = ts -tr.StillRunningAfter = server -tr.StillRunningAfter = server2 -tr.MakeCurlCommand("-H host:example.com http://127.0.0.1:{0}/case1".format(ts.Variables.port), ts=ts) -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Streams.stdout = Testers.ExcludesExpression("Could Not Connect", "Check response") - -# Should fail -trfail = Test.AddTestRun("Connect with bad client cert to first server") -trfail.StillRunningAfter = ts -trfail.StillRunningAfter = server -trfail.StillRunningAfter = server2 -trfail.MakeCurlCommand('-H host:example.com http://127.0.0.1:{0}/badcase1'.format(ts.Variables.port), ts=ts) -trfail.Processes.Default.ReturnCode = 0 -trfail.Processes.Default.Streams.stdout = Testers.ContainsExpression("Could Not Connect", "Check response") - -# Should succeed -trbar = Test.AddTestRun("Connect with correct client cert to second server") -trbar.StillRunningAfter = ts -trbar.StillRunningAfter = server -trbar.StillRunningAfter = server2 -trbar.MakeCurlCommand("-H host:bar.com http://127.0.0.1:{0}/case2".format(ts.Variables.port), ts=ts) -trbar.Processes.Default.ReturnCode = 0 -trbar.Processes.Default.Streams.stdout = Testers.ExcludesExpression("Could Not Connect", "Check response") - -# Should fail -trbarfail = Test.AddTestRun("Connect with bad client cert to second server") -trbarfail.StillRunningAfter = ts -trbarfail.StillRunningAfter = server -trbarfail.StillRunningAfter = server2 -trbarfail.MakeCurlCommand('-H host:bar.com http://127.0.0.1:{0}/badcase2'.format(ts.Variables.port), ts=ts) -trbarfail.Processes.Default.ReturnCode = 0 -trbarfail.Processes.Default.Streams.stdout = Testers.ContainsExpression("Could Not Connect", "Check response") diff --git a/tests/gold_tests/tls/tls_client_cert_override_plugin.test.py b/tests/gold_tests/tls/tls_client_cert_override_plugin.test.py deleted file mode 100644 index 039fd10f02b..00000000000 --- a/tests/gold_tests/tls/tls_client_cert_override_plugin.test.py +++ /dev/null @@ -1,227 +0,0 @@ -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import subprocess -import os - -Test.Summary = ''' -Test conf_remp to specify different client certificates to offer to the origin. Loading certs/keys via plugin. -''' - -ts = Test.MakeATSProcess("ts") - -cafile = "{0}/signer.pem".format(Test.RunDirectory) -cafile2 = "{0}/signer2.pem".format(Test.RunDirectory) -server = Test.MakeOriginServer( - "server", - ssl=True, - options={ - "--clientCA": cafile, - "--clientverify": "" - }, - clientcert="{0}/signed-foo.pem".format(Test.RunDirectory), - clientkey="{0}/signed-foo.key".format(Test.RunDirectory)) -server2 = Test.MakeOriginServer( - "server2", - ssl=True, - options={ - "--clientCA": cafile2, - "--clientverify": "" - }, - clientcert="{0}/signed2-bar.pem".format(Test.RunDirectory), - clientkey="{0}/signed-bar.key".format(Test.RunDirectory)) -server3 = Test.MakeOriginServer("server3") -server.Setup.Copy("ssl/signer.pem") -server.Setup.Copy("ssl/signer2.pem") -server.Setup.Copy("ssl/signed-foo.pem") -server.Setup.Copy("ssl/signed-foo.key") -server.Setup.Copy("ssl/signed2-foo.pem") -server.Setup.Copy("ssl/signed2-bar.pem") -server.Setup.Copy("ssl/signed-bar.key") -server2.Setup.Copy("ssl/signer.pem") -server2.Setup.Copy("ssl/signer2.pem") -server2.Setup.Copy("ssl/signed-foo.pem") -server2.Setup.Copy("ssl/signed-foo.key") -server2.Setup.Copy("ssl/signed2-foo.pem") -server2.Setup.Copy("ssl/signed2-bar.pem") -server2.Setup.Copy("ssl/signed-bar.key") - -request_header = {"headers": "GET / HTTP/1.1\r\nHost: example.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -response_header = { - "headers": "HTTP/1.1 200 OK\r\nConnection: close\r\nCache-Control: no-cache\r\n\r\n", - "timestamp": "1469733493.993", - "body": "" -} -server.addResponse("sessionlog.json", request_header, response_header) -request_header = {"headers": "GET / HTTP/1.1\r\nHost: bar.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -response_header = { - "headers": "HTTP/1.1 200 OK\r\nCache-Control: no-cache\r\nConnection: close\r\n\r\n", - "timestamp": "1469733493.993", - "body": "" -} -server.addResponse("sessionlog.json", request_header, response_header) - -ts.addSSLfile("ssl/server.pem") -ts.addSSLfile("ssl/server.key") -ts.addSSLfile("ssl/signed-foo.pem") -ts.addSSLfile("ssl/signed-foo.key") -ts.addSSLfile("ssl/signed2-foo.pem") -ts.addSSLfile("ssl/signed-bar.pem") -ts.addSSLfile("ssl/signed2-bar.pem") -ts.addSSLfile("ssl/signed-bar.key") - -ts.Disk.sni_yaml.AddLine('sni:') -ts.Disk.sni_yaml.AddLine('- fqdn: random') -ts.Disk.sni_yaml.AddLine(' verify_server_properties: NONE') -snipath = ts.Disk.sni_yaml.AbsPath - -Test.PrepareTestPlugin(os.path.join(Test.Variables.AtsTestPluginsDir, 'ssl_secret_load_test.so'), ts) - -shortdir = ts.Variables.SSLDir[0:ts.Variables.SSLDir.rfind("/")] - -ts.Disk.records_config.update( - { - 'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir), - 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts.Variables.SSLDir), - 'proxy.config.ssl.client.verify.server.policy': 'PERMISSIVE', - 'proxy.config.ssl.client.cert.path': '{0}'.format(shortdir), - 'proxy.config.ssl.client.cert.filename': 'signed-foo.pem', - 'proxy.config.ssl.client.private_key.path': '{0}'.format(shortdir), - 'proxy.config.ssl.client.private_key.filename': 'signed-foo.key', - 'proxy.config.exec_thread.autoconfig.scale': 1.0, - 'proxy.config.url_remap.pristine_host_hdr': 1, - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'ssl_secret_load|http|ssl', - }) - -ts.Disk.ssl_multicert_yaml.AddLines( - """ -ssl_multicert: - - dest_ip: "*" - ssl_cert_name: server.pem - ssl_key_name: server.key -""".split("\n")) - -ts.Disk.remap_config.AddLine( - 'map /case1 https://127.0.0.1:{0}/ @plugin=conf_remap.so @pparam=proxy.config.ssl.client.cert.filename={1} plugin=conf_remap.so @pparam=proxy.config.ssl.client.private_key.filename={2}' - .format(server.Variables.SSL_Port, "signed-foo.pem", "signed-foo.key")) -ts.Disk.remap_config.AddLine( - 'map /badcase1 https://127.0.0.1:{0}/ @plugin=conf_remap.so @pparam=proxy.config.ssl.client.cert.filename={1} plugin=conf_remap.so @pparam=proxy.config.ssl.client.private_key.filename={2}' - .format(server.Variables.SSL_Port, "signed2-foo.pem", "signed-foo.key")) -ts.Disk.remap_config.AddLine( - 'map /case2 https://127.0.0.1:{0}/ @plugin=conf_remap.so @pparam=proxy.config.ssl.client.cert.filename={1} plugin=conf_remap.so @pparam=proxy.config.ssl.client.private_key.filename={2}' - .format(server2.Variables.SSL_Port, "signed2-foo.pem", "signed-foo.key")) -ts.Disk.remap_config.AddLine( - 'map /badcase2 https://127.0.0.1:{0}/ @plugin=conf_remap.so @pparam=proxy.config.ssl.client.cert.filename={1} plugin=conf_remap.so @pparam=proxy.config.ssl.client.private_key.filename={2}' - .format(server2.Variables.SSL_Port, "signed-foo.pem", "signed-foo.key")) - -# Should succeed -tr = Test.AddTestRun("Connect with correct client cert to first server") -tr.Processes.Default.StartBefore(Test.Processes.ts) -tr.Processes.Default.StartBefore(server) -tr.Processes.Default.StartBefore(server2) -tr.StillRunningAfter = ts -tr.StillRunningAfter = server -tr.StillRunningAfter = server2 -tr.MakeCurlCommand("-H host:example.com http://127.0.0.1:{0}/case1".format(ts.Variables.port), ts=ts) -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Streams.stdout = Testers.ExcludesExpression("Could Not Connect", "Check response") - -# Should fail -trfail = Test.AddTestRun("Connect with bad client cert to first server") -trfail.StillRunningAfter = ts -trfail.StillRunningAfter = server -trfail.StillRunningAfter = server2 -trfail.MakeCurlCommand('-H host:example.com http://127.0.0.1:{0}/badcase1'.format(ts.Variables.port), ts=ts) -trfail.Processes.Default.ReturnCode = 0 -trfail.Processes.Default.Streams.stdout = Testers.ContainsExpression("Could Not Connect", "Check response") - -# Should succeed -trbar = Test.AddTestRun("Connect with correct client cert to second server") -trbar.StillRunningAfter = ts -trbar.StillRunningAfter = server -trbar.StillRunningAfter = server2 -trbar.MakeCurlCommand("-H host:bar.com http://127.0.0.1:{0}/case2".format(ts.Variables.port), ts=ts) -trbar.Processes.Default.ReturnCode = 0 -trbar.Processes.Default.Streams.stdout = Testers.ExcludesExpression("Could Not Connect", "Check response") - -# Should fail -trbarfail = Test.AddTestRun("Connect with bad client cert to second server") -trbarfail.StillRunningAfter = ts -trbarfail.StillRunningAfter = server -trbarfail.StillRunningAfter = server2 -trbarfail.MakeCurlCommand('-H host:bar.com http://127.0.0.1:{0}/badcase2'.format(ts.Variables.port), ts=ts) -trbarfail.Processes.Default.ReturnCode = 0 -trbarfail.Processes.Default.Streams.stdout = Testers.ContainsExpression("Could Not Connect", "Check response") - -# Test the case of updating certificate contents without changing file name. -trupdate = Test.AddTestRun("Update client cert file in place") -trupdate.StillRunningAfter = ts -trupdate.StillRunningAfter = server -trupdate.StillRunningAfter = server2 -# in the config/ssl directory for records.yaml -trupdate.Setup.CopyAs("ssl/signed-foo.pem", ".", "{0}/signed2-foo.pem".format(ts.Variables.SSLDir)) -trupdate.Processes.Default.Command = 'traffic_ctl config set proxy.config.ssl.client.cert.path {0}/; touch {1}'.format( - shortdir, snipath) -# Need to copy over the environment so traffic_ctl knows where to find the unix domain socket -trupdate.Processes.Default.Env = ts.Env -trupdate.Processes.Default.ReturnCode = 0 - -tr2reload = Test.AddConfigReload(ts, expect_tasks=["sni.yaml"], description="Reload config") -tr2reload.StillRunningAfter = server -tr2reload.StillRunningAfter = server2 - -tr3bar = Test.AddTestRun("Make request with other foo. badcase1 should now work") -tr3bar.Processes.Default.StartBefore(server3) -tr3bar.StillRunningAfter = ts -tr3bar.StillRunningAfter = server -tr3bar.StillRunningAfter = server2 -tr3bar.MakeCurlCommand(' -H host:foo.com http://127.0.0.1:{0}/badcase1'.format(ts.Variables.port), ts=ts) -tr3bar.Processes.Default.ReturnCode = 0 -tr3bar.Processes.Default.Streams.stdout = Testers.ExcludesExpression("Could Not Connect", "Check response") - -# Test the hot-reload feature. Update cert file and wait about. Should update without reload -trupdate = Test.AddTestRun("Update signed-foo cert file in place") -trupdate.StillRunningAfter = ts -trupdate.StillRunningAfter = server -trupdate.Setup.CopyAs("ssl/signed2-foo.pem", ".", "{0}/signed-foo.pem".format(ts.Variables.SSLDir)) -# For some reason the Setup.CopyAs does not change the modification time, so we touch -trupdate.Processes.Default.Command = 'touch {0}/signed-foo.pem {0}/signed-foo.key'.format(ts.Variables.SSLDir) -# Need to copy over the environment so traffic_ctl knows where to find the unix domain socket -trupdate.Processes.Default.Env = ts.Env -trupdate.Processes.Default.ReturnCode = 0 - -# The plugin will pull every 3 seconds. So wait 4 seconds and test again. -# case1 should fail -# badcase1 should succeed -tr = Test.AddTestRun("Retest case1") -tr.DelayStart = 4 -tr.ReturnCode = 0 -tr.StillRunningAfter = server -tr.StillRunningAfter = server2 -tr.StillRunningAfter = ts -tr.MakeCurlCommand("-H host:example.com http://127.0.0.1:{0}/case1".format(ts.Variables.port), ts=ts) -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Streams.stdout = Testers.ContainsExpression("Could Not Connect", "Check response") - -tr = Test.AddTestRun("Retest badcase1") -tr.ReturnCode = 0 -tr.StillRunningAfter = server -tr.StillRunningAfter = server2 -tr.StillRunningAfter = ts -tr.MakeCurlCommand("-H host:example.com http://127.0.0.1:{0}/badcase1".format(ts.Variables.port), ts=ts) -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Streams.stdout = Testers.ExcludesExpression("Could Not Connect", "Check response") diff --git a/tests/gold_tests/tls/tls_client_cert_plugin.test.py b/tests/gold_tests/tls/tls_client_cert_plugin.test.py deleted file mode 100644 index 1e5d6f9913a..00000000000 --- a/tests/gold_tests/tls/tls_client_cert_plugin.test.py +++ /dev/null @@ -1,283 +0,0 @@ -''' -Test offering client cert to origin, but using plugin for cert loading -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import subprocess -import os - -Test.Summary = ''' -Test offering client cert to origin, but using plugin for cert loading -''' - -ts = Test.MakeATSProcess("ts") -cafile = "{0}/signer.pem".format(Test.RunDirectory) -cafile2 = "{0}/signer2.pem".format(Test.RunDirectory) -# --clientverify: "" empty string because microserver does store_true for argparse, but options is a dictionary -server = Test.MakeOriginServer( - "server", - ssl=True, - options={ - "--clientCA": cafile, - "--clientverify": "" - }, - clientcert="{0}/signed-foo.pem".format(Test.RunDirectory), - clientkey="{0}/signed-foo.key".format(Test.RunDirectory)) -server2 = Test.MakeOriginServer( - "server2", - ssl=True, - options={ - "--clientCA": cafile2, - "--clientverify": "" - }, - clientcert="{0}/signed2-bar.pem".format(Test.RunDirectory), - clientkey="{0}/signed-bar.key".format(Test.RunDirectory)) -server3 = Test.MakeOriginServer("server3") -server.Setup.Copy("ssl/signer.pem") -server.Setup.Copy("ssl/signer2.pem") -server.Setup.Copy("ssl/signed-foo.pem") -server.Setup.Copy("ssl/signed-foo.key") -server.Setup.Copy("ssl/signed2-foo.pem") -server.Setup.Copy("ssl/signed2-bar.pem") -server.Setup.Copy("ssl/signed-bar.key") -server2.Setup.Copy("ssl/signer.pem") -server2.Setup.Copy("ssl/signer2.pem") -server2.Setup.Copy("ssl/signed-foo.pem") -server2.Setup.Copy("ssl/signed-foo.key") -server2.Setup.Copy("ssl/signed2-foo.pem") -server2.Setup.Copy("ssl/signed2-bar.pem") -server2.Setup.Copy("ssl/signed-bar.key") - -request_header = {"headers": "GET / HTTP/1.1\r\nHost: example.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -server.addResponse("sessionlog.json", request_header, response_header) -request_header = {"headers": "GET / HTTP/1.1\r\nHost: bar.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -server.addResponse("sessionlog.json", request_header, response_header) - -# -# Certs and keys loaded into the ts/ssl directory, but the paths in the -# configs omit the ssl subdirectory. The ssl_secret_load_test plugin adds this back in -ts.addSSLfile("ssl/server.pem") -ts.addSSLfile("ssl/server.key") -ts.addSSLfile("ssl/signed-foo.pem") -ts.addSSLfile("ssl/signed-foo.key") -ts.addSSLfile("ssl/signed2-foo.pem") -ts.addSSLfile("ssl/signed-bar.pem") -ts.addSSLfile("ssl/signed2-bar.pem") -ts.addSSLfile("ssl/signed-bar.key") - -Test.PrepareTestPlugin(os.path.join(Test.Variables.AtsTestPluginsDir, 'ssl_secret_load_test.so'), ts) - -ts.Disk.records_config.update( - { - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'ssl_secret_load_test|ssl', - 'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir), - 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts.Variables.SSLDir), - 'proxy.config.ssl.client.verify.server.policy': 'PERMISSIVE', - 'proxy.config.ssl.client.cert.path': '{0}/../'.format(ts.Variables.SSLDir), - 'proxy.config.ssl.client.cert.filename': 'signed-foo.pem', - 'proxy.config.ssl.client.private_key.path': '{0}/../'.format(ts.Variables.SSLDir), - 'proxy.config.ssl.client.private_key.filename': 'signed-foo.key', - 'proxy.config.exec_thread.autoconfig.scale': 1.0, - 'proxy.config.url_remap.pristine_host_hdr': 1, - }) - -ts.Disk.ssl_multicert_yaml.AddLines( - """ -ssl_multicert: - - dest_ip: "*" - ssl_cert_name: server.pem - ssl_key_name: server.key -""".split("\n")) - -ts.Disk.remap_config.AddLine('map /case1 https://127.0.0.1:{0}/'.format(server.Variables.SSL_Port)) -ts.Disk.remap_config.AddLine('map /case2 https://127.0.0.1:{0}/'.format(server2.Variables.SSL_Port)) - -ts.Disk.sni_yaml.AddLine('sni:') -ts.Disk.sni_yaml.AddLine('- fqdn: bar.com') -ts.Disk.sni_yaml.AddLine(' client_cert: {0}/../signed2-bar.pem'.format(ts.Variables.SSLDir)) -ts.Disk.sni_yaml.AddLine(' client_key: {0}/../signed-bar.key'.format(ts.Variables.SSLDir)) - -# Should succeed -tr = Test.AddTestRun("Connect with first client cert to first server") -tr.Processes.Default.StartBefore(Test.Processes.ts) -tr.Processes.Default.StartBefore(server) -tr.Processes.Default.StartBefore(server2) -tr.StillRunningAfter = ts -tr.StillRunningAfter = server -tr.StillRunningAfter = server2 -tr.MakeCurlCommand("-H host:example.com http://127.0.0.1:{0}/case1".format(ts.Variables.port), ts=ts) -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Streams.stdout = Testers.ExcludesExpression("Could Not Connect", "Check response") - -# Should fail -trfail = Test.AddTestRun("Connect with first client cert to second server") -trfail.StillRunningAfter = ts -trfail.StillRunningAfter = server -trfail.StillRunningAfter = server2 -trfail.MakeCurlCommand('-H host:example.com http://127.0.0.1:{0}/case2'.format(ts.Variables.port), ts=ts) -trfail.Processes.Default.ReturnCode = 0 -trfail.Processes.Default.Streams.stdout = Testers.ContainsExpression("Could Not Connect", "Check response") - -# Should succeed -trbar = Test.AddTestRun("Connect with signed2 bar to second server") -trbar.StillRunningAfter = ts -trbar.StillRunningAfter = server -trbar.StillRunningAfter = server2 -trbar.MakeCurlCommand("-H host:bar.com http://127.0.0.1:{0}/case2".format(ts.Variables.port), ts=ts) -trbar.Processes.Default.ReturnCode = 0 -trbar.Processes.Default.Streams.stdout = Testers.ExcludesExpression("Could Not Connect", "Check response") - -# Should fail -trbarfail = Test.AddTestRun("Connect with signed2 bar cert to first server") -trbarfail.StillRunningAfter = ts -trbarfail.StillRunningAfter = server -trbarfail.StillRunningAfter = server2 -trbarfail.MakeCurlCommand('-H host:bar.com http://127.0.0.1:{0}/case1'.format(ts.Variables.port), ts=ts) -trbarfail.Processes.Default.ReturnCode = 0 -trbarfail.Processes.Default.Streams.stdout = Testers.ContainsExpression("Could Not Connect", "Check response") - -tr2 = Test.AddTestRun("Update config files") -# Update the SNI config -snipath = ts.Disk.sni_yaml.AbsPath -recordspath = ts.Disk.records_config.AbsPath -tr2.Disk.File(snipath, id="sni_yaml", typename="ats:config"), -tr2.Disk.sni_yaml.AddLine('sni:') -tr2.Disk.sni_yaml.AddLine('- fqdn: bar.com') -tr2.Disk.sni_yaml.AddLine(' client_cert: {0}/../signed-bar.pem'.format(ts.Variables.SSLDir)) -tr2.Disk.sni_yaml.AddLine(' client_key: {0}/../signed-bar.key'.format(ts.Variables.SSLDir)) -# recreate the records.yaml with the cert filename changed -tr2.Disk.File(recordspath, id="records_config", typename="ats:config:records"), -tr2.Disk.records_config.update( - { - 'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir), - 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts.Variables.SSLDir), - 'proxy.config.ssl.client.verify.server.policy': 'PERMISSIVE', - 'proxy.config.ssl.client.cert.path': '{0}/../'.format(ts.Variables.SSLDir), - 'proxy.config.ssl.client.cert.filename': 'signed2-foo.pem', - 'proxy.config.ssl.client.private_key.path': '{0}/../'.format(ts.Variables.SSLDir), - 'proxy.config.ssl.client.private_key.filename': 'signed-foo.key', - 'proxy.config.url_remap.pristine_host_hdr': 1, - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'ssl_secret_load_test|ssl', - }) -tr2.StillRunningAfter = ts -tr2.StillRunningAfter = server -tr2.StillRunningAfter = server2 -tr2.Processes.Default.Command = 'echo Updated configs' -# Need to copy over the environment so traffic_ctl knows where to find the unix domain socket -tr2.Processes.Default.Env = ts.Env -tr2.Processes.Default.ReturnCode = 0 - -tr2reload = Test.AddConfigReload(ts, expect_tasks=["sni.yaml"], description="Reload config") -tr2reload.StillRunningAfter = server -tr2reload.StillRunningAfter = server2 - -# Should succeed -tr3bar = Test.AddTestRun("Make request with other bar cert to first server") -tr3bar.Processes.Default.StartBefore(server3) -tr3bar.StillRunningAfter = ts -tr3bar.StillRunningAfter = server -tr3bar.StillRunningAfter = server2 -tr3bar.MakeCurlCommand(' -H host:bar.com http://127.0.0.1:{0}/case1'.format(ts.Variables.port), ts=ts) -tr3bar.Processes.Default.ReturnCode = 0 -tr3bar.Processes.Default.Streams.stdout = Testers.ExcludesExpression("Could Not Connect", "Check response") - -# Should fail -tr3barfail = Test.AddTestRun("Make request with other bar cert to second server") -tr3barfail.StillRunningAfter = ts -tr3barfail.StillRunningAfter = server -tr3barfail.StillRunningAfter = server2 -tr3barfail.MakeCurlCommand(' -H host:bar.com http://127.0.0.1:{0}/case2'.format(ts.Variables.port), ts=ts) -tr3barfail.Processes.Default.ReturnCode = 0 -tr3barfail.Processes.Default.Streams.stdout = Testers.ContainsExpression("Could Not Connect", "Check response") - -# Should succeed -tr3 = Test.AddTestRun("Make request with other cert to second server") -# Wait for the reload to complete -tr3.StillRunningAfter = ts -tr3.StillRunningAfter = server -tr3.StillRunningAfter = server2 -tr3.MakeCurlCommand(' -H host:example.com http://127.0.0.1:{0}/case2'.format(ts.Variables.port), ts=ts) -tr3.Processes.Default.ReturnCode = 0 -tr3.Processes.Default.Streams.stdout = Testers.ExcludesExpression("Could Not Connect", "Check response") - -# Should fail -tr3fail = Test.AddTestRun("Make request with other cert to first server") -tr3fail.StillRunningAfter = ts -tr3fail.StillRunningAfter = server -tr3fail.StillRunningAfter = server2 -tr3fail.MakeCurlCommand(' -H host:example.com http://127.0.0.1:{0}/case1'.format(ts.Variables.port), ts=ts) -tr3fail.Processes.Default.ReturnCode = 0 -tr3fail.Processes.Default.Streams.stdout = Testers.ContainsExpression("Could Not Connect", "Check response") - -# Test the case of updating certificate contents without changing file name. -trupdate = Test.AddTestRun("Update client cert file in place") -trupdate.StillRunningAfter = ts -trupdate.StillRunningAfter = server -trupdate.StillRunningAfter = server2 -# Make a meaningless config change on the path so the records.yaml reload logic will trigger -trupdate.Setup.CopyAs("ssl/signed2-bar.pem", ".", "{0}/signed-bar.pem".format(ts.Variables.SSLDir)) -# in the config/ssl directory for records.yaml -trupdate.Setup.CopyAs("ssl/signed-foo.pem", ".", "{0}/signed2-foo.pem".format(ts.Variables.SSLDir)) -trupdate.Processes.Default.Command = 'traffic_ctl config set proxy.config.ssl.client.cert.path {0}/../; touch {1}; touch {2}'.format( - ts.Variables.SSLDir, snipath, recordspath) -# Need to copy over the environment so traffic_ctl knows where to find the unix domain socket -trupdate.Processes.Default.Env = ts.Env -trupdate.Processes.Default.ReturnCode = 0 - -trreload = Test.AddConfigReload(ts, expect_tasks=["sni.yaml"], description="Reload config after renaming certs") -trreload.StillRunningAfter = server -trreload.StillRunningAfter = server2 - -# Should succeed -tr4bar = Test.AddTestRun("Make request with renamed bar cert to second server") -tr4bar.StillRunningAfter = ts -tr4bar.StillRunningAfter = server -tr4bar.StillRunningAfter = server2 -tr4bar.MakeCurlCommand(' -H host:bar.com http://127.0.0.1:{0}/case2'.format(ts.Variables.port), ts=ts) -tr4bar.Processes.Default.ReturnCode = 0 -tr4bar.Processes.Default.Streams.stdout = Testers.ExcludesExpression("Could Not Connect", "Check response") - -# Should fail -tr4barfail = Test.AddTestRun("Make request with renamed bar cert to first server") -tr4barfail.StillRunningAfter = ts -tr4barfail.StillRunningAfter = server -tr4barfail.StillRunningAfter = server2 -tr4barfail.MakeCurlCommand(' -H host:bar.com http://127.0.0.1:{0}/case1'.format(ts.Variables.port), ts=ts) -tr4barfail.Processes.Default.ReturnCode = 0 -tr4barfail.Processes.Default.Streams.stdout = Testers.ContainsExpression("Could Not Connect", "Check response") - -# Should succeed -tr4 = Test.AddTestRun("Make request with renamed foo cert to first server") -tr4.StillRunningAfter = ts -tr4.StillRunningAfter = server -tr4.StillRunningAfter = server2 -tr4.MakeCurlCommand(' -H host:example.com http://127.0.0.1:{0}/case1'.format(ts.Variables.port), ts=ts) -tr4.Processes.Default.ReturnCode = 0 -tr4.Processes.Default.Streams.stdout = Testers.ExcludesExpression("Could Not Connect", "Check response") - -# Should fail -tr4fail = Test.AddTestRun("Make request with renamed foo cert to second server") -tr4fail.StillRunningAfter = ts -tr4fail.StillRunningAfter = server -tr4fail.StillRunningAfter = server2 -tr4fail.MakeCurlCommand(' -H host:example.com http://127.0.0.1:{0}/case2'.format(ts.Variables.port), ts=ts) -tr4fail.Processes.Default.ReturnCode = 0 -tr4fail.Processes.Default.Streams.stdout = Testers.ContainsExpression("Could Not Connect", "Check response") diff --git a/tests/gold_tests/tls/tls_client_verify.test.py b/tests/gold_tests/tls/tls_client_verify.test.py deleted file mode 100644 index 710a2353eae..00000000000 --- a/tests/gold_tests/tls/tls_client_verify.test.py +++ /dev/null @@ -1,240 +0,0 @@ -''' -Test requiring certificate from user agent -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -Test.Summary = ''' -Test various options for requiring certificate from client for mutual authentication TLS -''' - -ts = Test.MakeATSProcess("ts", enable_tls=True) -cafile = "{0}/signer.pem".format(Test.RunDirectory) -cafile2 = "{0}/signer2.pem".format(Test.RunDirectory) -server = Test.MakeOriginServer("server") -server2 = Test.MakeOriginServer("server2") - -request_header = {"headers": "GET / HTTP/1.1\r\nHost: example.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -server.addResponse("sessionlog.json", request_header, response_header) -request_header = {"headers": "GET / HTTP/1.1\r\nHost: bar.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -server.addResponse("sessionlog.json", request_header, response_header) - -ts.addSSLfile("ssl/server.pem") -ts.addSSLfile("ssl/server.key") -ts.addSSLfile("ssl/signer.pem") - -ts.Disk.records_config.update( - { - 'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir), - 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts.Variables.SSLDir), - 'proxy.config.url_remap.pristine_host_hdr': 1, - 'proxy.config.ssl.client.certification_level': 2, - 'proxy.config.ssl.CA.cert.filename': '{0}/signer.pem'.format(ts.Variables.SSLDir), - 'proxy.config.exec_thread.autoconfig.scale': 1.0, - 'proxy.config.ssl.TLSv1_3.enabled': 0 - }) - -ts.Disk.ssl_multicert_yaml.AddLines( - """ -ssl_multicert: - - dest_ip: "*" - ssl_cert_name: server.pem - ssl_key_name: server.key -""".split("\n")) - -# Just map everything through to origin. This test is concentrating on the user-agent side -ts.Disk.remap_config.AddLine('map / http://127.0.0.1:{0}/'.format(server.Variables.Port)) - -# Scenario 1: Default no client cert required. cert required for bar.com -ts.Disk.sni_yaml.AddLines( - [ - 'sni:', - '- fqdn: bob.bar.com', - ' verify_client: NONE', - '- fqdn: "bob.com"', - ' verify_client: STRICT', - '- fqdn: "*.foo.com"', - ' verify_client: NONE', - '- fqdn: "*.bar.com"', - ' verify_client: STRICT', - ]) - -ts.Disk.logging_yaml.AddLines( - ''' -logging: - formats: - - name: testformat - format: '% % % %' - logs: - - mode: ascii - format: testformat - filename: squid -'''.split("\n")) - -# to foo.com w/o client cert. Should fail -tr = Test.AddTestRun("Connect to foo.com without cert") -tr.Processes.Default.StartBefore(Test.Processes.ts) -tr.Processes.Default.StartBefore(server) -tr.StillRunningAfter = ts -tr.StillRunningAfter = server -tr.MakeCurlCommand( - "--tls-max 1.2 -k --resolve 'foo.com:{0}:127.0.0.1' https://foo.com:{0}/case1".format(ts.Variables.ssl_port), ts=ts) -tr.Processes.Default.ReturnCode = 35 - -tr = Test.AddTestRun("Connect to foo.com with bad cert") -tr.Setup.Copy("ssl/server.pem") -tr.Setup.Copy("ssl/server.key") -tr.StillRunningAfter = ts -tr.StillRunningAfter = server -tr.MakeCurlCommand( - "--tls-max 1.2 -k --cert ./server.pem --key ./server.key --resolve 'foo.com:{0}:127.0.0.1' https://foo.com:{0}/case2".format( - ts.Variables.ssl_port), - ts=ts) -# Should fail with badly signed certs -tr.Processes.Default.ReturnCode = 35 - -tr = Test.AddTestRun("Connect to foo.com with cert") -tr.Setup.Copy("ssl/signed-foo.pem") -tr.Setup.Copy("ssl/signed-foo.key") -tr.StillRunningAfter = ts -tr.StillRunningAfter = server -tr.MakeCurlCommand( - "--tls-max 1.2 -k --cert ./signed-foo.pem --key ./signed-foo.key --resolve 'foo.com:{0}:127.0.0.1' https://foo.com:{0}/case3" - .format(ts.Variables.ssl_port), - ts=ts) -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Streams.All = Testers.ExcludesExpression("error", "Check response") - -tr = Test.AddTestRun("Connect to bob.bar.com without cert") -tr.StillRunningAfter = ts -tr.StillRunningAfter = server -tr.MakeCurlCommand( - "--tls-max 1.2 -k --resolve 'bob.bar.com:{0}:127.0.0.1' https://bob.bar.com:{0}/case4".format(ts.Variables.ssl_port), ts=ts) -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Streams.All = Testers.ExcludesExpression("alert", "TLS handshake should succeed") - -tr = Test.AddTestRun("Connect to bob.bar.com with cert") -tr.Setup.Copy("ssl/signed-bob-bar.pem") -tr.Setup.Copy("ssl/signed-bar.key") -tr.StillRunningAfter = ts -tr.StillRunningAfter = server -tr.MakeCurlCommand( - "--tls-max 1.2 -k --cert ./signed-bob-bar.pem --key ./signed-bar.key --resolve 'bob.bar.com:{0}:127.0.0.1' https://bob.bar.com:{0}/case5" - .format(ts.Variables.ssl_port), - ts=ts) -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Streams.All = Testers.ExcludesExpression("error", "Check response") - -tr = Test.AddTestRun("Connect to bob.bar.com with bad cert") -tr.Setup.Copy("ssl/server.pem") -tr.Setup.Copy("ssl/server.key") -tr.StillRunningAfter = ts -tr.StillRunningAfter = server -tr.MakeCurlCommand( - "--tls-max 1.2 -k --cert ./server.pem --key ./server.key --resolve 'bob.bar.com:{0}:127.0.0.1' https://bob.bar.com:{0}/case6" - .format(ts.Variables.ssl_port), - ts=ts) -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Streams.All = Testers.ExcludesExpression("error", "Check response") - -tr = Test.AddTestRun("Connect to bob.foo.com without cert") -tr.StillRunningAfter = ts -tr.StillRunningAfter = server -tr.MakeCurlCommand( - "--tls-max 1.2 -k --resolve 'bob.foo.com:{0}:127.0.0.1' https://bob.foo.com:{0}/case7".format(ts.Variables.ssl_port), ts=ts) -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Streams.All = Testers.ExcludesExpression("alert", "TLS handshake should succeed") - -tr = Test.AddTestRun("Connect to bob.foo.com with cert") -tr.Setup.Copy("ssl/signed-bob-foo.pem") -tr.Setup.Copy("ssl/signed-foo.key") -tr.StillRunningAfter = ts -tr.StillRunningAfter = server -tr.MakeCurlCommand( - "--tls-max 1.2 -k --cert ./signed-bob-foo.pem --key ./signed-foo.key --resolve 'bob.foo.com:{0}:127.0.0.1' https://bob.foo.com:{0}/case8" - .format(ts.Variables.ssl_port), - ts=ts) -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Streams.All = Testers.ExcludesExpression("error", "Check response") - -tr = Test.AddTestRun("Connect to bob.foo.com with bad cert") -tr.Setup.Copy("ssl/server.pem") -tr.Setup.Copy("ssl/server.key") -tr.StillRunningAfter = ts -tr.StillRunningAfter = server -tr.MakeCurlCommand( - "--tls-max 1.2 -k --cert ./server.pem --key ./server.key --resolve 'bob.foo.com:{0}:127.0.0.1' https://bob.foo.com:{0}/case9" - .format(ts.Variables.ssl_port), - ts=ts) -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Streams.All = Testers.ExcludesExpression("error", "Check response") - -tr = Test.AddTestRun("Connect to bar.com without cert") -tr.StillRunningAfter = ts -tr.StillRunningAfter = server -tr.MakeCurlCommand( - "--tls-max 1.2 -k --resolve 'bar.com:{0}:127.0.0.1' https://bar.com:{0}/case10".format(ts.Variables.ssl_port), ts=ts) -tr.Processes.Default.ReturnCode = 35 - -tr = Test.AddTestRun("Connect to bar.com with cert") -tr.Setup.Copy("ssl/signed-bar.pem") -tr.Setup.Copy("ssl/signed-bar.key") -tr.StillRunningAfter = ts -tr.StillRunningAfter = server -tr.MakeCurlCommand( - "--tls-max 1.2 -k --cert ./signed-bar.pem --key ./signed-bar.key --resolve 'bar.com:{0}:127.0.0.1' https://bar.com:{0}/case11" - .format(ts.Variables.ssl_port), - ts=ts) -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Streams.All = Testers.ExcludesExpression("error", "TLS handshake should succeed") - -tr = Test.AddTestRun("Connect to bar.com with bad cert") -tr.Setup.Copy("ssl/server.pem") -tr.Setup.Copy("ssl/server.key") -tr.StillRunningAfter = ts -tr.StillRunningAfter = server -tr.MakeCurlCommand( - "--tls-max 1.2 -k --cert ./server.pem --key ./server.key --resolve 'bar.com:{0}:127.0.0.1' https://bar.com:{0}/case12".format( - ts.Variables.ssl_port), - ts=ts) -tr.Processes.Default.ReturnCode = 35 - -# Test that the fqdn's match completely. bob.com should require client certificate. bob.com.com should not -tr = Test.AddTestRun("Connect to bob.com without cert, should fail") -tr.StillRunningAfter = ts -tr.StillRunningAfter = server -tr.MakeCurlCommand( - "--tls-max 1.2 -k --resolve 'bob.com:{0}:127.0.0.1' https://bob.com:{0}/case13".format(ts.Variables.ssl_port), ts=ts) -tr.Processes.Default.ReturnCode = 35 - -tr = Test.AddTestRun("Connect to bob.foo.com without cert, should succeed") -tr.StillRunningAfter = ts -tr.StillRunningAfter = server -tr.MakeCurlCommand( - "--tls-max 1.2 -k --resolve 'bob.foo.com:{0}:127.0.0.1' https://bob.foo.com:{0}/case14".format(ts.Variables.ssl_port), ts=ts) -tr.Processes.Default.ReturnCode = 0 - -tr = Test.AddTestRun("Wait for the access log to write out") -tr.Processes.Default.StartBefore(server2, ready=When.FileExists(ts.Disk.squid_log)) -tr.StillRunningAfter = ts -tr.StillRunningAfter = server -tr.StillRunningAfter = server2 -tr.Processes.Default.Command = 'echo "Log file exists"' -tr.Processes.Default.ReturnCode = 0 - -ts.Disk.squid_log.Content = "gold/clientcert-accesslog.gold" diff --git a/tests/gold_tests/tls/tls_client_verify2.test.py b/tests/gold_tests/tls/tls_client_verify2.test.py deleted file mode 100644 index d8b00d36d93..00000000000 --- a/tests/gold_tests/tls/tls_client_verify2.test.py +++ /dev/null @@ -1,187 +0,0 @@ -''' -Test requiring certificate from user agent -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -Test.Summary = ''' -Test various options for requiring certificate from client for mutual authentication TLS -''' - -ts = Test.MakeATSProcess("ts", enable_tls=True) -cafile = "{0}/signer.pem".format(Test.RunDirectory) -cafile2 = "{0}/signer2.pem".format(Test.RunDirectory) -server = Test.MakeOriginServer("server") - -request_header = {"headers": "GET / HTTP/1.1\r\nHost: example.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -server.addResponse("sessionlog.json", request_header, response_header) -request_header = {"headers": "GET / HTTP/1.1\r\nHost: bar.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -server.addResponse("sessionlog.json", request_header, response_header) - -ts.addSSLfile("ssl/server.pem") -ts.addSSLfile("ssl/server.key") -ts.addSSLfile("ssl/signer.pem") - -ts.Disk.records_config.update( - { - 'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir), - 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts.Variables.SSLDir), - 'proxy.config.url_remap.pristine_host_hdr': 1, - 'proxy.config.ssl.client.certification_level': 0, - 'proxy.config.ssl.CA.cert.path': '', - 'proxy.config.exec_thread.autoconfig.scale': 1.0, - 'proxy.config.ssl.CA.cert.filename': '{0}/signer.pem'.format(ts.Variables.SSLDir) - }) - -ts.Disk.ssl_multicert_yaml.AddLines( - """ -ssl_multicert: - - dest_ip: "*" - ssl_cert_name: server.pem - ssl_key_name: server.key -""".split("\n")) - -# Just map everything through to origin. This test is concentrating on the user-agent side -ts.Disk.remap_config.AddLine('map / http://127.0.0.1:{0}/'.format(server.Variables.Port)) - -# Scenario 1: Default no client cert required. cert required for bar.com -ts.Disk.sni_yaml.AddLines( - [ - 'sni:', - '- fqdn: bob.bar.com', - ' verify_client: STRICT', - '- fqdn: "*.foo.com"', - ' verify_client: STRICT', - '- fqdn: "*.bar.com"', - ' verify_client: NONE', - ]) - -# to foo.com w/o client cert. Should succeed -tr = Test.AddTestRun("Connect to foo.com without cert") -tr.Processes.Default.StartBefore(Test.Processes.ts) -tr.Processes.Default.StartBefore(server) -tr.StillRunningAfter = ts -tr.StillRunningAfter = server -tr.MakeCurlCommand( - "--tls-max 1.2 -k --resolve 'foo.com:{0}:127.0.0.1' https://foo.com:{0}/case1".format(ts.Variables.ssl_port), ts=ts) -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Streams.All = Testers.ExcludesExpression("error", "Check response") - -tr = Test.AddTestRun("Connect to foo.com with cert") -tr.Setup.Copy("ssl/signed-foo.pem") -tr.Setup.Copy("ssl/signed-foo.key") -tr.StillRunningAfter = ts -tr.StillRunningAfter = server -tr.MakeCurlCommand( - "--tls-max 1.2 -k --cert ./signed-foo.pem --key ./signed-foo.key --resolve 'foo.com:{0}:127.0.0.1' https://foo.com:{0}/case1" - .format(ts.Variables.ssl_port), - ts=ts) -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Streams.All = Testers.ExcludesExpression("error", "Check response") - -tr = Test.AddTestRun("Connect to bob.bar.com without cert") -tr.StillRunningAfter = ts -tr.StillRunningAfter = server -tr.MakeCurlCommand( - "--tls-max 1.2 -k --resolve 'bob.bar.com:{0}:127.0.0.1' https://bob.bar.com:{0}/case1".format(ts.Variables.ssl_port), ts=ts) -tr.Processes.Default.ReturnCode = 35 - -tr = Test.AddTestRun("Connect to bob.bar.com with cert") -tr.Setup.Copy("ssl/signed-bob-bar.pem") -tr.Setup.Copy("ssl/signed-bar.key") -tr.StillRunningAfter = ts -tr.StillRunningAfter = server -tr.MakeCurlCommand( - "--tls-max 1.2 -k --cert ./signed-bob-bar.pem --key ./signed-bar.key --resolve 'bob.bar.com:{0}:127.0.0.1' https://bob.bar.com:{0}/case1" - .format(ts.Variables.ssl_port), - ts=ts) -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Streams.All = Testers.ExcludesExpression("error", "TLS handshake should succeed") - -tr = Test.AddTestRun("Connect to bob.bar.com with bad cert") -tr.Setup.Copy("ssl/server.pem") -tr.Setup.Copy("ssl/server.key") -tr.StillRunningAfter = ts -tr.StillRunningAfter = server -tr.MakeCurlCommand( - "--tls-max 1.2 -k --cert ./server.pem --key ./server.key --resolve 'bob.bar.com:{0}:127.0.0.1' https://bob.bar.com:{0}/case1" - .format(ts.Variables.ssl_port), - ts=ts) -tr.Processes.Default.ReturnCode = 35 - -tr = Test.AddTestRun("Connect to bob.foo.com without cert") -tr.StillRunningAfter = ts -tr.StillRunningAfter = server -tr.MakeCurlCommand( - "--tls-max 1.2 -k --resolve 'bob.foo.com:{0}:127.0.0.1' https://bob.foo.com:{0}/case1".format(ts.Variables.ssl_port), ts=ts) -tr.Processes.Default.ReturnCode = 35 - -tr = Test.AddTestRun("Connect to bob.foo.com with cert") -tr.Setup.Copy("ssl/signed-bob-foo.pem") -tr.Setup.Copy("ssl/signed-foo.key") -tr.StillRunningAfter = ts -tr.StillRunningAfter = server -tr.MakeCurlCommand( - "--tls-max 1.2 -k --cert ./signed-bob-foo.pem --key ./signed-foo.key --resolve 'bob.foo.com:{0}:127.0.0.1' https://bob.foo.com:{0}/case1" - .format(ts.Variables.ssl_port), - ts=ts) -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Streams.All = Testers.ExcludesExpression("error", "TLS handshake should succeed") - -tr = Test.AddTestRun("Connect to bob.foo.com with bad cert") -tr.Setup.Copy("ssl/server.pem") -tr.Setup.Copy("ssl/server.key") -tr.StillRunningAfter = ts -tr.StillRunningAfter = server -tr.MakeCurlCommand( - "--tls-max 1.2 -k --cert ./server.pem --key ./server.key --resolve 'bob.foo.com:{0}:127.0.0.1' https://bob.foo.com:{0}/case1" - .format(ts.Variables.ssl_port), - ts=ts) -tr.Processes.Default.ReturnCode = 35 - -tr = Test.AddTestRun("Connect to bar.com without cert") -tr.StillRunningAfter = ts -tr.StillRunningAfter = server -tr.MakeCurlCommand( - "--tls-max 1.2 -k --resolve 'bar.com:{0}:127.0.0.1' https://bar.com:{0}/case1".format(ts.Variables.ssl_port), ts=ts) -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Streams.All = Testers.ExcludesExpression("alert", "TLS handshake should succeed") - -tr = Test.AddTestRun("Connect to bar.com with cert") -tr.Setup.Copy("ssl/signed-bar.pem") -tr.Setup.Copy("ssl/signed-bar.key") -tr.StillRunningAfter = ts -tr.StillRunningAfter = server -tr.MakeCurlCommand( - "--tls-max 1.2 -k --cert ./signed-bar.pem --key ./signed-bar.key --resolve 'bar.com:{0}:127.0.0.1' https://bar.com:{0}/case1" - .format(ts.Variables.ssl_port), - ts=ts) -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Streams.All = Testers.ExcludesExpression("error", "Check response") - -tr = Test.AddTestRun("Connect to bar.com with bad cert") -tr.Setup.Copy("ssl/server.pem") -tr.Setup.Copy("ssl/server.key") -tr.StillRunningAfter = ts -tr.StillRunningAfter = server -tr.MakeCurlCommand( - "--tls-max 1.2 -k --cert ./server.pem --key ./server.key --resolve 'bar.com:{0}:127.0.0.1' https://bar.com:{0}/case1".format( - ts.Variables.ssl_port), - ts=ts) -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Streams.All = Testers.ExcludesExpression("alert unknown ca", "TLS handshake should succeed") diff --git a/tests/gold_tests/tls/tls_client_verify3.test.py b/tests/gold_tests/tls/tls_client_verify3.test.py deleted file mode 100644 index acda0e1cb2e..00000000000 --- a/tests/gold_tests/tls/tls_client_verify3.test.py +++ /dev/null @@ -1,146 +0,0 @@ -''' -Test per SNI server name selection of CA certs for validating cert sent by client. -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -Test.Summary = ''' -Test per SNI server name selection of CA certs for validating cert sent by client. -''' - -ts = Test.MakeATSProcess("ts", enable_tls=True) - -server = Test.MakeOriginServer("server") - -request_header = {"headers": "GET /xyz HTTP/1.1\r\nHost: example.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": "yadayadayada"} -server.addResponse("sessionlog.json", request_header, response_header) - -ts.addSSLfile("ssl/server.pem") -ts.addSSLfile("ssl/server.key") - -ts.Setup.Copy("ssl/bbb-ca.pem", ts.Variables.CONFIGDIR) -ts.Setup.Copy("ssl/bbb-signed.key", ts.Variables.SSLDir) -ts.Setup.Copy("ssl/bbb-signed.pem", ts.Variables.SSLDir) -ts.Setup.Copy("ssl/aaa-ca.pem", ts.Variables.SSLDir) -ts.Setup.Copy("ssl/ccc-ca.pem", ts.Variables.SSLDir) - -ts.Disk.records_config.update( - { - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'ssl', - 'proxy.config.ssl.server.cert.path': ts.Variables.SSLDir, - 'proxy.config.ssl.server.private_key.path': ts.Variables.SSLDir, - 'proxy.config.url_remap.pristine_host_hdr': 1, - 'proxy.config.ssl.client.certification_level': 2, - 'proxy.config.ssl.CA.cert.filename': f'{ts.Variables.SSLDir}/aaa-ca.pem', - 'proxy.config.ssl.TLSv1_3.enabled': 0 - }) - -ts.Disk.ssl_multicert_yaml.AddLines( - """ -ssl_multicert: - - ssl_cert_name: bbb-signed.pem - ssl_key_name: bbb-signed.key -""".split("\n")) -ts.Disk.ssl_multicert_yaml.AddLines( - """ -ssl_multicert: - - dest_ip: "*" - ssl_cert_name: server.pem - ssl_key_name: server.key -""".split("\n")) - -# Just map everything through to origin. This test is concentrating on the user-agent side. -ts.Disk.remap_config.AddLine(f'map / http://127.0.0.1:{server.Variables.Port}/') - -ts.Disk.sni_yaml.AddLines( - [ - 'sni:', - '- fqdn: bbb.com', - ' verify_client: STRICT', - ' verify_client_ca_certs: bbb-ca.pem', - '- fqdn: bbb-signed', - ' verify_client: STRICT', - ' verify_client_ca_certs: bbb-ca.pem', - '- fqdn: ccc.com', - ' verify_client: STRICT', - ' verify_client_ca_certs:', - f' file: {ts.Variables.SSLDir}/ccc-ca.pem', - ]) - -# Success test runs. - -tr = Test.AddTestRun() -tr.Processes.Default.StartBefore(Test.Processes.ts) -tr.Processes.Default.StartBefore(server) -tr.StillRunningAfter = ts -tr.StillRunningAfter = server -tr.MakeCurlCommand( - ("-v -k --tls-max 1.2 --cert {1}.pem --key {1}.key --resolve 'aaa.com:{0}:127.0.0.1'" + " https://aaa.com:{0}/xyz").format( - ts.Variables.ssl_port, Test.TestDirectory + "/ssl/aaa-signed"), - ts=ts) -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Streams.All = Testers.ExcludesExpression("error", "Check response") - -tr = Test.AddTestRun() -tr.StillRunningAfter = ts -tr.StillRunningAfter = server -tr.MakeCurlCommand( - ("-v -k --tls-max 1.2 --cert {1}.pem --key {1}.key --resolve 'bbb-signed:{0}:127.0.0.1'" + - " https://bbb-signed:{0}/xyz").format(ts.Variables.ssl_port, Test.TestDirectory + "/ssl/bbb-signed"), - ts=ts) -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Streams.All = Testers.ExcludesExpression("error", "Check response") - -tr = Test.AddTestRun() -tr.StillRunningAfter = ts -tr.StillRunningAfter = server -tr.MakeCurlCommand( - ("-v -k --tls-max 1.2 --cert {1}.pem --key {1}.key --resolve 'ccc.com:{0}:127.0.0.1'" + " https://ccc.com:{0}/xyz").format( - ts.Variables.ssl_port, Test.TestDirectory + "/ssl/ccc-signed"), - ts=ts) -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Streams.All = Testers.ExcludesExpression("error", "Check response") - -# Failure test runs. - -tr = Test.AddTestRun() -tr.StillRunningAfter = ts -tr.StillRunningAfter = server -tr.MakeCurlCommand( - ("-v -k --tls-max 1.2 --cert {1}.pem --key {1}.key --resolve 'aaa.com:{0}:127.0.0.1'" + " https://aaa.com:{0}/xyz").format( - ts.Variables.ssl_port, Test.TestDirectory + "/ssl/bbb-signed"), - ts=ts) -tr.Processes.Default.ReturnCode = 35 - -tr = Test.AddTestRun() -tr.StillRunningAfter = ts -tr.StillRunningAfter = server -tr.MakeCurlCommand( - ("-v -k --tls-max 1.2 --cert {1}.pem --key {1}.key --resolve 'bbb.com:{0}:127.0.0.1'" + " https://bbb.com:{0}/xyz").format( - ts.Variables.ssl_port, Test.TestDirectory + "/ssl/ccc-signed"), - ts=ts) -tr.Processes.Default.ReturnCode = 35 - -tr = Test.AddTestRun() -tr.StillRunningAfter = ts -tr.StillRunningAfter = server -tr.MakeCurlCommand( - (" -v -k --tls-max 1.2 --cert {1}.pem --key {1}.key --resolve 'ccc.com:{0}:127.0.0.1'" + " https://ccc.com:{0}/xyz").format( - ts.Variables.ssl_port, Test.TestDirectory + "/ssl/aaa-signed"), - ts=ts) -tr.Processes.Default.ReturnCode = 35 diff --git a/tests/gold_tests/tls/tls_client_versions.test.py b/tests/gold_tests/tls/tls_client_versions.test.py deleted file mode 100644 index 2aed6bc2c40..00000000000 --- a/tests/gold_tests/tls/tls_client_versions.test.py +++ /dev/null @@ -1,121 +0,0 @@ -''' -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -Test.Summary = ''' -Test TLS protocol offering based on SNI -''' - -# By default only offer TLSv1_2 -# for special domain foo.com only offer TLSv1 and TLSv1_1 - -Test.SkipUnless(Condition.HasOpenSSLVersion("1.1.1")) -Test.SkipUnless(Condition.HasLegacyTLSSupport()) -has_curl_tlsv1 = Condition.HasCurlTLSVersionSupport("1.0") - -# Define default ATS -ts = Test.MakeATSProcess("ts", enable_tls=True) -server = Test.MakeOriginServer("server", ssl=True) - -request_foo_header = {"headers": "GET / HTTP/1.1\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -response_foo_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": "foo ok"} -server.addResponse("sessionlog.json", request_foo_header, response_foo_header) - -# add ssl materials like key, certificates for the server -ts.addSSLfile("ssl/server.pem") -ts.addSSLfile("ssl/server.key") - -# Need no remap rules. Everything should be processed by sni - -# Make sure the TS server certs are different from the origin certs -ts.Disk.ssl_multicert_yaml.AddLines( - """ -ssl_multicert: - - dest_ip: "*" - ssl_cert_name: server.pem - ssl_key_name: server.key -""".split("\n")) - -cipher_suite = 'ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-SHA384:AES128-GCM-SHA256:AES256-GCM-SHA384:ECDHE-RSA-RC4-SHA:ECDHE-RSA-AES128-SHA:ECDHE-RSA-AES256-SHA:RC4-SHA:RC4-MD5:AES128-SHA:AES256-SHA:DES-CBC3-SHA!SRP:!DSS:!PSK:!aNULL:!eNULL:!SSLv2' -if Condition.HasOpenSSLVersion("3.0.0"): - cipher_suite += ":@SECLEVEL=0" - -ts.Disk.records_config.update( - { - 'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir), - 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts.Variables.SSLDir), - 'proxy.config.ssl.server.cipher_suite': cipher_suite, - 'proxy.config.ssl.client.CA.cert.path': '{0}'.format(ts.Variables.SSLDir), - 'proxy.config.url_remap.pristine_host_hdr': 1, - 'proxy.config.ssl.TLSv1': 0, - 'proxy.config.ssl.TLSv1_1': 0, - 'proxy.config.exec_thread.autoconfig.scale': 1.0, - 'proxy.config.ssl.TLSv1_2': 1 - }) - -# foo.com should only offer the older TLS protocols -# bar.com should terminate. -# empty SNI should tunnel to server_bar -ts.Disk.sni_yaml.AddLines([ - 'sni:', - '- fqdn: foo.com', - ' valid_tls_versions_in: [ TLSv1, TLSv1_1 ]', -]) - -# Target foo.com for TLSv1_2. Should fail -tr = Test.AddTestRun("foo.com TLSv1_2") -tr.Processes.Default.StartBefore(server) -tr.Processes.Default.StartBefore(Test.Processes.ts) -# Newer versions of OpenSSL further restrict the ciphers they accept. Setting -# the security level to 0 "retains compatibility with previous versions of -# OpenSSL." See: -# https://www.openssl.org/docs/manmaster/man3/SSL_CTX_set_security_level.html -tr.MakeCurlCommand( - "-v --ciphers DEFAULT@SECLEVEL=0 --tls-max 1.2 --tlsv1.2 --resolve 'foo.com:{0}:127.0.0.1' -k https://foo.com:{0}".format( - ts.Variables.ssl_port), - ts=ts) -tr.ReturnCode = 35 -tr.StillRunningAfter = ts - -# Target foo.com for TLSv1. Should succeed -if has_curl_tlsv1: - tr = Test.AddTestRun("foo.com TLSv1") - tr.MakeCurlCommand( - "-v --ciphers DEFAULT@SECLEVEL=0 --tls-max 1.0 --tlsv1 --resolve 'foo.com:{0}:127.0.0.1' -k https://foo.com:{0}".format( - ts.Variables.ssl_port), - ts=ts) - tr.ReturnCode = 0 - tr.StillRunningAfter = ts - -# Target bar.com for TLSv1. Should fail -if has_curl_tlsv1: - tr = Test.AddTestRun("bar.com TLSv1") - tr.MakeCurlCommand( - "-v --ciphers DEFAULT@SECLEVEL=0 --tls-max 1.0 --tlsv1 --resolve 'bar.com:{0}:127.0.0.1' -k https://bar.com:{0}".format( - ts.Variables.ssl_port), - ts=ts) - tr.ReturnCode = 35 - tr.StillRunningAfter = ts - -# Target bar.com for TLSv1_2. Should succeed -tr = Test.AddTestRun("bar.com TLSv1_2") -tr.MakeCurlCommand( - "-v --ciphers DEFAULT@SECLEVEL=0 --tls-max 1.2 --tlsv1.2 --resolve 'bar.com:{0}:127.0.0.1' -k https://bar.com:{0}".format( - ts.Variables.ssl_port), - ts=ts) -tr.ReturnCode = 0 -tr.StillRunningAfter = ts diff --git a/tests/gold_tests/tls/tls_client_versions_minmax.test.py b/tests/gold_tests/tls/tls_client_versions_minmax.test.py deleted file mode 100644 index e84324c459d..00000000000 --- a/tests/gold_tests/tls/tls_client_versions_minmax.test.py +++ /dev/null @@ -1,137 +0,0 @@ -''' -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -Test.Summary = ''' -Test TLS protocol offering based on SNI -''' - -# By default only offer TLSv1_2 -# for special domain foo.com only offer TLSv1 and TLSv1_1 - -Test.SkipUnless(Condition.HasOpenSSLVersion("1.1.1")) -Test.SkipUnless(Condition.HasLegacyTLSSupport()) -has_curl_tlsv1 = Condition.HasCurlTLSVersionSupport("1.0") -has_curl_tlsv1_1 = Condition.HasCurlTLSVersionSupport("1.1") - -# Define default ATS -ts = Test.MakeATSProcess("ts", enable_tls=True) -server = Test.MakeOriginServer("server", ssl=True) - -request_foo_header = {"headers": "GET / HTTP/1.1\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -response_foo_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": "foo ok"} -server.addResponse("sessionlog.json", request_foo_header, response_foo_header) - -# add ssl materials like key, certificates for the server -ts.addSSLfile("ssl/server.pem") -ts.addSSLfile("ssl/server.key") - -# Need no remap rules. Everything should be processed by sni - -# Make sure the TS server certs are different from the origin certs -ts.Disk.ssl_multicert_yaml.AddLines( - """ -ssl_multicert: - - dest_ip: "*" - ssl_cert_name: server.pem - ssl_key_name: server.key -""".split("\n")) - -ts.Disk.records_config.update( - { - 'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir), - 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts.Variables.SSLDir), - 'proxy.config.ssl.client.CA.cert.path': '{0}'.format(ts.Variables.SSLDir), - 'proxy.config.url_remap.pristine_host_hdr': 1, - 'proxy.config.ssl.server.version.min': 2, - 'proxy.config.ssl.server.version.max': 2, - 'proxy.config.ssl.TLSv1_2': 0, # This setting should be ignored in favor of a version range setting - 'proxy.config.exec_thread.autoconfig.scale': 1.0, - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'ssl', - }) - -cipher_suite = 'ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-SHA384:AES128-GCM-SHA256:AES256-GCM-SHA384:ECDHE-RSA-RC4-SHA:ECDHE-RSA-AES128-SHA:ECDHE-RSA-AES256-SHA:RC4-SHA:RC4-MD5:AES128-SHA:AES256-SHA:DES-CBC3-SHA!SRP:!DSS:!PSK:!aNULL:!eNULL:!SSLv2' -if Condition.HasOpenSSLVersion("3.0.0"): - cipher_suite += ":@SECLEVEL=0" - -# foo.com should only offer the older TLS protocols -# bar.com should terminate. -# empty SNI should tunnel to server_bar -ts.Disk.sni_yaml.AddLines( - [ - 'sni:', - '- fqdn: foo.com', - ' valid_tls_versions_in: [ TLSv1_2 ]', # This setting should be ignored in favor of a version range setting - ' valid_tls_version_min_in: TLSv1', - ' valid_tls_version_max_in: TLSv1_1', - f' server_cipher_suite: {cipher_suite}', - ]) - -# Target foo.com for TLSv1_2. Should fail -tr = Test.AddTestRun("foo.com TLSv1_2") -tr.Processes.Default.StartBefore(server) -tr.Processes.Default.StartBefore(Test.Processes.ts) -# Newer versions of OpenSSL further restrict the ciphers they accept. Setting -# the security level to 0 "retains compatibility with previous versions of -# OpenSSL." See: -# https://www.openssl.org/docs/manmaster/man3/SSL_CTX_set_security_level.html -tr.MakeCurlCommand( - "-v --ciphers DEFAULT@SECLEVEL=0 --tls-max 1.2 --tlsv1.2 --resolve 'foo.com:{0}:127.0.0.1' -k https://foo.com:{0}".format( - ts.Variables.ssl_port), - ts=ts) -tr.ReturnCode = 35 -tr.StillRunningAfter = ts - -# Target foo.com for TLSv1. Should succeed -if has_curl_tlsv1: - tr = Test.AddTestRun("foo.com TLSv1") - tr.MakeCurlCommand( - "-v --ciphers DEFAULT@SECLEVEL=0 --tls-max 1.0 --tlsv1 --resolve 'foo.com:{0}:127.0.0.1' -k https://foo.com:{0}".format( - ts.Variables.ssl_port), - ts=ts) - tr.ReturnCode = 0 - tr.StillRunningAfter = ts - -# Target foo.com for TLSv1_1. Should succeed -if has_curl_tlsv1_1: - tr = Test.AddTestRun("foo.com TLSv1_1") - tr.MakeCurlCommand( - "-v --ciphers DEFAULT@SECLEVEL=0 --tls-max 1.1 --tlsv1.1 --resolve 'foo.com:{0}:127.0.0.1' -k https://foo.com:{0}".format( - ts.Variables.ssl_port), - ts=ts) - tr.ReturnCode = 0 - tr.StillRunningAfter = ts - -# Target bar.com for TLSv1. Should fail -if has_curl_tlsv1: - tr = Test.AddTestRun("bar.com TLSv1") - tr.MakeCurlCommand( - "-v --ciphers DEFAULT@SECLEVEL=0 --tls-max 1.0 --tlsv1 --resolve 'bar.com:{0}:127.0.0.1' -k https://bar.com:{0}".format( - ts.Variables.ssl_port), - ts=ts) - tr.ReturnCode = 35 - tr.StillRunningAfter = ts - -# Target bar.com for TLSv1_2. Should succeed -tr = Test.AddTestRun("bar.com TLSv1_2") -tr.MakeCurlCommand( - "-v --ciphers DEFAULT@SECLEVEL=0 --tls-max 1.2 --tlsv1.2 --resolve 'bar.com:{0}:127.0.0.1' -k https://bar.com:{0}".format( - ts.Variables.ssl_port), - ts=ts) -tr.ReturnCode = 0 -tr.StillRunningAfter = ts diff --git a/tests/gold_tests/tls/tls_engine_abort.test.py b/tests/gold_tests/tls/tls_engine_abort.test.py deleted file mode 100644 index 98763647bda..00000000000 --- a/tests/gold_tests/tls/tls_engine_abort.test.py +++ /dev/null @@ -1,120 +0,0 @@ -''' -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import os -import sys - -Test.Summary = ''' -Abort TLS handshakes while an OpenSSL async job is mid-pause, to exercise the -SSLNetVConnection teardown path. When a handshake returns SSL_ERROR_WANT_ASYNC -an eventfd is registered on the poller with the connection as its target, and a -connection torn down before the async job finishes must deregister that eventfd -so the poller is not left pointing at a freed connection. Built under ASan the -server must survive the abort barrage with no sanitizer error and still serve a -normal request. -''' - -async_handshake = os.path.join(Test.Variables.AtsTestPluginsDir, 'async_handshake.so') - -Test.SkipUnless( - Condition.HasOpenSSLVersion('1.1.1'), - Condition.IsOpenSSL(), - Condition(lambda: os.path.isfile(async_handshake), async_handshake + " not found."), -) - -ts = Test.MakeATSProcess("ts", enable_tls=True) -server = Test.MakeOriginServer("server") - -# A wide pause window (well beyond the abort client's 0.4s handshake attempt) -# so the abort reliably lands while the async job is still in flight. -if os.path.isfile(async_handshake): - Test.PrepareTestPlugin(async_handshake, ts, '-delay-ms=2000') - -server.addResponse( - "sessionlog.json", { - "headers": "GET / HTTP/1.1\r\nuuid: basic\r\n\r\n", - "timestamp": "1469733493.993", - "body": "" - }, { - "headers": - "HTTP/1.1 200 OK\r\nServer: microserver\r\nConnection: close\r\nCache-Control: max-age=3600\r\nContent-Length: 2\r\n\r\n", - "timestamp": "1469733493.993", - "body": "ok" - }) - -ts.addSSLfile("ssl/server.pem") -ts.addSSLfile("ssl/server.key") - -ts.Disk.remap_config.AddLine('map / http://127.0.0.1:{0}'.format(server.Variables.Port)) - -ts.Disk.ssl_multicert_yaml.AddLines( - """ -ssl_multicert: - - dest_ip: "*" - ssl_cert_name: server.pem - ssl_key_name: server.key -""".split("\n")) - -ts.Disk.records_config.update( - { - 'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir), - 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts.Variables.SSLDir), - 'proxy.config.exec_thread.autoconfig.scale': 1.0, - 'proxy.config.ssl.async.handshake.enabled': 1, - 'proxy.config.diags.debug.enabled': 0, - 'proxy.config.diags.debug.tags': 'ssl' - }) - -# Fire a barrage of handshakes that abort while the async job is mid-pause. Correct -# teardown is validated by ATS surviving this with no crash and, under ASan, no -# sanitizer error. Without deregistration the connection is freed while its -# eventfd still has a live poller registration. -abort_client = os.path.join(Test.TestDirectory, 'tls_engine_abort.py') - -tr = Test.AddTestRun("abort-during-async-handshake") -tr.Processes.Default.Command = "{0} {1} {2} 30".format(sys.executable, abort_client, ts.Variables.ssl_port) -tr.ReturnCode = 0 -tr.Processes.Default.StartBefore(server) -tr.Processes.Default.StartBefore(Test.Processes.ts, ready=When.PortOpen(ts.Variables.ssl_port)) -tr.Processes.Default.Streams.All = Testers.ContainsExpression("sent 30 aborted handshakes", "Abort client ran") -tr.StillRunningAfter = ts -tr.StillRunningAfter = server - -# After the abort barrage, a normal request must still succeed: the server is -# healthy, not crashed or wedged. -tr2 = Test.AddTestRun("normal-request-after-aborts") -tr2.MakeCurlCommand("-k -v -H uuid:basic -H host:example.com https://127.0.0.1:{0}/".format(ts.Variables.ssl_port), ts=ts) -tr2.ReturnCode = 0 -tr2.Processes.Default.Streams.All = Testers.ContainsExpression(r"HTTP/(2|1\.1) 200", "Request succeeds after the abort barrage") -tr2.StillRunningAfter = ts -tr2.StillRunningAfter = server - -# The abort barrage must actually drive handshakes into the async pause, -# otherwise the eventfd is never registered and the test proves nothing. The -# async_handshake plugin's wake thread prints this to stderr (-> traffic.out) -# when it signals the eventfd at the end of its pause; that only happens if a -# handshake entered the WANT_ASYNC path and armed the eventfd, so its presence -# confirms the teardown path was exercised (and that the async job completed -# after the abort -- exactly the use-after-free window this fix closes). -ts.Disk.traffic_out.Content += Testers.ContainsExpression( - "sent async wake signal to", "Async job engaged on at least one handshake") - -# The server process must not have reported an AddressSanitizer error. ASan -# writes to stderr, which the harness binds to traffic.out -- not diags.log -- -# so the exclusion has to be checked against traffic.out to catch the UAF. -ts.Disk.traffic_out.Content += Testers.ExcludesExpression("AddressSanitizer", "No ASan error in the server") diff --git a/tests/gold_tests/tls/tls_flow_control.test.py b/tests/gold_tests/tls/tls_flow_control.test.py deleted file mode 100644 index f1da9221825..00000000000 --- a/tests/gold_tests/tls/tls_flow_control.test.py +++ /dev/null @@ -1,116 +0,0 @@ -''' -Exercise HTTP tunnel flow control (proxy.config.http.flow_control) on a TLS -client connection: a slow reader makes ATS throttle the origin and unthrottle as -the buffered data drains. The full response body must still be delivered. -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import os -import sys - -Test.Summary = __doc__ - - -class TestTlsFlowControl: - '''Verify a slow TLS reader under flow control still receives the whole body.''' - - # Comfortably larger than the water marks so the tunnel throttles and has to - # unthrottle many times over the transfer. - _body_len: int = 8 * 1024 * 1024 - _high_water: int = 64 * 1024 - _low_water: int = 32 * 1024 - - _server_counter: int = 0 - _ts_counter: int = 0 - - def __init__(self) -> None: - '''Declare the test Processes.''' - self._server = self._configure_server() - self._ts = self._configure_trafficserver() - - def _configure_server(self) -> 'Process': - '''Configure the origin server with a large response body. - - :return: The origin server Process. - ''' - server = Test.MakeOriginServer(f'server-{TestTlsFlowControl._server_counter}') - TestTlsFlowControl._server_counter += 1 - - request_header = {"headers": "GET /obj HTTP/1.1\r\nHost: ex.test\r\n\r\n", "timestamp": "1469733493.993", "body": ""} - response_header = { - "headers": - "HTTP/1.1 200 OK\r\nServer: microserver\r\nConnection: close\r\n" - f"Content-Length: {TestTlsFlowControl._body_len}\r\n\r\n", - "timestamp": "1469733493.993", - "body": "x" * TestTlsFlowControl._body_len - } - server.addResponse("sessionlog.json", request_header, response_header) - return server - - def _configure_trafficserver(self) -> 'Process': - '''Configure Traffic Server with HTTP flow control enabled over TLS. - - :return: The Traffic Server Process. - ''' - ts = Test.MakeATSProcess(f'ts-{TestTlsFlowControl._ts_counter}', enable_tls=True, enable_cache=False) - TestTlsFlowControl._ts_counter += 1 - - ts.addDefaultSSLFiles() - ts.Disk.ssl_multicert_yaml.AddLines( - """ -ssl_multicert: - - dest_ip: "*" - ssl_cert_name: server.pem - ssl_key_name: server.key -""".split("\n")) - ts.Disk.remap_config.AddLine(f'map / http://127.0.0.1:{self._server.Variables.Port}') - ts.Disk.records_config.update( - { - 'proxy.config.ssl.server.cert.path': f'{ts.Variables.SSLDir}', - 'proxy.config.ssl.server.private_key.path': f'{ts.Variables.SSLDir}', - # Small thresholds on a network sink: the tunnel reenable fires before - # the socket write, so the unthrottle depends on the buffer draining. - 'proxy.config.http.flow_control.enabled': 1, - 'proxy.config.http.flow_control.high_water': TestTlsFlowControl._high_water, - 'proxy.config.http.flow_control.low_water': TestTlsFlowControl._low_water, - }) - - ts.Disk.traffic_out.Content = Testers.ExcludesExpression( - "received signal|failed assertion", "ATS must not crash under TLS flow control") - ts.Disk.traffic_out.Content += Testers.ExcludesExpression( - "AddressSanitizer|use-after-free|runtime error:", "no memory-safety error under TLS flow control") - return ts - - def run(self) -> None: - '''Configure and run the TestRun.''' - tr = Test.AddTestRun("slow TLS reader under flow control must receive the whole body") - tr.Processes.Default.StartBefore(self._server) - tr.Processes.Default.StartBefore(self._ts) - tr.Processes.Default.Command = ( - f'{sys.executable} {os.path.join(Test.TestDirectory, "tls_flow_control_client.py")} ' - f'-p {self._ts.Variables.ssl_port} --host ex.test --path /obj ' - f'--expect-bytes {TestTlsFlowControl._body_len}') - tr.Processes.Default.ReturnCode = 0 - tr.Processes.Default.Streams.All = Testers.ContainsExpression( - "RESULT=PASS", "the full body must be delivered under flow control") - tr.Processes.Default.Streams.All += Testers.ContainsExpression( - f"BODY_BYTES={TestTlsFlowControl._body_len}", "every body byte must arrive (no flow-control stall)") - tr.StillRunningAfter = self._ts - tr.StillRunningAfter = self._server - - -TestTlsFlowControl().run() diff --git a/tests/gold_tests/tls/tls_forward_nonhttp.test.py b/tests/gold_tests/tls/tls_forward_nonhttp.test.py deleted file mode 100644 index 020a1f507de..00000000000 --- a/tests/gold_tests/tls/tls_forward_nonhttp.test.py +++ /dev/null @@ -1,83 +0,0 @@ -''' -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import os -import ports - -Test.Summary = ''' -Forwarding a non-HTTP protocol out of TLS -''' - -# Define default ATS -ts = Test.MakeATSProcess("ts", enable_tls=True) - -# add ssl materials like key, certificates for the server -ts.addSSLfile("ssl/server.pem") -ts.addSSLfile("ssl/server.key") - -# reserve a port of the s_client that will be released with 'ts' -ports.get_port(ts, 's_client_port') - -nameserver = Test.MakeDNServer("dns", default='127.0.0.1') - -# Need no remap rules. Everything should be processed by sni - -# Make sure the TS server certs are different from the origin certs -ts.Disk.ssl_multicert_yaml.AddLines( - """ -ssl_multicert: - - dest_ip: "*" - ssl_cert_name: server.pem - ssl_key_name: server.key -""".split("\n")) - -# Case 1, global config policy=permissive properties=signature -# override for foo.com policy=enforced properties=all -ts.Disk.records_config.update( - { - 'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir), - 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts.Variables.SSLDir), - 'proxy.config.http.connect_ports': '{0} {1}'.format(ts.Variables.ssl_port, ts.Variables.s_client_port), - 'proxy.config.exec_thread.autoconfig.scale': 1.0, - 'proxy.config.url_remap.pristine_host_hdr': 1, - 'proxy.config.dns.nameservers': f"127.0.0.1:{nameserver.Variables.Port}", - 'proxy.config.dns.resolv_conf': 'NULL' - }) -ts.addPrivateConnectAllowYaml() - -# foo.com should not terminate. Just tunnel to server_foo -# bar.com should terminate. Forward its tcp stream to server_bar -ts.Disk.sni_yaml.AddLines([ - "sni:", - "- fqdn: bar.com", - " forward_route: localhost:{0}".format(ts.Variables.s_client_port), -]) - -tr = Test.AddTestRun("forward-non-http") -tr.Setup.Copy("test-nc-s_client.sh") -cmd_args = ["sh", "test-nc-s_client.sh", str(ts.Variables.s_client_port), str(ts.Variables.ssl_port)] -if Condition.HasOpenSSLVersion("3.0.0"): - cmd_args += ["-ignore_unexpected_eof"] -tr.Processes.Default.Command = " ".join(cmd_args) -tr.ReturnCode = 0 -tr.Processes.Default.StartBefore(nameserver) -tr.Processes.Default.StartBefore(Test.Processes.ts) -tr.StillRunningAfter = ts -testout_path = os.path.join(Test.RunDirectory, "test.out") -tr.Disk.File(testout_path, id="testout") -tr.Processes.Default.Streams.All += Testers.IncludesExpression("This is a reply", "s_client should get response") diff --git a/tests/gold_tests/tls/tls_h2_cipher_suite.test.py b/tests/gold_tests/tls/tls_h2_cipher_suite.test.py deleted file mode 100644 index 9ab8ca5ca45..00000000000 --- a/tests/gold_tests/tls/tls_h2_cipher_suite.test.py +++ /dev/null @@ -1,94 +0,0 @@ -"""Verify that HTTP/2 is not negotiated with prohibited TLS cipher suites.""" - -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -Test.Summary = __doc__ - -Test.SkipUnless(Condition.HasOpenSSLVersion("1.1.1")) - - -class TestH2CipherSuite: - """Verify HTTP/2 cipher suite restrictions.""" - - def __init__(self) -> None: - """Configure Traffic Server and the cipher suite test runs.""" - self._ts = self._configure_traffic_server() - self._configure_allowed_cipher_test() - - for description, cipher in ( - ("non-ephemeral AEAD", "AES128-GCM-SHA256"), - ("ephemeral CBC", "ECDHE-RSA-AES128-SHA256"), - ): - self._configure_prohibited_cipher_test(description, cipher) - - def _configure_traffic_server(self) -> 'Process': - """Configure Traffic Server with allowed and prohibited cipher suites.""" - ts = Test.MakeATSProcess("ts", enable_tls=True) - ts.addSSLfile("ssl/server.pem") - ts.addSSLfile("ssl/server.key") - - ts.Disk.ssl_multicert_yaml.AddLines( - """ -ssl_multicert: - - dest_ip: "*" - ssl_cert_name: server.pem - ssl_key_name: server.key -""".split("\n")) - - ts.Disk.records_config.update( - { - "proxy.config.ssl.server.cert.path": ts.Variables.SSLDir, - "proxy.config.ssl.server.private_key.path": ts.Variables.SSLDir, - "proxy.config.ssl.server.version.min": 2, - "proxy.config.ssl.server.version.max": 2, - "proxy.config.ssl.server.cipher_suite": - "ECDHE-RSA-AES128-GCM-SHA256:" - "AES128-GCM-SHA256:" - "ECDHE-RSA-AES128-SHA256:" - "@SECLEVEL=0", - }) - return ts - - def _configure_allowed_cipher_test(self) -> None: - """Verify that an ephemeral AEAD cipher can negotiate HTTP/2.""" - tr = Test.AddTestRun("Allow HTTP/2 with an ephemeral AEAD cipher") - tr.Processes.Default.Command = ( - "openssl s_client -tls1_2 -cipher ECDHE-RSA-AES128-GCM-SHA256 " - f"-alpn h2,http/1.1 -connect 127.0.0.1:{self._ts.Variables.ssl_port} None: - """Verify that a prohibited cipher falls back to HTTP/1.1. - - :param description: A human-readable description of the cipher. - :param cipher: The OpenSSL cipher suite name. - """ - tr = Test.AddTestRun(f"Fall back to HTTP/1.1 with a prohibited {description} cipher") - tr.Processes.Default.Command = ( - "printf 'GET / HTTP/1.1\\r\\nHost: example.com\\r\\nConnection: close\\r\\n\\r\\n' | " - f"openssl s_client -ign_eof -tls1_2 -cipher {cipher} " - f"-alpn h2,http/1.1 -connect 127.0.0.1:{self._ts.Variables.ssl_port}") - tr.Processes.Default.Streams.All += Testers.IncludesExpression("ALPN protocol: http/1.1", "HTTP/1.1 should be negotiated") - tr.Processes.Default.Streams.All += Testers.IncludesExpression("HTTP/1.1 404", "The HTTP/1.1 request should be processed") - tr.ReturnCode = 0 - tr.StillRunningAfter = self._ts - - -TestH2CipherSuite() diff --git a/tests/gold_tests/tls/tls_hooks_client_verify.test.py b/tests/gold_tests/tls/tls_hooks_client_verify.test.py deleted file mode 100644 index 33c8b21ec81..00000000000 --- a/tests/gold_tests/tls/tls_hooks_client_verify.test.py +++ /dev/null @@ -1,121 +0,0 @@ -''' -Test SERVER_VERIFY_HOOK -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import os - -Test.Summary = ''' -Test different combinations of TLS handshake hooks to ensure they are applied consistently. -''' - -Test.SkipUnless(Condition.HasOpenSSLVersion("1.1.1")) - -ts = Test.MakeATSProcess("ts", enable_tls=True) -server = Test.MakeOriginServer("server", ssl=True) -request_header = {"headers": "GET / HTTP/1.1\r\nHost: www.example.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -# desired response form the origin server -response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -server.addResponse("sessionlog.json", request_header, response_header) - -ts.addSSLfile("ssl/server.pem") -ts.addSSLfile("ssl/server.key") -ts.addSSLfile("ssl/signer.pem") - -ts.Disk.records_config.update( - { - # Test looks for debug output from the plugin - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'ssl_client_verify_test', - 'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir), - 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts.Variables.SSLDir), - 'proxy.config.exec_thread.autoconfig.scale': 1.0, - 'proxy.config.ssl.CA.cert.filename': '{0}/signer.pem'.format(ts.Variables.SSLDir), - 'proxy.config.url_remap.pristine_host_hdr': 1 - }) - -ts.Disk.ssl_multicert_yaml.AddLines( - """ -ssl_multicert: - - dest_ip: "*" - ssl_cert_name: server.pem - ssl_key_name: server.key -""".split("\n")) - -ts.Disk.remap_config.AddLine( - 'map https://foo.com:{1}/ https://127.0.0.1:{0}'.format(server.Variables.SSL_Port, ts.Variables.ssl_port)) -ts.Disk.remap_config.AddLine( - 'map https://bar.com:{1}/ https://127.0.0.1:{0}'.format(server.Variables.SSL_Port, ts.Variables.ssl_port)) -ts.Disk.remap_config.AddLine( - 'map https://random.com:{1}/ https://127.0.0.1:{0}'.format(server.Variables.SSL_Port, ts.Variables.ssl_port)) - -ts.Disk.sni_yaml.AddLines([ - 'sni:', - '- fqdn: bar.com', - ' verify_client: STRICT', - '- fqdn: foo.com', - ' verify_client: STRICT', -]) - -Test.PrepareTestPlugin(os.path.join(Test.Variables.AtsTestPluginsDir, 'ssl_client_verify_test.so'), ts, '-count=2 -good=foo.com') - -tr = Test.AddTestRun("request good name") -tr.Setup.Copy("ssl/signed-foo.pem") -tr.Setup.Copy("ssl/signed-foo.key") -tr.Setup.Copy("ssl/signed-bar.pem") -tr.Setup.Copy("ssl/signed-bar.key") -tr.Processes.Default.StartBefore(server) -tr.Processes.Default.StartBefore(Test.Processes.ts) -tr.StillRunningAfter = ts -tr.StillRunningAfter = server -tr.MakeCurlCommand( - "--tls-max 1.2 -k --cert ./signed-foo.pem --key ./signed-foo.key --resolve 'foo.com:{0}:127.0.0.1' https://foo.com:{0}/case1" - .format(ts.Variables.ssl_port), - ts=ts) -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Streams.all = Testers.ExcludesExpression("Could Not Connect", "Curl attempt should have succeeded") - -tr2 = Test.AddTestRun("request bad name") -tr2.StillRunningAfter = ts -tr2.StillRunningAfter = server -tr2.MakeCurlCommand( - "--tls-max 1.2 -k --cert ./signed-bar.pem --key ./signed-bar.key --resolve 'foo.com:{0}:127.0.0.1' https://foo.com:{0}/case1" - .format(ts.Variables.ssl_port), - ts=ts) -tr2.Processes.Default.ReturnCode = 35 -tr2.Processes.Default.Streams.all = Testers.ContainsExpression("error", "Curl attempt should have failed") - -tr3 = Test.AddTestRun("request badly signed cert") -tr3.Setup.Copy("ssl/server.pem") -tr3.Setup.Copy("ssl/server.key") -tr3.StillRunningAfter = ts -tr3.StillRunningAfter = server -tr3.MakeCurlCommand( - "--tls-max 1.2 -k --cert ./server.pem --key ./server.key --resolve 'foo.com:{0}:127.0.0.1' https://foo.com:{0}/case1".format( - ts.Variables.ssl_port), - ts=ts) -tr3.Processes.Default.ReturnCode = 35 -tr3.Processes.Default.Streams.all = Testers.ContainsExpression("error", "Curl attempt should have failed") - -ts.Disk.traffic_out.Content += Testers.ContainsExpression( - r"Client verify callback 0 [\da-fx]+? - event is good good HS", "verify callback happens 2 times") -ts.Disk.traffic_out.Content += Testers.ContainsExpression( - r"Client verify callback 1 [\da-fx]+? - event is good good HS", "verify callback happens 2 times") -ts.Disk.traffic_out.Content += Testers.ContainsExpression( - r"Client verify callback 0 [\da-fx]+? - event is good error HS", "verify callback happens 2 times") -ts.Disk.traffic_out.Content += Testers.ContainsExpression( - r"Client verify callback 1 [\da-fx]+? - event is good error HS", "verify callback happens 2 times") diff --git a/tests/gold_tests/tls/tls_hooks_verify.test.py b/tests/gold_tests/tls/tls_hooks_verify.test.py deleted file mode 100644 index 8507e55e59c..00000000000 --- a/tests/gold_tests/tls/tls_hooks_verify.test.py +++ /dev/null @@ -1,113 +0,0 @@ -''' -Test SERVER_VERIFY_HOOK -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import os - -Test.Summary = ''' -Test different combinations of TLS handshake hooks to ensure they are applied consistently. -''' - -ts = Test.MakeATSProcess("ts", enable_tls=True) -server = Test.MakeOriginServer("server", ssl=True) -request_header = {"headers": "GET / HTTP/1.1\r\nHost: www.example.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -# desired response form the origin server -response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -server.addResponse("sessionlog.json", request_header, response_header) - -ts.addSSLfile("ssl/server.pem") -ts.addSSLfile("ssl/server.key") - -ts.Disk.records_config.update( - { - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'ssl_verify_test', - 'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir), - 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts.Variables.SSLDir), - 'proxy.config.ssl.client.verify.server.policy': 'ENFORCED', - 'proxy.config.ssl.client.verify.server.properties': 'NONE', - 'proxy.config.url_remap.pristine_host_hdr': 1 - }) - -ts.Disk.ssl_multicert_yaml.AddLines( - """ -ssl_multicert: - - dest_ip: "*" - ssl_cert_name: server.pem - ssl_key_name: server.key -""".split("\n")) - -ts.Disk.remap_config.AddLine( - 'map https://foo.com:{1}/ https://127.0.0.1:{0}'.format(server.Variables.SSL_Port, ts.Variables.ssl_port)) -ts.Disk.remap_config.AddLine( - 'map https://bar.com:{1}/ https://127.0.0.1:{0}'.format(server.Variables.SSL_Port, ts.Variables.ssl_port)) -ts.Disk.remap_config.AddLine( - 'map https://random.com:{1}/ https://127.0.0.1:{0}'.format(server.Variables.SSL_Port, ts.Variables.ssl_port)) - -ts.Disk.sni_yaml.AddLine('sni:') -ts.Disk.sni_yaml.AddLine('- fqdn: bar.com') -ts.Disk.sni_yaml.AddLine(' verify_server_policy: PERMISSIVE') - -Test.PrepareTestPlugin( - os.path.join(Test.Variables.AtsTestPluginsDir, 'ssl_verify_test.so'), ts, '-count=2 -bad=random.com -bad=bar.com') - -tr = Test.AddTestRun("request good name") -tr.Processes.Default.StartBefore(server) -tr.Processes.Default.StartBefore(Test.Processes.ts) -tr.StillRunningAfter = ts -tr.StillRunningAfter = server -tr.MakeCurlCommand("--resolve \"foo.com:{0}:127.0.0.1\" -k https://foo.com:{0}".format(ts.Variables.ssl_port), ts=ts) -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Streams.stdout = Testers.ExcludesExpression("Could Not Connect", "Curl attempt should have failed") - -tr2 = Test.AddTestRun("request bad name") -tr2.StillRunningAfter = ts -tr2.StillRunningAfter = server -tr2.MakeCurlCommand("--resolve \"random.com:{0}:127.0.0.1\" -k https://random.com:{0}".format(ts.Variables.ssl_port), ts=ts) -tr2.Processes.Default.ReturnCode = 0 -tr2.Processes.Default.Streams.stdout = Testers.ContainsExpression("Could Not Connect", "Curl attempt should have failed") - -tr3 = Test.AddTestRun("request bad name permissive") -tr3.StillRunningAfter = ts -tr3.StillRunningAfter = server -tr3.MakeCurlCommand("--resolve \"bar.com:{0}:127.0.0.1\" -k https://bar.com:{0}".format(ts.Variables.ssl_port), ts=ts) -tr3.Processes.Default.ReturnCode = 0 -tr3.Processes.Default.Streams.stdout = Testers.ExcludesExpression("Could Not Connect", "Curl attempt should have failed") - -# Overriding the built in ERROR check since we expect tr2 to fail -ts.Disk.diags_log.Content = Testers.ContainsExpression( - "WARNING: TS_EVENT_SSL_VERIFY_SERVER plugin failed the origin certificate check for 127.0.0.1. Action=Terminate SNI=random.com", - "random.com should fail") -ts.Disk.diags_log.Content += Testers.ContainsExpression( - "WARNING: TS_EVENT_SSL_VERIFY_SERVER plugin failed the origin certificate check for 127.0.0.1. Action=Continue SNI=bar.com", - "bar.com should fail but continue") -ts.Disk.diags_log.Content += Testers.ExcludesExpression("SNI=foo.com", "foo.com should not fail in any way") - -ts.Disk.traffic_out.Content += Testers.ContainsExpression( - r"Server verify callback 0 [\da-fx]+? - event is good SNI=foo.com good HS", "verify callback happens 2 times") -ts.Disk.traffic_out.Content += Testers.ContainsExpression( - r"Server verify callback 1 [\da-fx]+? - event is good SNI=foo.com good HS", "verify callback happens 2 times") -ts.Disk.traffic_out.Content += Testers.ContainsExpression( - r"Server verify callback 0 [\da-fx]+? - event is good SNI=random.com error HS", "verify callback happens 2 times") -ts.Disk.traffic_out.Content += Testers.ContainsExpression( - r"Server verify callback 1 [\da-fx]+? - event is good SNI=random.com error HS", "verify callback happens 2 times") -ts.Disk.traffic_out.Content += Testers.ContainsExpression( - r"Server verify callback 0 [\da-fx]+? - event is good SNI=bar.com error HS", "verify callback happens 2 times") -ts.Disk.traffic_out.Content += Testers.ContainsExpression( - r"Server verify callback 1 [\da-fx]+? - event is good SNI=bar.com error HS", "verify callback happens 2 times") -ts.Disk.traffic_out.Content += Testers.ContainsExpression("Server verify callback SNI APIs match=true", "verify SNI names match") diff --git a/tests/gold_tests/tls/tls_keepalive.test.py b/tests/gold_tests/tls/tls_keepalive.test.py deleted file mode 100644 index 7e393ed2260..00000000000 --- a/tests/gold_tests/tls/tls_keepalive.test.py +++ /dev/null @@ -1,115 +0,0 @@ -''' -Use pre-accept hook to verify that both requests are made over the same TLS session -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import os - -Test.Summary = ''' -Verify that the client-side keep alive is honored for TLS and different versions of HTTP -''' - -Test.SkipUnless(Condition.HasCurlFeature('http2')) - -ts = Test.MakeATSProcess("ts", enable_tls=True) -server = Test.MakeOriginServer("server") -request_header = {"headers": "GET / HTTP/1.1\r\nHost: www.example.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -# desired response form the origin server -response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -server.addResponse("sessionlog.json", request_header, response_header) - -ts.addSSLfile("ssl/server.pem") -ts.addSSLfile("ssl/server.key") - -ts.Disk.records_config.update( - { - 'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir), - 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts.Variables.SSLDir), - 'proxy.config.ssl.TLSv1_3.enabled': 0, - 'proxy.config.exec_thread.autoconfig.scale': 1.0, - 'proxy.config.log.max_secs_per_buffer': 1 - }) - -ts.Disk.ssl_multicert_yaml.AddLines( - """ -ssl_multicert: - - dest_ip: "*" - ssl_cert_name: server.pem - ssl_key_name: server.key -""".split("\n")) - -ts.Disk.remap_config.AddLine( - 'map https://example.com:{0} http://127.0.0.1:{1}'.format(ts.Variables.ssl_port, server.Variables.Port)) - -ts.Disk.logging_yaml.AddLines( - ''' -logging: - formats: - - name: testformat - format: '% %' - logs: - - mode: ascii - format: testformat - filename: squid -'''.split("\n")) - -Test.PrepareTestPlugin(os.path.join(Test.Variables.AtsTestPluginsDir, 'ssl_secret_load_test.so'), ts) - -tr = Test.AddTestRun("Test two HTTP/1.1 requests over one TLS connection") -tr.Processes.Default.StartBefore(server) -tr.Processes.Default.StartBefore(Test.Processes.ts) -tr.StillRunningAfter = ts -tr.StillRunningAfter = server -tr.MakeCurlCommand( - '-k -v --http1.1 -H \'host:example.com:{0}\' https://127.0.0.1:{0} https://127.0.0.1:{0}'.format(ts.Variables.ssl_port), ts=ts) -tr.Processes.Default.ReturnCode = 0 - -tr = Test.AddTestRun("Test two HTTP/1.1 requests over two TLS connections") -tr.StillRunningAfter = ts -tr.StillRunningAfter = server -tr.MakeCurlCommandMulti( - '{{curl}} -k -v --http1.1 -H \'host:example.com:{0}\' https://127.0.0.1:{0}; {{curl}} -k -v --http1.1 -H \'host:example.com:{0}\' https://127.0.0.1:{0}' - .format(ts.Variables.ssl_port), - ts=ts) -tr.Processes.Default.ReturnCode = 0 - -tr = Test.AddTestRun("Test two HTTP/2 requests over one TLS connection") -tr.StillRunningAfter = ts -tr.StillRunningAfter = server -tr.MakeCurlCommandMulti( - '{{curl}} -k -v --http2 -H \'host:example.com:{0}\' https://127.0.0.1:{0} https://127.0.0.1:{0}'.format(ts.Variables.ssl_port), - ts=ts) -tr.Processes.Default.ReturnCode = 0 - -tr = Test.AddTestRun("Test two HTTP/2 requests over two TLS connections") -tr.StillRunningAfter = ts -tr.StillRunningAfter = server -tr.MakeCurlCommandMulti( - '{{curl}} -k -v --http2 -H \'host:example.com:{0}\' https://127.0.0.1:{0}; {{curl}} -k -v --http1.1 -H \'host:example.com:{0}\' https://127.0.0.1:{0}' - .format(ts.Variables.ssl_port), - ts=ts) -tr.Processes.Default.ReturnCode = 0 - -# Just a check to flush out the traffic log until we have a clean shutdown for traffic_server -tr = Test.AddTestRun("Wait for the access log to write out") -tr.DelayStart = 5 -tr.StillRunningAfter = ts -tr.StillRunningAfter = server -tr.Processes.Default.Command = 'ls' -tr.Processes.Default.ReturnCode = 0 - -ts.Disk.squid_log.Content = "gold/accesslog.gold" diff --git a/tests/gold_tests/tls/tls_ocsp.test.py b/tests/gold_tests/tls/tls_ocsp.test.py deleted file mode 100644 index 8585b35f006..00000000000 --- a/tests/gold_tests/tls/tls_ocsp.test.py +++ /dev/null @@ -1,84 +0,0 @@ -''' -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import os - -Test.Summary = ''' -Test tls server prefetched OCSP responses -''' - -Test.SkipUnless( - Condition.HasCurlVersion("7.41.0"), # curl --cert-status option has been introduced in version 7.41.0 -) - -# Define default ATS -ts = Test.MakeATSProcess("ts", enable_tls=True) -server = Test.MakeOriginServer("server") -request_header = {"headers": "GET / HTTP/1.1\r\nHost: example.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -# desired response form the origin server -response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -server.addResponse("sessionlog.json", request_header, response_header) - -ts.addSSLfile("ssl/ca.ocsp.pem") -ts.addSSLfile("ssl/server.ocsp.pem") -ts.addSSLfile("ssl/server.ocsp.key") -ts.addSSLfile("ssl/ocsp_response.der") - -ts.Disk.remap_config.AddLine( - 'map https://example.com:{0} http://127.0.0.1:{1}'.format(ts.Variables.ssl_port, server.Variables.Port)) - -ts.Disk.ssl_multicert_yaml.AddLines( - """ - - -ssl_multicert: - - - - dest_ip: "*" - ssl_cert_name: server.ocsp.pem - ssl_key_name: server.ocsp.key - ssl_ocsp_name: ocsp_response.der - - -""".split("\n")) - -# Case 1, global config policy=permissive properties=signature -# override for foo.com policy=enforced properties=all -ts.Disk.records_config.update( - { - 'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir), - 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts.Variables.SSLDir), - 'proxy.config.ssl.server.cert_chain.filename': 'ca.ocsp.pem', - # enable prefetched OCSP responses - 'proxy.config.ssl.ocsp.response.path': '{0}'.format(ts.Variables.SSLDir), - 'proxy.config.ssl.ocsp.enabled': 1, - 'proxy.config.exec_thread.autoconfig.scale': 1.0, - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'ssl_ocsp' - }) - -tr = Test.AddTestRun("Check OCSP response using curl") -tr.Processes.Default.StartBefore(server) -tr.Processes.Default.StartBefore(Test.Processes.ts) -tr.StillRunningAfter = server -tr.StillRunningAfter = ts -tr.MakeCurlCommand( - "-v --cacert {0} --cert-status -H \")host:example.com\" https://127.0.0.1:{1}".format( - os.path.join(ts.Variables.SSLDir, "ca.ocsp.pem"), ts.Variables.ssl_port), - ts=ts) -tr.ReturnCode = 0 diff --git a/tests/gold_tests/tls/tls_origin_open_failed.test.py b/tests/gold_tests/tls/tls_origin_open_failed.test.py deleted file mode 100644 index 52092aadf0a..00000000000 --- a/tests/gold_tests/tls/tls_origin_open_failed.test.py +++ /dev/null @@ -1,78 +0,0 @@ -''' -Verify that a failed outbound TLS origin connection is surfaced to the client as -an error (5xx) without crashing ATS. -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import ports - -Test.Summary = __doc__ - - -class TestOriginOpenFailed: - '''Verify a failed outbound TLS connect is surfaced as a 5xx, not a crash.''' - - _ts_counter: int = 0 - - def __init__(self) -> None: - '''Declare the test Processes.''' - self._ts = self._configure_trafficserver() - - def _configure_trafficserver(self) -> 'Process': - '''Configure Traffic Server with an https origin whose connect fails. - - :return: The Traffic Server Process. - ''' - ts = Test.MakeATSProcess(f'ts-{TestOriginOpenFailed._ts_counter}') - TestOriginOpenFailed._ts_counter += 1 - - # Reserve a port for a TLS origin; its liveness is irrelevant since the - # source bind below fails before any connect is attempted. - ports.get_port(ts, 'origin_port') - - ts.Disk.remap_config.AddLine(f'map http://dead.test/ https://127.0.0.1:{ts.Variables.origin_port}/') - ts.Disk.records_config.update( - { - # Bind every outbound connection to a non-local source address - # (RFC 5737 documentation range) so bind() fails synchronously with - # EADDRNOTAVAIL, i.e. the connect fails before any TCP/TLS exchange - # rather than via a later, routable connect that would be refused - # asynchronously. - 'proxy.config.outgoing_ip_to_bind': '192.0.2.1', - 'proxy.config.http.connect_attempts_max_retries': 1, - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'http|ssl', - }) - - # Tearing down the failed outbound TLS connect must not crash ATS. - ts.Disk.traffic_out.Content = Testers.ExcludesExpression( - "received signal|failed assertion", "ATS must not crash on a failed outbound TLS connect") - return ts - - def run(self) -> None: - '''Configure and run the TestRun.''' - tr = Test.AddTestRun("a failed outbound TLS connect is surfaced cleanly") - tr.Processes.Default.StartBefore(self._ts) - tr.MakeCurlCommand( - f'-s -o /dev/null -w "%{{http_code}}" -H "Host: dead.test" http://127.0.0.1:{self._ts.Variables.port}/', ts=self._ts) - tr.Processes.Default.ReturnCode = 0 - # A failed origin connection is surfaced as a 5xx (502 Bad Gateway). - tr.Processes.Default.Streams.stdout = Testers.ContainsExpression("50[02]", "a failed origin connect yields a 5xx") - tr.StillRunningAfter = self._ts - - -TestOriginOpenFailed().run() diff --git a/tests/gold_tests/tls/tls_origin_post_abort.test.py b/tests/gold_tests/tls/tls_origin_post_abort.test.py deleted file mode 100644 index 4d989a26ef6..00000000000 --- a/tests/gold_tests/tls/tls_origin_post_abort.test.py +++ /dev/null @@ -1,114 +0,0 @@ -''' -When a TLS origin resets the connection while ATS is still sending it a POST -request body, ATS must fail the transaction promptly -- the transport error must -reach the request-body tunnel right away as VC_EVENT_ERROR, not sit until the -outbound inactivity timeout rescues it. -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import os -import sys - -Test.Summary = __doc__ - - -class TestOriginPostAbort: - '''Verify a TLS origin RST mid-POST-body fails the transaction promptly.''' - - _ts_counter: int = 0 - _origin_counter: int = 0 - - def __init__(self) -> None: - '''Declare the test Processes.''' - Test.GetTcpPort("origin_port") - self._ts = self._configure_trafficserver() - self._origin = self._configure_origin() - - def _configure_trafficserver(self) -> 'Process': - '''Configure Traffic Server with a TLS origin and a generous rescue timeout. - - :return: The Traffic Server Process. - ''' - ts = Test.MakeATSProcess(f'ts-{TestOriginPostAbort._ts_counter}', enable_cache=False) - TestOriginPostAbort._ts_counter += 1 - - ts.Disk.records_config.update( - { - 'proxy.config.url_remap.remap_required': 1, - 'proxy.config.http.connect_attempts_max_retries': 0, - 'proxy.config.http.connect_attempts_timeout': 15, - # The rescue timeout: a prompt failure must beat this by a wide margin. - 'proxy.config.http.transaction_no_activity_timeout_out': 15, - 'proxy.config.http.transaction_no_activity_timeout_in': 30, - # Keep the kernel send buffer small so the request body cannot be - # absorbed by the kernel: ATS must still be mid-send at the RST. - 'proxy.config.net.sock_send_buffer_size_out': 65536, - 'proxy.config.ssl.client.verify.server.policy': 'DISABLED', - 'proxy.config.diags.debug.enabled': 0, - 'proxy.config.diags.debug.tags': 'http|ssl|ssl_io', - }) - ts.Disk.remap_config.AddLine(f'map /post https://127.0.0.1:{Test.Variables.origin_port}') - - ts.Disk.traffic_out.Content = Testers.ExcludesExpression( - 'received signal|failed assertion', 'ATS must not crash handling the mid-body origin abort') - return ts - - def _configure_origin(self) -> 'Process': - '''Configure a raw-socket TLS origin that resets mid-request-body. - - The scenario needs a TLS origin that accepts the request body and then RSTs - the connection mid-body (SO_LINGER 0). Proxy Verifier can only close cleanly - (close_notify), so it cannot express a mid-body reset; hence the small - raw-socket origin helper rather than an ATSReplayTest. - - :return: The origin server Process. - ''' - origin = Test.Processes.Process( - f'origin-{TestOriginPostAbort._origin_counter}', - f'{sys.executable} {os.path.join(Test.TestDirectory, "tls_post_abort_origin.py")} ' - f'-p {Test.Variables.origin_port} ' - f'-c {os.path.join(Test.Variables.AtsTestToolsDir, "ssl", "server.pem")} -d 1.0') - TestOriginPostAbort._origin_counter += 1 - - # These markers prove the reset path was actually exercised: the origin - # completed the TLS handshake, received the forwarded request headers, and - # then reset the connection mid-body. Without them the client could pass on - # any prompt response -- e.g. a broken remap or a never-started origin that - # yields a fast 5xx without the body ever reaching a resetting origin. - origin.Streams.stdout = Testers.ContainsExpression( - 'request headers received', 'the origin must receive the forwarded request headers') - origin.Streams.stdout += Testers.ContainsExpression( - 'connection reset sent', 'the origin must reset the connection mid-body') - return origin - - def run(self) -> None: - '''Configure and run the TestRun.''' - tr = Test.AddTestRun("POST to a TLS origin that resets mid-body must fail promptly") - tr.Processes.Default.StartBefore(self._ts) - tr.Processes.Default.StartBefore(self._origin, ready=When.PortOpen(Test.Variables.origin_port)) - tr.Processes.Default.Command = ( - f'{sys.executable} {os.path.join(Test.TestDirectory, "tls_post_abort_client.py")} ' - f'-p {self._ts.Variables.port} -t 8') - tr.Processes.Default.ReturnCode = 0 - tr.Processes.Default.Streams.All += Testers.ContainsExpression( - 'PASS: transaction failed promptly', 'the POST must fail promptly on the origin RST') - tr.Processes.Default.Streams.All += Testers.ContainsExpression( - 'status-code: 5', 'ATS must surface the mid-body origin reset as a 5xx') - tr.StillRunningAfter = self._ts - - -TestOriginPostAbort().run() diff --git a/tests/gold_tests/tls/tls_origin_session_reuse.test.py b/tests/gold_tests/tls/tls_origin_session_reuse.test.py deleted file mode 100644 index 6889535c7b3..00000000000 --- a/tests/gold_tests/tls/tls_origin_session_reuse.test.py +++ /dev/null @@ -1,170 +0,0 @@ -''' -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import re - -Test.Summary = ''' -Test tls origin session reuse -''' - -# Define default ATS -ts1 = Test.MakeATSProcess("ts1", enable_tls=True) -ts2 = Test.MakeATSProcess("ts2", enable_tls=True) -ts3 = Test.MakeATSProcess("ts3", enable_tls=True) -ts4 = Test.MakeATSProcess("ts4", enable_tls=True) -server = Test.MakeOriginServer("server") - -# Add info the origin server responses -request_header = {'headers': 'GET / HTTP/1.1\r\nHost: www.example.com\r\n\r\n', 'timestamp': '1469733493.993', 'body': ''} -response_header = {'headers': 'HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n', 'timestamp': '1469733493.993', 'body': 'curl test'} -server.addResponse("sessionlog.json", request_header, response_header) - -# add ssl materials like key, certificates for the server -ts1.addSSLfile("ssl/server.pem") -ts1.addSSLfile("ssl/server.key") -ts2.addSSLfile("ssl/server.pem") -ts2.addSSLfile("ssl/server.key") -ts3.addSSLfile("ssl/server.pem") -ts3.addSSLfile("ssl/server.key") -ts4.addSSLfile("ssl/server.pem") -ts4.addSSLfile("ssl/server.key") - -ts1.Disk.remap_config.AddLine('map / http://127.0.0.1:{0}'.format(server.Variables.Port)) -ts2.Disk.remap_config.AddLines( - [ - 'map /reuse_session https://127.0.0.1:{0}'.format(ts1.Variables.ssl_port), - 'map /remove_oldest https://127.0.1.1:{0}'.format(ts1.Variables.ssl_port), - ]) -ts3.Disk.remap_config.AddLine('map / http://127.0.0.1:{0}'.format(server.Variables.Port)) -ts4.Disk.remap_config.AddLine('map / https://127.0.0.1:{0}'.format(ts3.Variables.ssl_port)) - -ts1.Disk.ssl_multicert_yaml.AddLines( - """ -ssl_multicert: - - dest_ip: "*" - ssl_cert_name: server.pem - ssl_key_name: server.key -""".split("\n")) -ts2.Disk.ssl_multicert_yaml.AddLines( - """ -ssl_multicert: - - dest_ip: "*" - ssl_cert_name: server.pem - ssl_key_name: server.key -""".split("\n")) -ts3.Disk.ssl_multicert_yaml.AddLines( - """ -ssl_multicert: - - dest_ip: "*" - ssl_cert_name: server.pem - ssl_key_name: server.key -""".split("\n")) -ts4.Disk.ssl_multicert_yaml.AddLines( - """ -ssl_multicert: - - dest_ip: "*" - ssl_cert_name: server.pem - ssl_key_name: server.key -""".split("\n")) - -ts1.Disk.records_config.update( - { - 'proxy.config.http.cache.http': 0, - 'proxy.config.ssl.server.cert.path': '{0}'.format(ts1.Variables.SSLDir), - 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts1.Variables.SSLDir), - 'proxy.config.exec_thread.autoconfig.scale': 1.0, - 'proxy.config.ssl.server.session_ticket.enable': 1, - 'proxy.config.ssl.origin_session_cache.enabled': 1, - 'proxy.config.ssl.origin_session_cache.size': 1, - 'proxy.config.ssl.client.verify.server.policy': 'PERMISSIVE', - }) -ts2.Disk.records_config.update( - { - 'proxy.config.http.cache.http': 0, - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'ssl.origin_session_cache', - 'proxy.config.ssl.server.cert.path': '{0}'.format(ts2.Variables.SSLDir), - 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts2.Variables.SSLDir), - 'proxy.config.exec_thread.autoconfig.scale': 1.0, - 'proxy.config.ssl.server.session_ticket.enable': 1, - 'proxy.config.ssl.origin_session_cache.enabled': 1, - 'proxy.config.ssl.origin_session_cache.size': 1, - 'proxy.config.ssl.client.verify.server.policy': 'PERMISSIVE', - }) -ts3.Disk.records_config.update( - { - 'proxy.config.http.cache.http': 0, - 'proxy.config.ssl.server.cert.path': '{0}'.format(ts3.Variables.SSLDir), - 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts3.Variables.SSLDir), - 'proxy.config.exec_thread.autoconfig.scale': 1.0, - 'proxy.config.ssl.server.session_ticket.enable': 1, - 'proxy.config.ssl.origin_session_cache.enabled': 1, - 'proxy.config.ssl.origin_session_cache.size': 1, - 'proxy.config.ssl.client.verify.server.policy': 'PERMISSIVE', - }) -ts4.Disk.records_config.update( - { - 'proxy.config.http.cache.http': 0, - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'ssl.origin_session_cache', - 'proxy.config.ssl.server.cert.path': '{0}'.format(ts4.Variables.SSLDir), - 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts4.Variables.SSLDir), - 'proxy.config.exec_thread.autoconfig.scale': 1.0, - 'proxy.config.ssl.server.session_ticket.enable': 1, - 'proxy.config.ssl.origin_session_cache.enabled': 0, - 'proxy.config.ssl.origin_session_cache.size': 1, - 'proxy.config.ssl.client.verify.server.policy': 'PERMISSIVE', - }) - -tr = Test.AddTestRun('new session then reuse') -tr.MakeCurlCommandMulti( - '{{curl}} https://127.0.0.1:{0}/reuse_session -k && {{curl}} https://127.0.0.1:{0}/reuse_session -k'.format( - ts2.Variables.ssl_port), - ts=ts2) -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.StartBefore(server) -tr.Processes.Default.StartBefore(ts1) -tr.Processes.Default.StartBefore(ts2) -tr.Processes.Default.Streams.All = Testers.ContainsExpression('curl test', 'Making sure the basics still work') -ts2.Disk.traffic_out.Content = Testers.ContainsExpression('new session to origin', '') -ts2.Disk.traffic_out.Content += Testers.ContainsExpression('reused session to origin', '') -tr.StillRunningAfter = server -tr.StillRunningAfter += ts1 -tr.StillRunningAfter += ts2 - -tr = Test.AddTestRun('remove oldest session, new session then reuse') -tr.MakeCurlCommandMulti( - '{{curl}} https://127.0.0.1:{0}/remove_oldest -k && {{curl}} https://127.0.0.1:{0}/remove_oldest -k'.format( - ts2.Variables.ssl_port), - ts=ts2) -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Streams.All = Testers.ContainsExpression('curl test', 'Making sure the basics still work') -ts2.Disk.traffic_out.Content = Testers.ContainsExpression('remove oldest session', '') -ts2.Disk.traffic_out.Content += Testers.ContainsExpression('new session to origin', '') -ts2.Disk.traffic_out.Content += Testers.ContainsExpression('reused session to origin', '') -tr.StillRunningAfter = server - -tr = Test.AddTestRun('disable origin session reuse, reuse should fail') -tr.MakeCurlCommandMulti( - '{{curl}} https://127.0.0.1:{0} -k && {{curl}} https://127.0.0.1:{0} -k'.format(ts4.Variables.ssl_port), ts=ts4) -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.StartBefore(ts3) -tr.Processes.Default.StartBefore(ts4) -tr.Processes.Default.Streams.All = Testers.ContainsExpression('curl test', 'Making sure the basics still work') -ts4.Disk.traffic_out.Content = Testers.ContainsExpression('new session to origin', '') -ts4.Disk.traffic_out.Content += Testers.ExcludesExpression('reused session to origin', '') diff --git a/tests/gold_tests/tls/tls_partial_blind_tunnel.test.py b/tests/gold_tests/tls/tls_partial_blind_tunnel.test.py deleted file mode 100644 index 767d97726cc..00000000000 --- a/tests/gold_tests/tls/tls_partial_blind_tunnel.test.py +++ /dev/null @@ -1,103 +0,0 @@ -''' -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -Test.Summary = 'Testing partial_blind_tunnel' - -ts = Test.MakeATSProcess("ts", enable_tls=True) -server_bar = Test.MakeOriginServer("server_bar", ssl=True) -nameserver = Test.MakeDNServer("dns", default='127.0.0.1') - -request_bar_header = {"headers": "GET / HTTP/1.1\r\nHost: bar.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -response_bar_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": "ok bar"} -server_bar.addResponse("sessionlog_bar.json", request_bar_header, response_bar_header) - -# add ssl materials like key, certificates for the server -ts.addSSLfile("ssl/signed-foo.pem") -ts.addSSLfile("ssl/signed-foo.key") -ts.addSSLfile("ssl/signed-bar.pem") -ts.addSSLfile("ssl/signed-bar.key") -ts.addSSLfile("ssl/signer.pem") - -# Need no remap rules. Everything should be processed by sni - -# Make sure the TS server certs are different from the origin certs -ts.Disk.ssl_multicert_yaml.AddLines( - """ -ssl_multicert: - - dest_ip: "*" - ssl_cert_name: signed-foo.pem - ssl_key_name: signed-foo.key -""".split("\n")) - -# Case 1, global config policy=permissive properties=signature -# override for foo.com policy=enforced properties=all -ts.Disk.records_config.update( - { - 'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir), - 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts.Variables.SSLDir), - 'proxy.config.http.connect_ports': '{0} {1}'.format(ts.Variables.ssl_port, server_bar.Variables.SSL_Port), - 'proxy.config.ssl.client.CA.cert.path': '{0}'.format(ts.Variables.SSLDir), - 'proxy.config.ssl.client.CA.cert.filename': 'signer.pem', - 'proxy.config.ssl.client.verify.server.policy': 'PERMISSIVE', - 'proxy.config.dns.nameservers': f"127.0.0.1:{nameserver.Variables.Port}", - 'proxy.config.dns.resolv_conf': 'NULL' - }) -ts.addPrivateConnectAllowYaml() - -# foo.com should terminate. and reconnect via TLS upstream to bar.com -ts.Disk.sni_yaml.AddLines( - [ - "sni:", - "- fqdn: 'foo.com'", - " partial_blind_route: 'localhost:{0}'".format(server_bar.Variables.SSL_Port), - ]) - -tr = Test.AddTestRun("Partial Blind Route") -tr.MakeCurlCommand("--http1.1 -v --resolve 'foo.com:{0}:127.0.0.1' -k https://foo.com:{0}".format(ts.Variables.ssl_port), ts=ts) -tr.ReturnCode = 0 -tr.Processes.Default.StartBefore(server_bar) -tr.Processes.Default.StartBefore(nameserver) -tr.Processes.Default.StartBefore(Test.Processes.ts) -tr.StillRunningAfter = ts - -tr.Processes.Default.Streams.All += Testers.ExcludesExpression("Could Not Connect", "Curl attempt should have succeeded") -tr.Processes.Default.Streams.All += Testers.ExcludesExpression( - "Not Found on Accelerato", "Should not try to remap on Traffic Server") -tr.Processes.Default.Streams.All += Testers.ContainsExpression("HTTP/1.1 200 OK", "Should get a successful response") -tr.Processes.Default.Streams.All += Testers.ContainsExpression("ok bar", "Body is expected") - -tr = Test.AddTestRun("Test Metrics") -tr.Processes.Default.Command = ( - f"{Test.Variables.AtsTestToolsDir}/stdout_wait" + " 'traffic_ctl metric get" + - " proxy.process.http.total_incoming_connections" + " proxy.process.http.total_client_connections" + - " proxy.process.http.total_client_connections_ipv4" + " proxy.process.http.total_client_connections_ipv6" + - " proxy.process.http.total_server_connections" + " proxy.process.http2.total_client_connections" + - " proxy.process.http.connect_requests" + " proxy.process.tunnel.total_client_connections_blind_tcp" + - " proxy.process.tunnel.current_client_connections_blind_tcp" + " proxy.process.tunnel.total_server_connections_blind_tcp" + - " proxy.process.tunnel.current_server_connections_blind_tcp" + " proxy.process.tunnel.total_client_connections_tls_tunnel" + - " proxy.process.tunnel.current_client_connections_tls_tunnel" + " proxy.process.tunnel.total_client_connections_tls_forward" + - " proxy.process.tunnel.current_client_connections_tls_forward" + - " proxy.process.tunnel.total_client_connections_tls_partial_blind" + - " proxy.process.tunnel.current_client_connections_tls_partial_blind" + - " proxy.process.tunnel.total_client_connections_tls_http" + " proxy.process.tunnel.current_client_connections_tls_http" + - " proxy.process.tunnel.total_server_connections_tls" + " proxy.process.tunnel.current_server_connections_tls'" + - f" {Test.TestDirectory}/gold/tls-partial-blind-tunnel-metrics.gold") -# Need to copy over the environment so traffic_ctl knows where to find the unix domain socket -tr.Processes.Default.Env = ts.Env -tr.Processes.Default.ReturnCode = 0 -tr.StillRunningAfter = ts diff --git a/tests/gold_tests/tls/tls_record_size.test.py b/tests/gold_tests/tls/tls_record_size.test.py deleted file mode 100644 index 9b497079539..00000000000 --- a/tests/gold_tests/tls/tls_record_size.test.py +++ /dev/null @@ -1,123 +0,0 @@ -''' -Exercise fixed and dynamic TLS record sizing. On a large TLS download the body -must arrive intact and application-data records on the wire must follow the -configured sizing strategy. -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import os -import sys - -Test.Summary = __doc__ - - -class TestRecordSize: - '''Verify fixed and dynamic TLS record sizing on large downloads.''' - - _server_counter: int = 0 - _ts_counter: int = 0 - - def __init__(self, max_record: int, body_len: int) -> None: - '''Declare the test Processes.''' - self._max_record = max_record - self._body_len = body_len - self._server = self._configure_server() - self._ts = self._configure_trafficserver() - - def _configure_server(self) -> 'Process': - '''Configure the origin server with a large response body. - - :return: The origin server Process. - ''' - server = Test.MakeOriginServer(f'server-{TestRecordSize._server_counter}') - TestRecordSize._server_counter += 1 - - body = "x" * self._body_len - request_header = {"headers": "GET /obj HTTP/1.1\r\nHost: ex.test\r\n\r\n", "timestamp": "1469733493.993", "body": ""} - response_header = { - "headers": - "HTTP/1.1 200 OK\r\nServer: microserver\r\nConnection: close\r\n" - f"Cache-Control: max-age=3600\r\nContent-Length: {self._body_len}\r\n\r\n", - "timestamp": "1469733493.993", - "body": body - } - server.addResponse("sessionlog.json", request_header, response_header) - return server - - def _configure_trafficserver(self) -> 'Process': - '''Configure Traffic Server with the requested record-size strategy. - - :return: The Traffic Server Process. - ''' - ts = Test.MakeATSProcess(f'ts-{TestRecordSize._ts_counter}', enable_tls=True) - TestRecordSize._ts_counter += 1 - - ts.addDefaultSSLFiles() - ts.Disk.ssl_multicert_yaml.AddLines( - """ -ssl_multicert: - - dest_ip: "*" - ssl_cert_name: server.pem - ssl_key_name: server.key -""".split("\n")) - ts.Disk.remap_config.AddLine(f'map / http://127.0.0.1:{self._server.Variables.Port}') - ts.Disk.records_config.update( - { - 'proxy.config.ssl.server.cert.path': f'{ts.Variables.SSLDir}', - 'proxy.config.ssl.server.private_key.path': f'{ts.Variables.SSLDir}', - 'proxy.config.ssl.max_record_size': self._max_record, - }) - if self._max_record == -1: - ts.Disk.traffic_out.Content = Testers.ExcludesExpression( - r'proxy\.config\.ssl\.max_record_size.*Validity Check error', - 'The dynamic record-size sentinel should pass records validation') - return ts - - def run(self) -> None: - '''Configure and run the TestRun. - - The client downloads the object and measures the TLS records on the wire. - ''' - if self._max_record == -1: - description = 'max_record_size=-1 dynamically sizes records on a large TLS download' - client_option = '--dynamic' - expected_output = 'PASS: TLS records ramp from small to large after the dynamic threshold' - else: - description = 'max_record_size>0 clamps records on a large TLS download' - client_option = f'--max-record {self._max_record}' - expected_output = 'PASS: every application-data record is within the configured clamp' - - tr = Test.AddTestRun(description) - tr.Processes.Default.StartBefore(self._server) - tr.Processes.Default.StartBefore(self._ts) - tr.Processes.Default.Command = ( - f'{sys.executable} {os.path.join(Test.TestDirectory, "tls_record_size_client.py")} ' - f'-p {self._ts.Variables.ssl_port} --host ex.test --path /obj ' - f'{client_option} --expect-bytes {self._body_len}') - tr.Processes.Default.ReturnCode = 0 - tr.Processes.Default.Streams.All += Testers.ContainsExpression( - expected_output, 'TLS records must follow the configured sizing strategy') - tr.StillRunningAfter = self._ts - tr.StillRunningAfter = self._server - - -# The fixed-size test response is comfortably larger than its 4,096-byte clamp, -# ensuring that many records exercise the clamp. The dynamic-sizing test -# response must exceed its 1,000,000-byte threshold by enough data to demonstrate -# both phases. -TestRecordSize(4096, 1024 * 1024).run() -TestRecordSize(-1, 2 * 1024 * 1024).run() diff --git a/tests/gold_tests/tls/tls_reload_under_load.test.py b/tests/gold_tests/tls/tls_reload_under_load.test.py deleted file mode 100644 index 1828da760d2..00000000000 --- a/tests/gold_tests/tls/tls_reload_under_load.test.py +++ /dev/null @@ -1,100 +0,0 @@ -''' -Existing cert/SNI reload tests reload while the server is idle. This one drives -continuous concurrent TLS handshakes and reloads ssl_multicert.yaml on top of -them, stressing the SSL/BIO ownership boundary of the layered TLS VConnection. -The swapped-in certificate must take effect, every handshake must succeed, and -ATS must not crash. -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import os -import sys - -Test.Summary = __doc__ - - -class TestTlsReloadUnderLoad: - '''Reload TLS certs while concurrent handshakes are in flight; serving must not break.''' - - _ts_counter: int = 0 - - def __init__(self) -> None: - '''Declare the test Processes.''' - self._ts = self._configure_trafficserver() - - def _configure_trafficserver(self) -> 'Process': - '''Configure Traffic Server with a swappable live certificate. - - :return: The Traffic Server Process. - ''' - ts = Test.MakeATSProcess(f'ts-{TestTlsReloadUnderLoad._ts_counter}', enable_tls=True, enable_cache=False) - TestTlsReloadUnderLoad._ts_counter += 1 - - # signed-bar is the live cert; signed2-bar shares its key and is copied over - # the live cert at reload time. Distinct fingerprints let the client prove the - # swap took effect under load. - ts.addSSLfile("ssl/signed-bar.pem") - ts.addSSLfile("ssl/signed-bar.key") - ts.addSSLfile("ssl/signed2-bar.pem") - - ts.Disk.ssl_multicert_yaml.AddLines( - """ -ssl_multicert: - - dest_ip: "*" - ssl_cert_name: signed-bar.pem - ssl_key_name: signed-bar.key -""".split("\n")) - ts.Disk.records_config.update( - { - 'proxy.config.ssl.server.cert.path': f'{ts.Variables.SSLDir}', - 'proxy.config.ssl.server.private_key.path': f'{ts.Variables.SSLDir}', - 'proxy.config.exec_thread.autoconfig.scale': 1.0, - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'ssl', - }) - - # The reload must actually have run (otherwise the test would be vacuous). - ts.Disk.diags_log.Content = Testers.ContainsExpression( - "ssl_multicert.yaml finished loading", "the cert configuration must reload while load is in flight") - # The reload-under-load must not crash or trip an assertion / sanitizer. - ts.Disk.traffic_out.Content = Testers.ExcludesExpression( - "received signal|failed assertion", "ATS must not crash reloading certs under load") - ts.Disk.traffic_out.Content += Testers.ExcludesExpression( - "AddressSanitizer|use-after-free|runtime error:", "no memory-safety error reloading certs under load") - return ts - - def run(self) -> None: - '''Configure and run the TestRun.''' - tr = Test.AddTestRun("reload certs while TLS handshakes are in flight") - tr.Processes.Default.StartBefore(self._ts) - tr.Processes.Default.Command = ( - f'{sys.executable} {os.path.join(Test.TestDirectory, "tls_reload_under_load_client.py")} ' - f'-p {self._ts.Variables.ssl_port} --sni bar.com --ssldir {self._ts.Variables.SSLDir} ' - f'--live-cert signed-bar.pem ' - f'--v2-cert {os.path.join(self._ts.Variables.SSLDir, "signed2-bar.pem")} ' - f'--reloads 3 --duration 8 --concurrency 4') - # traffic_ctl (invoked by the client to reload) needs the runroot environment. - tr.Processes.Default.Env = self._ts.Env - tr.Processes.Default.ReturnCode = 0 - tr.Processes.Default.Streams.All = Testers.ContainsExpression("RESULT=PASS", "load + reload run must pass") - tr.Processes.Default.Streams.All += Testers.ContainsExpression("FAILURES=0", "no handshake may fail across the reload") - tr.Processes.Default.Streams.All += Testers.ContainsExpression( - "CERT_CHANGED=1", "the swapped-in certificate must be served after the reload") - tr.StillRunningAfter = self._ts - - -TestTlsReloadUnderLoad().run() diff --git a/tests/gold_tests/tls/tls_renegotiation.test.py b/tests/gold_tests/tls/tls_renegotiation.test.py deleted file mode 100644 index 10d6c9db1e3..00000000000 --- a/tests/gold_tests/tls/tls_renegotiation.test.py +++ /dev/null @@ -1,129 +0,0 @@ -''' -With client renegotiation disallowed (the default), a TLSv1.2 client that asks to -renegotiate must have its connection refused without taking down the server. -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import os -import sys - -Test.Summary = __doc__ - -# Renegotiation only exists in TLS 1.2 and earlier. -Test.SkipUnless(Condition.HasOpenSSLVersion("1.1.1"), Condition.HasLegacyTLSSupport()) - - -class TestRenegotiationRefused: - '''Verify a refused client-initiated renegotiation does not crash ATS.''' - - _server_counter: int = 0 - _ts_counter: int = 0 - - def __init__(self) -> None: - '''Declare the test Processes.''' - self._server = self._configure_server() - self._ts = self._configure_trafficserver() - - def _configure_server(self) -> 'Process': - '''Configure the origin server. - - :return: The origin server Process. - ''' - server = Test.MakeOriginServer(f'server-{TestRenegotiationRefused._server_counter}') - TestRenegotiationRefused._server_counter += 1 - - request_header = {"headers": "GET / HTTP/1.1\r\nHost: example.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} - response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": "ok"} - server.addResponse("sessionlog.json", request_header, response_header) - return server - - def _configure_trafficserver(self) -> 'Process': - '''Configure Traffic Server to refuse client renegotiation (the default). - - :return: The Traffic Server Process. - ''' - ts = Test.MakeATSProcess(f'ts-{TestRenegotiationRefused._ts_counter}', enable_tls=True) - TestRenegotiationRefused._ts_counter += 1 - - ts.addSSLfile("ssl/server.pem") - ts.addSSLfile("ssl/server.key") - ts.Disk.ssl_multicert_yaml.AddLines( - """ -ssl_multicert: - - dest_ip: "*" - ssl_cert_name: server.pem - ssl_key_name: server.key -""".split("\n")) - ts.Disk.records_config.update( - { - 'proxy.config.ssl.server.cert.path': f'{ts.Variables.SSLDir}', - 'proxy.config.ssl.server.private_key.path': f'{ts.Variables.SSLDir}', - # The secure default: never honor a client-initiated renegotiation. - 'proxy.config.ssl.allow_client_renegotiation': 0, - # Renegotiation requires a sub-TLS1.3 protocol. - 'proxy.config.ssl.TLSv1_3.enabled': 0, - 'proxy.config.ssl.TLSv1_2': 1, - # So the renegotiation-detection log line below is emitted. - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'ssl_load', - }) - ts.Disk.remap_config.AddLine(f'map / http://127.0.0.1:{self._server.Variables.Port}') - - # The refused renegotiation must not abort the process. - ts.Disk.traffic_out.Content = Testers.ExcludesExpression( - "received signal|failed assertion", "ATS must refuse the renegotiation without crashing") - # ...and, on OpenSSL, it must actually reach ATS's renegotiation-detection - # path (otherwise a no-crash pass could mean the client never managed to - # renegotiate at all). BoringSSL rejects a peer-initiated renegotiation inside - # the library before ATS's info callback runs -- SSL_get_state() there only - # ever returns SSL_ST_INIT or SSL_ST_OK, never SSL_ST_RENEGOTIATE -- so the - # detection line is never logged. The no-crash check above still covers it. - if Condition.IsOpenSSL(): - ts.Disk.traffic_out.Content += Testers.ContainsExpression( - "trying to renegotiate from the client", "ATS must detect the client-initiated renegotiation") - return ts - - def run(self) -> None: - '''Configure and run the TestRuns.''' - # Complete a TLSv1.2 handshake, then ask to renegotiate. ATS must detect - # and refuse the renegotiation without aborting the process. - tr = Test.AddTestRun("client renegotiation is refused without crashing ATS") - tr.Processes.Default.StartBefore(self._server) - tr.Processes.Default.StartBefore(self._ts) - tr.Processes.Default.Command = ( - f'{sys.executable} {os.path.join(Test.TestDirectory, "tls_renegotiation_client.py")} ' - f'-p {self._ts.Variables.ssl_port} -s example.com') - tr.Processes.Default.ReturnCode = 0 - tr.StillRunningAfter = self._ts - tr.StillRunningAfter = self._server - - # ATS survived, so a normal request still succeeds. - tr = Test.AddTestRun("server still serves requests after the refused renegotiation") - tr.MakeCurlCommand( - ( - "-v --http1.1 --tls-max 1.2 --tlsv1.2 --ciphers DEFAULT@SECLEVEL=0 -k " - f"--resolve 'example.com:{self._ts.Variables.ssl_port}:127.0.0.1' " - f"https://example.com:{self._ts.Variables.ssl_port}/"), - ts=self._ts) - tr.Processes.Default.ReturnCode = 0 - # curl -v writes the response status line to stderr. - tr.Processes.Default.Streams.stderr = Testers.ContainsExpression( - "HTTP/1.1 200 OK", "request after renegotiation should succeed") - tr.StillRunningAfter = self._ts - - -TestRenegotiationRefused().run() diff --git a/tests/gold_tests/tls/tls_renegotiation_allowed.test.py b/tests/gold_tests/tls/tls_renegotiation_allowed.test.py deleted file mode 100644 index a5f58ff3581..00000000000 --- a/tests/gold_tests/tls/tls_renegotiation_allowed.test.py +++ /dev/null @@ -1,145 +0,0 @@ -''' -With client renegotiation allowed, a TLSv1.2 client that asks to renegotiate must -get a prompt answer from ATS (the renegotiation either completes or is refused -with an alert) rather than hanging. ATS must flush the SSL protocol output the -renegotiation generates; otherwise the answer is never sent and the client -stalls. -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import os -import sys - -from ports import get_port - -Test.Summary = __doc__ - -# Renegotiation only exists in TLS 1.2 and earlier. -Test.SkipUnless(Condition.HasOpenSSLVersion("1.1.1"), Condition.HasLegacyTLSSupport()) - - -class TestRenegotiationAllowed: - '''Verify an allowed client renegotiation is answered promptly, not stranded.''' - - _server_counter: int = 0 - _ts_counter: int = 0 - - def __init__(self) -> None: - '''Declare the test Processes.''' - self._server = self._configure_server() - self._ts = self._configure_trafficserver() - - def _configure_server(self) -> 'Process': - '''Configure a plain HTTP/1.0 origin that closes each connection. - - A python http.server origin answers each request on its own connection and - closes it, so ATS's client-facing TLS write face is fully drained between - requests. When the renegotiation ClientHello then arrives there is no - in-flight transport write to coincidentally carry the renegotiation answer - out -- exactly the condition under which a proxy that fails to flush its own - SSL protocol output would strand the answer and hang the client. (The - Proxy-Verifier microserver keeps the response connection in a state that - masks the stall, so it is unsuitable for this regression.) - - :return: The origin server Process. - ''' - server = Test.Processes.Process(f'server-{TestRenegotiationAllowed._server_counter}') - TestRenegotiationAllowed._server_counter += 1 - - origin_dir = os.path.join(Test.RunDirectory, "origin") - os.makedirs(origin_dir, exist_ok=True) - with open(os.path.join(origin_dir, "index.html"), "w") as f: - f.write("ok\n") - - origin_port = get_port(server, "http_port") - server.Command = f'{sys.executable} -m http.server {origin_port} --bind 127.0.0.1 --directory {origin_dir}' - server.Ready = When.PortOpenv4(origin_port) - # The server is a long-running process the harness terminates at the end of - # the test, so its exit status is whatever the signal leaves behind. - server.ReturnCode = Any(None, 0, -2, -15) - return server - - def _configure_trafficserver(self) -> 'Process': - '''Configure Traffic Server to honor client renegotiation. - - :return: The Traffic Server Process. - ''' - ts = Test.MakeATSProcess(f'ts-{TestRenegotiationAllowed._ts_counter}', enable_tls=True) - TestRenegotiationAllowed._ts_counter += 1 - - ts.addSSLfile("ssl/server.pem") - ts.addSSLfile("ssl/server.key") - ts.Disk.ssl_multicert_yaml.AddLines( - """ -ssl_multicert: - - dest_ip: "*" - ssl_cert_name: server.pem - ssl_key_name: server.key -""".split("\n")) - ts.Disk.records_config.update( - { - 'proxy.config.ssl.server.cert.path': f'{ts.Variables.SSLDir}', - 'proxy.config.ssl.server.private_key.path': f'{ts.Variables.SSLDir}', - # The operator opts in: honor a client-initiated renegotiation. - 'proxy.config.ssl.allow_client_renegotiation': 1, - # Renegotiation requires a sub-TLS1.3 protocol. - 'proxy.config.ssl.TLSv1_3.enabled': 0, - 'proxy.config.ssl.TLSv1_2': 1, - }) - ts.Disk.remap_config.AddLine(f'map / http://127.0.0.1:{self._server.Variables.http_port}') - - # The renegotiation must not abort the process. - ts.Disk.traffic_out.Content = Testers.ExcludesExpression( - "received signal|failed assertion", "ATS must service the renegotiation without crashing") - return ts - - def run(self) -> None: - '''Configure and run the TestRuns.''' - # Complete a TLSv1.2 handshake and one request, let the response drain, ask - # to renegotiate, then drive a second request. ATS must answer the - # renegotiation promptly; the helper reports RENEGOTIATION-STALLED (and - # exits non-zero) if the client hangs waiting for the answer. - tr = Test.AddTestRun("client renegotiation is answered promptly") - tr.Processes.Default.StartBefore(self._server) - tr.Processes.Default.StartBefore(self._ts) - tr.Processes.Default.Command = ( - f'{sys.executable} {os.path.join(Test.TestDirectory, "tls_renegotiation_allowed_client.py")} ' - f'-p {self._ts.Variables.ssl_port} -s example.com') - tr.Processes.Default.ReturnCode = 0 - tr.Processes.Default.Streams.stdout = Testers.ContainsExpression( - "RENEGOTIATION-COMPLETED|RENEGOTIATION-REFUSED-PROMPTLY", "the renegotiation must be answered, not stranded") - tr.Processes.Default.Streams.stdout += Testers.ExcludesExpression( - "RENEGOTIATION-STALLED", "the client must not hang waiting for the renegotiation answer") - tr.StillRunningAfter = self._ts - tr.StillRunningAfter = self._server - - # ATS survived and keeps serving, so a normal request still succeeds. - tr = Test.AddTestRun("server still serves requests after the renegotiation") - tr.MakeCurlCommand( - ( - "-v --http1.1 --tls-max 1.2 --tlsv1.2 --ciphers DEFAULT@SECLEVEL=0 -k " - f"--resolve 'example.com:{self._ts.Variables.ssl_port}:127.0.0.1' " - f"https://example.com:{self._ts.Variables.ssl_port}/"), - ts=self._ts) - tr.Processes.Default.ReturnCode = 0 - # curl -v writes the response status line to stderr. - tr.Processes.Default.Streams.stderr = Testers.ContainsExpression( - "HTTP/1.1 200 OK", "request after renegotiation should succeed") - tr.StillRunningAfter = self._ts - - -TestRenegotiationAllowed().run() diff --git a/tests/gold_tests/tls/tls_secret_update_default.test.py b/tests/gold_tests/tls/tls_secret_update_default.test.py deleted file mode 100644 index ae530c70ad8..00000000000 --- a/tests/gold_tests/tls/tls_secret_update_default.test.py +++ /dev/null @@ -1,234 +0,0 @@ -''' -Test default server certificate updates through TSSslSecretSet/TSSslSecretUpdate. -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import os -import re - -Test.Summary = ''' -Verify that secret updates refresh the default server SSL_CTX used without SNI. -''' - - -class TestDefaultSecretUpdate: - '''Verify default server certificate updates through the secret API.''' - - initial_cert: str = 'signed-bar.pem' - updated_cert: str = 'signed2-bar.pem' - key_file: str = 'signed-bar.key' - shadowed_cert: str = 'signed-foo.pem' - updated_shadowed_cert: str = 'signed2-foo.pem' - shadowed_key_file: str = 'signed-foo.key' - plugin: str = 'ssl_secret_load_test.so' - - def __init__(self) -> None: - '''Configure the ATS process, origin server, and test runs.''' - self._configure_server() - self._configure_traffic_server() - self._add_start_run() - self._add_config_touch_run() - self._add_config_reload_run() - self._add_config_reload_wait_run() - self._add_initial_certificate_run() - self._add_mtime_delay_run() - self._add_shadowed_certificate_update_run() - self._add_secret_update_wait_run(self.shadowed_cert) - self._add_unchanged_default_certificate_run() - self._add_default_certificate_update_run() - self._add_secret_update_wait_run(self.initial_cert) - self._add_updated_certificate_run() - - def _configure_server(self) -> 'Process': - '''Configure the origin server.''' - server = Test.MakeOriginServer('server') - self._server = server - - request_header = {'headers': 'GET / HTTP/1.1\r\nHost: doesnotmatter\r\n\r\n', 'timestamp': '1469733493.993', 'body': ''} - response_header = {'headers': 'HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n', 'timestamp': '1469733493.993', 'body': ''} - server.addResponse('sessionlog.json', request_header, response_header) - return server - - def _configure_traffic_server(self) -> 'Process': - '''Configure the Traffic Server process.''' - ts = Test.MakeATSProcess('ts', enable_tls=True) - self._ts = ts - - ts.addSSLfile(f'ssl/{self.initial_cert}') - ts.addSSLfile(f'ssl/{self.updated_cert}') - ts.addSSLfile(f'ssl/{self.key_file}') - ts.addSSLfile(f'ssl/{self.shadowed_cert}') - ts.addSSLfile(f'ssl/{self.updated_shadowed_cert}') - ts.addSSLfile(f'ssl/{self.shadowed_key_file}') - Test.PrepareTestPlugin(os.path.join(Test.Variables.AtsTestPluginsDir, self.plugin), ts) - - ts.Disk.records_config.update( - { - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'ssl_secret_load_test', - 'proxy.config.ssl.server.cert.path': f'{ts.Variables.SSLDir}/../', - 'proxy.config.ssl.server.private_key.path': f'{ts.Variables.SSLDir}/../', - 'proxy.config.ssl.server.multicert.concurrency': 1, - 'proxy.config.exec_thread.autoconfig.scale': 1.0, - 'proxy.config.url_remap.pristine_host_hdr': 1, - }) - - ts.Disk.ssl_multicert_yaml.AddLines( - [ - 'ssl_multicert:', - ' - dest_ip: "*"', - f' ssl_cert_name: {self.initial_cert}', - f' ssl_key_name: {self.key_file}', - ' - dest_ip: "*"', - f' ssl_cert_name: {self.shadowed_cert}', - f' ssl_key_name: {self.shadowed_key_file}', - ]) - ts.Disk.remap_config.AddLine(f'map / http://127.0.0.1:{self._server.Variables.Port}') - return ts - - def _add_start_run(self) -> 'TestRun': - '''Start ATS before reloading the multicert configuration serially.''' - tr = Test.AddTestRun('Start ATS') - tr.Processes.Default.Command = 'echo starting ATS' - tr.Processes.Default.ReturnCode = 0 - tr.Processes.Default.StartBefore(self._server) - tr.Processes.Default.StartBefore(self._ts) - self._keep_processes_running(tr) - return tr - - def _add_config_reload_run(self) -> 'TestRun': - '''Reload the multicert configuration with deterministic ordering.''' - tr = Test.AddTestRun('Reload the multicert configuration serially') - tr.Processes.Default.Command = 'traffic_ctl config reload' - tr.Processes.Default.Env = self._ts.Env - tr.Processes.Default.ReturnCode = 0 - self._keep_processes_running(tr) - return tr - - def _add_config_touch_run(self) -> 'TestRun': - '''Mark the multicert configuration for reload.''' - tr = Test.AddTestRun('Mark the multicert configuration for reload') - tr.Processes.Default.Command = f'touch {self._ts.Disk.ssl_multicert_yaml.AbsPath}' - tr.Processes.Default.ReturnCode = 0 - self._keep_processes_running(tr) - return tr - - def _add_config_reload_wait_run(self) -> 'TestRun': - '''Wait for the serial multicert configuration reload to finish.''' - tr = Test.AddTestRun('Wait for the multicert configuration reload') - tr.Processes.Default.Command = 'echo awaiting multicert reload' - tr.Processes.Default.Env = self._ts.Env - tr.Processes.Default.ReturnCode = 0 - await_reload = tr.Processes.Process('await_reload', 'sleep 30') - await_reload.Ready = When.FileContains(self._ts.Disk.diags_log.Name, 'ssl_multicert.yaml finished loading', 2) - tr.Processes.Default.StartBefore(await_reload) - self._keep_processes_running(tr) - return tr - - def _add_initial_certificate_run(self) -> 'TestRun': - '''Verify the original default certificate before the secret update.''' - tr = self._add_curl_run( - 'Initial default certificate', - expected_cn='signer.yahoo.com', - unexpected_cn='signer2.yahoo.com', - expected_description='Initial cert uses signer.', - unexpected_description='Initial cert is not updated yet.') - tr.Processes.Default.Streams.All += Testers.ContainsExpression(r'subject:.*CN=bar\.com', 'Default cert is bar.com.') - tr.Processes.Default.Streams.All += Testers.ExcludesExpression(r'subject:.*CN=foo\.com', 'Shadowed cert is not served.') - return tr - - def _add_mtime_delay_run(self) -> 'TestRun': - '''Make the replacement certificate mtime differ from the original.''' - tr = Test.AddTestRun('Make the cert mtime differ') - tr.Processes.Default.Command = 'sleep 2' - tr.Processes.Default.ReturnCode = 0 - self._keep_processes_running(tr) - return tr - - def _add_shadowed_certificate_update_run(self) -> 'TestRun': - '''Replace the certificate for the shadowed wildcard policy.''' - tr = Test.AddTestRun('Update the shadowed wildcard certificate') - tr.Setup.CopyAs(f'ssl/{self.updated_shadowed_cert}', '.', f'{self._ts.Variables.SSLDir}/{self.shadowed_cert}') - tr.Processes.Default.Command = f'touch {self._ts.Variables.SSLDir}/{self.shadowed_cert}' - tr.Processes.Default.ReturnCode = 0 - self._keep_processes_running(tr) - return tr - - def _add_unchanged_default_certificate_run(self) -> 'TestRun': - '''Verify that refreshing a shadowed wildcard does not replace the default.''' - tr = self._add_curl_run( - 'Default certificate unchanged after shadowed wildcard update', - expected_cn='signer.yahoo.com', - unexpected_cn='signer2.yahoo.com', - expected_description='Default cert still uses signer.', - unexpected_description='Shadowed wildcard cert did not become the default.') - tr.Processes.Default.Streams.All += Testers.ContainsExpression(r'subject:.*CN=bar\.com', 'Default cert is still bar.com.') - tr.Processes.Default.Streams.All += Testers.ExcludesExpression( - r'subject:.*CN=foo\.com', 'Shadowed foo.com cert is not served.') - return tr - - def _add_default_certificate_update_run(self) -> 'TestRun': - '''Replace the plugin backing default certificate file.''' - tr = Test.AddTestRun('Update the default certificate') - tr.Setup.CopyAs(f'ssl/{self.updated_cert}', '.', f'{self._ts.Variables.SSLDir}/{self.initial_cert}') - tr.Processes.Default.Command = f'touch {self._ts.Variables.SSLDir}/{self.initial_cert}' - tr.Processes.Default.ReturnCode = 0 - self._keep_processes_running(tr) - return tr - - def _add_secret_update_wait_run(self, secret_name: str) -> 'TestRun': - '''Wait for the test plugin to update the certificate secret.''' - tr = Test.AddTestRun(f'Wait for the {secret_name} secret update') - tr.Processes.Default.Command = 'echo awaiting secret update' - tr.Processes.Default.ReturnCode = 0 - await_update = tr.Processes.Process('await_update', 'sleep 30') - await_update.Ready = When.FileContains( - self._ts.Disk.traffic_out.Name, f'updated cert for secret .*{re.escape(secret_name)}') - tr.Processes.Default.StartBefore(await_update) - self._keep_processes_running(tr) - return tr - - def _add_updated_certificate_run(self) -> 'TestRun': - '''Verify the updated default certificate after the secret update.''' - return self._add_curl_run( - 'Updated default certificate', - expected_cn='signer2.yahoo.com', - unexpected_cn='signer.yahoo.com', - expected_description='Updated cert uses signer2.', - unexpected_description='Updated cert no longer uses signer.') - - def _add_curl_run( - self, name: str, expected_cn: str, unexpected_cn: str, expected_description: str, - unexpected_description: str) -> 'TestRun': - '''Add a curl run against ATS without SNI.''' - tr = Test.AddTestRun(name) - self._keep_processes_running(tr) - tr.MakeCurlCommand( - f"-k -v --http1.1 -H 'host: doesnotmatter' https://127.0.0.1:{self._ts.Variables.ssl_port}/", ts=self._ts) - tr.Processes.Default.ReturnCode = 0 - tr.Processes.Default.Streams.All = Testers.ContainsExpression(f'issuer:.*CN={re.escape(expected_cn)}', expected_description) - tr.Processes.Default.Streams.All += Testers.ExcludesExpression( - f'issuer:.*CN={re.escape(unexpected_cn)}', unexpected_description) - return tr - - def _keep_processes_running(self, tr: 'TestRun') -> None: - '''Keep ATS and the origin server running after a TestRun.''' - tr.StillRunningAfter = self._ts - tr.StillRunningAfter = self._server - - -TestDefaultSecretUpdate() diff --git a/tests/gold_tests/tls/tls_session_key_logging.test.py b/tests/gold_tests/tls/tls_session_key_logging.test.py deleted file mode 100644 index ecb269d6d6a..00000000000 --- a/tests/gold_tests/tls/tls_session_key_logging.test.py +++ /dev/null @@ -1,102 +0,0 @@ -''' -Test TLS secrets logging. -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import os - -Test.Summary = ''' -Test TLS secrets logging. -''' - - -class TlsKeyloggingTest: - - replay_file = "tls_session_key_logging.replay.yaml" - - server_counter = 0 - ts_counter = 0 - client_counter = 0 - - def __init__(self, enable_secrets_logging): - self.setupOriginServer() - self.setupTS(enable_secrets_logging) - - def setupOriginServer(self): - server_name = f"server_{TlsKeyloggingTest.server_counter}" - TlsKeyloggingTest.server_counter += 1 - self.server = Test.MakeVerifierServerProcess(server_name, TlsKeyloggingTest.replay_file) - - def setupTS(self, enable_secrets_logging): - ts_name = f"ts_{TlsKeyloggingTest.ts_counter}" - TlsKeyloggingTest.ts_counter += 1 - self.ts = Test.MakeATSProcess(ts_name, enable_tls=True, enable_cache=False) - - self.ts.addDefaultSSLFiles() - self.ts.Disk.records_config.update( - { - "proxy.config.ssl.server.cert.path": f'{self.ts.Variables.SSLDir}', - "proxy.config.ssl.server.private_key.path": f'{self.ts.Variables.SSLDir}', - "proxy.config.ssl.client.verify.server.policy": 'PERMISSIVE', - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'ssl_keylog' - }) - self.ts.Disk.ssl_multicert_yaml.AddLines( - """ -ssl_multicert: - - dest_ip: "*" - ssl_cert_name: server.pem - ssl_key_name: server.key -""".split("\n")) - self.ts.Disk.remap_config.AddLine(f'map / https://127.0.0.1:{self.server.Variables.https_port}') - - keylog_file = os.path.join(self.ts.Variables.LOGDIR, "tls_secrets.txt") - - # Remove the keylog_file configuration automatically configured via the - # trafficserver AuTest extension. - self.ts.Disk.records_config.update(''' - ssl: - keylog_file: null - ''') - - if enable_secrets_logging: - self.ts.Disk.records_config.update({ - 'proxy.config.ssl.keylog_file': keylog_file, - }) - - self.ts.Disk.diags_log.Content += Testers.ContainsExpression( - f"Opened {keylog_file} for TLS key logging", "Verify the user was notified of TLS secrets logging.") - self.ts.Disk.File(keylog_file, id="keylog", exists=True) - # It would be nice to verify the content of certain lines in the - # keylog file, but the content is dependent upon the particular TLS - # protocol version. Thus I'm hesitant to add ContainsExpression - # checks here which will be fragile and eventually become outdated. - else: - self.ts.Disk.File(keylog_file, exists=False) - - def run(self): - tr = Test.AddTestRun() - tr.Processes.Default.StartBefore(self.server) - tr.Processes.Default.StartBefore(self.ts) - - client_name = f"client_{TlsKeyloggingTest.client_counter}" - TlsKeyloggingTest.client_counter += 1 - tr.AddVerifierClientProcess(client_name, TlsKeyloggingTest.replay_file, https_ports=[self.ts.Variables.ssl_port]) - - -TlsKeyloggingTest(enable_secrets_logging=False).run() -TlsKeyloggingTest(enable_secrets_logging=True).run() diff --git a/tests/gold_tests/tls/tls_session_reuse.test.py b/tests/gold_tests/tls/tls_session_reuse.test.py deleted file mode 100644 index 3529be510fa..00000000000 --- a/tests/gold_tests/tls/tls_session_reuse.test.py +++ /dev/null @@ -1,160 +0,0 @@ -''' -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import re -import os - -Test.Summary = ''' -Test tls session reuse -''' - -# Define default ATS -ts1 = Test.MakeATSProcess("ts1", enable_tls=True) -ts2 = Test.MakeATSProcess("ts2", enable_tls=True) -server = Test.MakeOriginServer("server") - -# Add info the origin server responses -request_header = {"headers": "GET / HTTP/1.1\r\nHost: www.example.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -server.addResponse("sessionlog.json", request_header, response_header) - -# add ssl materials like key, certificates for the server -ts1.addSSLfile("ssl/server.pem") -ts1.addSSLfile("ssl/server.key") -ts2.addSSLfile("ssl/server.pem") -ts2.addSSLfile("ssl/server.key") - -ts1.Disk.remap_config.AddLine('map / http://127.0.0.1:{0}'.format(server.Variables.Port)) -ts2.Disk.remap_config.AddLine('map / http://127.0.0.1:{0}'.format(server.Variables.Port)) - -ts1.Disk.ssl_multicert_yaml.AddLines( - """ -ssl_multicert: - - dest_ip: "*" - ssl_cert_name: server.pem - ssl_key_name: server.key -""".split("\n")) -ts2.Disk.ssl_multicert_yaml.AddLines( - """ -ssl_multicert: - - dest_ip: "*" - ssl_cert_name: server.pem - ssl_key_name: server.key -""".split("\n")) - -ts1.Disk.records_config.update( - { - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'ssl', - 'proxy.config.ssl.server.cert.path': '{0}'.format(ts1.Variables.SSLDir), - 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts1.Variables.SSLDir), - 'proxy.config.ssl.server.cipher_suite': - 'ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-DSS-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-DSS-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA256:DHE-RSA-AES128-SHA256:DHE-DSS-AES128-SHA256:DHE-RSA-AES256-SHA:DHE-DSS-AES256-SHA:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!aECDH:!EDH-DSS-DES-CBC3-SHA:!EDH-RSA-DES-CBC3-SHA:!KRB5-DES-CBC3-SHA', - 'proxy.config.exec_thread.autoconfig.scale': 1.0, - 'proxy.config.ssl.server.session_ticket.enable': 1, - 'proxy.config.ssl.server.session_ticket.number': 2, - }) -ts2.Disk.records_config.update( - { - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'ssl', - 'proxy.config.ssl.server.cert.path': '{0}'.format(ts2.Variables.SSLDir), - 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts2.Variables.SSLDir), - 'proxy.config.ssl.server.cipher_suite': - 'ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-DSS-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-DSS-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA256:DHE-RSA-AES128-SHA256:DHE-DSS-AES128-SHA256:DHE-RSA-AES256-SHA:DHE-DSS-AES256-SHA:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!aECDH:!EDH-DSS-DES-CBC3-SHA:!EDH-RSA-DES-CBC3-SHA:!KRB5-DES-CBC3-SHA', - 'proxy.config.exec_thread.autoconfig.scale': 1.0, - 'proxy.config.ssl.server.session_ticket.enable': 0, - 'proxy.config.ssl.server.session_ticket.number': 0, - }) - - -def check_session(output_path, tls_ver, reuse_count): - retval = False - f = open(output_path, 'r') - if not f: - err = "Failed to open {0}".format(output_path) - return (retval, "Check session is reused", err) - - content = f.read() - match = re.findall(f'Reused, {tls_ver}', content) - if len(match) == reuse_count: - retval = True - err = "Reused successfully {0} times".format(len(match)) - else: - err = "Session is not being reused as expected" - f.close() - return (retval, "Check session is reused", err) - - -tr1 = Test.AddTestRun("TLSv1.2 Session Resumption Enabled") -tr1.Command = \ - 'printf "GET / HTTP/1.1\r\n" | openssl s_client -connect 127.0.0.1:{0} -sess_out {1} -tls1_2 && ' \ - 'printf "GET / HTTP/1.1\r\n" | openssl s_client -connect 127.0.0.1:{0} -sess_in {1} -tls1_2 && ' \ - 'printf "GET / HTTP/1.1\r\n" | openssl s_client -connect 127.0.0.1:{0} -sess_in {1} -tls1_2 && ' \ - 'printf "GET / HTTP/1.1\r\n" | openssl s_client -connect 127.0.0.1:{0} -sess_in {1} -tls1_2 && ' \ - 'printf "GET / HTTP/1.1\r\n" | openssl s_client -connect 127.0.0.1:{0} -sess_in {1} -tls1_2 && ' \ - 'printf "GET / HTTP/1.1\r\n" | openssl s_client -connect 127.0.0.1:{0} -sess_in {1} -tls1_2' \ - .format(ts1.Variables.ssl_port, os.path.join(Test.RunDirectory, 'sess1.dat')) -tr1.ReturnCode = 0 -tr1.Processes.Default.StartBefore(server) -tr1.Processes.Default.StartBefore(ts1) -tr1.Processes.Default.Streams.All.Content = Testers.Lambda( - lambda info, tester: check_session(tr1.Processes.Default.Streams.All.AbsPath, 'TLSv1.2', 5)) -tr1.StillRunningAfter += server -tr1.StillRunningAfter += ts1 - -tr2 = Test.AddTestRun("TLSv1.3 Session Resumption Enabled") -tr2.Command = \ - 'printf "GET / HTTP/1.1\r\n" | openssl s_client -connect 127.0.0.1:{0} -sess_out {1} -tls1_2 && ' \ - 'printf "GET / HTTP/1.1\r\n" | openssl s_client -connect 127.0.0.1:{0} -sess_in {1} -tls1_2 && ' \ - 'printf "GET / HTTP/1.1\r\n" | openssl s_client -connect 127.0.0.1:{0} -sess_in {1} -tls1_2 && ' \ - 'printf "GET / HTTP/1.1\r\n" | openssl s_client -connect 127.0.0.1:{0} -sess_in {1} -tls1_2 && ' \ - 'printf "GET / HTTP/1.1\r\n" | openssl s_client -connect 127.0.0.1:{0} -sess_in {1} -tls1_2 && ' \ - 'printf "GET / HTTP/1.1\r\n" | openssl s_client -connect 127.0.0.1:{0} -sess_in {1} -tls1_2' \ - .format(ts1.Variables.ssl_port, os.path.join(Test.RunDirectory, 'sess2.dat')) -tr2.ReturnCode = 0 -tr2.Processes.Default.Streams.All.Content = Testers.Lambda( - lambda info, tester: check_session(tr2.Processes.Default.Streams.All.AbsPath, 'TLSv1.2', 5)) -tr2.StillRunningAfter += server - -tr3 = Test.AddTestRun("TLSv1.2 Session Resumption Disabled") -tr3.Command = \ - 'printf "GET / HTTP/1.1\r\n" | openssl s_client -connect 127.0.0.1:{0} -sess_out {1} -tls1_2 && ' \ - 'printf "GET / HTTP/1.1\r\n" | openssl s_client -connect 127.0.0.1:{0} -sess_in {1} -tls1_2 && ' \ - 'printf "GET / HTTP/1.1\r\n" | openssl s_client -connect 127.0.0.1:{0} -sess_in {1} -tls1_2 && ' \ - 'printf "GET / HTTP/1.1\r\n" | openssl s_client -connect 127.0.0.1:{0} -sess_in {1} -tls1_2 && ' \ - 'printf "GET / HTTP/1.1\r\n" | openssl s_client -connect 127.0.0.1:{0} -sess_in {1} -tls1_2 && ' \ - 'printf "GET / HTTP/1.1\r\n" | openssl s_client -connect 127.0.0.1:{0} -sess_in {1} -tls1_2' \ - .format(ts2.Variables.ssl_port, os.path.join(Test.RunDirectory, 'sess3.dat')) -tr3.Processes.Default.StartBefore(ts2) -tr3.Processes.Default.Streams.All = Testers.ExcludesExpression('Reused', '') -tr3.Processes.Default.Streams.All += Testers.ContainsExpression('TLSv1.2', '') -tr3.StillRunningAfter += server -tr3.StillRunningAfter += ts2 - -tr4 = Test.AddTestRun("TLSv1.3 Session Resumption Disabled") -tr4.Command = \ - 'printf "GET / HTTP/1.1\r\n" | openssl s_client -connect 127.0.0.1:{0} -sess_out {1} -tls1_3 && ' \ - 'printf "GET / HTTP/1.1\r\n" | openssl s_client -connect 127.0.0.1:{0} -sess_in {1} -tls1_3 && ' \ - 'printf "GET / HTTP/1.1\r\n" | openssl s_client -connect 127.0.0.1:{0} -sess_in {1} -tls1_3 && ' \ - 'printf "GET / HTTP/1.1\r\n" | openssl s_client -connect 127.0.0.1:{0} -sess_in {1} -tls1_3 && ' \ - 'printf "GET / HTTP/1.1\r\n" | openssl s_client -connect 127.0.0.1:{0} -sess_in {1} -tls1_3 && ' \ - 'printf "GET / HTTP/1.1\r\n" | openssl s_client -connect 127.0.0.1:{0} -sess_in {1} -tls1_3' \ - .format(ts2.Variables.ssl_port, os.path.join(Test.RunDirectory, 'sess4.dat')) -tr4.Processes.Default.Streams.All = Testers.ExcludesExpression('Reused', '') -tr4.Processes.Default.Streams.All += Testers.ContainsExpression('TLSv1.3', '') diff --git a/tests/gold_tests/tls/tls_sni_groups.test.py b/tests/gold_tests/tls/tls_sni_groups.test.py deleted file mode 100644 index 79e9b208288..00000000000 --- a/tests/gold_tests/tls/tls_sni_groups.test.py +++ /dev/null @@ -1,113 +0,0 @@ -''' -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -Test.Summary = ''' -Test SNI configuration server_groups_list -''' -# The groups function was added in OpenSSL 1.1.1 -Test.SkipUnless(Condition.HasOpenSSLVersion("1.1.1")) - -# Define default ATS -ts = Test.MakeATSProcess("ts", enable_tls=True) -server = Test.MakeOriginServer("server", ssl=True) - -request_header = {"headers": "GET / HTTP/1.1\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": "foo ok"} -server.addResponse("sessionlog.json", request_header, response_header) - -# add ssl materials like key, certificates for the server -ts.addSSLfile("ssl/server.pem") -ts.addSSLfile("ssl/server.key") - -# Need no remap rules. Everything should be processed by sni - -# Make sure the TS server certs are different from the origin certs -ts.Disk.ssl_multicert_yaml.AddLines( - """ -ssl_multicert: - - dest_ip: "*" - ssl_cert_name: server.pem - ssl_key_name: server.key -""".split("\n")) - -ts.Disk.records_config.update( - { - 'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir), - 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts.Variables.SSLDir), - 'proxy.config.ssl.client.CA.cert.path': '{0}'.format(ts.Variables.SSLDir), - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'ssl_sni', - }) - -ts.Disk.sni_yaml.AddLines( - [ - 'sni:', - '- fqdn: aaa.com', - ' server_groups_list: X25519MLKEM768', - ' valid_tls_versions_in: [ TLSv1_3 ]', - ' server_TLSv1_3_cipher_suites: TLS_AES_256_GCM_SHA384', - '- fqdn: bbb.com', - ' server_groups_list: x25519', - ' valid_tls_versions_in: [ TLSv1_2 ]', - ' server_cipher_suite: ECDHE-RSA-AES256-GCM-SHA384', - '- fqdn: ccc.com', - ' server_groups_list: ABC123', - ' valid_tls_versions_in: [ TLSv1_2 ]', - ' server_cipher_suite: ECDHE-RSA-AES256-GCM-SHA384', - ]) - -tr = Test.AddTestRun("Test 0: x25519") -tr.Processes.Default.StartBefore(server) -tr.Processes.Default.StartBefore(Test.Processes.ts) -tr.MakeCurlCommand( - "-v --ciphers ECDHE-RSA-AES256-GCM-SHA384 --resolve 'bbb.com:{0}:127.0.0.1' -k https://bbb.com:{0}".format( - ts.Variables.ssl_port), - ts=ts) -tr.ReturnCode = 0 -tr.StillRunningAfter = ts -ts.Disk.traffic_out.Content += Testers.ContainsExpression( - "Setting groups list from server_groups_list to x25519", "Should log setting the server groups") -tr.Processes.Default.Streams.all = Testers.IncludesExpression( - f"SSL connection using TLSv1.2 / ECDHE-RSA-AES256-GCM-SHA384 / x25519", "Curl should log using x25519 in the SSL connection") - -tr = Test.AddTestRun("Test 1: fail") -tr.MakeCurlCommand( - "-v --ciphers ECDHE-RSA-AES256-GCM-SHA384 --resolve 'ccc.com:{0}:127.0.0.1' -k https://ccc.com:{0}".format( - ts.Variables.ssl_port), - ts=ts) -# The error code is 35, which indicates there was a ssl connection error -tr.ReturnCode = 35 -tr.StillRunningAfter = ts -tr.StillRunningAfter = server -ts.Disk.diags_log.Content = Testers.ContainsExpression( - "ERROR: Invalid server_groups_list: ABC123", "Curl attempt should have failed") - -# Hybrid ECDH PQ key exchange TLS groups were added in OpenSSL 3.5 -if Condition.HasOpenSSLVersion("3.5.0"): - tr = Test.AddTestRun("Test 2: X25519MLKEM768") - tr.MakeCurlCommand( - "-v --tls13-ciphers TLS_AES_256_GCM_SHA384 --resolve 'aaa.com:{0}:127.0.0.1' -k https://aaa.com:{0}".format( - ts.Variables.ssl_port), - ts=ts) - tr.ReturnCode = 0 - tr.StillRunningAfter = ts - ts.Disk.traffic_out.Content += Testers.ContainsExpression( - "Setting groups list from server_groups_list to X25519MLKEM768", "Should log setting the server groups") - tr.Processes.Default.Streams.all = Testers.IncludesExpression( - f"SSL connection using TLSv1.3 / TLS_AES_256_GCM_SHA384 / X25519MLKEM768", - f"Curl should log using X25519MLKEM768 in the SSL connection") diff --git a/tests/gold_tests/tls/tls_sni_host_policy.test.py b/tests/gold_tests/tls/tls_sni_host_policy.test.py deleted file mode 100644 index 30f08379971..00000000000 --- a/tests/gold_tests/tls/tls_sni_host_policy.test.py +++ /dev/null @@ -1,306 +0,0 @@ -''' -Test exercising host and SNI mismatch controls -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import os - -Test.Summary = ''' -Test exercising host and SNI mismatch controls -''' - -ts = Test.MakeATSProcess("ts", enable_tls=True) -cafile = "{0}/signer.pem".format(Test.RunDirectory) -cafile2 = "{0}/signer2.pem".format(Test.RunDirectory) -server = Test.MakeOriginServer("server") - -request_header = {"headers": "GET /case1 HTTP/1.1\r\nHost: example.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -server.addResponse("sessionlog.json", request_header, response_header) -request_header = {"headers": "GET / HTTP/1.1\r\nHost: bar.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -server.addResponse("sessionlog.json", request_header, response_header) - -ts.addSSLfile("ssl/server.pem") -ts.addSSLfile("ssl/server.key") -ts.addSSLfile("ssl/signer.pem") - -ts.Disk.records_config.update( - { - 'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir), - 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts.Variables.SSLDir), - 'proxy.config.url_remap.pristine_host_hdr': 1, - 'proxy.config.ssl.CA.cert.filename': '{0}/signer.pem'.format(ts.Variables.SSLDir), - 'proxy.config.exec_thread.autoconfig.scale': 1.0, - 'proxy.config.http.host_sni_policy': 2, - 'proxy.config.ssl.TLSv1_3.enabled': 0, - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'ssl', - }) - -ts.Disk.ssl_multicert_yaml.AddLines( - """ -ssl_multicert: - - dest_ip: "*" - ssl_cert_name: server.pem - ssl_key_name: server.key -""".split("\n")) - -# Just map everything through to origin. This test is concentrating on the user-agent side -ts.Disk.remap_config.AddLine('map / http://127.0.0.1:{0}/'.format(server.Variables.Port)) - -# Scenario 1: Default no client cert required. cert required for bar.com. -# Make boblite and bob mixed case to verify that we can match hostnames case -# insensitively. -ts.Disk.sni_yaml.AddLines( - [ - 'sni:', - '- fqdn: boBliTe', - ' verify_client: STRICT', - ' host_sni_policy: PERMISSIVE', - '- fqdn: bOb', - ' verify_client: STRICT', - '- fqdn: bob.bar.com', - ' verify_client: STRICT', - '- fqdn: dave.bob', - ' verify_client: STRICT', - '- fqdn: noipallow.example.com', - ' http2: off', - '- fqdn: ipallow_nomatch.example.com', - ' ip_allow: 192.168.1.1', - ]) - -# case 1 -# sni=Bob and host=dave. Do not provide client cert. This should match fqdn bOb which has -# verify_client: STRICT and thus should fail. -tr = Test.AddTestRun("Connect to bob without cert") -tr.Processes.Default.StartBefore(Test.Processes.ts) -tr.Processes.Default.StartBefore(server) -tr.StillRunningAfter = ts -tr.StillRunningAfter = server -tr.MakeCurlCommand( - "-v --tls-max 1.2 -k -H 'host:dave' --resolve 'Bob:{0}:127.0.0.1' https://Bob:{0}/case1".format(ts.Variables.ssl_port), ts=ts) -tr.Processes.Default.ReturnCode = 35 - -# case 2 -# sni=Bob and host=dave. Do provide client cert. This should match fqdn bOb which has -# verify_client: STRICT, but since the cert is good it should succeed. -tr = Test.AddTestRun("Connect to bob with good cert") -tr.Setup.Copy("ssl/signed-foo.pem") -tr.Setup.Copy("ssl/signed-foo.key") -tr.StillRunningAfter = ts -tr.StillRunningAfter = server -tr.MakeCurlCommand( - "-v --tls-max 1.2 -k --cert ./signed-foo.pem --key ./signed-foo.key -H 'host:dave' --resolve 'Bob:{0}:127.0.0.1' https://Bob:{0}/case1" - .format(ts.Variables.ssl_port), - ts=ts) -tr.Processes.Default.ReturnCode = 0 - -# case 3 -# sni=dave and host=bob. Do not provide client cert. This should not need a cert, but should still -# fail due to sni-host mismatch. -tr = Test.AddTestRun("Connect to dave without cert") -tr.StillRunningAfter = ts -tr.StillRunningAfter = server -tr.MakeCurlCommand( - "-v --tls-max 1.2 -k -H 'host:Bob' --resolve 'dave:{0}:127.0.0.1' https://dave:{0}/case1".format(ts.Variables.ssl_port), ts=ts) -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Streams.All = Testers.ContainsExpression("Access Denied", "Check response") - -# case 4 -# sni=dave and host=bob. Do provide client cert. Again, this should not need a cert, but provide -# one anyway and verify it fails due to sni-host mismatch. -tr = Test.AddTestRun("Connect to dave with cert") -tr.StillRunningAfter = ts -tr.StillRunningAfter = server -tr.MakeCurlCommand( - "-v --tls-max 1.2 -k --cert ./signed-foo.pem --key ./signed-foo.key -H 'host:bob' --resolve 'dave:{0}:127.0.0.1' https://dave:{0}/case1" - .format(ts.Variables.ssl_port), - ts=ts) -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Streams.All = Testers.ContainsExpression("Access Denied", "Check response") - -# case 5 -# sni=Bob and host=boB. Do provide client cert. Should succeed because the hosts match and the cert is provided. -tr = Test.AddTestRun("Connect to bob with cert") -tr.StillRunningAfter = ts -tr.StillRunningAfter = server -tr.MakeCurlCommand( - "--tls-max 1.2 -k --cert ./signed-foo.pem --key ./signed-foo.key -H 'host:boB' --resolve 'Bob:{0}:127.0.0.1' https://bob:{0}/case1" - .format(ts.Variables.ssl_port), - ts=ts) -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Streams.All = Testers.ExcludesExpression("Access Denied", "Check response") - -# case 6 -# sni=ellen and host=Boblite. Do not provide client cert. Should warn due to sni-host mismatch, -# but should note get an Access Denied because boblite has host_sni_policy: PERMISSIVE configured. -tr = Test.AddTestRun("Connect to ellen without cert") -tr.StillRunningAfter = ts -tr.StillRunningAfter = server -tr.MakeCurlCommand( - "-v --tls-max 1.2 -k -H 'host:Boblite' --resolve 'ellen:{0}:127.0.0.1' https://ellen:{0}/warnonly".format( - ts.Variables.ssl_port), - ts=ts) -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Streams.All = Testers.ExcludesExpression("Access Denied", "Check response") - -# case 7 -# sni=ellen and host=Boblite. Do provide client cert. This should behave the same as above because -# providing a client cert does not mean SNI and hostname will not be compared. -tr = Test.AddTestRun("Connect to ellen with cert") -tr.StillRunningAfter = ts -tr.StillRunningAfter = server -tr.MakeCurlCommand( - "-v --tls-max 1.2 -k --cert ./signed-foo.pem --key ./signed-foo.key -H 'host:Boblite' --resolve 'ellen:{0}:127.0.0.1' https://ellen:{0}/warnonly" - .format(ts.Variables.ssl_port), - ts=ts) -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Streams.All = Testers.ExcludesExpression("Access Denied", "Check response") - -# case 8 -# sni=ellen and host=fran. Do not provide client cert. No warning since neither name is mentioned in sni.yaml -tr = Test.AddTestRun("Connect to ellen without cert") -tr.StillRunningAfter = ts -tr.StillRunningAfter = server -tr.MakeCurlCommand( - "-v --tls-max 1.2 -k -H 'host:fran' --resolve 'ellen:{0}:127.0.0.1' https://ellen:{0}/warnonly".format(ts.Variables.ssl_port), - ts=ts) -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Streams.All = Testers.ExcludesExpression("Access Denied", "Check response") - -# case 9 -# sni=ellen and host=fran. Do provide client cert. No warning since neither name is mentioned in sni.yaml -tr = Test.AddTestRun("Connect to ellen with cert") -tr.StillRunningAfter = ts -tr.StillRunningAfter = server -tr.MakeCurlCommand( - "-v --tls-max 1.2 -k --cert ./signed-foo.pem --key ./signed-foo.key -H 'host:fran' --resolve 'ellen:{0}:127.0.0.1' https://ellen:{0}/warnonly" - .format(ts.Variables.ssl_port), - ts=ts) -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Streams.All = Testers.ExcludesExpression("Access Denied", "Check response") - -# case 10 -# sni=bob.bar.com and host=bob. Do provide client cert. SNI is longer than host but shares the -# same prefix. Should fail due to sni-host mismatch. -tr = Test.AddTestRun("Connect with SNI longer than host sharing prefix") -tr.StillRunningAfter = ts -tr.StillRunningAfter = server -tr.MakeCurlCommand( - "-v --tls-max 1.2 -k --cert ./signed-foo.pem --key ./signed-foo.key -H 'host:bob' --resolve 'bob.bar.com:{0}:127.0.0.1' https://bob.bar.com:{0}/case1" - .format(ts.Variables.ssl_port), - ts=ts) -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Streams.All = Testers.ContainsExpression("Access Denied", "Check response") - -# case 11 -# sni=bob and host=bob.bar.com. Do provide client cert. Host is longer than SNI but shares the -# same prefix. Should fail due to sni-host mismatch. -tr = Test.AddTestRun("Connect with host longer than SNI sharing prefix") -tr.StillRunningAfter = ts -tr.StillRunningAfter = server -tr.MakeCurlCommand( - "-v --tls-max 1.2 -k --cert ./signed-foo.pem --key ./signed-foo.key -H 'host:bob.bar.com' --resolve 'bob:{0}:127.0.0.1' https://bob:{0}/case1" - .format(ts.Variables.ssl_port), - ts=ts) -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Streams.All = Testers.ContainsExpression("Access Denied", "Check response") - -# case 12 -# sni=bob and host=dave.bob. Do provide client cert. Host ends with the SNI value but is a -# different hostname. Should fail due to sni-host mismatch. -tr = Test.AddTestRun("Connect with host ending with SNI value") -tr.StillRunningAfter = ts -tr.StillRunningAfter = server -tr.MakeCurlCommand( - "-v --tls-max 1.2 -k --cert ./signed-foo.pem --key ./signed-foo.key -H 'host:dave.bob' --resolve 'bob:{0}:127.0.0.1' https://bob:{0}/case1" - .format(ts.Variables.ssl_port), - ts=ts) -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Streams.All = Testers.ContainsExpression("Access Denied", "Check response") - -# case 13 -# sni=dave.bob and host=bob. Do provide client cert. SNI ends with the host value but is a -# different hostname. Should fail due to sni-host mismatch. -tr = Test.AddTestRun("Connect with SNI ending with host value") -tr.StillRunningAfter = ts -tr.StillRunningAfter = server -tr.MakeCurlCommand( - "-v --tls-max 1.2 -k --cert ./signed-foo.pem --key ./signed-foo.key -H 'host:bob' --resolve 'dave.bob:{0}:127.0.0.1' https://dave.bob:{0}/case1" - .format(ts.Variables.ssl_port), - ts=ts) -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Streams.All = Testers.ContainsExpression("Access Denied", "Check response") - -# case 14 -# sni=other.example.com and host=ipallow_nomatch.example.com. Host header matches SNI entry but -# client IP is NOT in ip_allow list. TestClientSNIAction should still return true (because ip_addrs is -# non-empty), which means host_sni_policy IS enforced and the mismatch triggers "Access Denied". -tr = Test.AddTestRun("Connect with ip_allow SNI entry not matching client IP should still enforce host_sni_policy") -tr.StillRunningAfter = ts -tr.StillRunningAfter = server -tr.MakeCurlCommand( - "-v --tls-max 1.2 -k -H 'host:ipallow_nomatch.example.com' --resolve 'other.example.com:{0}:127.0.0.1' https://other.example.com:{0}/case1" - .format(ts.Variables.ssl_port), - ts=ts) -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Streams.All = Testers.ContainsExpression("Access Denied", "Should get 403 due to host_sni_policy enforcement") - -# case 15 -# sni=other.example.com and host=noipallow.example.com. Host header matches SNI -# entry that only has http2: off configured (no verify_client, no ip_allow). This should -# NOT trigger host_sni_policy enforcement because the only action is a no-op. -tr = Test.AddTestRun("Connect with SNI entry having no ip_allow should not enforce host_sni_policy") -tr.StillRunningAfter = ts -tr.StillRunningAfter = server -tr.MakeCurlCommand( - "-v --tls-max 1.2 -k -H 'host:noipallow.example.com' --resolve 'other.example.com:{0}:127.0.0.1' https://other.example.com:{0}/case1" - .format(ts.Variables.ssl_port), - ts=ts) -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Streams.All = Testers.ExcludesExpression("Access Denied", "No 403 for non-ip_allow SNI entry") - -# Wait for the error.log entry to be written. -test_run = Test.AddAwaitFileContainsTestRun( - 'Await SNI mismatch error log entry.', - os.path.join(ts.Variables.LOGDIR, 'error.log'), - "SNI/hostname mismatch: connecting to .* for host='bob' sni='dave', returning a 403", -) - -ts.Disk.diags_log.Content += Testers.ContainsExpression( - "WARNING: SNI/hostname mismatch sni=dave host=bob action=terminate", "Should have warning on mismatch") -ts.Disk.diags_log.Content += Testers.ContainsExpression( - "WARNING: SNI/hostname mismatch sni=ellen host=Boblite action=continue", "Should have warning on mismatch") -ts.Disk.diags_log.Content += Testers.ContainsExpression( - "WARNING: SNI/hostname mismatch sni=bob.bar.com host=bob action=terminate", "Should have warning on prefix mismatch") -ts.Disk.diags_log.Content += Testers.ContainsExpression( - "WARNING: SNI/hostname mismatch sni=bob host=bob.bar.com action=terminate", "Should have warning on prefix mismatch") -ts.Disk.diags_log.Content += Testers.ContainsExpression( - "WARNING: SNI/hostname mismatch sni=bob host=dave.bob action=terminate", "Should have warning on suffix mismatch") -ts.Disk.diags_log.Content += Testers.ContainsExpression( - "WARNING: SNI/hostname mismatch sni=dave.bob host=bob action=terminate", "Should have warning on suffix mismatch") -ts.Disk.diags_log.Content += Testers.ExcludesExpression( - "WARNING: SNI/hostname mismatch sni=ellen host=fran", "Should not have warning on mismatch with non-policy host") -ts.Disk.diags_log.Content += Testers.ExcludesExpression( - "WARNING: SNI/hostname mismatch sni=other.example.com host=noipallow.example.com", - "Should not have warning for SNI entry with no ip_allow") - -test_run.Processes.Default.ReturnCode = 0 -ts.Disk.error_log.Content += Testers.ContainsExpression( - "SNI/hostname mismatch: connecting to .* for host='bob' sni='dave', returning a 403", - "error.log should contain information about the 403 response.") diff --git a/tests/gold_tests/tls/tls_sni_ip_allow.test.py b/tests/gold_tests/tls/tls_sni_ip_allow.test.py deleted file mode 100644 index b2ac0f5ebe8..00000000000 --- a/tests/gold_tests/tls/tls_sni_ip_allow.test.py +++ /dev/null @@ -1,184 +0,0 @@ -''' -Test exercising ip_allow configuration of sni.yaml -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import os - -Test.Summary = '''Test sni.yaml ip_allow.''' - - -class ConnectionType: - GET = 0 - TUNNEL = 1 - PROXY = 2 - - -class TestSniIpAllow: - '''Verify ip_allow of sni.yaml.''' - _dns_counter: int = 0 - _server_counter: int = 0 - _ts_counter: int = 0 - _client_counter: int = 0 - - def __init__(self, connect_type: int) -> None: - """Configure a test run. - :param connect_type: The type of connection to use. - """ - tr = Test.AddTestRun(f'Verify ip_allow of sni.yaml ({connect_type})') - - if connect_type == ConnectionType.GET: - self._replay_file: str = "replay/ip_allow.replay.yaml" - elif connect_type == ConnectionType.TUNNEL: - self._replay_file: str = "replay/ip_allow_tunnel.replay.yaml" - elif connect_type == ConnectionType.PROXY: - self._replay_file: str = "replay/ip_allow_proxy.replay.yaml" - else: - raise ValueError(f'Invalid connect_type: {connect_type}') - - self._dns = self._configure_dns(tr) - self._server = self._configure_server(tr) - self._ts = self._configure_trafficserver(tr, connect_type, self._dns, self._server) - self._configure_client(tr, self._dns, self._server, self._ts, connect_type) - - def _configure_dns(self, tr: 'TestRun') -> 'Process': - """Configure a DNS for the TestRun. - :param tr: The TestRun to configure with the DNS. - :return: The DNS Process. - """ - name = f'dns{TestSniIpAllow._dns_counter}' - dns = tr.MakeDNServer(name, default='127.0.0.1') - TestSniIpAllow._dns_counter += 1 - return dns - - def _configure_server(self, tr: 'TestRun') -> 'Process': - """Configure an Origin Server for the TestRun. - :param tr: The TestRun to configure with the Origin Server. - :return: The Origin Server Process. - """ - name = f'server{TestSniIpAllow._server_counter}' - server = tr.AddVerifierServerProcess(name, self._replay_file) - TestSniIpAllow._server_counter += 1 - server.Streams.All += Testers.ContainsExpression('allowed-request', 'The allowed request should be recieved.') - server.Streams.All += Testers.ExcludesExpression('blocked-request', 'The blocked request should not have been recieved.') - server.Streams.All += Testers.ExcludesExpression( - 'block.me.com', 'Nothing about the block.me.com sni should have been recieved.') - - return server - - def _configure_trafficserver(self, tr: 'TestRun', connect_type: int, dns: 'Process', server: 'Process') -> 'Process': - """Configure Traffic Server for the TestRun. - :param tr: The TestRun to configure with Traffic Server. - :param connect_type: The type of connection to use. - :param dns: The DNS Process. - :param server: The Origin Server Process. - :return: The Traffic Server Process. - """ - name = f'ts{TestSniIpAllow._ts_counter}' - ts = tr.MakeATSProcess(name, enable_tls=True, enable_cache=False, enable_proxy_protocol=True) - TestSniIpAllow._ts_counter += 1 - ts.Disk.sni_yaml.AddLines( - [ - 'sni:', - '- fqdn: block.me.com', - ' ip_allow: 192.168.10.1', # Therefore 127.0.0.1 should be blocked. - ]) - if connect_type == ConnectionType.TUNNEL: - ts.Disk.sni_yaml.AddLines([ - f' tunnel_route: backend.server.com:{server.Variables.https_port}', - ]) - if connect_type == ConnectionType.PROXY: - ts.Disk.sni_yaml.AddLines( - [ - '- fqdn: pp.block.me.com', - ' ip_allow: 192.168.10.1', # Therefore 1.2.3.4 should be blocked. - ]) - ts.Disk.sni_yaml.AddLines([ - '- fqdn: allow.me.com', - ' ip_allow: 127.0.0.1', - ]) - if connect_type == ConnectionType.TUNNEL: - ts.Disk.sni_yaml.AddLines([ - f' tunnel_route: backend.server.com:{server.Variables.https_port}', - ]) - if connect_type == ConnectionType.PROXY: - ts.Disk.sni_yaml.AddLines([ - '- fqdn: pp.allow.me.com', - ' ip_allow: 1.2.3.4', - ]) - ts.addDefaultSSLFiles() - ts.Disk.ssl_multicert_yaml.AddLines( - """ -ssl_multicert: - - dest_ip: "*" - ssl_cert_name: server.pem - ssl_key_name: server.key -""".split("\n")) - ts.Disk.remap_config.AddLine(f'map / http://remapped.backend.server.com:{server.Variables.http_port}/') - ts.Disk.records_config.update( - { - 'proxy.config.ssl.server.cert.path': ts.Variables.SSLDir, - 'proxy.config.ssl.server.private_key.path': ts.Variables.SSLDir, - 'proxy.config.ssl.client.verify.server.policy': 'PERMISSIVE', - 'proxy.config.dns.nameservers': f"127.0.0.1:{dns.Variables.Port}", - 'proxy.config.dns.resolv_conf': 'NULL', - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'http|ssl|proxyprotocol', - 'proxy.config.acl.subjects': 'PROXY,PEER', - }) - if connect_type == ConnectionType.TUNNEL: - ts.Disk.records_config.update({ - 'proxy.config.http.connect_ports': f"{server.Variables.https_port}", - }) - ts.addPrivateConnectAllowYaml() - return ts - - def _configure_client( - self, tr: 'TestRun', dns: 'Process', server: 'Process', ts: 'Process', connect_type: ConnectionType.GET) -> None: - """Configure the client for the TestRun. - :param tr: The TestRun to configure with the client. - :param dns: The DNS Process. - :param server: The Origin Server Process. - :param ts: The Traffic Server Process. - """ - name = f'client{TestSniIpAllow._client_counter}' - if connect_type == ConnectionType.PROXY: - p = tr.AddVerifierClientProcess( - name, - self._replay_file, - http_ports=[ts.Variables.proxy_protocol_port], - https_ports=[ts.Variables.proxy_protocol_ssl_port]) - else: - p = tr.AddVerifierClientProcess( - name, self._replay_file, http_ports=[ts.Variables.port], https_ports=[ts.Variables.ssl_port]) - TestSniIpAllow._client_counter += 1 - ts.StartBefore(server) - ts.StartBefore(dns) - p.StartBefore(ts) - - # Because the first connection will be aborted, the client will have a - # non-zero return code. - p.ReturnCode = 1 - - p.Streams.All += Testers.ContainsExpression('allowed-response', 'The response to the allowed request should be recieved.') - p.Streams.All += Testers.ExcludesExpression( - 'blocked-response', 'The response to the blocked request should not have been recieved.') - - -TestSniIpAllow(ConnectionType.GET) -TestSniIpAllow(ConnectionType.TUNNEL) -TestSniIpAllow(ConnectionType.PROXY) diff --git a/tests/gold_tests/tls/tls_sni_parent_failover.test.py b/tests/gold_tests/tls/tls_sni_parent_failover.test.py deleted file mode 100644 index ff64e168f5e..00000000000 --- a/tests/gold_tests/tls/tls_sni_parent_failover.test.py +++ /dev/null @@ -1,174 +0,0 @@ -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -Test.Summary = ''' -Test parent failover with SNI name handling, validating that Traffic Server -correctly fails over between HTTPS parents (including strategy-based parents) -while enforcing TLS server name verification. -''' - -# Define default ATS -ts = Test.MakeATSProcess( - "ts", - enable_tls=True, - enable_cache=False, -) - -server_foo = Test.MakeOriginServer( - "server_foo", - ssl=True, - options={ - "--key": "{0}/server-foo.key".format(Test.RunDirectory), - "--cert": "{0}/server-foo.pem".format(Test.RunDirectory), - }, -) -server_bar = Test.MakeOriginServer( - "server_bar", - ssl=True, - options={ - "--key": "{0}/server-bar.key".format(Test.RunDirectory), - "--cert": "{0}/server-bar.pem".format(Test.RunDirectory), - }, -) - -# default check request/response -request_foo_header = {"headers": "GET / HTTP/1.1\r\nHost: foo.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -response_foo_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": "foo ok"} -request_bar_header = {"headers": "GET / HTTP/1.1\r\nHost: bar.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -response_bar_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": "bar ok"} - -server_foo.addResponse("sessionlog.json", request_foo_header, response_foo_header) -server_bar.addResponse("sessionlog.json", request_bar_header, response_bar_header) - -# successful request to be served by bar.com -request_bar_header = {"headers": "GET /path HTTP/1.1\r\nHost: bar.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -response_bar_header = { - "headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", - "timestamp": "1469733493.993", - "body": "path bar ok" -} - -server_bar.addResponse("sessionlog.json", request_bar_header, response_bar_header) - -ts.addSSLfile("ssl/server-foo.pem") -ts.addSSLfile("ssl/server-foo.key") -ts.addSSLfile("ssl/server-bar.pem") -ts.addSSLfile("ssl/server-bar.key") - -dns = Test.MakeDNServer("dns") - -ts.Disk.records_config.update( - { - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'http|ssl|parent_select|next_hop', - 'proxy.config.ssl.client.verify.server.policy': 'ENFORCED', - 'proxy.config.ssl.client.verify.server.properties': 'NAME', - 'proxy.config.url_remap.pristine_host_hdr': 0, - 'proxy.config.dns.nameservers': '127.0.0.1:{0}'.format(dns.Variables.Port), - 'proxy.config.dns.resolv_conf': 'NULL', - 'proxy.config.exec_thread.autoconfig.scale': 1.0, - 'proxy.config.http.connect.down.policy': 1, # tls failures don't mark down - }) - -dns.addRecords(records={"foo.com.": ["127.0.0.1"]}) -dns.addRecords(records={"bar.com.": ["127.0.0.1"]}) -dns.addRecords(records={"parent.": ["127.0.0.1"]}) -dns.addRecords(records={"strategy.": ["127.0.0.1"]}) - -ts.Disk.remap_config.AddLines( - [ - "map http://parent https://parent", - "map http://strategy https://strategy @strategy=strat", - "map http://parent_prist https://parent @plugin=conf_remap.so @pparam=proxy.config.url_remap.pristine_host_hdr=1", - "map http://strategy_prist https://strategy @strategy=strat @plugin=conf_remap.so @pparam=proxy.config.url_remap.pristine_host_hdr=1", - ]) - -ts.Disk.parent_config.AddLine( - 'dest_domain=. port=443 parent="foo.com:{0}|1;bar.com:{1}|1" parent_retry=simple_retry parent_is_proxy=false go_direct=false simple_server_retry_responses="404" host_override=true' - .format(server_foo.Variables.SSL_Port, server_bar.Variables.SSL_Port)) - -# build strategies.yaml file -ts.Disk.File(ts.Variables.CONFIGDIR + "/strategies.yaml", id="strategies", typename="ats:config") - -s = ts.Disk.strategies -s.AddLine("groups:") -s.AddLines( - [ - f" - &gstrat", - f" - host: foo.com", - f" protocol:", - f" - scheme: https", - f" port: {server_foo.Variables.SSL_Port}", - f" weight: 1.0", - f" - host: bar.com", - f" protocol:", - f" - scheme: https", - f" port: {server_bar.Variables.SSL_Port}", - f" weight: 1.0", - ]) - -s.AddLine("strategies:") - -s.AddLines( - [ - f" - strategy: strat", - f" policy: first_live", - f" go_direct: false", - f" parent_is_proxy: false", - f" ignore_self_detect: true", - f" host_override: true", - f" groups:", - f" - *gstrat", - f" scheme: https", - f" failover:", - f" ring_mode: exhaust_ring", - f" response_codes:", - f" - 404", - ]) - -curl_args = f"-s -L -o /dev/stdout -D /dev/stderr -x localhost:{ts.Variables.port} " - -tr = Test.AddTestRun("request with failover, parent.config") -tr.Setup.Copy("ssl/server-foo.key") -tr.Setup.Copy("ssl/server-foo.pem") -tr.Setup.Copy("ssl/server-bar.key") -tr.Setup.Copy("ssl/server-bar.pem") -tr.MakeCurlCommand(curl_args + "http://parent/path", ts=ts) -tr.StillRunningAfter = ts -ps = tr.Processes.Default -ps.StartBefore(server_foo) -ps.StartBefore(server_bar) -ps.StartBefore(dns) -ps.StartBefore(Test.Processes.ts) -ps.Streams.stdout = Testers.ContainsExpression("path bar ok", "Expected 200 response from bar.com") - -tr = Test.AddTestRun("request with failover, strategies.yaml") -tr.MakeCurlCommand(curl_args + "http://strategy/path", ts=ts) -tr.StillRunningAfter = ts -ps = tr.Processes.Default -ps.Streams.stdout = Testers.ContainsExpression("path bar ok", "Expected 200 response from bar.com") - -tr = Test.AddTestRun("request with failover, parent.config pristine_host_hdr") -tr.MakeCurlCommand(curl_args + "http://parent_prist/path", ts=ts) -tr.StillRunningAfter = ts -ps = tr.Processes.Default -ps.Streams.stdout = Testers.ContainsExpression("path bar ok", "Expected 200 response from bar.com") - -tr = Test.AddTestRun("request with failover, strategies.yaml") -tr.MakeCurlCommand(curl_args + "http://strategy_prist/path", ts=ts) -tr.StillRunningAfter = ts -ps = tr.Processes.Default -ps.Streams.stdout = Testers.ContainsExpression("path bar ok", "Expected 200 response from bar.com") diff --git a/tests/gold_tests/tls/tls_sni_ticket.test.py b/tests/gold_tests/tls/tls_sni_ticket.test.py deleted file mode 100644 index d9d6d62abb2..00000000000 --- a/tests/gold_tests/tls/tls_sni_ticket.test.py +++ /dev/null @@ -1,277 +0,0 @@ -''' -Test sni.yaml session ticket overrides. -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import os -import re -from typing import Any - -Test.Summary = ''' -Test sni.yaml session ticket overrides -''' - -Test.SkipUnless(Condition.HasOpenSSLVersion('1.1.1')) -Test.Setup.Copy('file.ticket') - - -class TlsSniTicketTest: - _server_is_started = False - _ts_on_started = False - _ts_off_started = False - - def __init__(self) -> None: - """ - Initialize shared test state and configure the ATS processes. - """ - self.ticket_file = os.path.join(Test.RunDirectory, 'file.ticket') - self.setupOriginServer() - self.setupEnabledTS() - self.setupDisabledTS() - - def setupOriginServer(self) -> None: - """ - Configure the origin server with a simple response for all requests. - """ - request_header = { - 'headers': 'GET / HTTP/1.1\r\nHost: tickets.example.com\r\n\r\n', - 'timestamp': '1469733493.993', - 'body': '' - } - response_header = { - 'headers': 'HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n', - 'timestamp': '1469733493.993', - 'body': 'ticket test' - } - self.server = Test.MakeOriginServer('server') - self.server.addResponse('sessionlog.json', request_header, response_header) - - def setupTS( - self, - name: str, - sni_name: str, - global_ticket_enabled: int, - global_ticket_number: int, - sni_ticket_enabled: int, - sni_ticket_number: int | None = None) -> Any: - """ - Configure an ATS process for one SNI ticket override scenario. - - :param name: ATS process name. - :param sni_name: SNI hostname matched in sni.yaml. - :param global_ticket_enabled: Process-wide session ticket enable setting. - :param global_ticket_number: Process-wide TLSv1.3 ticket count. - :param sni_ticket_enabled: Per-SNI session ticket enable override. - :param sni_ticket_number: Per-SNI TLSv1.3 ticket count override. - :return: Configured ATS process. - """ - ts = Test.MakeATSProcess(name, enable_tls=True) - - ts.addSSLfile('ssl/server.pem') - ts.addSSLfile('ssl/server.key') - ts.Disk.remap_config.AddLine(f'map / http://127.0.0.1:{self.server.Variables.Port}') - ts.Disk.ssl_multicert_yaml.AddLines( - """ -ssl_multicert: - - dest_ip: "*" - ssl_cert_name: server.pem - ssl_key_name: server.key -""".split("\n")) - - ts.Disk.records_config.update( - { - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'ssl|http', - 'proxy.config.ssl.server.cert.path': f'{ts.Variables.SSLDir}', - 'proxy.config.ssl.server.private_key.path': f'{ts.Variables.SSLDir}', - 'proxy.config.exec_thread.autoconfig.scale': 1.0, - 'proxy.config.ssl.server.session_ticket.enable': global_ticket_enabled, - 'proxy.config.ssl.server.session_ticket.number': global_ticket_number, - 'proxy.config.ssl.server.ticket_key.filename': self.ticket_file, - }) - - sni_lines = [ - 'sni:', - f'- fqdn: {sni_name}', - f' ssl_ticket_enabled: {sni_ticket_enabled}', - ] - if sni_ticket_number is not None: - sni_lines.append(f' ssl_ticket_number: {sni_ticket_number}') - ts.Disk.sni_yaml.AddLines(sni_lines) - - return ts - - def setupEnabledTS(self) -> None: - """ - Create the ATS process whose SNI rule enables tickets. - """ - self.ts_on = self.setupTS('ts_on', 'tickets-on.com', 0, 0, 1, 3) - - def setupDisabledTS(self) -> None: - """ - Create the ATS process whose SNI rule disables tickets. - """ - self.ts_off = self.setupTS('ts_off', 'tickets-off.com', 1, 2, 0) - - def start_processes_if_needed( - self, tr: Any, start_server: bool = False, start_ts_on: bool = False, start_ts_off: bool = False) -> None: - """ - Register one-time StartBefore hooks for the processes needed by a test run. - - :param tr: The AuTest run definition being configured. - :param start_server: Whether the origin server should be started for this run. - :param start_ts_on: Whether the tickets-enabled ATS process should be started for this run. - :param start_ts_off: Whether the tickets-disabled ATS process should be started for this run. - """ - if start_server and not TlsSniTicketTest._server_is_started: - tr.Processes.Default.StartBefore(self.server) - TlsSniTicketTest._server_is_started = True - - if start_ts_on and not TlsSniTicketTest._ts_on_started: - tr.Processes.Default.StartBefore(self.ts_on) - TlsSniTicketTest._ts_on_started = True - - if start_ts_off and not TlsSniTicketTest._ts_off_started: - tr.Processes.Default.StartBefore(self.ts_off) - TlsSniTicketTest._ts_off_started = True - - @staticmethod - def check_regex_count(output_path: str, pattern: str, expected_count: int, description: str) -> tuple[bool, str, str]: - """ - Count regex matches in a process output file. - - :param output_path: Path to the output file to inspect. - :param pattern: Regex pattern to count. - :param expected_count: Expected number of matches. - :param description: Description reported by the tester. - :return: AuTest lambda result tuple. - """ - with open(output_path, 'r') as f: - content = f.read() - - matches = re.findall(pattern, content) - if len(matches) == expected_count: - return (True, description, f'Found {len(matches)} matches for {pattern}') - return (False, description, f'Expected {expected_count} matches for {pattern}, found {len(matches)}') - - @staticmethod - def session_reuse_command(port: int, servername: str) -> str: - """ - Build a TLSv1.2 resumption command for a specific SNI name. - - :param port: ATS TLS listening port. - :param servername: SNI hostname to send with the connection. - :return: Shell command for repeated TLSv1.2 session reuse attempts. - """ - return ( - f'session_path=`mktemp` && ' - f'printf "GET / HTTP/1.1\\r\\nHost: {servername}\\r\\n\\r\\n" | ' - f'openssl s_client -connect 127.0.0.1:{port} -servername {servername} -sess_out "$$session_path" -tls1_2 && ' - f'printf "GET / HTTP/1.1\\r\\nHost: {servername}\\r\\n\\r\\n" | ' - f'openssl s_client -connect 127.0.0.1:{port} -servername {servername} -sess_in "$$session_path" -tls1_2 && ' - f'printf "GET / HTTP/1.1\\r\\nHost: {servername}\\r\\n\\r\\n" | ' - f'openssl s_client -connect 127.0.0.1:{port} -servername {servername} -sess_in "$$session_path" -tls1_2 && ' - f'printf "GET / HTTP/1.1\\r\\nHost: {servername}\\r\\n\\r\\n" | ' - f'openssl s_client -connect 127.0.0.1:{port} -servername {servername} -sess_in "$$session_path" -tls1_2 && ' - f'printf "GET / HTTP/1.1\\r\\nHost: {servername}\\r\\n\\r\\n" | ' - f'openssl s_client -connect 127.0.0.1:{port} -servername {servername} -sess_in "$$session_path" -tls1_2 && ' - f'printf "GET / HTTP/1.1\\r\\nHost: {servername}\\r\\n\\r\\n" | ' - f'openssl s_client -connect 127.0.0.1:{port} -servername {servername} -sess_in "$$session_path" -tls1_2') - - def add_tls12_enabled_run(self) -> None: - """ - Register the TLSv1.2 resumption test for the enabled SNI case. - """ - tr = Test.AddTestRun('sni.yaml enables TLSv1.2 ticket resumption') - tr.Command = TlsSniTicketTest.session_reuse_command(self.ts_on.Variables.ssl_port, 'tickets-on.com') - tr.ReturnCode = 0 - self.start_processes_if_needed(tr, start_server=True, start_ts_on=True) - tr.Processes.Default.Streams.All.Content = Testers.Lambda( - lambda info, tester: TlsSniTicketTest.check_regex_count( - tr.Processes.Default.Streams.All.AbsPath, r'Reused, TLSv1\.2', 5, - 'Check that tickets-on.com reuses TLSv1.2 sessions')) - tr.StillRunningAfter += self.server - tr.StillRunningAfter += self.ts_on - - def add_tls13_enabled_run(self) -> None: - """ - Register the TLSv1.3 ticket count test for the enabled SNI case. - """ - tr = Test.AddTestRun('sni.yaml sets TLSv1.3 ticket count') - tr.Command = ( - f'printf "GET / HTTP/1.1\\r\\nHost: tickets-on.com\\r\\nConnection: close\\r\\n\\r\\n" | ' - f'openssl s_client -connect 127.0.0.1:{self.ts_on.Variables.ssl_port} -servername tickets-on.com -tls1_3 -msg -ign_eof') - tr.ReturnCode = 0 - self.start_processes_if_needed(tr, start_server=True, start_ts_on=True) - - # BoringSSL only exposes SSL_CTX_set_num_tickets() (context-level), - # not SSL_set_num_tickets() (per-connection), so the per-SNI - # ssl_ticket_number override cannot change the ticket count. The - # CTX-level value for ts_on is 0, so expect 0 tickets. - if Condition.IsBoringSSL(): - expected_count = 0 - description = 'Check that tickets-on.com receives no TLSv1.3 tickets (BoringSSL ignores per-SNI ticket count)' - else: - expected_count = 3 - description = 'Check that tickets-on.com receives three TLSv1.3 tickets' - - tr.Processes.Default.Streams.All.Content = Testers.Lambda( - lambda info, tester: TlsSniTicketTest.check_regex_count( - tr.Processes.Default.Streams.All.AbsPath, r'NewSessionTicket', expected_count, description)) - tr.StillRunningAfter += self.server - tr.StillRunningAfter += self.ts_on - - def add_tls12_disabled_run(self) -> None: - """ - Register the TLSv1.2 non-resumption test for the disabled SNI case. - """ - tr = Test.AddTestRun('sni.yaml disables TLSv1.2 ticket resumption') - tr.Command = TlsSniTicketTest.session_reuse_command(self.ts_off.Variables.ssl_port, 'tickets-off.com') - self.start_processes_if_needed(tr, start_server=True, start_ts_off=True) - tr.Processes.Default.Streams.All = Testers.ExcludesExpression('Reused', 'tickets-off.com should not reuse TLSv1.2 sessions') - tr.Processes.Default.Streams.All += Testers.ContainsExpression('TLSv1.2', 'tickets-off.com should still negotiate TLSv1.2') - tr.StillRunningAfter += self.server - tr.StillRunningAfter += self.ts_off - - def add_tls13_disabled_run(self) -> None: - """ - Register the TLSv1.3 no-ticket test for the disabled SNI case. - """ - tr = Test.AddTestRun('sni.yaml disables TLSv1.3 ticket issuance') - tr.Command = ( - f'printf "GET / HTTP/1.1\\r\\nHost: tickets-off.com\\r\\nConnection: close\\r\\n\\r\\n" | ' - f'openssl s_client -connect 127.0.0.1:{self.ts_off.Variables.ssl_port} -servername tickets-off.com -tls1_3 -msg -ign_eof' - ) - self.start_processes_if_needed(tr, start_server=True, start_ts_off=True) - tr.Processes.Default.Streams.All.Content = Testers.Lambda( - lambda info, tester: TlsSniTicketTest.check_regex_count( - tr.Processes.Default.Streams.All.AbsPath, r'NewSessionTicket', 0, - 'Check that tickets-off.com receives no TLSv1.3 tickets')) - tr.StillRunningAfter += self.server - tr.StillRunningAfter += self.ts_off - - def run(self) -> None: - """ - Register all AuTest runs for the SNI ticket override coverage. - """ - self.add_tls12_enabled_run() - self.add_tls13_enabled_run() - self.add_tls12_disabled_run() - self.add_tls13_disabled_run() - - -TlsSniTicketTest().run() diff --git a/tests/gold_tests/tls/tls_sni_with_port.test.py b/tests/gold_tests/tls/tls_sni_with_port.test.py deleted file mode 100644 index 29fc7fb4e3a..00000000000 --- a/tests/gold_tests/tls/tls_sni_with_port.test.py +++ /dev/null @@ -1,212 +0,0 @@ -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import functools -from typing import Any, Callable, Dict, Optional - -from ports import get_port - -Test.Summary = 'Tests SNI port-based routing' - -TestParams = Dict[str, Any] - - -class TestSNIWithPort: - """Configure a test for SNI port-based routing .""" - - replay_filepath: str = "tls_sni_with_port.replay.yaml" - client_counter: int = 0 - server_counter: int = 0 - ts_counter: int = 0 - - def __init__(self, name: str, /, autorun) -> None: - """Initialize the test. - - :param name: The name of the test. - """ - self.name = name - self.autorun = autorun - - def _init_run(self) -> "TestRun": - """Initialize processes for the test run.""" - - tr = Test.AddTestRun(self.name) - server_one = TestSNIWithPort.configure_server(tr, "yay.com") - server_two = TestSNIWithPort.configure_server(tr, "oof.com") - server_three = TestSNIWithPort.configure_server(tr, "wow.com") - self._configure_traffic_server(tr, server_one, server_two, server_three) - - tr.Processes.Default.StartBefore(server_one) - tr.Processes.Default.StartBefore(server_two) - tr.Processes.Default.StartBefore(self._ts) - - return { - "tr": tr, - "ts": self._ts, - "server_one": server_one, - "server_two": server_two, - "server_three": server_three, - "port_one": self._port_one, - "port_two": self._port_two, - "port_three": self._port_three, - "port_unmapped": self._port_unmapped - } - - @classmethod - def runner(cls, name: str, autorun: bool = True) -> Optional[Callable]: - """Create a runner for a test case. - - :param autorun: Run the test case once it's set up. Default is True. - """ - test = cls(name, autorun=autorun)._prepare_test_case - return test - - def _prepare_test_case(self, func: Callable) -> Callable: - """Set up a test case and possibly run it. - - :param func: The test case to set up. - """ - functools.wraps(func) - test_params = self._init_run() - - def wrapper(*args, **kwargs) -> Any: - return func(test_params, *args, **kwargs) - - if self.autorun: - wrapper() - return wrapper - - @staticmethod - def configure_server(tr: "TestRun", domain: str): - server = tr.AddVerifierServerProcess(f"server{TestSNIWithPort.server_counter}.{domain}", TestSNIWithPort.replay_filepath) - TestSNIWithPort.server_counter += 1 - - return server - - def _configure_traffic_server(self, tr: "TestRun", server_one: "Process", server_two: "Process", server_three: "Process"): - """Configure Traffic Server. - - :param tr: The TestRun object to associate the ts process with. - """ - ts = tr.MakeATSProcess(f"ts-{TestSNIWithPort.ts_counter}", select_ports=False, enable_tls=True) - TestSNIWithPort.ts_counter += 1 - - ts.addDefaultSSLFiles() - self._port_one = get_port(ts, "PortOne") - self._port_two = get_port(ts, "PortTwo") - self._port_three = get_port(ts, "PortThree") - self._port_unmapped = get_port(ts, "UnspecifiedPort") - ts.Disk.records_config.update( - { - 'proxy.config.ssl.server.cert.path': f"{ts.Variables.SSLDir}", - 'proxy.config.ssl.server.private_key.path': f"{ts.Variables.SSLDir}", - 'proxy.config.http.server_ports': - f"{self._port_one}:ssl {self._port_two}:ssl {self._port_three}:ssl {self._port_unmapped}:ssl", - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'dns|http|ssl|sni', - }) - - ts.Disk.remap_config.AddLine(f"map / http://127.0.0.1:{server_three.Variables.http_port}") - ts.addPrivateConnectAllowYaml(methods='[ CONNECT, GET ]') - - ts.Disk.sni_yaml.AddLines( - [ - "sni:", - "- fqdn: yay.example.com", - f" inbound_port_ranges: {self._port_one}-{self._port_one}", - f" tunnel_route: localhost:{server_one.Variables.https_port}", - "- fqdn: yay.example.com", - " inbound_port_ranges:", - f" - {self._port_two}", - f" - {self._port_three}", - f" tunnel_route: localhost:{server_two.Variables.https_port}", - ]) - - ts.Disk.ssl_multicert_yaml.AddLines( - """ -ssl_multicert: - - dest_ip: "*" - ssl_cert_name: server.pem - ssl_key_name: server.key -""".split("\n")) - - self._ts = ts - - -# Tests start. - - -@TestSNIWithPort.runner("Test that a request to a port not in the SNI does not get through.") -def test0(params: TestParams) -> None: - client = params["tr"].AddVerifierClientProcess( - f"client0", TestSNIWithPort.replay_filepath, https_ports=[params["port_unmapped"]], keys="conn_remapped") - - params["tr"].Processes.Default.ReturnCode = 0 - params["ts"].Disk.diags_log.Content += Testers.ExcludesExpression( - "unsupported key 'inbound_port_range'", "we should not warn about the key") - params["ts"].Disk.traffic_out.Content += Testers.IncludesExpression( - "not available in the map", "the request should not match an SNI") - params["server_one"].Streams.All.Content += Testers.ExcludesExpression( - r"Received (\(with headers\) )?an HTTP/1 (Content-Length )?body of 16 bytes for key conn_remapped", - "the request should not go to server one") - params["server_two"].Streams.All.Content += Testers.ExcludesExpression( - r"Received (\(with headers\) )?an HTTP/1 (Content-Length )?body of 16 bytes for key conn_remapped", - "the request should not go to server two") - params["server_three"].Streams.All.Content += Testers.IncludesExpression( - r"Received (\(with headers\) )?an HTTP/1 (Content-Length )?body of 16 bytes for key conn_remapped", - "request was remaped to server three") - - -@TestSNIWithPort.runner("Test that a request to a port one goes to server one.") -def test1(params: TestParams) -> None: - client = params["tr"].AddVerifierClientProcess( - f"client1", TestSNIWithPort.replay_filepath, https_ports=[params["port_one"]], keys="conn_accepted") - - params["tr"].Processes.Default.ReturnCode = 0 - params["server_one"].Streams.All.Content += Testers.IncludesExpression( - r"Received (\(with headers\) )?an HTTP/1 (Content-Length )?body of 16 bytes for key conn_accepted", - "the request should go to server one") - params["server_two"].Streams.All.Content += Testers.ExcludesExpression( - r"Received (\(with headers\) )?an HTTP/1 (Content-Length )?body of 16 bytes for key conn_accepted", - "the request should not go to server two") - - -@TestSNIWithPort.runner("Test that a request to port two goes to server two.") -def test2(params: TestParams) -> None: - client = params["tr"].AddVerifierClientProcess( - f"client2", TestSNIWithPort.replay_filepath, https_ports=[params["port_two"]], keys="conn_accepted") - - params["tr"].Processes.Default.ReturnCode = 0 - params["server_two"].Streams.All.Content += Testers.IncludesExpression( - r"Received (\(with headers\) )?an HTTP/1 (Content-Length )?body of 16 bytes for key conn_accepted", - "the request should go to server two") - params["server_one"].Streams.All.Content += Testers.ExcludesExpression( - r"Received (\(with headers\) )?an HTTP/1 (Content-Length )?body of 16 bytes for key conn_accepted", - "the request should not go to server one") - - -@TestSNIWithPort.runner("Test that a request to port three goes to server two.") -def test3(params: TestParams) -> None: - client = params["tr"].AddVerifierClientProcess( - f"client3", TestSNIWithPort.replay_filepath, https_ports=[params["port_three"]], keys="conn_accepted") - - params["tr"].Processes.Default.ReturnCode = 0 - params["server_two"].Streams.All.Content += Testers.IncludesExpression( - r"Received (\(with headers\) )?an HTTP/1 (Content-Length )?body of 16 bytes for key conn_accepted", - "the request should go to server two") - params["server_one"].Streams.All.Content += Testers.ExcludesExpression( - r"Received (\(with headers\) )?an HTTP/1 (Content-Length )?body of 16 bytes for key conn_accepted", - "the request should not go to server one") diff --git a/tests/gold_tests/tls/tls_sni_yaml_reload.test.py b/tests/gold_tests/tls/tls_sni_yaml_reload.test.py deleted file mode 100644 index cadd04e5e85..00000000000 --- a/tests/gold_tests/tls/tls_sni_yaml_reload.test.py +++ /dev/null @@ -1,123 +0,0 @@ -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -Test.Summary = ''' -Test reloading sni.yaml behaves as expected -''' - -sni_domain = 'example.com' - -ts = Test.MakeATSProcess("ts", enable_tls=True, disable_log_checks=True) -server = Test.MakeOriginServer("server") -server2 = Test.MakeOriginServer("server3") -request_header = {"headers": f"GET / HTTP/1.1\r\nHost: {sni_domain}\r\n\r\n", "timestamp": "1469733493.993", "body": ""} - -response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -server.addResponse("sessionlog.json", request_header, response_header) - -ts.Disk.records_config.update( - { - 'proxy.config.ssl.server.cert.path': ts.Variables.SSLDir, - 'proxy.config.ssl.server.private_key.path': ts.Variables.SSLDir, - 'proxy.config.ssl.CA.cert.filename': f'{ts.Variables.SSLDir}/signer.pem', - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'ssl|http', - 'proxy.config.diags.output.debug': 'L', - }) - -ts.addDefaultSSLFiles() -ts.addSSLfile("ssl/signed-foo.pem") -ts.addSSLfile("ssl/signed-foo.key") -ts.addSSLfile("ssl/signer.pem") - -ts.Disk.remap_config.AddLine(f'map / http://127.0.0.1:{server.Variables.Port}') - -ts.Disk.ssl_multicert_yaml.AddLines( - """ -ssl_multicert: - - dest_ip: "*" - ssl_cert_name: server.pem - ssl_key_name: server.key -""".split("\n")) - -ts.Disk.ssl_multicert_yaml.AddLines( - """ -ssl_multicert: - - dest_ip: "*" - ssl_cert_name: server.pem - ssl_key_name: server.key -""".split("\n")) - -ts.Disk.sni_yaml.AddLines( - f""" - sni: - - fqdn: {sni_domain} - http2: off - client_cert: {ts.Variables.SSLDir}/signed-foo.pem - client_key: {ts.Variables.SSLDir}/signed-foo.key - verify_client: STRICT - """.split('\n')) - -tr = Test.AddTestRun(f'ensure we can connect for SNI {sni_domain}') -tr.Setup.Copy("ssl/signed-foo.pem") -tr.Setup.Copy("ssl/signed-foo.key") -tr.Processes.Default.StartBefore(Test.Processes.ts) -tr.Processes.Default.StartBefore(server) -tr.StillRunningAfter = ts -tr.StillRunningAfter = server -tr.MakeCurlCommand( - f"-q --tls-max 1.2 -s -v -k --cert ./signed-foo.pem --key ./signed-foo.key --resolve '{sni_domain}:{ts.Variables.ssl_port}:127.0.0.1' https://{sni_domain}:{ts.Variables.ssl_port}", - ts=ts) -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Streams.stdout = Testers.ExcludesExpression("Could Not Connect", "Verify curl could successfully connect") -tr.Processes.Default.Streams.stderr = Testers.IncludesExpression(f"CN={sni_domain}", f"Verify curl used the {sni_domain} SNI") - -# This config reload should fail because it references non-existent TLS key files -trupd = Test.AddTestRun("Update config file") -# Update the configs - this will overwrite the sni.yaml file -sniyamlpath = ts.Disk.sni_yaml.AbsPath -trupd.Disk.File(sniyamlpath, id="sni_yaml", typename="ats:config") -trupd.Disk.sni_yaml.AddLines( - f""" - sni: - - fqdn: {sni_domain} - http2: on - client_cert: {ts.Variables.SSLDir}/signed-notexist.pem - client_key: {ts.Variables.SSLDir}/signed-notexist.key - verify_client: STRICT - """.split('\n')) - -trupd.StillRunningAfter = ts -trupd.StillRunningAfter = server -trupd.Processes.Default.Command = 'echo Updated configs' -trupd.Processes.Default.Env = ts.Env -trupd.Processes.Default.ReturnCode = 0 - -tr2reload = Test.AddConfigReload(ts, expect="fail", expect_tasks=["sni.yaml"], description="Reload config") -tr2reload.StillRunningAfter = server - -tr3 = Test.AddTestRun(f"Make request again for {sni_domain} that should still work") -tr3.Setup.Copy("ssl/signed-bar.pem") -tr3.Setup.Copy("ssl/signed-bar.key") -tr3.Processes.Default.StartBefore(server2) -tr3.StillRunningAfter = ts -tr3.StillRunningAfter = server -tr3.MakeCurlCommand( - f"-q --tls-max 1.2 -s -v -k --cert ./signed-bar.pem --key ./signed-bar.key --resolve '{sni_domain}:{ts.Variables.ssl_port}:127.0.0.1' https://{sni_domain}:{ts.Variables.ssl_port}", - ts=ts) -tr3.Processes.Default.ReturnCode = 0 -# since the 2nd config with http2 turned on should have failed and used the prior config, verify http2 was not used -tr3.Processes.Default.Streams.stderr = Testers.ExcludesExpression("GET / HTTP/2", "Confirm that HTTP2 is still not used") diff --git a/tests/gold_tests/tls/tls_ticket.test.py b/tests/gold_tests/tls/tls_ticket.test.py deleted file mode 100644 index 28eec527bce..00000000000 --- a/tests/gold_tests/tls/tls_ticket.test.py +++ /dev/null @@ -1,121 +0,0 @@ -''' -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import re - -Test.Summary = ''' -Test tls tickets -''' - -# Define default ATS -ts = Test.MakeATSProcess("ts", enable_tls=True) -ts2 = Test.MakeATSProcess("ts2", enable_tls=True) -server = Test.MakeOriginServer("server") - -# Add info the origin server responses -request_header = {"headers": "GET / HTTP/1.1\r\nHost: www.example.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -server.addResponse("sessionlog.json", request_header, response_header) - -# add ssl materials like key, certificates for the server -ts.addSSLfile("ssl/server.pem") -ts.addSSLfile("ssl/server.key") -ts2.addSSLfile("ssl/server.pem") -ts2.addSSLfile("ssl/server.key") - -ts.Disk.remap_config.AddLine('map / http://127.0.0.1:{0}'.format(server.Variables.Port)) -ts2.Disk.remap_config.AddLine('map / http://127.0.0.1:{0}'.format(server.Variables.Port)) - -ts.Disk.ssl_multicert_yaml.AddLines( - """ -ssl_multicert: - - dest_ip: "*" - ssl_cert_name: server.pem - ssl_key_name: server.key -""".split("\n")) -ts2.Disk.ssl_multicert_yaml.AddLines( - """ -ssl_multicert: - - dest_ip: "*" - ssl_cert_name: server.pem - ssl_key_name: server.key -""".split("\n")) - -ts.Disk.records_config.update( - { - 'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir), - 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts.Variables.SSLDir), - 'proxy.config.exec_thread.autoconfig.scale': 1.0, - 'proxy.config.ssl.server.session_ticket.enable': '1', - 'proxy.config.ssl.server.ticket_key.filename': '../../file.ticket' - }) -ts2.Disk.records_config.update( - { - 'proxy.config.ssl.server.cert.path': '{0}'.format(ts2.Variables.SSLDir), - 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts2.Variables.SSLDir), - 'proxy.config.ssl.server.session_ticket.enable': '1', - 'proxy.config.exec_thread.autoconfig.scale': 1.0, - 'proxy.config.ssl.server.ticket_key.filename': '../../file.ticket' - }) - -tr = Test.AddTestRun("Create ticket") -tr.Setup.Copy('file.ticket') -tr.Command = 'printf "GET / HTTP/1.0\r\n" | openssl s_client -tls1_2 -connect 127.0.0.1:{0} -sess_out ticket.out'.format( - ts.Variables.ssl_port) -tr.ReturnCode = 0 -tr.Processes.Default.StartBefore(server) -tr.Processes.Default.StartBefore(Test.Processes.ts) -path1 = tr.Processes.Default.Streams.stdout.AbsPath -tr.StillRunningAfter = server - -# Pull out session created in tr to test for session id in tr2 - - -def checkSession(ev): - retval = False - f1 = open(path1, 'r') - f2 = open(path2, 'r') - err = "Session ids match" - if not f1 or not f2: - err = "Failed to open {0} or {1}".format(path1, path2) - return (retval, "Check that session ids match", err) - - f1Content = f1.read() - f2Content = f2.read() - match1 = re.findall('Session-ID: ([0-9A-F]+)', f1Content) - match2 = re.findall('Session-ID: ([0-9A-F]+)', f2Content) - - if match1 and match2: - if match1[0] == match2[0]: - err = "{0} and {1} do match".format(match1[0], match2[0]) - retval = True - else: - err = "{0} and {1} do not match".format(match1[0], match2[0]) - else: - err = "Didn't find session id" - return (retval, "Check that session ids match", err) - - -tr2 = Test.AddTestRun("Test ticket") -tr2.Setup.Copy('file.ticket') -tr2.Command = 'printf "GET / HTTP/1.0\r\n" | openssl s_client -tls1_2 -connect 127.0.0.1:{0} -sess_in ticket.out'.format( - ts2.Variables.ssl_port) -tr2.Processes.Default.StartBefore(Test.Processes.ts2) -tr2.ReturnCode = 0 -path2 = tr2.Processes.Default.Streams.stdout.AbsPath -tr2.Processes.Default.Streams.All.Content = Testers.Lambda(checkSession) diff --git a/tests/gold_tests/tls/tls_tunnel.test.py b/tests/gold_tests/tls/tls_tunnel.test.py deleted file mode 100644 index 724406ffd83..00000000000 --- a/tests/gold_tests/tls/tls_tunnel.test.py +++ /dev/null @@ -1,405 +0,0 @@ -''' -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -from ports import get_port -import sys - -Test.Summary = ''' -Test tunneling based on SNI -''' - -# Define default ATS -ts = Test.MakeATSProcess("ts", enable_tls=True, enable_proxy_protocol=True) -server_foo = Test.MakeOriginServer("server_foo", ssl=True) -server_bar = Test.MakeOriginServer("server_bar", ssl=True) -server2 = Test.MakeOriginServer("server2") -# The following server will listen on a port that is not in the connect_ports -# list. -server_forbidden = Test.MakeOriginServer("forbidden", ssl=True) -dns = Test.MakeDNServer("dns") - -request_foo_header = {"headers": "GET / HTTP/1.1\r\nHost: foo.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -request_bar_header = {"headers": "GET / HTTP/1.1\r\nHost: bar.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -request_pp_header = { - "headers": "GET /proxy_protocol HTTP/1.1\r\nHost: proxy.protocol.port.com\r\n\r\n", - "timestamp": "1469733493.993", - "body": "" -} -response_foo_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": "foo ok"} -response_bar_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": "bar ok"} -response_pp_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": "pp ok"} -server_foo.addResponse("sessionlog.json", request_foo_header, response_foo_header) -server_foo.addResponse("sessionlog.json", request_pp_header, response_pp_header) -server_bar.addResponse("sessionlog.json", request_bar_header, response_bar_header) -server_forbidden.addResponse("sessionlog.json", request_pp_header, response_pp_header) - -# Server to interact with a split CLIENT_HELLO in a blind tunnel. -split_client_hello_server = Test.Processes.Process('receive_split_client_hello_server') -server_port = get_port(split_client_hello_server, 'tcp_port') -server_command = f'{sys.executable} receive_split_client_hello.py 127.0.0.1 {server_port}' -split_client_hello_server.Command = server_command - -# add ssl materials like key, certificates for the server -ts.addSSLfile("ssl/signed-foo.pem") -ts.addSSLfile("ssl/signed-foo.key") -ts.addSSLfile("ssl/signed-bar.pem") -ts.addSSLfile("ssl/signed-bar.key") -ts.addSSLfile("ssl/server.pem") -ts.addSSLfile("ssl/server.key") -ts.addSSLfile("ssl/signer.pem") -ts.addSSLfile("ssl/signer.key") - -dns.addRecords(records={"localhost": ["127.0.0.1"]}) -dns.addRecords(records={"one.testmatch": ["127.0.0.1"]}) -dns.addRecords(records={"two.example.one": ["127.0.0.1"]}) -dns.addRecords(records={"backend.incoming.port.com": ["127.0.0.1"]}) -dns.addRecords(records={"backend.proxy.protocol.port.com": ["127.0.0.1"]}) -dns.addRecords(records={"backend.wildcard.with.incoming.port.com": ["127.0.0.1"]}) -dns.addRecords(records={"backend.wildcard.with.proxy.protocol.port.com": ["127.0.0.1"]}) -# Need no remap rules. Everything should be processed by sni - -# Make sure the TS server certs are different from the origin certs -ts.Disk.ssl_multicert_yaml.AddLines( - """ -ssl_multicert: - - dest_ip: "*" - ssl_cert_name: signed-foo.pem - ssl_key_name: signed-foo.key -""".split("\n")) - -# Case 1, global config policy=permissive properties=signature -# override for foo.com policy=enforced properties=all -ts.Disk.records_config.update( - { - 'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir), - 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts.Variables.SSLDir), - 'proxy.config.http.connect_ports': - '{0} {1} {2}'.format(ts.Variables.ssl_port, server_foo.Variables.SSL_Port, server_bar.Variables.SSL_Port), - 'proxy.config.ssl.client.CA.cert.path': '{0}'.format(ts.Variables.SSLDir), - 'proxy.config.ssl.client.CA.cert.filename': 'signer.pem', - 'proxy.config.exec_thread.autoconfig.scale': 1.0, - 'proxy.config.url_remap.pristine_host_hdr': 1, - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'http|ssl|proxyprotocol', - 'proxy.config.dns.nameservers': f'127.0.0.1:{dns.Variables.Port}', - 'proxy.config.dns.resolv_conf': 'NULL' - }) -ts.addPrivateConnectAllowYaml() - -# foo.com should not terminate. Just tunnel to server_foo -# bar.com should terminate. Forward its tcp stream to server_bar -# empty SNI should tunnel to server_bar -ts.Disk.sni_yaml.AddLines( - [ - 'sni:', - '- fqdn: foo.com', - f" tunnel_route: localhost:{server_foo.Variables.SSL_Port}", - '- fqdn: slashdot.org', - f" tunnel_route: 127.0.0.1:{split_client_hello_server.Variables.tcp_port}", - "- fqdn: '*.bar.com'", - f" tunnel_route: localhost:{server_foo.Variables.SSL_Port}", - "- fqdn: '*.match.com'", - f" tunnel_route: $1.testmatch:{server_foo.Variables.SSL_Port}", - "- fqdn: '*.ok.two.com'", - f" tunnel_route: two.example.$1:{server_foo.Variables.SSL_Port}", - "- fqdn: ''", # No SNI sent - f" tunnel_route: localhost:{server_bar.Variables.SSL_Port}", - "- fqdn: 'incoming.port.com'", - " tunnel_route: backend.incoming.port.com:{inbound_local_port}", - "- fqdn: 'proxy.protocol.port.com'", - " tunnel_route: backend.proxy.protocol.port.com:{proxy_protocol_port}", - "- fqdn: '*.backend.incoming.port.com'", - " tunnel_route: backend.$1.incoming.port.com:{inbound_local_port}", - "- fqdn: '*.with.incoming.port.com'", - " tunnel_route: backend.$1.with.incoming.port.com:{inbound_local_port}", - "- fqdn: '*.with.proxy.protocol.port.com'", - " tunnel_route: backend.$1.with.proxy.protocol.port.com:{proxy_protocol_port}", - ]) - -tr = Test.AddTestRun("foo.com Tunnel-test") -tr.TimeOut = 5 -tr.MakeCurlCommand("-v --resolve 'foo.com:{0}:127.0.0.1' -k https://foo.com:{0}".format(ts.Variables.ssl_port), ts=ts) -tr.ReturnCode = 0 -tr.Processes.Default.StartBefore(server_foo) -tr.Processes.Default.StartBefore(server_bar) -tr.Processes.Default.StartBefore(dns) -tr.Processes.Default.StartBefore(Test.Processes.ts) -tr.StillRunningAfter = ts -tr.StillRunningAfter = server_foo -tr.StillRunningAfter = server_bar -tr.StillRunningAfter = dns -tr.Processes.Default.Streams.All += Testers.ExcludesExpression("Could Not Connect", "Curl attempt should have succeeded") -tr.Processes.Default.Streams.All += Testers.ExcludesExpression( - "Not Found on Accelerato", "Should not try to remap on Traffic Server") -tr.Processes.Default.Streams.All += Testers.ExcludesExpression("CN=foo.com", "Should not TLS terminate on Traffic Server") -tr.Processes.Default.Streams.All += Testers.ContainsExpression("HTTP/1.1 200 OK", "Should get a successful response") -tr.Processes.Default.Streams.All += Testers.ExcludesExpression("ATS", "Do not terminate on Traffic Server") -tr.Processes.Default.Streams.All += Testers.ContainsExpression("foo ok", "Should get a response from bar") - -tr = Test.AddTestRun("bob.bar.com Tunnel-test") -tr.TimeOut = 5 -tr.MakeCurlCommand("-v --resolve 'bob.bar.com:{0}:127.0.0.1' -k https://bob.bar.com:{0}".format(ts.Variables.ssl_port), ts=ts) -tr.ReturnCode = 0 -tr.StillRunningAfter = ts -tr.Processes.Default.Streams.All += Testers.ExcludesExpression("Could Not Connect", "Curl attempt should have succeeded") -tr.Processes.Default.Streams.All += Testers.ExcludesExpression( - "Not Found on Accelerato", "Should not try to remap on Traffic Server") -tr.Processes.Default.Streams.All += Testers.ExcludesExpression("CN=foo.com", "Should not TLS terminate on Traffic Server") -tr.Processes.Default.Streams.All += Testers.ContainsExpression("HTTP/1.1 200 OK", "Should get a successful response") -tr.Processes.Default.Streams.All += Testers.ExcludesExpression("ATS", "Do not terminate on Traffic Server") -tr.Processes.Default.Streams.All += Testers.ContainsExpression("foo ok", "Should get a response from bar") - -tr = Test.AddTestRun("bar.com no Tunnel-test") -tr.TimeOut = 5 -tr.MakeCurlCommand("-v --resolve 'bar.com:{0}:127.0.0.1' -k https://bar.com:{0}".format(ts.Variables.ssl_port), ts=ts) -tr.ReturnCode = 0 -tr.StillRunningAfter = ts -tr.Processes.Default.Streams.All += Testers.ExcludesExpression("Could Not Connect", "Curl attempt should have succeeded") -tr.Processes.Default.Streams.All += Testers.ContainsExpression("Not Found on Accelerato", "Terminates on on Traffic Server") -tr.Processes.Default.Streams.All += Testers.ContainsExpression("ATS", "Terminate on Traffic Server") - -tr = Test.AddTestRun("no SNI Tunnel-test") -tr.TimeOut = 5 -tr.MakeCurlCommand("-v -k https://127.0.0.1:{0}".format(ts.Variables.ssl_port), ts=ts) -tr.ReturnCode = 0 -tr.StillRunningAfter = ts -tr.Processes.Default.Streams.All += Testers.ExcludesExpression("Could Not Connect", "Curl attempt should have succeeded") -tr.Processes.Default.Streams.All += Testers.ExcludesExpression( - "Not Found on Accelerato", "Should not try to remap on Traffic Server") -tr.Processes.Default.Streams.All += Testers.ContainsExpression("HTTP/1.1 200 OK", "Should get a successful response") -tr.Processes.Default.Streams.All += Testers.ExcludesExpression("ATS", "Do not terminate on Traffic Server") -tr.Processes.Default.Streams.All += Testers.ContainsExpression("bar ok", "Should get a response from bar") - -tr = Test.AddTestRun("one.match.com Tunnel-test") -tr.TimeOut = 5 -tr.MakeCurlCommand( - "-vvv --resolve 'one.match.com:{0}:127.0.0.1' -k https://one.match.com:{0}".format(ts.Variables.ssl_port), ts=ts) -tr.ReturnCode = 0 -tr.StillRunningAfter = ts -tr.Processes.Default.Streams.All += Testers.ExcludesExpression("Could Not Connect", "Curl attempt should have succeeded") -tr.Processes.Default.Streams.All += Testers.ExcludesExpression( - "Not Found on Accelerato", "Should not try to remap on Traffic Server") -tr.Processes.Default.Streams.All += Testers.ExcludesExpression("CN=foo.com", "Should not TLS terminate on Traffic Server") -tr.Processes.Default.Streams.All += Testers.ContainsExpression("HTTP/1.1 200 OK", "Should get a successful response") -tr.Processes.Default.Streams.All += Testers.ExcludesExpression("ATS", "Do not terminate on Traffic Server") -tr.Processes.Default.Streams.All += Testers.ContainsExpression("foo ok", "Should get a response from tm") - -tr = Test.AddTestRun("one.ok.two.com Tunnel-test") -tr.TimeOut = 5 -tr.MakeCurlCommand( - "-vvv --resolve 'one.ok.two.com:{0}:127.0.0.1' -k https://one.ok.two.com:{0}".format(ts.Variables.ssl_port), ts=ts) -tr.ReturnCode = 0 -tr.StillRunningAfter = ts -tr.Processes.Default.Streams.All += Testers.ExcludesExpression("Could Not Connect", "Curl attempt should have succeeded") -tr.Processes.Default.Streams.All += Testers.ExcludesExpression( - "Not Found on Accelerato", "Should not try to remap on Traffic Server") -tr.Processes.Default.Streams.All += Testers.ExcludesExpression("CN=foo.com", "Should not TLS terminate on Traffic Server") -tr.Processes.Default.Streams.All += Testers.ContainsExpression("HTTP/1.1 200 OK", "Should get a successful response") -tr.Processes.Default.Streams.All += Testers.ExcludesExpression("ATS", "Do not terminate on Traffic Server") -tr.Processes.Default.Streams.All += Testers.ContainsExpression("foo ok", "Should get a response from tm") - -tr = Test.AddTestRun("test {inbound_local_port}") -tr.TimeOut = 5 -tr.MakeCurlCommand( - "-vvv --resolve 'incoming.port.com:{0}:127.0.0.1' -k https://incoming.port.com:{0}".format(ts.Variables.ssl_port), ts=ts) -# The tunnel connecting to the outgoing port which is the same as the incoming -# port (per the `inbound_local_port` configuration) will result in ATS -# connecting back to itself. This will result in a connection close and a -# non-zero return code from curl. In production, the server will listen on the -# same port as ATS but have a different IP. -tr.ReturnCode = 35 -tr.StillRunningAfter = ts -ts.Disk.traffic_out.Content += Testers.ContainsExpression( - f"CONNECT tunnel://backend.incoming.port.com:{ts.Variables.ssl_port} HTTP/1.1", "Verify a CONNECT request is handled") -ts.Disk.traffic_out.Content += Testers.ContainsExpression("HTTP/1.1 400 Cycle Detected", "The loop should be detected") - -tr = Test.AddTestRun("test {proxy_protocol_port}") -tr.TimeOut = 5 -tr.Setup.Copy('proxy_protocol_client.py') -tr.Processes.Default.Command = ( - f'{sys.executable} proxy_protocol_client.py ' - f'127.0.0.1 {ts.Variables.proxy_protocol_ssl_port} proxy.protocol.port.com ' - f'127.0.0.1 127.0.0.1 60123 {server_foo.Variables.SSL_Port} ' - f'2 --https') -tr.ReturnCode = 0 -tr.StillRunningAfter = ts -tr.Processes.Default.Streams.All += Testers.ContainsExpression("HTTP/1.1 200 OK", "Verify a successful response is received") - -# This test run causes an exception in proxy_protocol_client.py -# -tr = Test.AddTestRun("test proxy_protocol_port - not in connect_ports") -tr.TimeOut = 5 -tr.Processes.Default.StartBefore(server_forbidden) -tr.Setup.Copy('proxy_protocol_client.py') -# Note that server_forbidden is listing on a port that is not in the -# connect_ports list. Therefore the tunnel should be rejected. -rejected_port = server_forbidden.Variables.SSL_Port -tr.Processes.Default.Command = ( - f'{sys.executable} proxy_protocol_client.py ' - f'127.0.0.1 {ts.Variables.proxy_protocol_ssl_port} proxy.protocol.port.com ' - f'127.0.0.1 127.0.0.1 60123 {rejected_port} ' - f'2 --https') -tr.ReturnCode = 1 -tr.StillRunningAfter = ts -tr.Processes.Default.Streams.All += Testers.ContainsExpression("ssl.SSL.*Error:.*EOF", "Verify a the handshake failed") -ts.Disk.traffic_out.Content += Testers.ContainsExpression( - f"Rejected a tunnel to port {rejected_port} not in connect_ports", "Verify the tunnel was rejected") - -tr = Test.AddTestRun("test wildcard with inbound_local_port") -tr.TimeOut = 5 -tr.MakeCurlCommand( - "-vvv --resolve 'wildcard.with.incoming.port.com:{0}:127.0.0.1' -k https://wildcard.with.incoming.port.com:{0}".format( - ts.Variables.ssl_port), - ts=ts) - -# See the inbound_local_port test above for the explanation of the return code. -tr.ReturnCode = 35 -tr.StillRunningAfter = ts -ts.Disk.traffic_out.Content += Testers.ContainsExpression( - rf"Destination now is \[backend.wildcard.with.incoming.port.com:{ts.Variables.ssl_port}\]", - "Verify the tunnel destination is expanded correctly.") -ts.Disk.traffic_out.Content += Testers.ContainsExpression( - f"CONNECT tunnel://backend.wildcard.with.incoming.port.com:{ts.Variables.ssl_port} HTTP/1.1", - "Verify a CONNECT request is handled") -ts.Disk.traffic_out.Content += Testers.ContainsExpression("HTTP/1.1 400 Cycle Detected", "The loop should be detected") - -tr = Test.AddTestRun("test wildcard with proxy_protocol_port") -tr.TimeOut = 5 -tr.Setup.Copy('proxy_protocol_client.py') -tr.Processes.Default.Command = ( - f'{sys.executable} proxy_protocol_client.py ' - f'127.0.0.1 {ts.Variables.proxy_protocol_ssl_port} wildcard.with.proxy.protocol.port.com ' - f'127.0.0.1 127.0.0.1 60123 {server_foo.Variables.SSL_Port} ' - f'2 --https') -tr.ReturnCode = 0 -tr.StillRunningAfter = ts -ts.Disk.traffic_out.Content += Testers.ContainsExpression( - rf"Destination now is \[backend.wildcard.with.proxy.protocol.port.com:{server_foo.Variables.SSL_Port}\]", - "Verify the tunnel destination is expanded correctly.") -tr.Processes.Default.Streams.All += Testers.ContainsExpression("HTTP/1.1 200 OK", "Verify a successful response is received") - -# Regression: a tunnel_route that combines $N match groups with a port variable -# must still enforce connect_ports. An earlier bug let MATCH_GROUPS clear the -# dynamic-port flag set by MAP_WITH_PROXY_PROTOCOL_PORT, bypassing the check. -tr = Test.AddTestRun("test wildcard with proxy_protocol_port - not in connect_ports") -tr.TimeOut = 5 -tr.Setup.Copy('proxy_protocol_client.py') -wildcard_rejected_port = server_forbidden.Variables.SSL_Port -tr.Processes.Default.Command = ( - f'{sys.executable} proxy_protocol_client.py ' - f'127.0.0.1 {ts.Variables.proxy_protocol_ssl_port} wildcard.with.proxy.protocol.port.com ' - f'127.0.0.1 127.0.0.1 60123 {wildcard_rejected_port} ' - f'2 --https') -tr.ReturnCode = 1 -tr.StillRunningAfter = ts -tr.Processes.Default.Streams.All += Testers.ContainsExpression("ssl.SSL.*Error:.*EOF", "Verify the handshake failed") -ts.Disk.traffic_out.Content += Testers.ContainsExpression( - f"Rejected a tunnel to port {wildcard_rejected_port} not in connect_ports", - "Verify the tunnel was rejected even though the route uses a $N match group") - -# Update sni file and reload -tr = Test.AddTestRun("Update config files") -# Update the SNI config -snipath = ts.Disk.sni_yaml.AbsPath -recordspath = ts.Disk.records_config.AbsPath -tr.Disk.File(snipath, id="sni_yaml", typename="ats:config"), -tr.Disk.sni_yaml.AddLines([ - 'sni:', - '- fqdn: bar.com', - ' tunnel_route: localhost:{0}'.format(server_bar.Variables.SSL_Port), -]) -tr.StillRunningAfter = ts -tr.StillRunningAfter = server_foo -tr.StillRunningAfter = server_bar -tr.Processes.Default.Env = ts.Env -tr.Processes.Default.Command = 'echo Updated configs' -tr.Processes.Default.ReturnCode = 0 - -# Test a large CLIENT_HELLO. -tr = Test.AddTestRun("Single large CLIENT_HELLO blind tunnel test") -tr.TimeOut = 5 -tr.Setup.Copy('split_client_hello.py') -tr.Setup.Copy('receive_split_client_hello.py') -p = tr.Processes.Default -p.StartBefore(split_client_hello_server, ready=When.PortOpen(server_port)) -p.Command = f'{sys.executable} split_client_hello.py 127.0.0.1 {ts.Variables.ssl_port} -s 0' -p.ReturnCode = 0 -tr.StillRunningAfter = ts -tr.StillRunningAfter = dns -p.Streams.All += Testers.ContainsExpression('dummy SERVER_HELLO', 'Verify a dummy SERVER_HELLO response is received') -p.Streams.All += Testers.ContainsExpression('data: 0', 'Verify that the first data packet was received.') -p.Streams.All += Testers.ContainsExpression('data: 1', 'Verify that the second data packet was received.') - -# Test a CLIENT_HELLO split over two TCP packets. -tr = Test.AddTestRun("Split large CLIENT_HELLO blind tunnel test") -tr.TimeOut = 5 -tr.Setup.Copy('split_client_hello.py') -tr.Setup.Copy('receive_split_client_hello.py') -p = tr.Processes.Default -p.Command = f'{sys.executable} split_client_hello.py 127.0.0.1 {ts.Variables.ssl_port}' -p.ReturnCode = 0 -tr.StillRunningAfter = ts -tr.StillRunningAfter = dns -p.Streams.All += Testers.ContainsExpression('dummy SERVER_HELLO', 'Verify a dummy SERVER_HELLO response is received') -p.Streams.All += Testers.ContainsExpression('data: 0', 'Verify that the first data packet was received.') -p.Streams.All += Testers.ContainsExpression('data: 1', 'Verify that the second data packet was received.') - -trreload = Test.AddConfigReload(ts, expect="success", expect_tasks=["sni.yaml"], description="Reload config") -trreload.StillRunningAfter = server_foo -trreload.StillRunningAfter = server_bar - -# Should terminate on traffic_server (not tunnel) -tr = Test.AddTestRun("foo.com no Tunnel-test") -tr.TimeOut = 30 -tr.StillRunningAfter = ts -tr.Processes.Default.StartBefore(server2) -tr.MakeCurlCommand("-v --resolve 'foo.com:{0}:127.0.0.1' -k https://foo.com:{0}".format(ts.Variables.ssl_port), ts=ts) -tr.Processes.Default.Streams.All += Testers.ContainsExpression("Not Found on Accelerato", "Terminates on on Traffic Server") -tr.Processes.Default.Streams.All += Testers.ContainsExpression("ATS", "Terminate on Traffic Server") -tr.Processes.Default.Streams.All += Testers.ExcludesExpression("Could Not Connect", "Curl attempt should have succeeded") - -# Should tunnel to server_bar -tr = Test.AddTestRun("bar.com Tunnel-test") -tr.MakeCurlCommand("-v --resolve 'bar.com:{0}:127.0.0.1' -k https://bar.com:{0}".format(ts.Variables.ssl_port), ts=ts) -tr.ReturnCode = 0 -tr.StillRunningAfter = ts -tr.Processes.Default.Streams.All += Testers.ExcludesExpression("Could Not Connect", "Curl attempt should have succeeded") -tr.Processes.Default.Streams.All += Testers.ExcludesExpression("Not Found on Accelerato", "Terminates on on Traffic Server") -tr.Processes.Default.Streams.All += Testers.ExcludesExpression("ATS", "Terminate on Traffic Server") -tr.Processes.Default.Streams.All += Testers.ContainsExpression("bar ok", "Should get a response from bar") - -tr = Test.AddTestRun("Test Metrics") -tr.Processes.Default.Command = ( - f"{Test.Variables.AtsTestToolsDir}/stdout_wait" + " 'traffic_ctl metric get" + - " proxy.process.http.total_incoming_connections" + " proxy.process.http.total_client_connections" + - " proxy.process.http.total_client_connections_ipv4" + " proxy.process.http.total_client_connections_ipv6" + - " proxy.process.http.total_server_connections" + " proxy.process.http2.total_client_connections" + - " proxy.process.http.connect_requests" + " proxy.process.tunnel.total_client_connections_blind_tcp" + - " proxy.process.tunnel.current_client_connections_blind_tcp" + " proxy.process.tunnel.total_server_connections_blind_tcp" + - " proxy.process.tunnel.current_server_connections_blind_tcp" + " proxy.process.tunnel.total_client_connections_tls_tunnel" + - " proxy.process.tunnel.current_client_connections_tls_tunnel" + " proxy.process.tunnel.total_client_connections_tls_forward" + - " proxy.process.tunnel.current_client_connections_tls_forward" + - " proxy.process.tunnel.total_client_connections_tls_partial_blind" + - " proxy.process.tunnel.current_client_connections_tls_partial_blind" + - " proxy.process.tunnel.total_client_connections_tls_http" + " proxy.process.tunnel.current_client_connections_tls_http" + - " proxy.process.tunnel.total_server_connections_tls" + " proxy.process.tunnel.current_server_connections_tls'" + - f" {Test.TestDirectory}/gold/tls-tunnel-metrics.gold") -# Need to copy over the environment so traffic_ctl knows where to find the unix domain socket -tr.Processes.Default.Env = ts.Env -tr.Processes.Default.ReturnCode = 0 -tr.StillRunningAfter = ts diff --git a/tests/gold_tests/tls/tls_tunnel_forward.test.py b/tests/gold_tests/tls/tls_tunnel_forward.test.py deleted file mode 100644 index a73b8d3f923..00000000000 --- a/tests/gold_tests/tls/tls_tunnel_forward.test.py +++ /dev/null @@ -1,156 +0,0 @@ -''' -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -Test.Summary = ''' -Test tunneling and forwarding based on SNI -''' - -# Define default ATS -ts = Test.MakeATSProcess("ts", enable_tls=True) -server_foo = Test.MakeOriginServer("server_foo", ssl=True) -server_bar = Test.MakeOriginServer("server_bar", ssl=False) -server_random = Test.MakeOriginServer("server_random", ssl=False) -nameserver = Test.MakeDNServer("dns", default='127.0.0.1') - -request_foo_header = {"headers": "GET / HTTP/1.1\r\nHost: foo.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -request_bar_header = {"headers": "GET / HTTP/1.1\r\nHost: bar.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -request_random_header = {"headers": "GET / HTTP/1.1\r\nHost: random.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -response_foo_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": "ok foo"} -response_bar_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": "ok bar"} -response_random_header = { - "headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", - "timestamp": "1469733493.993", - "body": "ok random" -} -server_foo.addResponse("sessionlog_foo.json", request_foo_header, response_foo_header) -server_bar.addResponse("sessionlog_bar.json", request_bar_header, response_bar_header) -server_random.addResponse("sessionlog_random.json", request_random_header, response_random_header) - -# add ssl materials like key, certificates for the server -ts.addSSLfile("ssl/signed-foo.pem") -ts.addSSLfile("ssl/signed-foo.key") -ts.addSSLfile("ssl/signed-bar.pem") -ts.addSSLfile("ssl/signed-bar.key") -ts.addSSLfile("ssl/server.pem") -ts.addSSLfile("ssl/server.key") -ts.addSSLfile("ssl/signer.pem") -ts.addSSLfile("ssl/signer.key") - -# Need no remap rules. Everything should be processed by sni - -# Make sure the TS server certs are different from the origin certs -ts.Disk.ssl_multicert_yaml.AddLines( - """ -ssl_multicert: - - dest_ip: "*" - ssl_cert_name: signed-foo.pem - ssl_key_name: signed-foo.key -""".split("\n")) - -# Case 1, global config policy=permissive properties=signature -# override for foo.com policy=enforced properties=all -ts.Disk.records_config.update( - { - 'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir), - 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts.Variables.SSLDir), - 'proxy.config.http.connect_ports': - '{0} {1} {2} {3}'.format( - ts.Variables.ssl_port, server_foo.Variables.SSL_Port, server_bar.Variables.Port, server_random.Variables.Port), - 'proxy.config.ssl.client.CA.cert.path': '{0}'.format(ts.Variables.SSLDir), - 'proxy.config.ssl.client.CA.cert.filename': 'signer.pem', - 'proxy.config.exec_thread.autoconfig.scale': 1.0, - 'proxy.config.url_remap.pristine_host_hdr': 1, - 'proxy.config.dns.nameservers': f"127.0.0.1:{nameserver.Variables.Port}", - 'proxy.config.dns.resolv_conf': 'NULL' - }) -ts.addPrivateConnectAllowYaml() - -# foo.com should not terminate. Just tunnel to server_foo -# bar.com should terminate. Forward its tcp stream to server_bar -ts.Disk.sni_yaml.AddLines( - [ - "sni:", - "- fqdn: 'foo.com'", - " tunnel_route: 'localhost:{0}'".format(server_foo.Variables.SSL_Port), - "- fqdn: 'bar.com'", - " forward_route: 'localhost:{0}'".format(server_bar.Variables.Port), - "- fqdn: ''", # default case - " forward_route: 'localhost:{0}'".format(server_random.Variables.Port), - ]) - -tr = Test.AddTestRun("Tunnel-test") -tr.MakeCurlCommand("-v --resolve 'foo.com:{0}:127.0.0.1' -k https://foo.com:{0}".format(ts.Variables.ssl_port), ts=ts) -tr.ReturnCode = 0 -tr.Processes.Default.StartBefore(server_foo) -tr.Processes.Default.StartBefore(server_bar) -tr.Processes.Default.StartBefore(server_random) -tr.Processes.Default.StartBefore(nameserver) -tr.Processes.Default.StartBefore(Test.Processes.ts) -tr.StillRunningAfter = ts -tr.Processes.Default.Streams.All += Testers.ExcludesExpression("Could Not Connect", "Curl attempt should have succeeded") -tr.Processes.Default.Streams.All += Testers.ExcludesExpression( - "Not Found on Accelerato", "Should not try to remap on Traffic Server") -tr.Processes.Default.Streams.All += Testers.ExcludesExpression("CN=foo.com", "Should not TLS terminate on Traffic Server") -tr.Processes.Default.Streams.All += Testers.ContainsExpression("HTTP/1.1 200 OK", "Should get a successful response") -tr.Processes.Default.Streams.All += Testers.ContainsExpression("ok foo", "Body is expected") - -tr2 = Test.AddTestRun("Forward-test") -tr2.MakeCurlCommand( - "-v --http1.1 -H 'host:bar.com' --resolve 'bar.com:{0}:127.0.0.1' -k https://bar.com:{0}".format(ts.Variables.ssl_port), ts=ts) -tr2.ReturnCode = 0 -tr2.StillRunningAfter = server_bar -tr2.StillRunningAfter = ts -tr2.Processes.Default.Streams.All += Testers.ExcludesExpression("Could Not Connect", "Curl attempt should have succeeded") -tr2.Processes.Default.Streams.All += Testers.ExcludesExpression( - "Not Found on Accelerato", "Should not try to remap on Traffic Server") -tr2.Processes.Default.Streams.All += Testers.ContainsExpression("CN=foo.com", "Should TLS terminate on Traffic Server") -tr2.Processes.Default.Streams.All += Testers.ContainsExpression("HTTP/1.1 200 OK", "Should get a successful response") -tr2.Processes.Default.Streams.All += Testers.ContainsExpression("ok bar", "Body is expected") - -tr3 = Test.AddTestRun("no-sni-forward-test") -tr3.MakeCurlCommand("--http1.1 -v -k -H 'host:random.com' https://127.0.0.1:{0}".format(ts.Variables.ssl_port), ts=ts) -tr3.ReturnCode = 0 -tr3.StillRunningAfter = server_random -tr3.StillRunningAfter = ts -tr3.Processes.Default.Streams.All += Testers.ExcludesExpression("Could Not Connect", "Curl attempt should have succeeded") -tr3.Processes.Default.Streams.All += Testers.ExcludesExpression( - "Not Found on Accelerato", "Should not try to remap on Traffic Server") -tr3.Processes.Default.Streams.All += Testers.ContainsExpression("CN=foo.com", "Should TLS terminate on Traffic Server") -tr3.Processes.Default.Streams.All += Testers.ContainsExpression("HTTP/1.1 200 OK", "Should get a successful response") -tr3.Processes.Default.Streams.All += Testers.ContainsExpression("ok random", "Body is expected") - -tr = Test.AddTestRun("Test Metrics") -tr.Processes.Default.Command = ( - f"{Test.Variables.AtsTestToolsDir}/stdout_wait" + " 'traffic_ctl metric get" + - " proxy.process.http.total_incoming_connections" + " proxy.process.http.total_client_connections" + - " proxy.process.http.total_client_connections_ipv4" + " proxy.process.http.total_client_connections_ipv6" + - " proxy.process.http.total_server_connections" + " proxy.process.http2.total_client_connections" + - " proxy.process.http.connect_requests" + " proxy.process.tunnel.total_client_connections_blind_tcp" + - " proxy.process.tunnel.current_client_connections_blind_tcp" + " proxy.process.tunnel.total_server_connections_blind_tcp" + - " proxy.process.tunnel.current_server_connections_blind_tcp" + " proxy.process.tunnel.total_client_connections_tls_tunnel" + - " proxy.process.tunnel.current_client_connections_tls_tunnel" + " proxy.process.tunnel.total_client_connections_tls_forward" + - " proxy.process.tunnel.current_client_connections_tls_forward" + - " proxy.process.tunnel.total_client_connections_tls_partial_blind" + - " proxy.process.tunnel.current_client_connections_tls_partial_blind" + - " proxy.process.tunnel.total_client_connections_tls_http" + " proxy.process.tunnel.current_client_connections_tls_http" + - " proxy.process.tunnel.total_server_connections_tls" + " proxy.process.tunnel.current_server_connections_tls'" + - f" {Test.TestDirectory}/gold/tls-tunnel-forward-metrics.gold") -# Need to copy over the environment so traffic_ctl knows where to find the unix domain socket -tr.Processes.Default.Env = ts.Env -tr.Processes.Default.ReturnCode = 0 -tr.StillRunningAfter = ts diff --git a/tests/gold_tests/tls/tls_verify.test.py b/tests/gold_tests/tls/tls_verify.test.py deleted file mode 100644 index 890bae18ce6..00000000000 --- a/tests/gold_tests/tls/tls_verify.test.py +++ /dev/null @@ -1,165 +0,0 @@ -''' -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -Test.Summary = ''' -Test tls server certificate verification options -''' - -# Define default ATS -ts = Test.MakeATSProcess("ts", enable_tls=True) -server_foo = Test.MakeOriginServer( - "server_foo", - ssl=True, - options={ - "--key": "{0}/signed-foo.key".format(Test.RunDirectory), - "--cert": "{0}/signed-foo.pem".format(Test.RunDirectory) - }) -server_bar = Test.MakeOriginServer( - "server_bar", - ssl=True, - options={ - "--key": "{0}/signed-bar.key".format(Test.RunDirectory), - "--cert": "{0}/signed-bar.pem".format(Test.RunDirectory) - }) -server_wild = Test.MakeOriginServer( - "server_wild", - ssl=True, - options={ - "--key": "{0}/signed-wild.key".format(Test.RunDirectory), - "--cert": "{0}/signed-wild.pem".format(Test.RunDirectory) - }) -server = Test.MakeOriginServer("server", ssl=True) - -request_foo_header = {"headers": "GET / HTTP/1.1\r\nHost: foo.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -request_bad_foo_header = {"headers": "GET / HTTP/1.1\r\nHost: badfoo.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -request_bar_header = {"headers": "GET / HTTP/1.1\r\nHost: bar.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -request_bad_bar_header = {"headers": "GET / HTTP/1.1\r\nHost: badbar.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -server_foo.addResponse("sessionlog.json", request_foo_header, response_header) -server_foo.addResponse("sessionlog.json", request_bad_foo_header, response_header) -server_bar.addResponse("sessionlog.json", request_bar_header, response_header) -server_bar.addResponse("sessionlog.json", request_bad_bar_header, response_header) -server_wild.addResponse("sessionlog.json", request_bar_header, response_header) - -# add ssl materials like key, certificates for the server -ts.addSSLfile("ssl/signed-foo.pem") -ts.addSSLfile("ssl/signed-foo.key") -ts.addSSLfile("ssl/signed-bar.pem") -ts.addSSLfile("ssl/signed-bar.key") -ts.addSSLfile("ssl/server.pem") -ts.addSSLfile("ssl/server.key") -ts.addSSLfile("ssl/signer.pem") -ts.addSSLfile("ssl/signer.key") -ts.addSSLfile("ssl/signed-wild.key") -ts.addSSLfile("ssl/signed-wild.pem") - -ts.Disk.remap_config.AddLine('map / https://127.0.0.1:{0}'.format(server.Variables.SSL_Port)) -ts.Disk.remap_config.AddLine('map https://foo.com/ https://127.0.0.1:{0}'.format(server_foo.Variables.SSL_Port)) -ts.Disk.remap_config.AddLine('map https://bad_foo.com/ https://127.0.0.1:{0}'.format(server_foo.Variables.SSL_Port)) -ts.Disk.remap_config.AddLine('map https://bar.com/ https://127.0.0.1:{0}'.format(server_bar.Variables.SSL_Port)) -ts.Disk.remap_config.AddLine('map https://bad_bar.com/ https://127.0.0.1:{0}'.format(server_bar.Variables.SSL_Port)) -ts.Disk.remap_config.AddLine('map https://foo.wild.com/ https://127.0.0.1:{0}'.format(server_wild.Variables.SSL_Port)) -ts.Disk.remap_config.AddLine('map https://foo_bar.wild.com/ https://127.0.0.1:{0}'.format(server_wild.Variables.SSL_Port)) - -ts.Disk.ssl_multicert_yaml.AddLines( - """ -ssl_multicert: - - dest_ip: "*" - ssl_cert_name: server.pem - ssl_key_name: server.key -""".split("\n")) - -# Case 1, global config policy=permissive properties=signature -# override for foo.com policy=enforced properties=all -ts.Disk.records_config.update( - { - 'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir), - 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts.Variables.SSLDir), - # set global policy - 'proxy.config.ssl.client.verify.server.policy': 'PERMISSIVE', - 'proxy.config.ssl.client.verify.server.properties': 'SIGNATURE', - 'proxy.config.ssl.client.CA.cert.path': '{0}'.format(ts.Variables.SSLDir), - 'proxy.config.ssl.client.CA.cert.filename': 'signer.pem', - 'proxy.config.exec_thread.autoconfig.scale': 1.0, - 'proxy.config.url_remap.pristine_host_hdr': 1 - }) - -ts.Disk.sni_yaml.AddLines( - [ - 'sni:', - '- fqdn: bar.com', - ' verify_server_policy: ENFORCED', - ' verify_server_properties: ALL', - '- fqdn: "*.wild.com"', - ' verify_server_policy: ENFORCED', - ' verify_server_properties: ALL', - '- fqdn: bad_bar.com', - ' verify_server_policy: ENFORCED', - ' verify_server_properties: ALL', - ]) - -tr = Test.AddTestRun("Permissive-Test") -tr.Setup.Copy("ssl/signed-foo.key") -tr.Setup.Copy("ssl/signed-foo.pem") -tr.Setup.Copy("ssl/signed-bar.key") -tr.Setup.Copy("ssl/signed-bar.pem") -tr.Setup.Copy("ssl/signed-wild.pem") -tr.Setup.Copy("ssl/signed-wild.key") -tr.MakeCurlCommand("-v -k -H \"host: foo.com\" https://127.0.0.1:{0}".format(ts.Variables.ssl_port), ts=ts) -tr.ReturnCode = 0 -tr.Processes.Default.StartBefore(server_foo) -tr.Processes.Default.StartBefore(server_bar) -tr.Processes.Default.StartBefore(server) -tr.Processes.Default.StartBefore(server_wild) -tr.Processes.Default.StartBefore(Test.Processes.ts) -tr.StillRunningAfter = server -tr.StillRunningAfter = ts -tr.Processes.Default.Streams.stdout = Testers.ExcludesExpression("Could Not Connect", "Curl attempt should have succeeded") - -tr2 = Test.AddTestRun("Override-enforcing-Test") -tr2.MakeCurlCommand("-v -k -H \"host: bar.com\" https://127.0.0.1:{0}".format(ts.Variables.ssl_port), ts=ts) -tr2.ReturnCode = 0 -tr2.StillRunningAfter = server -tr2.StillRunningAfter = ts -tr2.Processes.Default.Streams.stdout = Testers.ExcludesExpression("Could Not Connect", "Curl attempt should have succeeded") - -tr3 = Test.AddTestRun("Override-enforcing-Test-fail-name-check") -tr3.MakeCurlCommand("-v -k -H \"host: bad_bar.com\" https://127.0.0.1:{0}".format(ts.Variables.ssl_port), ts=ts) -tr3.Processes.Default.Streams.stdout = Testers.ContainsExpression("Could Not Connect", "Curl attempt should have failed") -tr3.ReturnCode = 0 -tr3.StillRunningAfter = server -tr3.StillRunningAfter = ts - -tr4 = Test.AddTestRun("Exercise-wildcard-cert-name-check") -tr4.MakeCurlCommand("-v -k -H \"host: foo.wild.com\" https://127.0.0.1:{0}".format(ts.Variables.ssl_port), ts=ts) -tr4.Processes.Default.Streams.stdout = Testers.ExcludesExpression("Could Not Connect", "Curl attempt should have succeeded") -tr4.ReturnCode = 0 -tr4.StillRunningAfter = server -tr4.StillRunningAfter = ts - -tr5 = Test.AddTestRun("Exercise-wildcard-cert-underscore-name-check") -tr5.MakeCurlCommand("-v -k -H \"host: foo_bar.wild.com\" https://127.0.0.1:{0}".format(ts.Variables.ssl_port), ts=ts) -tr5.Processes.Default.Streams.stdout = Testers.ExcludesExpression("Could Not Connect", "Curl attempt should have succeeded") -tr5.ReturnCode = 0 -tr5.StillRunningAfter = server -tr5.StillRunningAfter = ts - -# Over riding the built in ERROR check since we expect tr3 to fail -ts.Disk.diags_log.Content = Testers.ExcludesExpression("verification failed", "Make sure the signatures didn't fail") -ts.Disk.diags_log.Content += Testers.ContainsExpression( - r"WARNING: SNI \(bad_bar.com\) not in certificate", "Make sure bad_bar name checked failed.") diff --git a/tests/gold_tests/tls/tls_verify2.test.py b/tests/gold_tests/tls/tls_verify2.test.py deleted file mode 100644 index 72f261eab0e..00000000000 --- a/tests/gold_tests/tls/tls_verify2.test.py +++ /dev/null @@ -1,161 +0,0 @@ -''' -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -Test.Summary = ''' -Test tls server certificate verification options -''' - -# Define default ATS -ts = Test.MakeATSProcess("ts", enable_tls=True) -server_foo = Test.MakeOriginServer( - "server_foo", - ssl=True, - options={ - "--key": "{0}/signed-foo.key".format(Test.RunDirectory), - "--cert": "{0}/signed-foo.pem".format(Test.RunDirectory) - }) -server_bar = Test.MakeOriginServer( - "server_bar", - ssl=True, - options={ - "--key": "{0}/signed-bar.key".format(Test.RunDirectory), - "--cert": "{0}/signed-bar.pem".format(Test.RunDirectory) - }) -server = Test.MakeOriginServer("server", ssl=True) - -request_foo_header = {"headers": "GET / HTTP/1.1\r\nHost: foo.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -request_bad_foo_header = {"headers": "GET / HTTP/1.1\r\nHost: bad_foo.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -request_bar_header = {"headers": "GET / HTTP/1.1\r\nHost: bar.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -request_bad_bar_header = {"headers": "GET / HTTP/1.1\r\nHost: bad_bar.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -server_foo.addResponse("sessionlog.json", request_foo_header, response_header) -server_foo.addResponse("sessionlog.json", request_bad_foo_header, response_header) -server_bar.addResponse("sessionlog.json", request_bar_header, response_header) -server_bar.addResponse("sessionlog.json", request_bad_bar_header, response_header) - -# add ssl materials like key, certificates for the server -ts.addSSLfile("ssl/signed-foo.pem") -ts.addSSLfile("ssl/signed-foo.key") -ts.addSSLfile("ssl/signed-bar.pem") -ts.addSSLfile("ssl/signed-bar.key") -ts.addSSLfile("ssl/server.pem") -ts.addSSLfile("ssl/server.key") -ts.addSSLfile("ssl/signer.pem") -ts.addSSLfile("ssl/signer.key") - -ts.Disk.remap_config.AddLine('map https://foo.com/ https://127.0.0.1:{0}'.format(server_foo.Variables.SSL_Port)) -ts.Disk.remap_config.AddLine('map https://bad_foo.com/ https://127.0.0.1:{0}'.format(server_foo.Variables.SSL_Port)) -ts.Disk.remap_config.AddLine('map https://bar.com/ https://127.0.0.1:{0}'.format(server_bar.Variables.SSL_Port)) -ts.Disk.remap_config.AddLine('map https://bad_bar.com/ https://127.0.0.1:{0}'.format(server_bar.Variables.SSL_Port)) -ts.Disk.remap_config.AddLine('map / https://127.0.0.1:{0}'.format(server.Variables.SSL_Port)) - -ts.Disk.ssl_multicert_yaml.AddLines( - """ -ssl_multicert: - - dest_ip: "*" - ssl_cert_name: server.pem - ssl_key_name: server.key -""".split("\n")) - -# Case 1, global config policy=permissive properties=signature -# override for foo.com policy=enforced properties=all -ts.Disk.records_config.update( - { - 'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir), - 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts.Variables.SSLDir), - # set global policy - 'proxy.config.ssl.client.verify.server.policy': 'ENFORCED', - 'proxy.config.ssl.client.verify.server.properties': 'ALL', - 'proxy.config.ssl.client.CA.cert.path': '{0}'.format(ts.Variables.SSLDir), - 'proxy.config.ssl.client.CA.cert.filename': 'signer.pem', - 'proxy.config.exec_thread.autoconfig.scale': 1.0, - 'proxy.config.url_remap.pristine_host_hdr': 1 - }) - -ts.Disk.sni_yaml.AddLine('sni:') -ts.Disk.sni_yaml.AddLine('- fqdn: bar.com') -ts.Disk.sni_yaml.AddLine(' verify_server_policy: PERMISSIVE') -ts.Disk.sni_yaml.AddLine(' verify_server_properties: SIGNATURE') -ts.Disk.sni_yaml.AddLine('- fqdn: bad_bar.com') -ts.Disk.sni_yaml.AddLine(' verify_server_policy: PERMISSIVE') -ts.Disk.sni_yaml.AddLine(' verify_server_properties: SIGNATURE') -ts.Disk.sni_yaml.AddLine('- fqdn: random.com') -ts.Disk.sni_yaml.AddLine(' verify_server_policy: DISABLED') - -tr = Test.AddTestRun("default-enforce") -tr.Setup.Copy("ssl/signed-foo.key") -tr.Setup.Copy("ssl/signed-foo.pem") -tr.Setup.Copy("ssl/signed-bar.key") -tr.Setup.Copy("ssl/signed-bar.pem") -tr.MakeCurlCommand("-k -H \"host: foo.com\" https://127.0.0.1:{0}".format(ts.Variables.ssl_port), ts=ts) -tr.ReturnCode = 0 -tr.Processes.Default.StartBefore(server_foo) -tr.Processes.Default.StartBefore(server_bar) -tr.Processes.Default.StartBefore(server) -tr.Processes.Default.StartBefore(Test.Processes.ts) -tr.StillRunningAfter = server -tr.StillRunningAfter = ts -tr.Processes.Default.Streams.stdout = Testers.ExcludesExpression("Could Not Connect", "Curl attempt should have succeeded") - -tr2 = Test.AddTestRun("override-disabled") -tr2.MakeCurlCommand("-k -H \"host: random.com\" https://127.0.0.1:{0}".format(ts.Variables.ssl_port), ts=ts) -tr2.ReturnCode = 0 -tr2.StillRunningAfter = server -tr2.StillRunningAfter = ts -tr2.Processes.Default.Streams.stdout = Testers.ExcludesExpression("Could Not Connect", "Curl attempt should have succeeded") - -tr3 = Test.AddTestRun("override-permissive") -tr3.MakeCurlCommand("-k -H \"host: bar.com\" https://127.0.0.1:{0}".format(ts.Variables.ssl_port), ts=ts) -tr3.ReturnCode = 0 -tr3.StillRunningAfter = server -tr3.StillRunningAfter = ts -tr3.Processes.Default.Streams.stdout = Testers.ExcludesExpression("Could Not Connect", "Curl attempt should have succeeded") - -tr4 = Test.AddTestRun("override-permissive-bad-name") -tr4.MakeCurlCommand("-k -H \"host: bad_bar.com\" https://127.0.0.1:{0}".format(ts.Variables.ssl_port), ts=ts) -tr4.ReturnCode = 0 -tr4.StillRunningAfter = server -tr4.StillRunningAfter = ts -tr4.Processes.Default.Streams.stdout = Testers.ExcludesExpression("Could Not Connect", "Curl attempt should have succeeded") - -tr5 = Test.AddTestRun("default-enforce-bad-sig") -tr5.MakeCurlCommand("-k -H \"host: random2.com\" https://127.0.0.1:{0}".format(ts.Variables.ssl_port), ts=ts) -tr5.ReturnCode = 0 -tr5.Processes.Default.Streams.stdout = Testers.ContainsExpression("Could Not Connect", "Curl attempt should have failed") -tr5.StillRunningAfter = server -tr5.StillRunningAfter = ts - -tr6 = Test.AddTestRun("default-enforce-fail") -tr6.MakeCurlCommand("-k -H \"host: bad_foo.com\" https://127.0.0.1:{0}".format(ts.Variables.ssl_port), ts=ts) -tr6.ReturnCode = 0 -tr6.Processes.Default.Streams.stdout = Testers.ContainsExpression("Could Not Connect", "Curl attempt should have failed") -tr6.StillRunningAfter = server -tr6.StillRunningAfter = ts - -# No name checking for the sig-only permissive override for bad_bar -ts.Disk.diags_log.Content += Testers.ExcludesExpression( - r"WARNING: SNI \(bad_bar.com\) not in certificate", "bad_bar name checked should be skipped.") -ts.Disk.diags_log.Content = Testers.ExcludesExpression( - r"WARNING: SNI \(foo.com\) not in certificate", "foo name checked should be skipped.") -# No checking for the self-signed on random.com. No messages -ts.Disk.diags_log.Content += Testers.ExcludesExpression( - r"WARNING: Core server certificate verification failed for \(random.com\)", "signature check for random.com should be skipped") -ts.Disk.diags_log.Content += Testers.ContainsExpression( - r"WARNING: Core server certificate verification failed for \(random2.com\)", "signature check for random.com should fail'") -ts.Disk.diags_log.Content += Testers.ContainsExpression( - r"WARNING: SNI \(bad_foo.com\) not in certificate", "bad_foo name checked should be checked.") diff --git a/tests/gold_tests/tls/tls_verify3.test.py b/tests/gold_tests/tls/tls_verify3.test.py deleted file mode 100644 index 0603499aa34..00000000000 --- a/tests/gold_tests/tls/tls_verify3.test.py +++ /dev/null @@ -1,156 +0,0 @@ -''' -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -Test.Summary = ''' -Test tls server certificate verification options -''' - -# Define default ATS -ts = Test.MakeATSProcess("ts", enable_tls=True) -server_foo = Test.MakeOriginServer( - "server_foo", - ssl=True, - options={ - "--key": "{0}/signed-foo.key".format(Test.RunDirectory), - "--cert": "{0}/signed-foo.pem".format(Test.RunDirectory) - }) -server_bar = Test.MakeOriginServer( - "server_bar", - ssl=True, - options={ - "--key": "{0}/signed-bar.key".format(Test.RunDirectory), - "--cert": "{0}/signed-bar.pem".format(Test.RunDirectory) - }) -server = Test.MakeOriginServer("server", ssl=True) - -request_foo_header = {"headers": "GET / HTTP/1.1\r\nHost: foo.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -request_bad_foo_header = {"headers": "GET / HTTP/1.1\r\nHost: badfoo.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -request_bar_header = {"headers": "GET / HTTP/1.1\r\nHost: bar.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -request_bad_bar_header = {"headers": "GET / HTTP/1.1\r\nHost: badbar.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -server_foo.addResponse("sessionlog.json", request_foo_header, response_header) -server_foo.addResponse("sessionlog.json", request_bad_foo_header, response_header) -server_bar.addResponse("sessionlog.json", request_bar_header, response_header) -server_bar.addResponse("sessionlog.json", request_bad_bar_header, response_header) - -# add ssl materials like key, certificates for the server -ts.addSSLfile("ssl/signed-foo.pem") -ts.addSSLfile("ssl/signed-foo.key") -ts.addSSLfile("ssl/signed-bar.pem") -ts.addSSLfile("ssl/signed-bar.key") -ts.addSSLfile("ssl/server.pem") -ts.addSSLfile("ssl/server.key") -ts.addSSLfile("ssl/signer.pem") -ts.addSSLfile("ssl/signer.key") - -ts.Disk.remap_config.AddLine( - 'map https://foo.com:{1}/ https://127.0.0.1:{0}'.format(server_foo.Variables.SSL_Port, ts.Variables.ssl_port)) -ts.Disk.remap_config.AddLine( - 'map https://bob.foo.com:{1}/ https://127.0.0.1:{0}'.format(server_foo.Variables.SSL_Port, ts.Variables.ssl_port)) -ts.Disk.remap_config.AddLine( - 'map https://bar.com:{1}/ https://127.0.0.1:{0}'.format(server_bar.Variables.SSL_Port, ts.Variables.ssl_port)) -ts.Disk.remap_config.AddLine( - 'map https://bob.bar.com:{1}/ https://127.0.0.1:{0}'.format(server_bar.Variables.SSL_Port, ts.Variables.ssl_port)) -ts.Disk.remap_config.AddLine('map / https://127.0.0.1:{0}'.format(server.Variables.SSL_Port)) - -ts.Disk.ssl_multicert_yaml.AddLines( - """ -ssl_multicert: - - dest_ip: "*" - ssl_cert_name: server.pem - ssl_key_name: server.key -""".split("\n")) - -# Case 1, global config policy=permissive properties=signature -# override for foo.com policy=enforced properties=all -ts.Disk.records_config.update( - { - 'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir), - 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts.Variables.SSLDir), - # set global policy - 'proxy.config.ssl.client.verify.server.policy': 'PERMISSIVE', - 'proxy.config.ssl.client.verify.server.properties': 'ALL', - 'proxy.config.ssl.client.CA.cert.path': '{0}'.format(ts.Variables.SSLDir), - 'proxy.config.ssl.client.CA.cert.filename': 'signer.pem', - 'proxy.config.exec_thread.autoconfig.scale': 1.0, - 'proxy.config.url_remap.pristine_host_hdr': 1 - }) - -ts.Disk.sni_yaml.AddLines( - [ - 'sni:', - '- fqdn: bob.bar.com', - ' verify_server_policy: ENFORCED', - ' verify_server_properties: ALL', - '- fqdn: "*.foo.com"', - ' verify_server_policy: ENFORCED', - ' verify_server_properties: SIGNATURE', - "- fqdn: '*.bar.com'", - ' verify_server_policy: DISABLED', - ]) - -tr = Test.AddTestRun("foo.com Permissive-Test") -tr.Setup.Copy("ssl/signed-foo.key") -tr.Setup.Copy("ssl/signed-foo.pem") -tr.Setup.Copy("ssl/signed-bar.key") -tr.Setup.Copy("ssl/signed-bar.pem") -tr.MakeCurlCommand("-v -k --resolve 'foo.com:{0}:127.0.0.1' https://foo.com:{0}".format(ts.Variables.ssl_port), ts=ts) -tr.ReturnCode = 0 -tr.Processes.Default.StartBefore(Test.Processes.ts) -tr.Processes.Default.StartBefore(server_foo) -tr.Processes.Default.StartBefore(server_bar) -tr.Processes.Default.StartBefore(server) -tr.StillRunningAfter = server -tr.StillRunningAfter = ts -tr.Processes.Default.Streams.stdout = Testers.ExcludesExpression("Could Not Connect", "Curl attempt should have succeeded") - -tr = Test.AddTestRun("my.random.com Permissive-Test log failure") -tr.MakeCurlCommand("-v -k --resolve 'my.random.com:{0}:127.0.0.1' https://my.random.com:{0}".format(ts.Variables.ssl_port), ts=ts) -tr.ReturnCode = 0 -tr.StillRunningAfter = server -tr.StillRunningAfter = ts -tr.Processes.Default.Streams.stdout = Testers.ExcludesExpression("Could Not Connect", "Curl attempt should have succeeded") - -tr2 = Test.AddTestRun("bob.bar.com Override-enforcing-Test") -tr2.MakeCurlCommand("-v -k --resolve 'bob.bar.com:{0}:127.0.0.1' https://bob.bar.com:{0}/".format(ts.Variables.ssl_port), ts=ts) -tr2.ReturnCode = 0 -tr2.StillRunningAfter = server -tr2.StillRunningAfter = ts -tr2.Processes.Default.Streams.stdout = Testers.ContainsExpression("Could Not Connect", "Curl attempt should have succeeded") - -tr3 = Test.AddTestRun("bob.foo.com override-enforcing-name-test") -tr3.MakeCurlCommand("-v -k --resolve 'bob.foo.com:{0}:127.0.0.1' https://bob.foo.com:{0}/".format(ts.Variables.ssl_port), ts=ts) -tr3.Processes.Default.Streams.stdout = Testers.ExcludesExpression("Could Not Connect", "Curl attempt should not fail") -tr3.ReturnCode = 0 -tr3.StillRunningAfter = server -tr3.StillRunningAfter = ts - -tr3 = Test.AddTestRun("random.bar.com override-no-test") -tr3.MakeCurlCommand( - "-v -k --resolve 'random.bar.com:{0}:127.0.0.1' https://random.bar.com:{0}".format(ts.Variables.ssl_port), ts=ts) -tr3.Processes.Default.Streams.stdout = Testers.ExcludesExpression("Could Not Connect", "Curl attempt should not fail") -tr3.ReturnCode = 0 -tr3.StillRunningAfter = server -tr3.StillRunningAfter = ts - -# Over riding the built in ERROR check since we expect tr3 to fail -ts.Disk.diags_log.Content = Testers.ContainsExpression( - r"WARNING: SNI \(bob.bar.com\) not in certificate", "Make sure bob.bar name checked failed.") -ts.Disk.diags_log.Content += Testers.ContainsExpression( - r"WARNING: Core server certificate verification failed for \(my.random.com\). Action=Continue", - "Make sure default permissive action takes") diff --git a/tests/gold_tests/tls/tls_verify4.test.py b/tests/gold_tests/tls/tls_verify4.test.py deleted file mode 100644 index 6bddabf38aa..00000000000 --- a/tests/gold_tests/tls/tls_verify4.test.py +++ /dev/null @@ -1,188 +0,0 @@ -''' -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -Test.Summary = ''' -Test tls server certificate verification options -Specifically update via traffic_ctl (reloadable) -''' - -# Define default ATS -ts = Test.MakeATSProcess("ts", select_ports=True, enable_tls=True) -server_foo = Test.MakeOriginServer( - "server_foo", - ssl=True, - options={ - "--key": "{0}/signed-foo.key".format(Test.RunDirectory), - "--cert": "{0}/signed-foo.pem".format(Test.RunDirectory) - }) -server_bar = Test.MakeOriginServer( - "server_bar", - ssl=True, - options={ - "--key": "{0}/signed-bar.key".format(Test.RunDirectory), - "--cert": "{0}/signed-bar.pem".format(Test.RunDirectory) - }) -server = Test.MakeOriginServer("server", ssl=True) - -request_foo_header = {"headers": "GET / HTTP/1.1\r\nHost: foo.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -request_bad_foo_header = {"headers": "GET / HTTP/1.1\r\nHost: bad_foo.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -request_bar_header = {"headers": "GET / HTTP/1.1\r\nHost: bar.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -request_bad_bar_header = {"headers": "GET / HTTP/1.1\r\nHost: bad_bar.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -server_foo.addResponse("sessionlog.json", request_foo_header, response_header) -server_foo.addResponse("sessionlog.json", request_bad_foo_header, response_header) -server_bar.addResponse("sessionlog.json", request_bar_header, response_header) -server_bar.addResponse("sessionlog.json", request_bad_bar_header, response_header) - -# add ssl materials like key, certificates for the server -ts.addSSLfile("ssl/signed-foo.pem") -ts.addSSLfile("ssl/signed-foo.key") -ts.addSSLfile("ssl/signed-bar.pem") -ts.addSSLfile("ssl/signed-bar.key") -ts.addSSLfile("ssl/server.pem") -ts.addSSLfile("ssl/server.key") -ts.addSSLfile("ssl/signer.pem") -ts.addSSLfile("ssl/signer.key") - -ts.Disk.remap_config.AddLine('map https://foo.com/ https://127.0.0.1:{0}'.format(server_foo.Variables.SSL_Port)) -ts.Disk.remap_config.AddLine('map https://bad_foo.com/ https://127.0.0.1:{0}'.format(server_foo.Variables.SSL_Port)) -ts.Disk.remap_config.AddLine('map https://bar.com/ https://127.0.0.1:{0}'.format(server_bar.Variables.SSL_Port)) -ts.Disk.remap_config.AddLine('map https://bad_bar.com/ https://127.0.0.1:{0}'.format(server_bar.Variables.SSL_Port)) -ts.Disk.remap_config.AddLine('map / https://127.0.0.1:{0}'.format(server.Variables.SSL_Port)) - -ts.Disk.ssl_multicert_yaml.AddLines( - """ -ssl_multicert: - - dest_ip: "*" - ssl_cert_name: server.pem - ssl_key_name: server.key -""".split("\n")) - -# Case 1, global config policy=permissive properties=signature -# override for foo.com policy=enforced properties=all -ts.Disk.records_config.update( - { - 'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir), - 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts.Variables.SSLDir), - 'proxy.config.ssl.server.cipher_suite': - 'ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-SHA384:AES128-GCM-SHA256:AES256-GCM-SHA384:ECDHE-RSA-RC4-SHA:ECDHE-RSA-AES128-SHA:ECDHE-RSA-AES256-SHA:RC4-SHA:RC4-MD5:AES128-SHA:AES256-SHA:DES-CBC3-SHA!SRP:!DSS:!PSK:!aNULL:!eNULL:!SSLv2', - # set global policy - 'proxy.config.ssl.client.verify.server.policy': 'ENFORCED', - 'proxy.config.ssl.client.verify.server.properties': 'ALL', - 'proxy.config.ssl.client.CA.cert.path': '{0}'.format(ts.Variables.SSLDir), - 'proxy.config.ssl.client.CA.cert.filename': 'signer.pem', - 'proxy.config.exec_thread.autoconfig.scale': 1.0, - 'proxy.config.url_remap.pristine_host_hdr': 1, - 'proxy.config.diags.debug.enabled': 0, - 'proxy.config.diags.debug.tags': 'ssl' - }) - -tr = Test.AddTestRun("default-enforce-bad-sig") -tr.Setup.Copy("ssl/signed-foo.key") -tr.Setup.Copy("ssl/signed-foo.pem") -tr.Setup.Copy("ssl/signed-bar.key") -tr.Setup.Copy("ssl/signed-bar.pem") -tr.MakeCurlCommand("-k -H \"host: random2.com\" https://127.0.0.1:{0}".format(ts.Variables.ssl_port), ts=ts) -tr.ReturnCode = 0 -tr.Processes.Default.StartBefore(server_foo) -tr.Processes.Default.StartBefore(server_bar) -tr.Processes.Default.StartBefore(server) -tr.Processes.Default.StartBefore(Test.Processes.ts) -tr.StillRunningAfter = server -tr.StillRunningAfter = ts -tr.Processes.Default.Streams.stdout = Testers.ContainsExpression("Could Not Connect", "Curl attempt should have failed") - -tr2 = Test.AddTestRun("Update config files") -recordspath = ts.Disk.records_config.AbsPath -# recreate the records.config with the cert filename changed -tr2.Disk.File(recordspath, id="records_config", typename="ats:config:records"), -tr2.Disk.records_config.update( - { - 'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir), - 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts.Variables.SSLDir), - 'proxy.config.ssl.server.cipher_suite': - 'ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-SHA384:AES128-GCM-SHA256:AES256-GCM-SHA384:ECDHE-RSA-RC4-SHA:ECDHE-RSA-AES128-SHA:ECDHE-RSA-AES256-SHA:RC4-SHA:RC4-MD5:AES128-SHA:AES256-SHA:DES-CBC3-SHA!SRP:!DSS:!PSK:!aNULL:!eNULL:!SSLv2', - # set global policy - 'proxy.config.ssl.client.verify.server.policy': 'PERMISSIVE', - 'proxy.config.ssl.client.verify.server.properties': 'ALL', - 'proxy.config.ssl.client.CA.cert.path': '{0}'.format(ts.Variables.SSLDir), - 'proxy.config.ssl.client.CA.cert.filename': 'signer.pem', - 'proxy.config.exec_thread.autoconfig.scale': 1.0, - 'proxy.config.url_remap.pristine_host_hdr': 1, - 'proxy.config.diags.debug.enabled': 0, - 'proxy.config.diags.debug.tags': 'ssl' - }) -tr2.StillRunningAfter = ts -tr2.StillRunningAfter = server -tr2.Processes.Default.Command = 'echo Updated configs' -# Need to copy over the environment so traffic_ctl knows where to find the unix domain socket -tr2.Processes.Default.Env = ts.Env -tr2.Processes.Default.ReturnCode = 0 - -trreload = Test.AddConfigReload(ts, description="Reload config") -trreload.StillRunningAfter = server - -tragain = Test.AddTestRun("permissive-after-update") -tragain.MakeCurlCommand("-k -H \"host: random3.com\" https://127.0.0.1:{0}".format(ts.Variables.ssl_port), ts=ts) -tragain.ReturnCode = 0 -tragain.StillRunningAfter = server -tragain.StillRunningAfter = ts -tragain.Processes.Default.Streams.stdout = Testers.ExcludesExpression("Could Not Connect", "Curl attempt should have succeeded") - -tr2 = Test.AddTestRun("Update config files to enforced") -recordspath = ts.Disk.records_config.AbsPath -# recreate the records.config with the cert filename changed -tr2.Disk.File(recordspath, id="records_config", typename="ats:config:records"), -tr2.Disk.records_config.update( - { - 'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir), - 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts.Variables.SSLDir), - 'proxy.config.ssl.server.cipher_suite': - 'ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-SHA384:AES128-GCM-SHA256:AES256-GCM-SHA384:ECDHE-RSA-RC4-SHA:ECDHE-RSA-AES128-SHA:ECDHE-RSA-AES256-SHA:RC4-SHA:RC4-MD5:AES128-SHA:AES256-SHA:DES-CBC3-SHA!SRP:!DSS:!PSK:!aNULL:!eNULL:!SSLv2', - # set global policy - 'proxy.config.ssl.client.verify.server.policy': 'ENFORCED', - 'proxy.config.ssl.client.verify.server.properties': 'ALL', - 'proxy.config.ssl.client.CA.cert.path': '{0}'.format(ts.Variables.SSLDir), - 'proxy.config.ssl.client.CA.cert.filename': 'signer.pem', - 'proxy.config.exec_thread.autoconfig.scale': 1.0, - 'proxy.config.url_remap.pristine_host_hdr': 1, - 'proxy.config.diags.debug.enabled': 0, - 'proxy.config.diags.debug.tags': 'ssl' - }) -tr2.StillRunningAfter = ts -tr2.StillRunningAfter = server -tr2.Processes.Default.Command = 'echo Updated configs to ENFORCED' -# Need to copy over the environment so traffic_ctl knows where to find the unix domain socket -tr2.Processes.Default.Env = ts.Env -tr2.Processes.Default.ReturnCode = 0 - -trreload = Test.AddConfigReload(ts, description="Reload config to ENFORCED") -trreload.StillRunningAfter = server - -tragain = Test.AddTestRun("enforced-after-update") -tragain.MakeCurlCommand("-k -H \"host: random4.com\" https://127.0.0.1:{0}".format(ts.Variables.ssl_port), ts=ts) -tragain.ReturnCode = 0 -tragain.StillRunningAfter = server -tragain.StillRunningAfter = ts -tragain.Processes.Default.Streams.stdout = Testers.ContainsExpression("Could Not Connect", "Curl attempt should have succeeded") - -# No name checking for the sig-only permissive override for bad_bar -ts.Disk.diags_log.Content += Testers.ContainsExpression( - r"Core server certificate verification failed for \(random3.com\). Action=Continue", "Permissive can connect") -ts.Disk.diags_log.Content += Testers.ContainsExpression( - r"Core server certificate verification failed for \(random2.com\). Action=Terminate", "Enforced cannot connect") diff --git a/tests/gold_tests/tls/tls_verify_base.test.py b/tests/gold_tests/tls/tls_verify_base.test.py deleted file mode 100644 index 7617bee1c14..00000000000 --- a/tests/gold_tests/tls/tls_verify_base.test.py +++ /dev/null @@ -1,140 +0,0 @@ -''' -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -Test.Summary = ''' -Test tls server certificate verification options -''' - -# Define default ATS -ts = Test.MakeATSProcess("ts", enable_tls=True) -server_foo = Test.MakeOriginServer( - "server_foo", - ssl=True, - options={ - "--key": "{0}/signed-foo.key".format(Test.RunDirectory), - "--cert": "{0}/signed-foo.pem".format(Test.RunDirectory) - }) -server_bar = Test.MakeOriginServer( - "server_bar", - ssl=True, - options={ - "--key": "{0}/signed-bar.key".format(Test.RunDirectory), - "--cert": "{0}/signed-bar.pem".format(Test.RunDirectory) - }) -server = Test.MakeOriginServer("server", ssl=True) - -request_foo_header = {"headers": "GET / HTTP/1.1\r\nHost: foo.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -request_bad_foo_header = {"headers": "GET / HTTP/1.1\r\nHost: badfoo.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -request_bar_header = {"headers": "GET / HTTP/1.1\r\nHost: bar.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -request_bad_bar_header = {"headers": "GET / HTTP/1.1\r\nHost: badbar.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -server_foo.addResponse("sessionlog.json", request_foo_header, response_header) -server_foo.addResponse("sessionlog.json", request_bad_foo_header, response_header) -server_bar.addResponse("sessionlog.json", request_bar_header, response_header) -server_bar.addResponse("sessionlog.json", request_bad_bar_header, response_header) - -# add ssl materials like key, certificates for the server -ts.addSSLfile("ssl/signed-foo.pem") -ts.addSSLfile("ssl/signed-foo.key") -ts.addSSLfile("ssl/signed-bar.pem") -ts.addSSLfile("ssl/signed-bar.key") -ts.addSSLfile("ssl/server.pem") -ts.addSSLfile("ssl/server.key") -ts.addSSLfile("ssl/signer.pem") -ts.addSSLfile("ssl/signer.key") - -ts.Disk.remap_config.AddLine('map / https://127.0.0.1:{0}'.format(server.Variables.SSL_Port)) -ts.Disk.remap_config.AddLine('map https://foo.com/ https://127.0.0.1:{0}'.format(server_foo.Variables.SSL_Port)) -ts.Disk.remap_config.AddLine('map https://bad_foo.com/ https://127.0.0.1:{0}'.format(server_foo.Variables.SSL_Port)) -ts.Disk.remap_config.AddLine('map https://bar.com/ https://127.0.0.1:{0}'.format(server_bar.Variables.SSL_Port)) -ts.Disk.remap_config.AddLine('map https://bad_bar.com/ https://127.0.0.1:{0}'.format(server_bar.Variables.SSL_Port)) - -ts.Disk.ssl_multicert_yaml.AddLines( - """ -ssl_multicert: - - dest_ip: "*" - ssl_cert_name: server.pem - ssl_key_name: server.key -""".split("\n")) - -# Case 1, global config policy=permissive properties=signature -# override for foo.com policy=enforced properties=all -ts.Disk.records_config.update( - { - 'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir), - 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts.Variables.SSLDir), - 'proxy.config.ssl.client.CA.cert.path': '{0}'.format(ts.Variables.SSLDir), - 'proxy.config.ssl.client.CA.cert.filename': 'signer.pem', - 'proxy.config.url_remap.pristine_host_hdr': 1, - 'proxy.config.exec_thread.autoconfig.scale': 1.0, - 'proxy.config.ssl.client.sni_policy': 'host', - 'proxy.config.ssl.client.verify.server.policy': 'PERMISSIVE' - }) - -ts.Disk.sni_yaml.AddLines( - [ - 'sni:', - '- fqdn: bar.com', - ' verify_server_policy: ENFORCED', - ' verify_server_properties: ALL', - '- fqdn: bad_bar.com', - ' verify_server_policy: ENFORCED', - ' verify_server_properties: ALL', - ]) - -tr = Test.AddTestRun("Permissive-Test") -tr.Setup.Copy("ssl/signed-foo.key") -tr.Setup.Copy("ssl/signed-foo.pem") -tr.Setup.Copy("ssl/signed-bar.key") -tr.Setup.Copy("ssl/signed-bar.pem") -tr.MakeCurlCommand("-v -k -H \"host: foo.com\" https://127.0.0.1:{0}".format(ts.Variables.ssl_port), ts=ts) -tr.ReturnCode = 0 -tr.Processes.Default.StartBefore(server_foo) -tr.Processes.Default.StartBefore(server_bar) -tr.Processes.Default.StartBefore(server) -tr.Processes.Default.StartBefore(Test.Processes.ts) -tr.StillRunningAfter = server -tr.StillRunningAfter = ts -tr.Processes.Default.Streams.stdout = Testers.ExcludesExpression("Could Not Connect", "Curl attempt should have succeeded") - -tr = Test.AddTestRun("Permissive-Test with logged failure") -tr.MakeCurlCommand("-v -k -H \"host: random.com\" https://127.0.0.1:{0}".format(ts.Variables.ssl_port), ts=ts) -tr.ReturnCode = 0 -tr.StillRunningAfter = server -tr.StillRunningAfter = ts -tr.Processes.Default.Streams.stdout = Testers.ExcludesExpression("Could Not Connect", "Curl attempt should have succeeded") - -tr2 = Test.AddTestRun("Override-enforcing-Test") -tr2.MakeCurlCommand("-v -k -H \"host: bar.com\" https://127.0.0.1:{0}".format(ts.Variables.ssl_port), ts=ts) -tr2.ReturnCode = 0 -tr2.StillRunningAfter = server -tr2.StillRunningAfter = ts -tr2.Processes.Default.Streams.stdout = Testers.ExcludesExpression("Could Not Connect", "Curl attempt should have succeeded") - -tr3 = Test.AddTestRun("Override-enforcing-Test-fail-name-check") -tr3.MakeCurlCommand("-v -k -H \"host: bad_bar.com\" https://127.0.0.1:{0}".format(ts.Variables.ssl_port), ts=ts) -tr3.Processes.Default.Streams.stdout = Testers.ContainsExpression("Could Not Connect", "Curl attempt should have failed") -tr3.ReturnCode = 0 -tr3.StillRunningAfter = server -tr3.StillRunningAfter = ts - -# Over riding the built in ERROR check since we expect tr3 to fail -ts.Disk.diags_log.Content = Testers.ContainsExpression( - r"WARNING: SNI \(bad_bar.com\) not in certificate. Action=Terminate", "Make sure bad_bar name checked failed.") -ts.Disk.diags_log.Content += Testers.ContainsExpression( - r"WARNING: SNI \(random.com\) not in certificate. Action=Continue ", "Permissive failure for random") diff --git a/tests/gold_tests/tls/tls_verify_ca_override.test.py b/tests/gold_tests/tls/tls_verify_ca_override.test.py deleted file mode 100644 index 1774f18150f..00000000000 --- a/tests/gold_tests/tls/tls_verify_ca_override.test.py +++ /dev/null @@ -1,141 +0,0 @@ -''' -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -Test.Summary = ''' -Test tls server certificate verification options. Exercise conf_remap for ca bundle path and file. -''' - -# Define default ATS -ts = Test.MakeATSProcess("ts") -server1 = Test.MakeOriginServer( - "server1", - ssl=True, - options={ - "--key": "{0}/signed-foo.key".format(Test.RunDirectory), - "--cert": "{0}/signed-foo.pem".format(Test.RunDirectory) - }) -server2 = Test.MakeOriginServer( - "server2", - ssl=True, - options={ - "--key": "{0}/signed-foo.key".format(Test.RunDirectory), - "--cert": "{0}/signed2-foo.pem".format(Test.RunDirectory) - }) - -request_foo_header = {"headers": "GET / HTTP/1.1\r\nHost: foo.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -request_bad_foo_header = {"headers": "GET / HTTP/1.1\r\nHost: bad_foo.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -request_bar_header = {"headers": "GET / HTTP/1.1\r\nHost: bar.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -request_bad_bar_header = {"headers": "GET / HTTP/1.1\r\nHost: bad_bar.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -server1.addResponse("sessionlog.json", request_foo_header, response_header) -server1.addResponse("sessionlog.json", request_bad_foo_header, response_header) -server2.addResponse("sessionlog.json", request_bar_header, response_header) -server2.addResponse("sessionlog.json", request_bad_bar_header, response_header) - -# add ssl materials like key, certificates for the server -ts.addSSLfile("ssl/signed-foo.pem") -ts.addSSLfile("ssl/signed2-foo.pem") -ts.addSSLfile("ssl/signed-foo.key") -ts.addSSLfile("ssl/server.pem") -ts.addSSLfile("ssl/server.key") -ts.addSSLfile("ssl/signer.pem") -ts.addSSLfile("ssl/signer.key") -ts.addSSLfile("ssl/signer2.pem") -ts.addSSLfile("ssl/signer2.key") - - -def ca_cert_overrides(filename): - return ( - f'@pparam=proxy.config.ssl.client.CA.cert.path={ts.Variables.SSLDir} ' - f'@pparam=proxy.config.ssl.client.CA.cert.filename={filename}') - - -ts.Disk.remap_config.AddLine( - f'map /case1 https://127.0.0.1:{server1.Variables.SSL_Port}/ ' - f'@plugin=conf_remap.so {ca_cert_overrides("signer.pem")}') -ts.Disk.remap_config.AddLine( - f'map /badcase1 https://127.0.0.1:{server1.Variables.SSL_Port}/ ' - f'@plugin=conf_remap.so {ca_cert_overrides("signer2.pem")}') -ts.Disk.remap_config.AddLine( - f'map /case2 https://127.0.0.1:{server2.Variables.SSL_Port}/ ' - f'@plugin=conf_remap.so {ca_cert_overrides("signer2.pem")}') -ts.Disk.remap_config.AddLine( - f'map /badcase2 https://127.0.0.1:{server2.Variables.SSL_Port}/ ' - f'@plugin=conf_remap.so {ca_cert_overrides("signer.pem")}') - -ts.Disk.ssl_multicert_yaml.AddLines( - """ -ssl_multicert: - - dest_ip: "*" - ssl_cert_name: server.pem - ssl_key_name: server.key -""".split("\n")) - -# Case 1, global config policy=permissive properties=signature -# override for foo.com policy=enforced properties=all -ts.Disk.records_config.update( - { - 'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir), - 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts.Variables.SSLDir), - # set global policy - 'proxy.config.ssl.client.verify.server.policy': 'ENFORCED', - 'proxy.config.ssl.client.verify.server.properties': 'SIGNATURE', - 'proxy.config.ssl.client.CA.cert.path': '/tmp', - 'proxy.config.ssl.client.CA.cert.filename': '{0}/signer.pem'.format(ts.Variables.SSLDir), - 'proxy.config.exec_thread.autoconfig.scale': 1.0, - 'proxy.config.url_remap.pristine_host_hdr': 1 - }) - -# Should succeed -tr = Test.AddTestRun("Use correct ca bundle for server 1") -tr.MakeCurlCommand('-k -H \"host: foo.com\" http://127.0.0.1:{0}/case1'.format(ts.Variables.port), ts=ts) -tr.ReturnCode = 0 -tr.Setup.Copy("ssl/signed-foo.key") -tr.Setup.Copy("ssl/signed-foo.pem") -tr.Setup.Copy("ssl/signed2-foo.pem") -tr.Processes.Default.StartBefore(server1) -tr.Processes.Default.StartBefore(server2) -tr.Processes.Default.StartBefore(Test.Processes.ts) -tr.StillRunningAfter = server1 -tr.StillRunningAfter = ts -# Should succeed. No message -tr.Processes.Default.Streams.stdout = Testers.ExcludesExpression("Could Not Connect", "Curl attempt should have succeeded") - -tr2 = Test.AddTestRun("Use incorrect ca bundle for server 1") -tr2.MakeCurlCommand("-k -H \"host: bar.com\" http://127.0.0.1:{0}/badcase1".format(ts.Variables.port), ts=ts) -tr2.ReturnCode = 0 -tr2.StillRunningAfter = server1 -tr2.StillRunningAfter = ts -# Should succeed, but will be message in log about name mismatch -tr2.Processes.Default.Streams.stdout = Testers.ContainsExpression("Could Not Connect", "Curl attempt should have succeeded") - -tr2 = Test.AddTestRun("Use correct ca bundle for server 2") -tr2.MakeCurlCommand("-k -H \"host: random.com\" http://127.0.0.1:{0}/case2".format(ts.Variables.port), ts=ts) -tr2.ReturnCode = 0 -tr2.StillRunningAfter = server2 -tr2.StillRunningAfter = ts -# Should succeed, but will be message in log about signature -tr2.Processes.Default.Streams.stdout = Testers.ExcludesExpression("Could Not Connect", "Curl attempt should have succeeded") - -tr3 = Test.AddTestRun("User incorrect ca bundle for server 2") -tr3.MakeCurlCommand("-k -H \"host: foo.com\" http://127.0.0.1:{0}/badcase2".format(ts.Variables.port), ts=ts) -tr3.ReturnCode = 0 -tr3.StillRunningAfter = server2 -tr3.StillRunningAfter = ts -# Should succeed. No error messages -tr3.Processes.Default.Streams.stdout = Testers.ContainsExpression("Could Not Connect", "Curl attempt should have succeeded") diff --git a/tests/gold_tests/tls/tls_verify_not_pristine.test.py b/tests/gold_tests/tls/tls_verify_not_pristine.test.py deleted file mode 100644 index 2d7723d7038..00000000000 --- a/tests/gold_tests/tls/tls_verify_not_pristine.test.py +++ /dev/null @@ -1,110 +0,0 @@ -''' -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -Test.Summary = ''' -Test tls server certificate verification without a pristine host header -''' - -# Define default ATS -ts = Test.MakeATSProcess("ts", enable_tls=True) -server_foo = Test.MakeOriginServer( - "server_foo", - ssl=True, - options={ - "--key": "{0}/signed-foo.key".format(Test.RunDirectory), - "--cert": "{0}/signed-foo.pem".format(Test.RunDirectory) - }) -server = Test.MakeOriginServer("server", ssl=True) -dns = Test.MakeDNServer("dns") - -request_foo_header = {"headers": "GET / HTTP/1.1\r\nHost: foo.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -request_bad_foo_header = {"headers": "GET / HTTP/1.1\r\nHost: badfoo.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -server_foo.addResponse("sessionlog.json", request_foo_header, response_header) -server_foo.addResponse("sessionlog.json", request_bad_foo_header, response_header) - -# add ssl materials like key, certificates for the server -ts.addSSLfile("ssl/signed-foo.pem") -ts.addSSLfile("ssl/signed-foo.key") -ts.addSSLfile("ssl/signed-bar.pem") -ts.addSSLfile("ssl/signed-bar.key") -ts.addSSLfile("ssl/server.pem") -ts.addSSLfile("ssl/server.key") -ts.addSSLfile("ssl/signer.pem") -ts.addSSLfile("ssl/signer.key") - -ts.Disk.remap_config.AddLine( - 'map https://bar.com:{0}/ https://foo.com:{1}'.format(ts.Variables.ssl_port, server_foo.Variables.SSL_Port)) -ts.Disk.remap_config.AddLine( - 'map https://foo.com:{0}/ https://bar.com:{1}'.format(ts.Variables.ssl_port, server_foo.Variables.SSL_Port)) - -ts.Disk.ssl_multicert_yaml.AddLines( - """ -ssl_multicert: - - dest_ip: "*" - ssl_cert_name: server.pem - ssl_key_name: server.key -""".split("\n")) - -# Case 1, global config policy=permissive properties=signature -# override for foo.com policy=enforced properties=all -ts.Disk.records_config.update( - { - 'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir), - 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts.Variables.SSLDir), - # set global policy - 'proxy.config.ssl.client.verify.server.policy': 'ENFORCED', - 'proxy.config.ssl.client.verify.server.properties': 'ALL', - 'proxy.config.ssl.client.CA.cert.path': '{0}'.format(ts.Variables.SSLDir), - 'proxy.config.ssl.client.CA.cert.filename': 'signer.pem', - 'proxy.config.url_remap.pristine_host_hdr': 0, - 'proxy.config.dns.nameservers': '127.0.0.1:{0}'.format(dns.Variables.Port), - 'proxy.config.exec_thread.autoconfig.scale': 1.0, - 'proxy.config.dns.resolv_conf': 'NULL' - }) - -dns.addRecords(records={"foo.com.": ["127.0.0.1"]}) -dns.addRecords(records={"bar.com.": ["127.0.0.1"]}) - -# bar.com in. foo.com out. Should verify -tr = Test.AddTestRun("Enforced-Good-Test") -tr.Setup.Copy("ssl/signed-foo.key") -tr.Setup.Copy("ssl/signed-foo.pem") -tr.Setup.Copy("ssl/signed-bar.key") -tr.Setup.Copy("ssl/signed-bar.pem") -tr.MakeCurlCommand("-v --resolve 'bar.com:{0}:127.0.0.1' -k https://bar.com:{0}".format(ts.Variables.ssl_port), ts=ts) -tr.ReturnCode = 0 -tr.Processes.Default.StartBefore(server_foo) -tr.Processes.Default.StartBefore(dns) -tr.Processes.Default.StartBefore(Test.Processes.ts) -tr.StillRunningAfter = server -tr.StillRunningAfter = ts -tr.Processes.Default.Streams.stdout = Testers.ExcludesExpression("Could Not Connect", "Curl attempt should have succeeded") - -# foo.com in. bar.com out. Should not verify -tr2 = Test.AddTestRun("Enforced-bad-test") -tr2.MakeCurlCommand("-v -k --resolve 'foo.com:{0}:127.0.0.1' https://foo.com:{0}".format(ts.Variables.ssl_port), ts=ts) -tr2.ReturnCode = 0 -tr2.StillRunningAfter = server -tr2.StillRunningAfter = ts -tr2.Processes.Default.Streams.stdout = Testers.ContainsExpression("Could Not Connect", "Curl attempt should not have succeeded") - -# Over riding the built in ERROR check since we expect tr3 to fail -ts.Disk.diags_log.Content = Testers.ExcludesExpression("verification failed", "Make sure the signatures didn't fail") -ts.Disk.diags_log.Content += Testers.ContainsExpression( - r"WARNING: SNI \(bar.com\) not in certificate", "Make sure bad_bar name checked failed.") diff --git a/tests/gold_tests/tls/tls_verify_override.test.py b/tests/gold_tests/tls/tls_verify_override.test.py deleted file mode 100644 index 5fc323da3cf..00000000000 --- a/tests/gold_tests/tls/tls_verify_override.test.py +++ /dev/null @@ -1,277 +0,0 @@ -''' -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -Test.Summary = ''' -Test tls server certificate verification options. Exercise conf_remap -''' - -# Define default ATS -ts = Test.MakeATSProcess("ts") -server_foo = Test.MakeOriginServer( - "server_foo", - ssl=True, - options={ - "--key": "{0}/signed-foo.key".format(Test.RunDirectory), - "--cert": "{0}/signed-foo.pem".format(Test.RunDirectory) - }) -server_bar = Test.MakeOriginServer( - "server_bar", - ssl=True, - options={ - "--key": "{0}/signed-bar.key".format(Test.RunDirectory), - "--cert": "{0}/signed-bar.pem".format(Test.RunDirectory) - }) -server = Test.MakeOriginServer("server", ssl=True) - -dns = Test.MakeDNServer("dns") - -request_foo_header = {"headers": "GET / HTTP/1.1\r\nHost: foo.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -request_bad_foo_header = {"headers": "GET / HTTP/1.1\r\nHost: bad_foo.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -request_bar_header = {"headers": "GET / HTTP/1.1\r\nHost: bar.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -request_bad_bar_header = {"headers": "GET / HTTP/1.1\r\nHost: bad_bar.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -server_foo.addResponse("sessionlog.json", request_foo_header, response_header) -server_foo.addResponse("sessionlog.json", request_bad_foo_header, response_header) -server_bar.addResponse("sessionlog.json", request_bar_header, response_header) -server_bar.addResponse("sessionlog.json", request_bad_bar_header, response_header) - -# add ssl materials like key, certificates for the server -ts.addSSLfile("ssl/signed-foo.pem") -ts.addSSLfile("ssl/signed-foo.key") -ts.addSSLfile("ssl/signed-bar.pem") -ts.addSSLfile("ssl/signed-bar.key") -ts.addSSLfile("ssl/server.pem") -ts.addSSLfile("ssl/server.key") -ts.addSSLfile("ssl/signer.pem") -ts.addSSLfile("ssl/signer.key") - -ts.Disk.remap_config.AddLine('map http://foo.com/basictobar https://bar.com:{0}'.format(server_bar.Variables.SSL_Port)) -ts.Disk.remap_config.AddLine('map http://foo.com/basic https://foo.com:{0}'.format(server_foo.Variables.SSL_Port)) -ts.Disk.remap_config.AddLine( - 'map http://foo.com/override https://foo.com:{0} @plugin=conf_remap.so @pparam=proxy.config.ssl.client.verify.server.policy=ENFORCED' - .format(server_foo.Variables.SSL_Port)) -ts.Disk.remap_config.AddLine('map http://bar.com/basic https://bar.com:{0}'.format(server_foo.Variables.SSL_Port)) -ts.Disk.remap_config.AddLine( - 'map http://bar.com/overridedisabled https://bar.com:{0} @plugin=conf_remap.so @pparam=proxy.config.ssl.client.verify.server.policy=DISABLED' - .format(server_foo.Variables.SSL_Port)) -ts.Disk.remap_config.AddLine( - 'map http://bad_bar.com/overridedisabled https://bar.com:{0} @plugin=conf_remap.so @pparam=proxy.config.ssl.client.verify.server.policy=DISABLED' - .format(server_foo.Variables.SSL_Port)) -ts.Disk.remap_config.AddLine( - 'map http://bar.com/overridesignature https://bar.com:{0} @plugin=conf_remap.so @pparam=proxy.config.ssl.client.verify.server.properties=SIGNATURE @plugin=conf_remap.so @pparam=proxy.config.ssl.client.verify.server.policy=ENFORCED' - .format(server_foo.Variables.SSL_Port)) -ts.Disk.remap_config.AddLine( - 'map http://bar.com/overridenone https://bar.com:{0} @plugin=conf_remap.so @pparam=proxy.config.ssl.client.verify.server.properties=NONE @plugin=conf_remap.so @pparam=proxy.config.ssl.client.verify.server.policy=ENFORCED' - .format(server_foo.Variables.SSL_Port)) -ts.Disk.remap_config.AddLine( - 'map http://bar.com/overrideenforced https://bar.com:{0} @plugin=conf_remap.so @pparam=proxy.config.ssl.client.verify.server.policy=ENFORCED' - .format(server_foo.Variables.SSL_Port)) -ts.Disk.remap_config.AddLine('map /basic https://random.com:{0}'.format(server.Variables.SSL_Port)) -ts.Disk.remap_config.AddLine( - 'map /overrideenforce https://127.0.0.1:{0} @plugin=conf_remap.so @pparam=proxy.config.ssl.client.verify.server.policy=ENFORCED' - .format(server.Variables.SSL_Port)) -ts.Disk.remap_config.AddLine( - 'map /overridename https://127.0.0.1:{0} @plugin=conf_remap.so @pparam=proxy.config.ssl.client.verify.server.properties=NAME' - .format(server.Variables.SSL_Port)) -ts.Disk.remap_config.AddLine( - 'map /snipolicyfooremap https://foo.com:{0} @plugin=conf_remap.so @pparam=proxy.config.ssl.client.verify.server.properties=NAME @plugin=conf_remap.so @pparam=proxy.config.ssl.client.verify.server.policy=ENFORCED @plugin=conf_remap.so @pparam=proxy.config.ssl.client.sni_policy=remap' - .format(server_bar.Variables.SSL_Port)) -ts.Disk.remap_config.AddLine( - 'map /snipolicyfoohost https://foo.com:{0} @plugin=conf_remap.so @pparam=proxy.config.ssl.client.verify.server.properties=NAME @plugin=conf_remap.so @pparam=proxy.config.ssl.client.verify.server.policy=ENFORCED @plugin=conf_remap.so @pparam=proxy.config.ssl.client.sni_policy=host' - .format(server_bar.Variables.SSL_Port)) -ts.Disk.remap_config.AddLine( - 'map /snipolicybarremap https://bar.com:{0} @plugin=conf_remap.so @pparam=proxy.config.ssl.client.verify.server.properties=NAME @plugin=conf_remap.so @pparam=proxy.config.ssl.client.verify.server.policy=ENFORCED @plugin=conf_remap.so @pparam=proxy.config.ssl.client.sni_policy=remap' - .format(server_bar.Variables.SSL_Port)) -ts.Disk.remap_config.AddLine( - 'map /snipolicybarhost https://bar.com:{0} @plugin=conf_remap.so @pparam=proxy.config.ssl.client.verify.server.properties=NAME @plugin=conf_remap.so @pparam=proxy.config.ssl.client.verify.server.policy=ENFORCED @plugin=conf_remap.so @pparam=proxy.config.ssl.client.sni_policy=host' - .format(server_bar.Variables.SSL_Port)) - -ts.Disk.ssl_multicert_yaml.AddLines( - """ -ssl_multicert: - - dest_ip: "*" - ssl_cert_name: server.pem - ssl_key_name: server.key -""".split("\n")) - -# Case 1, global config policy=permissive properties=signature -# override for foo.com policy=enforced properties=all -ts.Disk.records_config.update( - { - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'ssl', - 'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir), - 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts.Variables.SSLDir), - # set global policy - 'proxy.config.ssl.client.verify.server.policy': 'PERMISSIVE', - 'proxy.config.ssl.client.verify.server.properties': 'ALL', - 'proxy.config.ssl.client.CA.cert.path': '{0}'.format(ts.Variables.SSLDir), - 'proxy.config.ssl.client.CA.cert.filename': 'signer.pem', - 'proxy.config.url_remap.pristine_host_hdr': 1, - 'proxy.config.dns.nameservers': '127.0.0.1:{0}'.format(dns.Variables.Port), - 'proxy.config.dns.resolv_conf': 'NULL', - 'proxy.config.exec_thread.autoconfig.scale': 1.0, - 'proxy.config.ssl.client.sni_policy': 'remap', - }) - -dns.addRecords(records={"foo.com.": ["127.0.0.1"]}) -dns.addRecords(records={"bar.com.": ["127.0.0.1"]}) -dns.addRecords(records={"random.com.": ["127.0.0.1"]}) - -# Should succeed without message -tr = Test.AddTestRun("default-permissive-success") -tr.Setup.Copy("ssl/signed-foo.key") -tr.Setup.Copy("ssl/signed-foo.pem") -tr.Setup.Copy("ssl/signed-bar.key") -tr.Setup.Copy("ssl/signed-bar.pem") -tr.MakeCurlCommand('-v -k -H \"host: foo.com\" http://127.0.0.1:{0}/basic'.format(ts.Variables.port), ts=ts) -tr.ReturnCode = 0 -tr.Processes.Default.StartBefore(dns) -tr.Processes.Default.StartBefore(server_foo) -tr.Processes.Default.StartBefore(server_bar) -tr.Processes.Default.StartBefore(server) -tr.Processes.Default.StartBefore(Test.Processes.ts) -tr.StillRunningAfter = server -tr.StillRunningAfter = ts -# Should succeed. No message -tr.Processes.Default.Streams.All = Testers.ExcludesExpression("Could Not Connect", "Curl attempt should have succeeded") -tr.Processes.Default.Streams.All += Testers.ContainsExpression("200 OK", "Curl attempt should have succeeded") - -tr2 = Test.AddTestRun("default-permissive-fail") -tr2.MakeCurlCommand("-v -k -H \"host: bar.com\" http://127.0.0.1:{0}/basic".format(ts.Variables.port), ts=ts) -tr2.ReturnCode = 0 -tr2.StillRunningAfter = server -tr2.StillRunningAfter = ts -# Should succeed, but will be message in log about name mismatch -tr2.Processes.Default.Streams.All = Testers.ExcludesExpression("Could Not Connect", "Curl attempt should have succeeded") -tr2.Processes.Default.Streams.All += Testers.ContainsExpression("200 OK", "Curl attempt should have succeeded") - -tr2 = Test.AddTestRun("default-permissive-fail2") -tr2.MakeCurlCommand("-v -k -H \"host: random.com\" http://127.0.0.1:{0}/basic".format(ts.Variables.port), ts=ts) -tr2.ReturnCode = 0 -tr2.StillRunningAfter = server -tr2.StillRunningAfter = ts -# Should succeed, but will be message in log about signature -tr2.Processes.Default.Streams.All = Testers.ExcludesExpression("Could Not Connect", "Curl attempt should have succeeded") -tr2.Processes.Default.Streams.All += Testers.ContainsExpression("404 Not Found", "Curl attempt should have succeeded") - -tr3 = Test.AddTestRun("default-foo-to-bar") -tr3.MakeCurlCommand("-k -v -H \"host: foo.com\" http://127.0.0.1:{0}/basictobar".format(ts.Variables.port), ts=ts) -tr3.ReturnCode = 0 -tr3.StillRunningAfter = server -tr3.StillRunningAfter = ts -# Should succeed. No error messages -tr3.Processes.Default.Streams.All = Testers.ExcludesExpression("Could Not Connect", "Curl attempt should have succeeded") -tr3.Processes.Default.Streams.All += Testers.ContainsExpression("200 OK", "Curl attempt should have succeeded") - -tr3 = Test.AddTestRun("override-foo") -tr3.MakeCurlCommand("-k -v -H \"host: foo.com\" http://127.0.0.1:{0}/override".format(ts.Variables.port), ts=ts) -tr3.ReturnCode = 0 -tr3.StillRunningAfter = server -tr3.StillRunningAfter = ts -# Should succeed. No error messages -tr3.Processes.Default.Streams.All = Testers.ExcludesExpression("Could Not Connect", "Curl attempt should have succeeded") -tr3.Processes.Default.Streams.All += Testers.ContainsExpression("200 OK", "Curl attempt should have succeeded") - -tr4 = Test.AddTestRun("override-bar-disabled") -tr4.MakeCurlCommand("-k -v -H \"host: bad_bar.com\" http://127.0.0.1:{0}/overridedisabled".format(ts.Variables.port), ts=ts) -tr4.ReturnCode = 0 -tr4.StillRunningAfter = server -tr4.StillRunningAfter = ts -# Succeed. No error messages -tr4.Processes.Default.Streams.All = Testers.ExcludesExpression("Could Not Connect", "Curl attempt should have succeeded") -tr4.Processes.Default.Streams.All += Testers.ContainsExpression("200 OK", "Curl attempt should have succeeded") - -tr5 = Test.AddTestRun("override-bar-signature-enforced") -tr5.MakeCurlCommand("-k -v -H \"host: bar.com\" http://127.0.0.1:{0}/overridesignature".format(ts.Variables.port), ts=ts) -tr5.ReturnCode = 0 -tr5.Processes.Default.Streams.All = Testers.ExcludesExpression("Could Not Connect", "Curl attempt should have succeeded") -tr5.Processes.Default.Streams.All += Testers.ContainsExpression("200 OK", "Curl attempt should have succeeded") -tr5.StillRunningAfter = server -tr5.StillRunningAfter = ts - -tr5a = Test.AddTestRun("override-bar-none-permissive") -tr5a.MakeCurlCommand("-k -v -H \"host: bar.com\" http://127.0.0.1:{0}/overridenone".format(ts.Variables.port), ts=ts) -tr5a.ReturnCode = 0 -tr5a.Processes.Default.Streams.All = Testers.ExcludesExpression("Could Not Connect", "Curl attempt should have succeeded") -tr5a.Processes.Default.Streams.All += Testers.ContainsExpression("200 OK", "Curl attempt should have succeeded") -tr5a.StillRunningAfter = server -tr5a.StillRunningAfter = ts - -tr6 = Test.AddTestRun("override-bar-enforced") -tr6.MakeCurlCommand("-v -k -H \"host: bar.com\" http://127.0.0.1:{0}/overrideenforced".format(ts.Variables.port), ts=ts) -tr6.ReturnCode = 0 -# Should fail -tr6.Processes.Default.Streams.All = Testers.ContainsExpression("Could Not Connect", "Curl attempt should have failed") -tr6.StillRunningAfter = server -tr6.StillRunningAfter = ts - -# Should succeed -tr = Test.AddTestRun("foo-to-bar-sni-policy-remap") -tr.MakeCurlCommand("-v -k -H \"host: foo.com\" http://127.0.0.1:{0}/snipolicybarremap".format(ts.Variables.port), ts=ts) -tr.ReturnCode = 0 -tr.StillRunningAfter = server -tr.StillRunningAfter = ts -tr.Processes.Default.Streams.All = Testers.ExcludesExpression("Could not connect", "Curl attempt should succeed") -tr.Processes.Default.Streams.All += Testers.ContainsExpression("200 OK", "Curl attempt should have succeeded") - -# Should fail -tr = Test.AddTestRun("foo-to-bar-sni-policy-host") -tr.MakeCurlCommand("-v -k -H \"host: foo.com\" http://127.0.0.1:{0}/snipolicybarhost".format(ts.Variables.port), ts=ts) -tr.ReturnCode = 0 -tr.StillRunningAfter = server -tr.StillRunningAfter = ts -tr.Processes.Default.Streams.All = Testers.ContainsExpression("Could not connect", "Curl attempt should fail") - -# Should fail -tr = Test.AddTestRun("bar-to-foo-sni-policy-remap") -tr.MakeCurlCommand("-v -k -H \"host: bar.com\" http://127.0.0.1:{0}/snipolicyfooremap".format(ts.Variables.port), ts=ts) -tr.ReturnCode = 0 -tr.StillRunningAfter = server -tr.StillRunningAfter = ts -tr.Processes.Default.Streams.stdout = Testers.ContainsExpression("Could not connect", "Curl attempt should fail") - -# Should succeed -tr = Test.AddTestRun("bar-to-foo-sni-policy-host") -tr.MakeCurlCommand("-v -k -H \"host: bar.com\" http://127.0.0.1:{0}/snipolicyfoohost".format(ts.Variables.port), ts=ts) -tr.ReturnCode = 0 -tr.StillRunningAfter = server -tr.StillRunningAfter = ts -tr.Processes.Default.Streams.stdout = Testers.ExcludesExpression("Could not connect", "Curl attempt should succeed") - -# Over riding the built in ERROR check since we expect some cases to fail - -# checks on random.com should fail with message only -ts.Disk.diags_log.Content = Testers.ContainsExpression( - r"WARNING: Core server certificate verification failed for \(random.com\). Action=Continue Error=self.signed certificate server=random.com\(127.0.0.1\) depth=0", - "Warning for self signed certificate") -# permissive failure for bar.com -ts.Disk.diags_log.Content += Testers.ContainsExpression( - r"WARNING: SNI \(bar.com\) not in certificate. Action=Continue server=bar.com\(127.0.0.1\)", - "Warning on missing name for bar.com") -# name check failure for random.com -ts.Disk.diags_log.Content += Testers.ContainsExpression( - r"WARNING: SNI \(random.com\) not in certificate. Action=Continue server=random.com\(127.0.0.1\)", - "Warning on missing name for random.com") -# name check failure for bar.com -ts.Disk.diags_log.Content += Testers.ContainsExpression( - r"WARNING: SNI \(bar.com\) not in certificate. Action=Terminate server=bar.com\(127.0.0.1\)", - "Failure on missing name for bar.com") -# See if the explicitly set default sni_policy of remap works. -ts.Disk.diags_log.Content += Testers.ExcludesExpression( - r"WARNING: SNI \(foo.com\) not in certificate. Action=Continue", "Warning on missing name for foo.com") diff --git a/tests/gold_tests/tls/tls_verify_override_base.test.py b/tests/gold_tests/tls/tls_verify_override_base.test.py deleted file mode 100644 index ab07dbaeed8..00000000000 --- a/tests/gold_tests/tls/tls_verify_override_base.test.py +++ /dev/null @@ -1,264 +0,0 @@ -''' -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -Test.Summary = ''' -Test tls server certificate verification options. Exercise conf_remap -''' - -# Define default ATS -ts = Test.MakeATSProcess("ts", enable_tls=True) -server_foo = Test.MakeOriginServer( - "server_foo", - ssl=True, - options={ - "--key": "{0}/signed-foo.key".format(Test.RunDirectory), - "--cert": "{0}/signed-foo.pem".format(Test.RunDirectory) - }) -server_bar = Test.MakeOriginServer( - "server_bar", - ssl=True, - options={ - "--key": "{0}/signed-bar.key".format(Test.RunDirectory), - "--cert": "{0}/signed-bar.pem".format(Test.RunDirectory) - }) -server = Test.MakeOriginServer("server", ssl=True) - -dns = Test.MakeDNServer("dns") - -request_foo_header = {"headers": "GET / HTTP/1.1\r\nHost: foo.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -request_bad_foo_header = {"headers": "GET / HTTP/1.1\r\nHost: bad_foo.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -request_bar_header = {"headers": "GET / HTTP/1.1\r\nHost: bar.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -request_bad_bar_header = {"headers": "GET / HTTP/1.1\r\nHost: bad_bar.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -server_foo.addResponse("sessionlog.json", request_foo_header, response_header) -server_foo.addResponse("sessionlog.json", request_bad_foo_header, response_header) -server_bar.addResponse("sessionlog.json", request_bar_header, response_header) -server_bar.addResponse("sessionlog.json", request_bad_bar_header, response_header) - -# add ssl materials like key, certificates for the server -ts.addSSLfile("ssl/signed-foo.pem") -ts.addSSLfile("ssl/signed-foo.key") -ts.addSSLfile("ssl/signed-bar.pem") -ts.addSSLfile("ssl/signed-bar.key") -ts.addSSLfile("ssl/server.pem") -ts.addSSLfile("ssl/server.key") -ts.addSSLfile("ssl/signer.pem") -ts.addSSLfile("ssl/signer.key") - -ts.Disk.remap_config.AddLine('map http://foo.com/basic https://foo.com:{0}'.format(server_foo.Variables.SSL_Port)) -ts.Disk.remap_config.AddLine( - 'map http://foo.com/override https://foo.com:{0} @plugin=conf_remap.so @pparam=proxy.config.ssl.client.verify.server.policy=ENFORCED' - .format(server_foo.Variables.SSL_Port)) -ts.Disk.remap_config.AddLine('map http://bar.com/basic https://bar.com:{0}'.format(server_foo.Variables.SSL_Port)) -ts.Disk.remap_config.AddLine( - 'map http://bar.com/overridedisabled https://bar.com:{0} @plugin=conf_remap.so @pparam=proxy.config.ssl.client.verify.server.policy=DISABLED' - .format(server_foo.Variables.SSL_Port)) -ts.Disk.remap_config.AddLine( - 'map http://bar.com/overridesignature https://bar.com:{0} @plugin=conf_remap.so @pparam=proxy.config.ssl.client.verify.server.properties=SIGNATURE @plugin=conf_remap.so @pparam=proxy.config.ssl.client.verify.server.policy=ENFORCED' - .format(server_foo.Variables.SSL_Port)) -ts.Disk.remap_config.AddLine( - 'map http://bar.com/overrideenforced https://bar.com:{0} @plugin=conf_remap.so @pparam=proxy.config.ssl.client.verify.server.policy=ENFORCED' - .format(server_foo.Variables.SSL_Port)) -ts.Disk.remap_config.AddLine('map /basic https://127.0.0.1:{0}'.format(server.Variables.SSL_Port)) -ts.Disk.remap_config.AddLine( - 'map /overrideenforce https://127.0.0.1:{0} @plugin=conf_remap.so @pparam=proxy.config.ssl.client.verify.server.policy=ENFORCED' - .format(server.Variables.SSL_Port)) -ts.Disk.remap_config.AddLine( - 'map /overridename https://127.0.0.1:{0} @plugin=conf_remap.so @pparam=proxy.config.ssl.client.verify.server.properties=NAME' - .format(server.Variables.SSL_Port)) -ts.Disk.remap_config.AddLine( - 'map /snipolicyfooremap https://foo.com:{0} @plugin=conf_remap.so @pparam=proxy.config.ssl.client.verify.server.properties=NAME @plugin=conf_remap.so @pparam=proxy.config.ssl.client.verify.server.policy=ENFORCED @plugin=conf_remap.so @pparam=proxy.config.ssl.client.sni_policy=remap' - .format(server_bar.Variables.SSL_Port)) -ts.Disk.remap_config.AddLine( - 'map /snipolicyfoohost https://foo.com:{0} @plugin=conf_remap.so @pparam=proxy.config.ssl.client.verify.server.properties=NAME @plugin=conf_remap.so @pparam=proxy.config.ssl.client.verify.server.policy=ENFORCED @plugin=conf_remap.so @pparam=proxy.config.ssl.client.sni_policy=host' - .format(server_bar.Variables.SSL_Port)) -ts.Disk.remap_config.AddLine( - 'map /snipolicyfooservername https://foo.com:{0} @plugin=conf_remap.so @pparam=proxy.config.ssl.client.verify.server.properties=NAME @plugin=conf_remap.so @pparam=proxy.config.ssl.client.verify.server.policy=ENFORCED @plugin=conf_remap.so @pparam=proxy.config.ssl.client.sni_policy=server_name' - .format(server_bar.Variables.SSL_Port)) -ts.Disk.remap_config.AddLine( - 'map /snipolicybarremap https://bar.com:{0} @plugin=conf_remap.so @pparam=proxy.config.ssl.client.verify.server.properties=NAME @plugin=conf_remap.so @pparam=proxy.config.ssl.client.verify.server.policy=ENFORCED @plugin=conf_remap.so @pparam=proxy.config.ssl.client.sni_policy=remap' - .format(server_bar.Variables.SSL_Port)) -ts.Disk.remap_config.AddLine( - 'map /snipolicybarhost https://bar.com:{0} @plugin=conf_remap.so @pparam=proxy.config.ssl.client.verify.server.properties=NAME @plugin=conf_remap.so @pparam=proxy.config.ssl.client.verify.server.policy=ENFORCED @plugin=conf_remap.so @pparam=proxy.config.ssl.client.sni_policy=host' - .format(server_bar.Variables.SSL_Port)) -ts.Disk.remap_config.AddLine( - 'map /snipolicybarservername https://bar.com:{0} @plugin=conf_remap.so @pparam=proxy.config.ssl.client.verify.server.properties=NAME @plugin=conf_remap.so @pparam=proxy.config.ssl.client.verify.server.policy=ENFORCED @plugin=conf_remap.so @pparam=proxy.config.ssl.client.sni_policy=server_name' - .format(server_bar.Variables.SSL_Port)) - -ts.Disk.ssl_multicert_yaml.AddLines( - """ -ssl_multicert: - - dest_ip: "*" - ssl_cert_name: server.pem - ssl_key_name: server.key -""".split("\n")) - -# Case 1, global config policy=permissive properties=signature -# override for foo.com policy=enforced properties=all -ts.Disk.records_config.update( - { - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'ssl', - 'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir), - 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts.Variables.SSLDir), - 'proxy.config.ssl.client.CA.cert.path': '{0}'.format(ts.Variables.SSLDir), - 'proxy.config.ssl.client.CA.cert.filename': 'signer.pem', - 'proxy.config.url_remap.pristine_host_hdr': 1, - 'proxy.config.exec_thread.autoconfig.scale': 1.0, - 'proxy.config.dns.nameservers': '127.0.0.1:{0}'.format(dns.Variables.Port), - 'proxy.config.dns.resolv_conf': 'NULL', - 'proxy.config.ssl.client.verify.server.policy': 'PERMISSIVE', - 'proxy.config.http.connect.down.policy': 1, # Don't count TLS failures when deciding whehter the server is down. - }) - -dns.addRecords(records={"foo.com.": ["127.0.0.1"]}) -dns.addRecords(records={"bar.com.": ["127.0.0.1"]}) - -# Should succeed without message -tr = Test.AddTestRun("default-permissive-success") -tr.Setup.Copy("ssl/signed-foo.key") -tr.Setup.Copy("ssl/signed-foo.pem") -tr.Setup.Copy("ssl/signed-bar.key") -tr.Setup.Copy("ssl/signed-bar.pem") -tr.MakeCurlCommand('-k -H \"host: foo.com\" http://127.0.0.1:{0}/basic'.format(ts.Variables.port), ts=ts) -tr.ReturnCode = 0 -tr.Processes.Default.StartBefore(dns) -tr.Processes.Default.StartBefore(server_foo) -tr.Processes.Default.StartBefore(server_bar) -tr.Processes.Default.StartBefore(server) -tr.Processes.Default.StartBefore(Test.Processes.ts) -tr.StillRunningAfter = server -tr.StillRunningAfter = ts -# Should succeed. No message -tr.Processes.Default.Streams.stdout = Testers.ExcludesExpression("Could Not Connect", "Curl attempt should have succeeded") - -tr2 = Test.AddTestRun("default-permissive-fail") -tr2.MakeCurlCommand("-k -H \"host: bar.com\" http://127.0.0.1:{0}/basic".format(ts.Variables.port), ts=ts) -tr2.ReturnCode = 0 -tr2.StillRunningAfter = server -tr2.StillRunningAfter = ts -# Should succeed, but will be message in log about name mismatch -tr2.Processes.Default.Streams.stdout = Testers.ExcludesExpression("Could Not Connect", "Curl attempt should have succeeded") - -tr2 = Test.AddTestRun("default-permissive-fail2") -tr2.MakeCurlCommand("-k -H \"host: random.com\" http://127.0.0.1:{0}/basic".format(ts.Variables.port), ts=ts) -tr2.ReturnCode = 0 -tr2.StillRunningAfter = server -tr2.StillRunningAfter = ts -# Should succeed, but will be message in log about signature -tr2.Processes.Default.Streams.stdout = Testers.ExcludesExpression("Could Not Connect", "Curl attempt should have succeeded") - -tr3 = Test.AddTestRun("override-foo") -tr3.MakeCurlCommand("-k -H \"host: foo.com\" http://127.0.0.1:{0}/override".format(ts.Variables.port), ts=ts) -tr3.ReturnCode = 0 -tr3.StillRunningAfter = server -tr3.StillRunningAfter = ts -# Should succeed. No error messages -tr3.Processes.Default.Streams.stdout = Testers.ExcludesExpression("Could Not Connect", "Curl attempt should have succeeded") - -tr4 = Test.AddTestRun("override-bar-disabled") -tr4.MakeCurlCommand("-k -H \"host: bad_bar.com\" http://127.0.0.1:{0}/overridedisabled".format(ts.Variables.port), ts=ts) -tr4.ReturnCode = 0 -tr4.StillRunningAfter = server -tr4.StillRunningAfter = ts -# Succeed. No error messages -tr4.Processes.Default.Streams.stdout = Testers.ExcludesExpression("Could Not Connect", "Curl attempt should have succeeded") - -tr5 = Test.AddTestRun("override-bar-signature-enforced") -tr5.MakeCurlCommand("-k -H \"host: bar.com\" http://127.0.0.1:{0}/overridesignature".format(ts.Variables.port), ts=ts) -tr5.ReturnCode = 0 -tr5.Processes.Default.Streams.stdout = Testers.ExcludesExpression("Could Not Connect", "Curl attempt should have succeeded") -tr5.StillRunningAfter = server -tr5.StillRunningAfter = ts - -tr6 = Test.AddTestRun("override-bar-enforced") -tr6.MakeCurlCommand("-k -H \"host: bar.com\" http://127.0.0.1:{0}/overrideenforced".format(ts.Variables.port), ts=ts) -tr6.ReturnCode = 0 -# Should fail -tr6.Processes.Default.Streams.stdout = Testers.ContainsExpression("Could Not Connect", "Curl attempt should have failed") -tr6.StillRunningAfter = server -tr6.StillRunningAfter = ts - -# Should succeed -tr = Test.AddTestRun("foo-to-bar-sni-policy-remap") -tr.MakeCurlCommand("-k -H \"host: foo.com\" http://127.0.0.1:{0}/snipolicybarremap".format(ts.Variables.port), ts=ts) -tr.ReturnCode = 0 -tr.StillRunningAfter = server -tr.StillRunningAfter = ts -tr.Processes.Default.Streams.stdout = Testers.ExcludesExpression("Could not connect", "Curl attempt should succeed") - -# Should fail -tr = Test.AddTestRun("foo-to-bar-sni-policy-host") -tr.MakeCurlCommand("-k -H \"host: foo.com\" http://127.0.0.1:{0}/snipolicybarhost".format(ts.Variables.port), ts=ts) -tr.ReturnCode = 0 -tr.StillRunningAfter = server -tr.StillRunningAfter = ts -tr.Processes.Default.Streams.stdout = Testers.ContainsExpression("Could not connect", "Curl attempt should fail") - -# Should fail -tr = Test.AddTestRun("foo-to-bar-sni-policy-servername") -tr.MakeCurlCommand( - "-k --resolve foo.com:{0}:127.0.0.1 https://foo.com:{0}/snipolicybarservername".format(ts.Variables.ssl_port), ts=ts) -tr.ReturnCode = 0 -tr.StillRunningAfter = server -tr.StillRunningAfter = ts -tr.Processes.Default.Streams.stdout = Testers.ContainsExpression("Could not connect", "Curl attempt should fail") - -# Should fail -tr = Test.AddTestRun("bar-to-foo-sni-policy-remap") -tr.MakeCurlCommand("-k -H \"host: bar.com\" http://127.0.0.1:{0}/snipolicyfooremap".format(ts.Variables.port), ts=ts) -tr.ReturnCode = 0 -tr.StillRunningAfter = server -tr.StillRunningAfter = ts -tr.Processes.Default.Streams.stdout = Testers.ContainsExpression("Could not connect", "Curl attempt should fail") - -# Should succeed -tr = Test.AddTestRun("bar-to-foo-sni-policy-host") -tr.MakeCurlCommand("-k -H \"host: bar.com\" http://127.0.0.1:{0}/snipolicyfoohost".format(ts.Variables.port), ts=ts) -tr.ReturnCode = 0 -tr.StillRunningAfter = server -tr.StillRunningAfter = ts -tr.Processes.Default.Streams.stdout = Testers.ExcludesExpression("Could not connect", "Curl attempt should succeed") - -# Should succeed -tr = Test.AddTestRun("bar-to-foo-sni-policy-servername") -tr.MakeCurlCommand( - "-k --resolve bar.com:{0}:127.0.0.1 https://bar.com:{0}/snipolicyfooservername".format(ts.Variables.ssl_port), ts=ts) -tr.ReturnCode = 0 -tr.StillRunningAfter = server -tr.StillRunningAfter = ts -tr.Processes.Default.Streams.stdout = Testers.ExcludesExpression("Could not connect", "Curl attempt should succeed") - -# Over riding the built in ERROR check since we expect some cases to fail - -# checks on random.com should fail with message only -ts.Disk.diags_log.Content = Testers.ContainsExpression( - r"WARNING: Core server certificate verification failed for \(random.com\). Action=Continue Error=self.signed certificate server=127.0.0.1\(127.0.0.1\) depth=0", - "Warning for self signed certificate") -# permissive failure for bar.com -ts.Disk.diags_log.Content += Testers.ContainsExpression( - r"WARNING: SNI \(bar.com\) not in certificate. Action=Continue server=bar.com\(127.0.0.1\)", - "Warning on missing name for bar.com") -# name check failure for random.com -ts.Disk.diags_log.Content += Testers.ContainsExpression( - r"WARNING: SNI \(random.com\) not in certificate. Action=Continue server=127.0.0.1\(127.0.0.1\)", - "Warning on missing name for random.com") -# name check failure for bar.com -ts.Disk.diags_log.Content += Testers.ContainsExpression( - r"WARNING: SNI \(bar.com\) not in certificate. Action=Terminate server=bar.com\(127.0.0.1\)", - "Failure on missing name for bar.com") diff --git a/tests/gold_tests/tls/tls_verify_override_sni.test.py b/tests/gold_tests/tls/tls_verify_override_sni.test.py deleted file mode 100644 index f671d44bd5e..00000000000 --- a/tests/gold_tests/tls/tls_verify_override_sni.test.py +++ /dev/null @@ -1,159 +0,0 @@ -''' -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -Test.Summary = ''' -Test tls server certificate verification options. Exercise conf_remap -''' - -# Define default ATS -ts = Test.MakeATSProcess("ts") -cafile = "{0}/signer.pem".format(Test.RunDirectory) - -server_foo = Test.MakeOriginServer( - "server_foo", - ssl=True, - options={ - "--key": "{0}/signed-foo.key".format(Test.RunDirectory), - "--cert": "{0}/signed-foo.pem".format(Test.RunDirectory), - "--clientCA": cafile, - "--clientverify": "" - }, - clientcert="{0}/signed-bar.pem".format(Test.RunDirectory), - clientkey="{0}/signed-bar.key".format(Test.RunDirectory)) -server_bar = Test.MakeOriginServer( - "server_bar", - ssl=True, - options={ - "--key": "{0}/signed-foo.key".format(Test.RunDirectory), - "--cert": "{0}/signed-foo.pem".format(Test.RunDirectory), - "--clientCA": cafile, - "--clientverify": "" - }, - clientcert="{0}/signed-bar.pem".format(Test.RunDirectory), - clientkey="{0}/signed-bar.key".format(Test.RunDirectory)) - -dns = Test.MakeDNServer("dns") - -request_foo_header = {"headers": "GET / HTTP/1.1\r\nHost: foo.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -request_bad_foo_header = {"headers": "GET / HTTP/1.1\r\nHost: bad_foo.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -request_bar_header = {"headers": "GET / HTTP/1.1\r\nHost: bar.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -request_bad_bar_header = {"headers": "GET / HTTP/1.1\r\nHost: bad_bar.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -server_foo.addResponse("sessionlog.json", request_foo_header, response_header) -server_foo.addResponse("sessionlog.json", request_bad_foo_header, response_header) -server_bar.addResponse("sessionlog.json", request_bar_header, response_header) -server_bar.addResponse("sessionlog.json", request_bad_bar_header, response_header) - -server_bar.Setup.Copy("ssl/signer.pem") -server_bar.Setup.Copy("ssl/signer2.pem") -server_foo.Setup.Copy("ssl/signer.pem") -server_foo.Setup.Copy("ssl/signer2.pem") - -# add ssl materials like key, certificates for the server -ts.addSSLfile("ssl/signed-foo.pem") -ts.addSSLfile("ssl/signed-foo.key") -ts.addSSLfile("ssl/signed-bar.pem") -ts.addSSLfile("ssl/signed-bar.key") -ts.addSSLfile("ssl/server.pem") -ts.addSSLfile("ssl/server.key") -ts.addSSLfile("ssl/signer.pem") -ts.addSSLfile("ssl/signer.key") - -ts.Disk.remap_config.AddLine('map http://foo.com/defaultbar https://bar.com:{0}'.format(server_bar.Variables.SSL_Port)) -ts.Disk.remap_config.AddLine('map http://foo.com/default https://foo.com:{0}'.format(server_foo.Variables.SSL_Port)) -ts.Disk.remap_config.AddLine( - 'map http://foo.com/overridepolicy https://bar.com:{0} @plugin=conf_remap.so @pparam=proxy.config.ssl.client.verify.server.policy=ENFORCED' - .format(server_foo.Variables.SSL_Port)) -ts.Disk.remap_config.AddLine( - 'map http://foo.com/overrideproperties https://bar.com:{0} @plugin=conf_remap.so @pparam=proxy.config.ssl.client.verify.server.properties=SIGNATURE' - .format(server_foo.Variables.SSL_Port)) - -ts.Disk.ssl_multicert_yaml.AddLines( - """ -ssl_multicert: - - dest_ip: "*" - ssl_cert_name: server.pem - ssl_key_name: server.key -""".split("\n")) - -# global config policy=permissive properties=all -ts.Disk.records_config.update( - { - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'ssl', - 'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir), - 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts.Variables.SSLDir), - # set global policy - 'proxy.config.ssl.client.verify.server.policy': 'PERMISSIVE', - 'proxy.config.ssl.client.verify.server.properties': 'ALL', - 'proxy.config.ssl.client.CA.cert.path': '{0}'.format(ts.Variables.SSLDir), - 'proxy.config.ssl.client.CA.cert.filename': 'signer.pem', - 'proxy.config.url_remap.pristine_host_hdr': 1, - 'proxy.config.dns.nameservers': '127.0.0.1:{0}'.format(dns.Variables.Port), - 'proxy.config.dns.resolv_conf': 'NULL', - 'proxy.config.exec_thread.autoconfig.scale': 1.0, - 'proxy.config.ssl.client.sni_policy': 'remap' - }) - -ts.Disk.sni_yaml.AddLines( - [ - 'sni:', - '- fqdn: bar.com', - ' client_cert: "{0}/signed-foo.pem"'.format(ts.Variables.SSLDir), - ' client_key: "{0}/signed-foo.key"'.format(ts.Variables.SSLDir), - ]) - -dns.addRecords(records={"foo.com.": ["127.0.0.1"]}) -dns.addRecords(records={"bar.com.": ["127.0.0.1"]}) -dns.addRecords(records={"random.com.": ["127.0.0.1"]}) - -# Should succeed with message -# exercise default settings -tr = Test.AddTestRun("default-permissive-success") -tr.Setup.Copy("ssl/signed-foo.key") -tr.Setup.Copy("ssl/signed-foo.pem") -tr.Setup.Copy("ssl/signed-bar.key") -tr.Setup.Copy("ssl/signed-bar.pem") -tr.MakeCurlCommand('-k -H \"host: foo.com\" http://127.0.0.1:{0}/defaultbar'.format(ts.Variables.port), ts=ts) -tr.ReturnCode = 0 -tr.Processes.Default.StartBefore(dns) -tr.Processes.Default.StartBefore(server_foo) -tr.Processes.Default.StartBefore(server_bar) -tr.Processes.Default.StartBefore(Test.Processes.ts) -tr.StillRunningAfter = ts -tr.Processes.Default.Streams.stdout = Testers.ExcludesExpression("Could Not Connect", "Curl attempt should have succeeded") - -# should fail. Exercise the override -tr2 = Test.AddTestRun("policy-override-fail") -tr2.MakeCurlCommand("-k -H \"host: foo.com\" http://127.0.0.1:{0}/overridepolicy".format(ts.Variables.port), ts=ts) -tr2.ReturnCode = 0 -tr2.StillRunningAfter = ts -tr2.Processes.Default.Streams.stdout = Testers.ContainsExpression("Could Not Connect", "Curl attempt should fail") - -# should succeed with an error message -tr2 = Test.AddTestRun("properties-override-permissive") -tr2.MakeCurlCommand("-k -H \"host: foo.com\" http://127.0.0.1:{0}/overrideproperties".format(ts.Variables.port), ts=ts) -tr2.ReturnCode = 0 -tr2.StillRunningAfter = ts -tr2.Processes.Default.Streams.stdout = Testers.ExcludesExpression("Could Not Connect", "Curl attempt should have succeeded") - -# Over riding the built in ERROR check since we expect some cases to fail -ts.Disk.diags_log.Content = Testers.ContainsExpression( - r"WARNING: SNI \(bar.com\) not in certificate. Action=Continue server=bar.com", "Warning for mismatch name not enforcing") -ts.Disk.diags_log.Content += Testers.ContainsExpression( - r" WARNING: SNI \(bar.com\) not in certificate. Action=Terminate server=bar.com", "Warning for enforcing mismatch") diff --git a/tests/gold_tests/tls_hooks/tls_hooks.test.py b/tests/gold_tests/tls_hooks/tls_hooks.test.py deleted file mode 100644 index db73db2a786..00000000000 --- a/tests/gold_tests/tls_hooks/tls_hooks.test.py +++ /dev/null @@ -1,80 +0,0 @@ -''' -Test 1 preaccept callback (without delay) -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import os -import re - -Test.Summary = ''' -Test different combinations of TLS handshake hooks to ensure they are applied consistently. -''' - -ts = Test.MakeATSProcess("ts", enable_tls=True) -server = Test.MakeOriginServer("server") -request_header = {"headers": "GET / HTTP/1.1\r\nHost: www.example.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -# desired response form the origin server -response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -server.addResponse("sessionlog.json", request_header, response_header) - -ts.addDefaultSSLFiles() - -ts.Disk.records_config.update( - { - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.show_location': 0, - 'proxy.config.diags.debug.tags': 'ssl_hook_test', - 'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir), - 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts.Variables.SSLDir), - 'proxy.config.ssl.TLSv1_3.enabled': 0, - }) - -ts.Disk.ssl_multicert_yaml.AddLines( - """ -ssl_multicert: - - dest_ip: "*" - ssl_cert_name: server.pem - ssl_key_name: server.key -""".split("\n")) - -ts.Disk.remap_config.AddLine( - 'map https://example.com:{0} http://127.0.0.1:{1}'.format(ts.Variables.ssl_port, server.Variables.Port)) - -Test.PrepareTestPlugin(os.path.join(Test.Variables.AtsTestPluginsDir, 'ssl_hook_test.so'), ts, '-preaccept=1') - -tr = Test.AddTestRun("Test one preaccept hook") -tr.Processes.Default.StartBefore(server) -tr.Processes.Default.StartBefore(Test.Processes.ts) -tr.StillRunningAfter = ts -tr.StillRunningAfter = server -tr.MakeCurlCommand('-v -k -H \'host:example.com:{0}\' https://127.0.0.1:{0}'.format(ts.Variables.ssl_port), ts=ts) -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Streams.stdout = "gold/preaccept-1.gold" -tr.Processes.Default.Streams.All = Testers.ExcludesExpression( - "TLSv1.3 (IN), TLS handshake, Finished (20):", "Should not negotiate a TLSv1.3 connection") - -ts.Disk.traffic_out.Content = "gold/ts-preaccept-1.gold" - -# the preaccept may get triggered twice because the test framework creates a TCP connection before handing off to traffic_server -preacceptstring = "Pre accept callback 0" -ts.Disk.traffic_out.Content = Testers.ContainsExpression( - r"\A(?:(?!{0}).)*{0}.*({0})?(?!.*{0}).*\Z".format(preacceptstring), - "Pre accept message appears only once or twice", - reflags=re.S | re.M) - -tr.Processes.Default.TimeOut = 15 -tr.TimeOut = 15 diff --git a/tests/gold_tests/tls_hooks/tls_hooks10.test.py b/tests/gold_tests/tls_hooks/tls_hooks10.test.py deleted file mode 100644 index 0c32f87d231..00000000000 --- a/tests/gold_tests/tls_hooks/tls_hooks10.test.py +++ /dev/null @@ -1,68 +0,0 @@ -''' -Test one delayed cert callback and two immediate cert callbacks -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import os - -Test.Summary = ''' -Test different combinations of TLS handshake hooks to ensure they are applied consistently. -''' - -ts = Test.MakeATSProcess("ts", enable_tls=True) -server = Test.MakeOriginServer("server") -request_header = {"headers": "GET / HTTP/1.1\r\nHost: www.example.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -# desired response form the origin server -response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -server.addResponse("sessionlog.json", request_header, response_header) - -ts.addDefaultSSLFiles() - -ts.Disk.records_config.update( - { - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.show_location': 0, - 'proxy.config.diags.debug.tags': 'ssl_hook_test', - 'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir), - 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts.Variables.SSLDir), - }) - -ts.Disk.ssl_multicert_yaml.AddLines( - """ -ssl_multicert: - - dest_ip: "*" - ssl_cert_name: server.pem - ssl_key_name: server.key -""".split("\n")) - -ts.Disk.remap_config.AddLine( - 'map https://example.com:{0} http://127.0.0.1:{1}'.format(ts.Variables.ssl_port, server.Variables.Port)) - -Test.PrepareTestPlugin(os.path.join(Test.Variables.AtsTestPluginsDir, 'ssl_hook_test.so'), ts, '-cert=1 -i=2') - -tr = Test.AddTestRun("Test a combination of delayed and immediate cert hooks") -tr.Processes.Default.StartBefore(server) -tr.Processes.Default.StartBefore(Test.Processes.ts) -tr.StillRunningAfter = ts -tr.StillRunningAfter = server -tr.MakeCurlCommand('-k -H \'host:example.com:{0}\' https://127.0.0.1:{0}'.format(ts.Variables.ssl_port), ts=ts) -tr.Processes.Default.ReturnCode = 0 - -ts.Disk.traffic_out.Content = "gold/ts-cert-1-im-2.gold" - -tr.Processes.Default.TimeOut = 15 -tr.TimeOut = 15 diff --git a/tests/gold_tests/tls_hooks/tls_hooks11.test.py b/tests/gold_tests/tls_hooks/tls_hooks11.test.py deleted file mode 100644 index 2c5a6b72d3f..00000000000 --- a/tests/gold_tests/tls_hooks/tls_hooks11.test.py +++ /dev/null @@ -1,75 +0,0 @@ -''' -Test one delayed preaccept callback -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import os -import re - -Test.Summary = ''' -Test different combinations of TLS handshake hooks to ensure they are applied consistently. -''' - -ts = Test.MakeATSProcess("ts", enable_tls=True) -server = Test.MakeOriginServer("server") -request_header = {"headers": "GET / HTTP/1.1\r\nHost: www.example.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -# desired response form the origin server -response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -server.addResponse("sessionlog.json", request_header, response_header) - -ts.addDefaultSSLFiles() - -ts.Disk.records_config.update( - { - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.show_location': 0, - 'proxy.config.diags.debug.tags': 'ssl_hook_test', - 'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir), - 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts.Variables.SSLDir), - }) - -ts.Disk.ssl_multicert_yaml.AddLines( - """ -ssl_multicert: - - dest_ip: "*" - ssl_cert_name: server.pem - ssl_key_name: server.key -""".split("\n")) - -ts.Disk.remap_config.AddLine( - 'map https://example.com:{0} http://127.0.0.1:{1}'.format(ts.Variables.ssl_port, server.Variables.Port)) - -Test.PrepareTestPlugin(os.path.join(Test.Variables.AtsTestPluginsDir, 'ssl_hook_test.so'), ts, '-d=1') - -tr = Test.AddTestRun("Test one delayed preaccept hook") -tr.Processes.Default.StartBefore(server) -tr.Processes.Default.StartBefore(Test.Processes.ts) -tr.StillRunningAfter = ts -tr.StillRunningAfter = server -tr.MakeCurlCommand('-k -H \'host:example.com:{0}\' https://127.0.0.1:{0}'.format(ts.Variables.ssl_port), ts=ts) -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Streams.stdout = "gold/preaccept-1.gold" - -ts.Disk.traffic_out.Content = "gold/ts-preaccept-delayed-1.gold" - -preacceptstring = "Pre accept delay callback 0" -ts.Disk.traffic_out.Content = Testers.ContainsExpression( - r"\A(?:(?!{0}).)*{0}.*({0})?(?!.*{0}).*\Z".format(preacceptstring), - "Pre accept message appears only once or twice", - reflags=re.S | re.M) -tr.Processes.Default.TimeOut = 15 -tr.TimeOut = 15 diff --git a/tests/gold_tests/tls_hooks/tls_hooks12.test.py b/tests/gold_tests/tls_hooks/tls_hooks12.test.py deleted file mode 100644 index aed35b57768..00000000000 --- a/tests/gold_tests/tls_hooks/tls_hooks12.test.py +++ /dev/null @@ -1,68 +0,0 @@ -''' -Test a combination of delayed and immediate preaccept callbacks -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import os - -Test.Summary = ''' -Test different combinations of TLS handshake hooks to ensure they are applied consistently. -''' - -ts = Test.MakeATSProcess("ts", enable_tls=True) -server = Test.MakeOriginServer("server") -request_header = {"headers": "GET / HTTP/1.1\r\nHost: www.example.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -# desired response form the origin server -response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -server.addResponse("sessionlog.json", request_header, response_header) - -ts.addDefaultSSLFiles() - -ts.Disk.records_config.update( - { - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.show_location': 0, - 'proxy.config.diags.debug.tags': 'ssl_hook_test', - 'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir), - 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts.Variables.SSLDir), - }) - -ts.Disk.ssl_multicert_yaml.AddLines( - """ -ssl_multicert: - - dest_ip: "*" - ssl_cert_name: server.pem - ssl_key_name: server.key -""".split("\n")) - -ts.Disk.remap_config.AddLine( - 'map https://example.com:{0} http://127.0.0.1:{1}'.format(ts.Variables.ssl_port, server.Variables.Port)) - -Test.PrepareTestPlugin(os.path.join(Test.Variables.AtsTestPluginsDir, 'ssl_hook_test.so'), ts, '-p=2 -d=1') - -tr = Test.AddTestRun("Test combination of delayed and immediate preaccept hook2") -tr.Processes.Default.StartBefore(server) -tr.Processes.Default.StartBefore(Test.Processes.ts) -tr.StillRunningAfter = ts -tr.StillRunningAfter = server -tr.MakeCurlCommand('-k -H \'host:example.com:{0}\' https://127.0.0.1:{0}'.format(ts.Variables.ssl_port), ts=ts) -tr.Processes.Default.ReturnCode = 0 - -ts.Disk.traffic_out.Content = "gold/ts-preaccept-delayed-1-immdate-2.gold" - -tr.Processes.Default.TimeOut = 15 -tr.TimeOut = 15 diff --git a/tests/gold_tests/tls_hooks/tls_hooks13.test.py b/tests/gold_tests/tls_hooks/tls_hooks13.test.py deleted file mode 100644 index 515fd584936..00000000000 --- a/tests/gold_tests/tls_hooks/tls_hooks13.test.py +++ /dev/null @@ -1,68 +0,0 @@ -''' -Test one outbound start and two outbound close -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import os - -Test.Summary = ''' -Test different combinations of TLS handshake hooks to ensure they are applied consistently. -''' - -ts = Test.MakeATSProcess("ts", enable_tls=True) -server = Test.MakeOriginServer("server", ssl=True) -request_header = {"headers": "GET / HTTP/1.1\r\nHost: www.example.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -# desired response form the origin server -response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -server.addResponse("sessionlog.json", request_header, response_header) - -ts.addDefaultSSLFiles() - -ts.Disk.records_config.update( - { - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.show_location': 0, - 'proxy.config.diags.debug.tags': 'ssl_hook_test', - 'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir), - 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts.Variables.SSLDir), - }) - -ts.Disk.ssl_multicert_yaml.AddLines( - """ -ssl_multicert: - - dest_ip: "*" - ssl_cert_name: server.pem - ssl_key_name: server.key -""".split("\n")) - -ts.Disk.remap_config.AddLine( - 'map https://example.com:{0} https://127.0.0.1:{1}'.format(ts.Variables.ssl_port, server.Variables.SSL_Port)) - -Test.PrepareTestPlugin(os.path.join(Test.Variables.AtsTestPluginsDir, 'ssl_hook_test.so'), ts, '-out_start=1 -out_close=2') - -tr = Test.AddTestRun("Test outbound start and close") -tr.Processes.Default.StartBefore(server) -tr.Processes.Default.StartBefore(Test.Processes.ts) -tr.StillRunningAfter = ts -tr.StillRunningAfter = server -tr.MakeCurlCommand('-k -H \'host:example.com:{0}\' https://127.0.0.1:{0}'.format(ts.Variables.ssl_port), ts=ts) -tr.Processes.Default.ReturnCode = 0 - -ts.Disk.traffic_out.Content = "gold/ts-out-start-close-2.gold" - -tr.Processes.Default.TimeOut = 15 -tr.TimeOut = 15 diff --git a/tests/gold_tests/tls_hooks/tls_hooks14.test.py b/tests/gold_tests/tls_hooks/tls_hooks14.test.py deleted file mode 100644 index 4128b58497c..00000000000 --- a/tests/gold_tests/tls_hooks/tls_hooks14.test.py +++ /dev/null @@ -1,69 +0,0 @@ -''' -Test two outbound start delayed hooks -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import os - -Test.Summary = ''' -Test different combinations of TLS handshake hooks to ensure they are applied consistently. -''' - -ts = Test.MakeATSProcess("ts", enable_tls=True) -server = Test.MakeOriginServer("server", ssl=True) -request_header = {"headers": "GET / HTTP/1.1\r\nHost: www.example.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -# desired response form the origin server -response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -server.addResponse("sessionlog.json", request_header, response_header) - -ts.addDefaultSSLFiles() - -ts.Disk.records_config.update( - { - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.show_location': 0, - 'proxy.config.diags.debug.tags': 'ssl_hook_test', - 'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir), - 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts.Variables.SSLDir), - 'proxy.config.ssl.client.verify.server.policy': 'PERMISSIVE', - }) - -ts.Disk.ssl_multicert_yaml.AddLines( - """ -ssl_multicert: - - dest_ip: "*" - ssl_cert_name: server.pem - ssl_key_name: server.key -""".split("\n")) - -ts.Disk.remap_config.AddLine( - 'map https://example.com:{0} https://127.0.0.1:{1}'.format(ts.Variables.ssl_port, server.Variables.SSL_Port)) - -Test.PrepareTestPlugin(os.path.join(Test.Variables.AtsTestPluginsDir, 'ssl_hook_test.so'), ts, '-out_start_delay=2') - -tr = Test.AddTestRun("Test outbound delay start") -tr.Processes.Default.StartBefore(server) -tr.Processes.Default.StartBefore(Test.Processes.ts) -tr.StillRunningAfter = ts -tr.StillRunningAfter = server -tr.MakeCurlCommand('-k -H \'host:example.com:{0}\' https://127.0.0.1:{0}'.format(ts.Variables.ssl_port), ts=ts) -tr.Processes.Default.ReturnCode = 0 - -ts.Disk.traffic_out.Content = "gold/ts-out-delay-start-2.gold" - -tr.Processes.Default.TimeOut = 15 -tr.TimeOut = 15 diff --git a/tests/gold_tests/tls_hooks/tls_hooks15.test.py b/tests/gold_tests/tls_hooks/tls_hooks15.test.py deleted file mode 100644 index e43a26f883a..00000000000 --- a/tests/gold_tests/tls_hooks/tls_hooks15.test.py +++ /dev/null @@ -1,68 +0,0 @@ -''' -Test one delayed preaccept callback -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import os - -Test.Summary = ''' -Test different combinations of TLS handshake hooks to ensure they are applied consistently. -''' - -ts = Test.MakeATSProcess("ts", enable_tls=True) -server = Test.MakeOriginServer("server", ssl=True) -request_header = {"headers": "GET / HTTP/1.1\r\nHost: www.example.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -# desired response form the origin server -response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -server.addResponse("sessionlog.json", request_header, response_header) - -ts.addDefaultSSLFiles() - -ts.Disk.records_config.update( - { - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.show_location': 0, - 'proxy.config.diags.debug.tags': 'ssl_hook_test', - 'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir), - 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts.Variables.SSLDir), - }) - -ts.Disk.ssl_multicert_yaml.AddLines( - """ -ssl_multicert: - - dest_ip: "*" - ssl_cert_name: server.pem - ssl_key_name: server.key -""".split("\n")) - -ts.Disk.remap_config.AddLine( - 'map https://example.com:{0} https://127.0.0.1:{1}'.format(ts.Variables.ssl_port, server.Variables.SSL_Port)) - -Test.PrepareTestPlugin(os.path.join(Test.Variables.AtsTestPluginsDir, 'ssl_hook_test.so'), ts, '-close=2 -out_close=1') - -tr = Test.AddTestRun("Test one delayed preaccept hook") -tr.Processes.Default.StartBefore(server) -tr.Processes.Default.StartBefore(Test.Processes.ts) -tr.StillRunningAfter = ts -tr.StillRunningAfter = server -tr.MakeCurlCommand('-k -H \'host:example.com:{0}\' https://127.0.0.1:{0}'.format(ts.Variables.ssl_port), ts=ts) -tr.Processes.Default.ReturnCode = 0 - -ts.Disk.traffic_out.Content = "gold/ts-close-out-close.gold" - -tr.Processes.Default.TimeOut = 15 -tr.TimeOut = 15 diff --git a/tests/gold_tests/tls_hooks/tls_hooks16.test.py b/tests/gold_tests/tls_hooks/tls_hooks16.test.py deleted file mode 100644 index 9a8a083be88..00000000000 --- a/tests/gold_tests/tls_hooks/tls_hooks16.test.py +++ /dev/null @@ -1,76 +0,0 @@ -''' -Test single immediate client hello hook -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import os -import re - -Test.Summary = ''' -Test different combinations of TLS handshake hooks to ensure they are applied consistently. -''' - -Test.SkipUnless(Condition.HasOpenSSLVersion("1.1.1")) - -ts = Test.MakeATSProcess("ts", enable_tls=True) -server = Test.MakeOriginServer("server") -request_header = {"headers": "GET / HTTP/1.1\r\nHost: www.example.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -# desired response form the origin server -response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -server.addResponse("sessionlog.json", request_header, response_header) - -ts.addDefaultSSLFiles() - -ts.Disk.records_config.update( - { - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.show_location': 0, - 'proxy.config.diags.debug.tags': 'ssl_hook_test', - 'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir), - 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts.Variables.SSLDir), - }) - -ts.Disk.ssl_multicert_yaml.AddLines( - """ -ssl_multicert: - - dest_ip: "*" - ssl_cert_name: server.pem - ssl_key_name: server.key -""".split("\n")) - -ts.Disk.remap_config.AddLine( - 'map https://example.com:{1} http://127.0.0.1:{0}'.format(server.Variables.Port, ts.Variables.ssl_port)) - -Test.PrepareTestPlugin(os.path.join(Test.Variables.AtsTestPluginsDir, 'ssl_hook_test.so'), ts, '-client_hello_imm=1') - -tr = Test.AddTestRun("Test one immediate client hello hook") -tr.Processes.Default.StartBefore(server) -tr.Processes.Default.StartBefore(Test.Processes.ts) -tr.StillRunningAfter = ts -tr.StillRunningAfter = server -tr.MakeCurlCommand('-k -H \'host:example.com:{0}\' https://127.0.0.1:{0}'.format(ts.Variables.ssl_port), ts=ts) -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Streams.stdout = "gold/client-hello-1.gold" - -ts.Disk.traffic_out.Content = "gold/ts-client-hello-1.gold" - -snistring = "Client Hello callback 0" -ts.Disk.traffic_out.Content = Testers.ContainsExpression( - r"\A(?:(?!{0}).)*{0}(?!.*{0}).*\Z".format(snistring), "Client Hello message appears only once", reflags=re.S | re.M) - -tr.Processes.Default.TimeOut = 15 -tr.TimeOut = 15 diff --git a/tests/gold_tests/tls_hooks/tls_hooks17.test.py b/tests/gold_tests/tls_hooks/tls_hooks17.test.py deleted file mode 100644 index 0b54d7a9e45..00000000000 --- a/tests/gold_tests/tls_hooks/tls_hooks17.test.py +++ /dev/null @@ -1,76 +0,0 @@ -''' -Test single delayed client hello hook -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import os -import re - -Test.Summary = ''' -Test different combinations of TLS handshake hooks to ensure they are applied consistently. -''' - -Test.SkipUnless(Condition.HasOpenSSLVersion("1.1.1"),) - -ts = Test.MakeATSProcess("ts", enable_tls=True) -server = Test.MakeOriginServer("server") -request_header = {"headers": "GET / HTTP/1.1\r\nHost: www.example.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -# desired response form the origin server -response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -server.addResponse("sessionlog.json", request_header, response_header) - -ts.addDefaultSSLFiles() - -ts.Disk.records_config.update( - { - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.show_location': 0, - 'proxy.config.diags.debug.tags': 'ssl_hook_test', - 'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir), - 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts.Variables.SSLDir), - }) - -ts.Disk.ssl_multicert_yaml.AddLines( - """ -ssl_multicert: - - dest_ip: "*" - ssl_cert_name: server.pem - ssl_key_name: server.key -""".split("\n")) - -ts.Disk.remap_config.AddLine( - 'map https://example.com:{1} http://127.0.0.1:{0}'.format(server.Variables.Port, ts.Variables.ssl_port)) - -Test.PrepareTestPlugin(os.path.join(Test.Variables.AtsTestPluginsDir, 'ssl_hook_test.so'), ts, '-client_hello=1 -close=1') - -tr = Test.AddTestRun("Test one delayed client hello hook") -tr.Processes.Default.StartBefore(server) -tr.Processes.Default.StartBefore(Test.Processes.ts) -tr.StillRunningAfter = ts -tr.StillRunningAfter = server -tr.MakeCurlCommand('-k -H \'host:example.com:{0}\' https://127.0.0.1:{0}'.format(ts.Variables.ssl_port), ts=ts) -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Streams.stdout = "gold/client-hello-1.gold" - -ts.Disk.traffic_out.Content = "gold/ts-client-hello-delayed-1.gold" - -snistring = "Client Hello callback 0" -ts.Disk.traffic_out.Content = Testers.ContainsExpression( - r"\A(?:(?!{0}).)*{0}(?!.*{0}).*\Z".format(snistring), "Client Hello message appears only once", reflags=re.S | re.M) - -tr.Processes.Default.TimeOut = 15 -tr.TimeOut = 15 diff --git a/tests/gold_tests/tls_hooks/tls_hooks18.test.py b/tests/gold_tests/tls_hooks/tls_hooks18.test.py deleted file mode 100644 index c7e03faeb6d..00000000000 --- a/tests/gold_tests/tls_hooks/tls_hooks18.test.py +++ /dev/null @@ -1,79 +0,0 @@ -''' -Test two delayed client hello callbacks -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import os -import re - -Test.Summary = ''' -Test different combinations of TLS handshake hooks to ensure they are applied consistently. -''' - -Test.SkipUnless(Condition.HasOpenSSLVersion("1.1.1"),) - -ts = Test.MakeATSProcess("ts", enable_tls=True) -server = Test.MakeOriginServer("server") -request_header = {"headers": "GET / HTTP/1.1\r\nHost: www.example.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -# desired response form the origin server -response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -server.addResponse("sessionlog.json", request_header, response_header) - -ts.addDefaultSSLFiles() - -ts.Disk.records_config.update( - { - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.show_location': 0, - 'proxy.config.diags.debug.tags': 'ssl_hook_test', - 'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir), - 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts.Variables.SSLDir), - }) - -ts.Disk.ssl_multicert_yaml.AddLines( - """ -ssl_multicert: - - dest_ip: "*" - ssl_cert_name: server.pem - ssl_key_name: server.key -""".split("\n")) - -ts.Disk.remap_config.AddLine( - 'map https://example.com:{1} http://127.0.0.1:{0}'.format(server.Variables.Port, ts.Variables.ssl_port)) - -Test.PrepareTestPlugin(os.path.join(Test.Variables.AtsTestPluginsDir, 'ssl_hook_test.so'), ts, '-client_hello=2 -close=1') - -tr = Test.AddTestRun("Test two client hello hooks") -tr.Processes.Default.StartBefore(server) -tr.Processes.Default.StartBefore(Test.Processes.ts) -tr.StillRunningAfter = ts -tr.StillRunningAfter = server -tr.MakeCurlCommand('-k -H \'host:example.com:{0}\' https://127.0.0.1:{0}'.format(ts.Variables.ssl_port), ts=ts) -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Streams.stdout = "gold/preaccept-1.gold" - -ts.Disk.traffic_out.Content = "gold/ts-client-hello-2.gold" - -certstring0 = "Client Hello callback 0" -certstring1 = "Client Hello callback 1" -ts.Disk.traffic_out.Content = Testers.ContainsExpression( - r"\A(?:(?!{0}).)*{0}(?!.*{0}).*\Z".format(certstring0), "Cert message appears only once", reflags=re.S | re.M) -ts.Disk.traffic_out.Content = Testers.ContainsExpression( - r"\A(?:(?!{0}).)*{0}(?!.*{0}).*\Z".format(certstring1), "Cert message appears only once", reflags=re.S | re.M) - -tr.Processes.Default.TimeOut = 15 -tr.TimeOut = 15 diff --git a/tests/gold_tests/tls_hooks/tls_hooks2.test.py b/tests/gold_tests/tls_hooks/tls_hooks2.test.py deleted file mode 100644 index 0323002f1c9..00000000000 --- a/tests/gold_tests/tls_hooks/tls_hooks2.test.py +++ /dev/null @@ -1,74 +0,0 @@ -''' -Test single SNI hook -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import os -import re - -Test.Summary = ''' -Test different combinations of TLS handshake hooks to ensure they are applied consistently. -''' - -ts = Test.MakeATSProcess("ts", enable_tls=True) -server = Test.MakeOriginServer("server") -request_header = {"headers": "GET / HTTP/1.1\r\nHost: www.example.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -# desired response form the origin server -response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -server.addResponse("sessionlog.json", request_header, response_header) - -ts.addDefaultSSLFiles() - -ts.Disk.records_config.update( - { - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.show_location': 0, - 'proxy.config.diags.debug.tags': 'ssl_hook_test', - 'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir), - 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts.Variables.SSLDir), - }) - -ts.Disk.ssl_multicert_yaml.AddLines( - """ -ssl_multicert: - - dest_ip: "*" - ssl_cert_name: server.pem - ssl_key_name: server.key -""".split("\n")) - -ts.Disk.remap_config.AddLine( - 'map https://example.com:{0} http://127.0.0.1:{1}'.format(ts.Variables.ssl_port, server.Variables.Port)) - -Test.PrepareTestPlugin(os.path.join(Test.Variables.AtsTestPluginsDir, 'ssl_hook_test.so'), ts, '-sni=1') - -tr = Test.AddTestRun("Test one sni hook") -tr.Processes.Default.StartBefore(server) -tr.Processes.Default.StartBefore(Test.Processes.ts) -tr.StillRunningAfter = ts -tr.StillRunningAfter = server -tr.MakeCurlCommand('-k -H \'host:example.com:{0}\' https://127.0.0.1:{0}'.format(ts.Variables.ssl_port), ts=ts) -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Streams.stdout = "gold/preaccept-1.gold" - -ts.Disk.traffic_out.Content = "gold/ts-sni-1.gold" - -snistring = "SNI callback 0" -ts.Disk.traffic_out.Content = Testers.ContainsExpression( - r"\A(?:(?!{0}).)*{0}(?!.*{0}).*\Z".format(snistring), "SNI message appears only once", reflags=re.S | re.M) - -tr.Processes.Default.TimeOut = 15 -tr.TimeOut = 15 diff --git a/tests/gold_tests/tls_hooks/tls_hooks3.test.py b/tests/gold_tests/tls_hooks/tls_hooks3.test.py deleted file mode 100644 index 23aeaa9797b..00000000000 --- a/tests/gold_tests/tls_hooks/tls_hooks3.test.py +++ /dev/null @@ -1,74 +0,0 @@ -''' -Test single delayed cert callback -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import os -import re - -Test.Summary = ''' -Test different combinations of TLS handshake hooks to ensure they are applied consistently. -''' - -ts = Test.MakeATSProcess("ts", enable_tls=True) -server = Test.MakeOriginServer("server") -request_header = {"headers": "GET / HTTP/1.1\r\nHost: www.example.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -# desired response form the origin server -response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -server.addResponse("sessionlog.json", request_header, response_header) - -ts.addDefaultSSLFiles() - -ts.Disk.records_config.update( - { - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.show_location': 0, - 'proxy.config.diags.debug.tags': 'ssl_hook_test', - 'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir), - 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts.Variables.SSLDir), - }) - -ts.Disk.ssl_multicert_yaml.AddLines( - """ -ssl_multicert: - - dest_ip: "*" - ssl_cert_name: server.pem - ssl_key_name: server.key -""".split("\n")) - -ts.Disk.remap_config.AddLine( - 'map https://example.com:{0} http://127.0.0.1:{1}'.format(ts.Variables.ssl_port, server.Variables.Port)) - -Test.PrepareTestPlugin(os.path.join(Test.Variables.AtsTestPluginsDir, 'ssl_hook_test.so'), ts, '-cert=1') - -tr = Test.AddTestRun("Test one cert hook") -tr.Processes.Default.StartBefore(server) -tr.Processes.Default.StartBefore(Test.Processes.ts) -tr.StillRunningAfter = ts -tr.StillRunningAfter = server -tr.MakeCurlCommand('-k -H \'host:example.com:{0}\' https://127.0.0.1:{0}'.format(ts.Variables.ssl_port), ts=ts) -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Streams.stdout = "gold/preaccept-1.gold" - -ts.Disk.traffic_out.Content = "gold/ts-cert-1.gold" - -certstring = "Cert callback 0" -ts.Disk.traffic_out.Content = Testers.ContainsExpression( - r"\A(?:(?!{0}).)*{0}(?!.*{0}).*\Z".format(certstring), "Cert message appears only once", reflags=re.S | re.M) - -tr.Processes.Default.TimeOut = 15 -tr.TimeOut = 15 diff --git a/tests/gold_tests/tls_hooks/tls_hooks4.test.py b/tests/gold_tests/tls_hooks/tls_hooks4.test.py deleted file mode 100644 index 7068c838e4f..00000000000 --- a/tests/gold_tests/tls_hooks/tls_hooks4.test.py +++ /dev/null @@ -1,82 +0,0 @@ -''' -Test 1 preaccept, 1 sni, and 1 cert callback (with delay) -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import os -import re - -Test.Summary = ''' -Test different combinations of TLS handshake hooks to ensure they are applied consistently. -''' - -ts = Test.MakeATSProcess("ts", enable_tls=True) -server = Test.MakeOriginServer("server") -request_header = {"headers": "GET / HTTP/1.1\r\nHost: www.example.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -# desired response form the origin server -response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -server.addResponse("sessionlog.json", request_header, response_header) - -ts.addDefaultSSLFiles() - -ts.Disk.records_config.update( - { - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.show_location': 0, - 'proxy.config.diags.debug.tags': 'ssl_hook_test', - 'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir), - 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts.Variables.SSLDir), - }) - -ts.Disk.ssl_multicert_yaml.AddLines( - """ -ssl_multicert: - - dest_ip: "*" - ssl_cert_name: server.pem - ssl_key_name: server.key -""".split("\n")) - -ts.Disk.remap_config.AddLine( - 'map https://example.com:{0} http://127.0.0.1:{1}'.format(ts.Variables.ssl_port, server.Variables.Port)) - -Test.PrepareTestPlugin(os.path.join(Test.Variables.AtsTestPluginsDir, 'ssl_hook_test.so'), ts, '-cert=1 -sni=1 -preaccept=1') - -tr = Test.AddTestRun("Test one sni, one preaccept, and one cert hook") -tr.Processes.Default.StartBefore(server) -tr.Processes.Default.StartBefore(Test.Processes.ts) -tr.StillRunningAfter = ts -tr.StillRunningAfter = server -tr.MakeCurlCommand('-k -H \'host:example.com:{0}\' https://127.0.0.1:{0}'.format(ts.Variables.ssl_port), ts=ts) -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Streams.stdout = "gold/preaccept-1.gold" - -ts.Disk.traffic_out.Content = "gold/ts-preaccept1-sni1-cert1.gold" -snistring = "SNI callback 0" -preacceptstring = "Pre accept callback 0" -certstring = "Cert callback 0" -ts.Disk.traffic_out.Content = Testers.ContainsExpression( - r"\A(?:(?!{0}).)*{0}(?!.*{0}).*\Z".format(snistring), "SNI message appears only once", reflags=re.S | re.M) -# the preaccept may get triggered twice because the test framework creates a TCP connection before handing off to traffic_server -ts.Disk.traffic_out.Content += Testers.ContainsExpression( - r"\A(?:(?!{0}).)*{0}.*({0})?(?!.*{0}).*\Z".format(preacceptstring), - "Pre accept message appears only once or twice", - reflags=re.S | re.M) -ts.Disk.traffic_out.Content += Testers.ContainsExpression( - r"\A(?:(?!{0}).)*{0}(?!.*{0}).*\Z".format(certstring), "Cert message appears only once", reflags=re.S | re.M) - -tr.Processes.Default.TimeOut = 15 -tr.TimeOut = 15 diff --git a/tests/gold_tests/tls_hooks/tls_hooks6.test.py b/tests/gold_tests/tls_hooks/tls_hooks6.test.py deleted file mode 100644 index 1b2635f4349..00000000000 --- a/tests/gold_tests/tls_hooks/tls_hooks6.test.py +++ /dev/null @@ -1,82 +0,0 @@ -''' -Test 1 preaccept callback (without delay) two times -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import os -import re - -Test.Summary = ''' -Test different combinations of TLS handshake hooks to ensure they are applied consistently. -''' - -ts = Test.MakeATSProcess("ts", enable_tls=True) -server = Test.MakeOriginServer("server") -request_header = {"headers": "GET / HTTP/1.1\r\nHost: www.example.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -# desired response form the origin server -response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -server.addResponse("sessionlog.json", request_header, response_header) - -ts.addDefaultSSLFiles() - -ts.Disk.records_config.update( - { - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.show_location': 0, - 'proxy.config.diags.debug.tags': 'ssl_hook_test', - 'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir), - 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts.Variables.SSLDir), - }) - -ts.Disk.ssl_multicert_yaml.AddLines( - """ -ssl_multicert: - - dest_ip: "*" - ssl_cert_name: server.pem - ssl_key_name: server.key -""".split("\n")) - -ts.Disk.remap_config.AddLine( - 'map https://example.com:{0} http://127.0.0.1:{1}'.format(ts.Variables.ssl_port, server.Variables.Port)) - -Test.PrepareTestPlugin(os.path.join(Test.Variables.AtsTestPluginsDir, 'ssl_hook_test.so'), ts, '-preaccept=2') - -tr = Test.AddTestRun("Test two preaccept hooks") -tr.Processes.Default.StartBefore(server) -tr.Processes.Default.StartBefore(Test.Processes.ts) -tr.StillRunningAfter = ts -tr.StillRunningAfter = server -tr.MakeCurlCommand('-k -H \'host:example.com:{0}\' https://127.0.0.1:{0}'.format(ts.Variables.ssl_port), ts=ts) -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Streams.stdout = "gold/preaccept-1.gold" - -ts.Disk.traffic_out.Content = "gold/ts-preaccept-2.gold" - -# the preaccept may get triggered twice because the test framework creates a TCP connection before handing off to traffic_server -preacceptstring0 = "Pre accept callback 0" -preacceptstring1 = "Pre accept callback 1" -ts.Disk.traffic_out.Content = Testers.ContainsExpression( - r"\A(?:(?!{0}).)*{0}.*({0})?(?!.*{0}).*\Z".format(preacceptstring0), - "Pre accept message appears only once or twice", - reflags=re.S | re.M) -ts.Disk.traffic_out.Content = Testers.ContainsExpression( - r"\A(?:(?!{0}).)*{0}.*({0})?(?!.*{0}).*\Z".format(preacceptstring1), - "Pre accept message appears only once or twice", - reflags=re.S | re.M) - -tr.Processes.Default.TimeOut = 15 -tr.TimeOut = 15 diff --git a/tests/gold_tests/tls_hooks/tls_hooks7.test.py b/tests/gold_tests/tls_hooks/tls_hooks7.test.py deleted file mode 100644 index e664b717daa..00000000000 --- a/tests/gold_tests/tls_hooks/tls_hooks7.test.py +++ /dev/null @@ -1,77 +0,0 @@ -''' -Test two SNI hooks -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import os -import re - -Test.Summary = ''' -Test different combinations of TLS handshake hooks to ensure they are applied consistently. -''' - -ts = Test.MakeATSProcess("ts", enable_tls=True) -server = Test.MakeOriginServer("server") -request_header = {"headers": "GET / HTTP/1.1\r\nHost: www.example.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -# desired response form the origin server -response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -server.addResponse("sessionlog.json", request_header, response_header) - -ts.addDefaultSSLFiles() - -ts.Disk.records_config.update( - { - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.show_location': 0, - 'proxy.config.diags.debug.tags': 'ssl_hook_test', - 'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir), - 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts.Variables.SSLDir), - }) - -ts.Disk.ssl_multicert_yaml.AddLines( - """ -ssl_multicert: - - dest_ip: "*" - ssl_cert_name: server.pem - ssl_key_name: server.key -""".split("\n")) - -ts.Disk.remap_config.AddLine( - 'map https://example.com:{0} http://127.0.0.1:{1}'.format(ts.Variables.ssl_port, server.Variables.Port)) - -Test.PrepareTestPlugin(os.path.join(Test.Variables.AtsTestPluginsDir, 'ssl_hook_test.so'), ts, '-sni=2') - -tr = Test.AddTestRun("Test two sni hooks") -tr.Processes.Default.StartBefore(server) -tr.Processes.Default.StartBefore(Test.Processes.ts) -tr.StillRunningAfter = ts -tr.StillRunningAfter = server -tr.MakeCurlCommand('-k -H \'host:example.com:{0}\' https://127.0.0.1:{0}'.format(ts.Variables.ssl_port), ts=ts) -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Streams.stdout = "gold/preaccept-1.gold" - -ts.Disk.traffic_out.Content = "gold/ts-sni-2.gold" - -snistring0 = "SNI callback 0" -snistring1 = "SNI callback 1" -ts.Disk.traffic_out.Content = Testers.ContainsExpression( - r"\A(?:(?!{0}).)*{0}(?!.*{0}).*\Z".format(snistring0), "SNI message appears only once", reflags=re.S | re.M) -ts.Disk.traffic_out.Content = Testers.ContainsExpression( - r"\A(?:(?!{0}).)*{0}(?!.*{0}).*\Z".format(snistring1), "SNI message appears only once", reflags=re.S | re.M) - -tr.Processes.Default.TimeOut = 15 -tr.TimeOut = 15 diff --git a/tests/gold_tests/tls_hooks/tls_hooks8.test.py b/tests/gold_tests/tls_hooks/tls_hooks8.test.py deleted file mode 100644 index be08cc17371..00000000000 --- a/tests/gold_tests/tls_hooks/tls_hooks8.test.py +++ /dev/null @@ -1,77 +0,0 @@ -''' -Test two delayed cert callbacks -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import os -import re - -Test.Summary = ''' -Test different combinations of TLS handshake hooks to ensure they are applied consistently. -''' - -ts = Test.MakeATSProcess("ts", enable_tls=True) -server = Test.MakeOriginServer("server") -request_header = {"headers": "GET / HTTP/1.1\r\nHost: www.example.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -# desired response form the origin server -response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -server.addResponse("sessionlog.json", request_header, response_header) - -ts.addDefaultSSLFiles() - -ts.Disk.records_config.update( - { - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.show_location': 0, - 'proxy.config.diags.debug.tags': 'ssl_hook_test', - 'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir), - 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts.Variables.SSLDir), - }) - -ts.Disk.ssl_multicert_yaml.AddLines( - """ -ssl_multicert: - - dest_ip: "*" - ssl_cert_name: server.pem - ssl_key_name: server.key -""".split("\n")) - -ts.Disk.remap_config.AddLine( - 'map https://example.com:{0} http://127.0.0.1:{1}'.format(ts.Variables.ssl_port, server.Variables.Port)) - -Test.PrepareTestPlugin(os.path.join(Test.Variables.AtsTestPluginsDir, 'ssl_hook_test.so'), ts, '-cert=2') - -tr = Test.AddTestRun("Test two cert hooks") -tr.Processes.Default.StartBefore(server) -tr.Processes.Default.StartBefore(Test.Processes.ts) -tr.StillRunningAfter = ts -tr.StillRunningAfter = server -tr.MakeCurlCommand('-k -H \'host:example.com:{0}\' https://127.0.0.1:{0}'.format(ts.Variables.ssl_port), ts=ts) -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Streams.stdout = "gold/preaccept-1.gold" - -ts.Disk.traffic_out.Content = "gold/ts-cert-2.gold" - -certstring0 = "Cert callback 0" -certstring1 = "Cert callback 1" -ts.Disk.traffic_out.Content = Testers.ContainsExpression( - r"\A(?:(?!{0}).)*{0}(?!.*{0}).*\Z".format(certstring0), "Cert message appears only once", reflags=re.S | re.M) -ts.Disk.traffic_out.Content = Testers.ContainsExpression( - r"\A(?:(?!{0}).)*{0}(?!.*{0}).*\Z".format(certstring1), "Cert message appears only once", reflags=re.S | re.M) - -tr.Processes.Default.TimeOut = 15 -tr.TimeOut = 15 diff --git a/tests/gold_tests/tls_hooks/tls_hooks9.test.py b/tests/gold_tests/tls_hooks/tls_hooks9.test.py deleted file mode 100644 index 343c29f54ae..00000000000 --- a/tests/gold_tests/tls_hooks/tls_hooks9.test.py +++ /dev/null @@ -1,73 +0,0 @@ -''' -Test one immediate cert callback -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import os -import re - -Test.Summary = ''' -Test different combinations of TLS handshake hooks to ensure they are applied consistently. -''' - -ts = Test.MakeATSProcess("ts", enable_tls=True) -server = Test.MakeOriginServer("server") -request_header = {"headers": "GET / HTTP/1.1\r\nHost: www.example.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -# desired response form the origin server -response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -server.addResponse("sessionlog.json", request_header, response_header) - -ts.addDefaultSSLFiles() - -ts.Disk.records_config.update( - { - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.show_location': 0, - 'proxy.config.diags.debug.tags': 'ssl_hook_test', - 'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir), - 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts.Variables.SSLDir), - }) - -ts.Disk.ssl_multicert_yaml.AddLines( - """ -ssl_multicert: - - dest_ip: "*" - ssl_cert_name: server.pem - ssl_key_name: server.key -""".split("\n")) - -ts.Disk.remap_config.AddLine( - 'map https://example.com:{0} http://127.0.0.1:{1}'.format(ts.Variables.ssl_port, server.Variables.Port)) - -Test.PrepareTestPlugin(os.path.join(Test.Variables.AtsTestPluginsDir, 'ssl_hook_test.so'), ts, '-i=1') - -tr = Test.AddTestRun("Test one immediate cert hooks") -tr.Processes.Default.StartBefore(server) -tr.Processes.Default.StartBefore(Test.Processes.ts) -tr.StillRunningAfter = ts -tr.StillRunningAfter = server -tr.MakeCurlCommand('-k -H \'host:example.com:{0}\' https://127.0.0.1:{0}'.format(ts.Variables.ssl_port), ts=ts) -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Streams.stdout = "gold/preaccept-1.gold" - -ts.Disk.traffic_out.Content = "gold/ts-cert-im-1.gold" - -certstring0 = "Cert callback 0" -ts.Disk.traffic_out.Content = Testers.ContainsExpression( - r"\A(?:(?!{0}).)*{0}(?!.*{0}).*\Z".format(certstring0), "Cert message appears only once", reflags=re.S | re.M) -tr.Processes.Default.TimeOut = 15 -tr.TimeOut = 15 diff --git a/tests/gold_tests/tls_hooks/tls_hooks_close_while_parked.test.py b/tests/gold_tests/tls_hooks/tls_hooks_close_while_parked.test.py deleted file mode 100644 index 89fe0a50a3f..00000000000 --- a/tests/gold_tests/tls_hooks/tls_hooks_close_while_parked.test.py +++ /dev/null @@ -1,88 +0,0 @@ -''' -Verify that a plugin's VCONN_CLOSE hook runs when the connection closes while it is -parked in a TLS handshake hook. -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import os - -Test.Summary = ''' -A connection that closes while parked in a TLS handshake hook must still deliver -TS_VCONN_CLOSE_HOOK to the plugin. -''' - -Test.SkipUnless(Condition.HasOpenSSLVersion("1.1.1"),) - -ts = Test.MakeATSProcess("ts", enable_tls=True) -server = Test.MakeOriginServer("server") -server.addResponse( - "sessionlog.json", { - "headers": "GET / HTTP/1.1\r\nHost: www.example.com\r\n\r\n", - "timestamp": "1469733493.993", - "body": "" - }, { - "headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", - "timestamp": "1469733493.993", - "body": "" - }) - -ts.addDefaultSSLFiles() - -ts.Disk.records_config.update( - { - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.show_location': 0, - 'proxy.config.diags.debug.tags': 'ssl_hook_test', - # Fire the handshake timeout while the plugin still has the handshake parked (2s park). - 'proxy.config.ssl.handshake_timeout_in': 1, - 'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir), - 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts.Variables.SSLDir), - }) - -ts.Disk.ssl_multicert_yaml.AddLines( - """ -ssl_multicert: - - dest_ip: "*" - ssl_cert_name: server.pem - ssl_key_name: server.key -""".split("\n")) - -ts.Disk.remap_config.AddLine( - 'map https://example.com:{1} http://127.0.0.1:{0}'.format(server.Variables.Port, ts.Variables.ssl_port)) - -# The delayed client hello callback parks the handshake for 2 seconds before it reenables. -Test.PrepareTestPlugin(os.path.join(Test.Variables.AtsTestPluginsDir, 'ssl_hook_test.so'), ts, '-client_hello=1 -close=1') - -# Give up after 1 second, which is inside the 2 second park, so the connection closes while it -# is still suspended in the client hello hook. curl reports operation timed out (exit 28). -tr = Test.AddTestRun("Client disconnects while parked in the client hello hook") -tr.Processes.Default.StartBefore(server) -tr.Processes.Default.StartBefore(Test.Processes.ts) -tr.StillRunningAfter = ts -tr.StillRunningAfter = server -tr.MakeCurlCommand('-k --max-time 1 -H \'host:example.com:{0}\' https://127.0.0.1:{0}'.format(ts.Variables.ssl_port), ts=ts) -tr.Processes.Default.ReturnCode = 28 -tr.Processes.Default.TimeOut = 15 -tr.TimeOut = 15 - -# The handshake really was parked. -ts.Disk.traffic_out.Content = Testers.ContainsExpression("Client Hello callback 0", "the handshake parked in the client hello hook") - -# The close hook must still fire, with the correct event. Before the fix, callHooks() advanced -# curHook within the client hello hook list instead of the close hook list, so this never ran. -ts.Disk.traffic_out.Content += Testers.ContainsExpression( - "Close callback 0 .* - event is good", "the close hook ran for the parked connection") diff --git a/tests/gold_tests/traffic_ctl/convert_plugin_config/convert_plugin_config.test.py b/tests/gold_tests/traffic_ctl/convert_plugin_config/convert_plugin_config.test.py deleted file mode 100644 index 4e691730a98..00000000000 --- a/tests/gold_tests/traffic_ctl/convert_plugin_config/convert_plugin_config.test.py +++ /dev/null @@ -1,68 +0,0 @@ -''' -Test the traffic_ctl config convert plugin_config command. -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -Test.Summary = 'Test traffic_ctl config convert plugin_config command.' - -ts = Test.MakeATSProcess("ts", enable_cache=False) - -# Test 1: Basic plugin.config conversion. -tr = Test.AddTestRun("Test basic plugin.config conversion") -tr.Setup.Copy('legacy_config/basic.config') -tr.Processes.Default.Command = 'traffic_ctl config convert plugin_config basic.config -' -tr.Processes.Default.Streams.stdout = "gold/basic.yaml" -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Env = ts.Env -tr.Processes.Default.StartBefore(ts) -tr.StillRunningAfter = ts - -# Test 2: Commented-out lines become enabled: false. -tr = Test.AddTestRun("Test commented lines converted to disabled entries") -tr.Setup.Copy('legacy_config/commented.config') -tr.Processes.Default.Command = 'traffic_ctl config convert plugin_config commented.config -' -tr.Processes.Default.Streams.stdout = "gold/commented.yaml" -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Env = ts.Env -tr.StillRunningAfter = ts - -# Test 3: Quoted arguments. -tr = Test.AddTestRun("Test plugin.config with quoted arguments") -tr.Setup.Copy('legacy_config/quoted.config') -tr.Processes.Default.Command = 'traffic_ctl config convert plugin_config quoted.config -' -tr.Processes.Default.Streams.stdout = "gold/quoted.yaml" -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Env = ts.Env -tr.StillRunningAfter = ts - -# Test 4: Output to file instead of stdout. -tr = Test.AddTestRun("Test output to file") -tr.Setup.Copy('legacy_config/basic.config') -tr.Processes.Default.Command = 'traffic_ctl config convert plugin_config basic.config generated.yaml > /dev/null && cat generated.yaml' -tr.Processes.Default.Streams.stdout = "gold/basic.yaml" -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Env = ts.Env -tr.StillRunningAfter = ts - -# Test 5: --skip-disabled omits commented-out plugins from output. -tr = Test.AddTestRun("Test --skip-disabled drops disabled entries") -tr.Setup.Copy('legacy_config/commented.config') -tr.Processes.Default.Command = 'traffic_ctl config convert plugin_config --skip-disabled commented.config -' -tr.Processes.Default.Streams.stdout = "gold/skip_disabled.yaml" -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Env = ts.Env -tr.StillRunningAfter = ts diff --git a/tests/gold_tests/traffic_ctl/convert_ssl_multicert/convert_ssl_multicert.test.py b/tests/gold_tests/traffic_ctl/convert_ssl_multicert/convert_ssl_multicert.test.py deleted file mode 100644 index c0ffbcfb1d8..00000000000 --- a/tests/gold_tests/traffic_ctl/convert_ssl_multicert/convert_ssl_multicert.test.py +++ /dev/null @@ -1,60 +0,0 @@ -''' -Test the traffic_ctl config convert ssl_multicert command. -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -Test.Summary = 'Test traffic_ctl config convert ssl_multicert command.' - -# Create an ATS process to get the environment with PATH set correctly. -ts = Test.MakeATSProcess("ts", enable_cache=False) - -# Test 1: Basic config conversion. -tr = Test.AddTestRun("Test basic ssl_multicert.config conversion") -tr.Setup.Copy('legacy_config/basic.config') -tr.Processes.Default.Command = 'traffic_ctl config convert ssl_multicert basic.config -' -tr.Processes.Default.Streams.stdout = "gold/basic.yaml" -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Env = ts.Env -tr.Processes.Default.StartBefore(ts) -tr.StillRunningAfter = ts - -# Test 2: Full config with all options. -tr = Test.AddTestRun("Test full ssl_multicert.config conversion with all options") -tr.Setup.Copy('legacy_config/full.config') -tr.Processes.Default.Command = 'traffic_ctl config convert ssl_multicert full.config -' -tr.Processes.Default.Streams.stdout = "gold/full.yaml" -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Env = ts.Env -tr.StillRunningAfter = ts - -# Test 3: Config with quoted values. -tr = Test.AddTestRun("Test ssl_multicert.config with quoted values") -tr.Setup.Copy('legacy_config/quoted.config') -tr.Processes.Default.Command = 'traffic_ctl config convert ssl_multicert quoted.config -' -tr.Processes.Default.Streams.stdout = "gold/quoted.yaml" -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Env = ts.Env -tr.StillRunningAfter = ts - -# Test 4: Output to file. -tr = Test.AddTestRun("Test output to file") -tr.Setup.Copy('legacy_config/basic.config') -tr.Processes.Default.Command = 'traffic_ctl config convert ssl_multicert basic.config generated.yaml > /dev/null && cat generated.yaml' -tr.Processes.Default.Streams.stdout = "gold/basic.yaml" -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Env = ts.Env -tr.StillRunningAfter = ts diff --git a/tests/gold_tests/traffic_ctl/convert_storage/convert_storage.test.py b/tests/gold_tests/traffic_ctl/convert_storage/convert_storage.test.py deleted file mode 100644 index cf437c9a64a..00000000000 --- a/tests/gold_tests/traffic_ctl/convert_storage/convert_storage.test.py +++ /dev/null @@ -1,68 +0,0 @@ -''' -Test the traffic_ctl config convert storage command. -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -Test.Summary = 'Test traffic_ctl config convert storage command.' - -# Create an ATS process to get the environment with PATH set correctly. -ts = Test.MakeATSProcess("ts", enable_cache=False) - -# Test 1: Basic conversion (spans only, no volume=N annotations). -tr = Test.AddTestRun("Test basic storage.config + volume.config conversion") -tr.Setup.Copy('legacy_config/basic.storage.config') -tr.Setup.Copy('legacy_config/basic.volume.config') -tr.Processes.Default.Command = \ - 'traffic_ctl config convert storage basic.storage.config basic.volume.config -' -tr.Processes.Default.Streams.stdout = "gold/basic.yaml" -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Env = ts.Env -tr.Processes.Default.StartBefore(ts) -tr.StillRunningAfter = ts - -# Test 2: Exclusive volume assignments (volume=N per span line). -tr = Test.AddTestRun("Test storage.config with exclusive volume=N span assignments") -tr.Setup.Copy('legacy_config/exclusive.storage.config') -tr.Setup.Copy('legacy_config/exclusive.volume.config') -tr.Processes.Default.Command = \ - 'traffic_ctl config convert storage exclusive.storage.config exclusive.volume.config -' -tr.Processes.Default.Streams.stdout = "gold/exclusive.yaml" -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Env = ts.Env -tr.StillRunningAfter = ts - -# Test 3: Spans only, no volume.config (missing volume.config is treated as empty). -tr = Test.AddTestRun("Test storage.config with no volume.config (spans only)") -tr.Setup.Copy('legacy_config/no_volumes.storage.config') -tr.Processes.Default.Command = \ - 'traffic_ctl config convert storage no_volumes.storage.config /nonexistent/volume.config -' -tr.Processes.Default.Streams.stdout = "gold/no_volumes.yaml" -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Env = ts.Env -tr.StillRunningAfter = ts - -# Test 4: Output to file. -tr = Test.AddTestRun("Test output to file") -tr.Setup.Copy('legacy_config/basic.storage.config') -tr.Setup.Copy('legacy_config/basic.volume.config') -tr.Processes.Default.Command = \ - 'traffic_ctl config convert storage basic.storage.config basic.volume.config generated.yaml' \ - ' > /dev/null && cat generated.yaml' -tr.Processes.Default.Streams.stdout = "gold/basic.yaml" -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Env = ts.Env -tr.StillRunningAfter = ts diff --git a/tests/gold_tests/traffic_ctl/remap_inc/remap_inc.test.py b/tests/gold_tests/traffic_ctl/remap_inc/remap_inc.test.py deleted file mode 100644 index 7e7824cb29e..00000000000 --- a/tests/gold_tests/traffic_ctl/remap_inc/remap_inc.test.py +++ /dev/null @@ -1,60 +0,0 @@ -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -Test.Summary = ''' -Test traffic_ctl config reload with remap.config .include directive -''' - -Test.ContinueOnFail = False - -# Define ATS and configure -ts = Test.MakeATSProcess("ts", enable_cache=False) -nameserver = Test.MakeDNServer("dns", default='127.0.0.1') - -ts.Disk.File(ts.Variables.CONFIGDIR + "/test.inc", id="test_cfg", typename="ats:config") -ts.Disk.test_cfg.AddLine( - "map http://example.two/ http://yada.com/ " + "@plugin=conf_remap.so @pparam=proxy.config.url_remap.pristine_host_hdr=1") - -ts.Disk.remap_config.AddLine("map http://example.one/ http://yada.com/") -ts.Disk.remap_config.AddLine(".include test.inc") -ts.Disk.remap_config.AddLine("map http://example.three/ http://yada.com/") - -# minimal configuration -ts.Disk.records_config.update( - { - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'regex_remap|url_rewrite|plugin_factory', - 'proxy.config.dns.nameservers': f"127.0.0.1:{nameserver.Variables.Port}", - }) - -tr = Test.AddTestRun("Start TS, then update test.inc") -tr.Processes.Default.StartBefore(Test.Processes.ts) -tr.Processes.Default.StartBefore(nameserver) -test_inc_path = ts.Variables.CONFIGDIR + "/test.inc" -tr.Processes.Default.Command = ( - f"rm -f {test_inc_path} ; " + f"echo 'map http://example.four/ http://localhost/ @plugin=generator.so' > {test_inc_path}") -tr.Processes.Default.ReturnCode = 0 -tr.StillRunningAfter = ts - -tr = Test.AddConfigReload(ts, expect_tasks=["remap.config"], description="Reload remap.config") -tr.StillRunningAfter = ts - -tr = Test.AddTestRun("Get response from generator") -tr.MakeCurlCommand(f'--proxy 127.0.0.1:{ts.Variables.port} http://example.four/nocache/5', ts=ts) -tr.Processes.Default.ReturnCode = 0 -tr.StillRunningAfter = ts -tr.Processes.Default.Streams.All = Testers.ContainsExpression("xxxxx", "Contains generated text") -tr.Processes.Default.Streams.All += Testers.ExcludesExpression("xxxxxx", "Not too much data") diff --git a/tests/gold_tests/traffic_ctl/show_ssl_multicert/show_ssl_multicert.test.py b/tests/gold_tests/traffic_ctl/show_ssl_multicert/show_ssl_multicert.test.py deleted file mode 100644 index 984df9b7f18..00000000000 --- a/tests/gold_tests/traffic_ctl/show_ssl_multicert/show_ssl_multicert.test.py +++ /dev/null @@ -1,91 +0,0 @@ -''' -Test the traffic_ctl config ssl-multicert show command. -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -Test.Summary = 'Test traffic_ctl config ssl-multicert show command.' - - -class ShowSSLMulticert: - - def __init__(self): - self.setup_ts() - self.setup_show_default() - self.setup_show_json() - self.setup_show_yaml() - - def setup_ts(self): - self._ts = Test.MakeATSProcess("ts", enable_cache=False, enable_tls=True) - self._ts.addDefaultSSLFiles() - self._ts.Disk.ssl_multicert_yaml.AddLines( - """ -ssl_multicert: - - ssl_cert_name: server.pem - dest_ip: "*" - ssl_key_name: server.key -""".split("\n")) - self._ts.Disk.records_config.update( - { - 'proxy.config.ssl.server.cert.path': f'{self._ts.Variables.SSLDir}', - 'proxy.config.ssl.server.private_key.path': f'{self._ts.Variables.SSLDir}', - }) - - def setup_show_default(self): - tr = Test.AddTestRun("Test ssl-multicert show (default YAML format)") - tr.Processes.Default.Command = 'traffic_ctl config ssl-multicert show' - tr.Processes.Default.Streams.stdout = "gold/show_yaml.gold" - tr.Processes.Default.ReturnCode = 0 - tr.Processes.Default.Env = self._ts.Env - tr.Processes.Default.StartBefore(self._ts) - tr.StillRunningAfter = self._ts - - def setup_show_json(self): - # Test with explicit --json flag. - tr = Test.AddTestRun("Test ssl-multicert show --json") - tr.Processes.Default.Command = 'traffic_ctl config ssl-multicert show --json' - tr.Processes.Default.Streams.stdout = "gold/show_json.gold" - tr.Processes.Default.ReturnCode = 0 - tr.Processes.Default.Env = self._ts.Env - tr.StillRunningAfter = self._ts - - # Test with short -j flag. - tr = Test.AddTestRun("Test ssl-multicert show -j") - tr.Processes.Default.Command = 'traffic_ctl config ssl-multicert show -j' - tr.Processes.Default.Streams.stdout = "gold/show_json.gold" - tr.Processes.Default.ReturnCode = 0 - tr.Processes.Default.Env = self._ts.Env - tr.StillRunningAfter = self._ts - - def setup_show_yaml(self): - # Test with --yaml flag. - tr = Test.AddTestRun("Test ssl-multicert show --yaml") - tr.Processes.Default.Command = 'traffic_ctl config ssl-multicert show --yaml' - tr.Processes.Default.Streams.stdout = "gold/show_yaml.gold" - tr.Processes.Default.ReturnCode = 0 - tr.Processes.Default.Env = self._ts.Env - tr.StillRunningAfter = self._ts - - # Test with short -y flag. - tr = Test.AddTestRun("Test ssl-multicert show -y") - tr.Processes.Default.Command = 'traffic_ctl config ssl-multicert show -y' - tr.Processes.Default.Streams.stdout = "gold/show_yaml.gold" - tr.Processes.Default.ReturnCode = 0 - tr.Processes.Default.Env = self._ts.Env - tr.StillRunningAfter = self._ts - - -ShowSSLMulticert() diff --git a/tests/gold_tests/traffic_ctl/traffic_ctl_config_output.test.py b/tests/gold_tests/traffic_ctl/traffic_ctl_config_output.test.py deleted file mode 100644 index 69f8a1cf7be..00000000000 --- a/tests/gold_tests/traffic_ctl/traffic_ctl_config_output.test.py +++ /dev/null @@ -1,141 +0,0 @@ -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. -import sys -import os - -# To include util classes -sys.path.insert(0, f'{Test.TestDirectory}') - -from traffic_ctl_test_utils import Make_traffic_ctl -# import ruamel.yaml Uncomment only when GoldFilePathFor is used. - -Test.Summary = ''' -Test traffic_ctl config output responses. -''' - -Test.ContinueOnFail = True - -records_yaml = ''' - udp: - threads: 1 - diags: - debug: - enabled: 1 - tags: rpc - throttling_interval_msec: 0 - ''' - -traffic_ctl = Make_traffic_ctl(Test, records_yaml) - -##### CONFIG GET - -# Test 0: YAML output -traffic_ctl.config().get("proxy.config.diags.debug.tags").as_records().validate_with_goldfile("t1_yaml.gold") -# Test 1: Default output -traffic_ctl.config().get("proxy.config.diags.debug.enabled").validate_with_text("proxy.config.diags.debug.enabled: 1") -# Test 2: Default output with default. -traffic_ctl.config().get("proxy.config.diags.debug.tags").with_default() \ - .validate_with_text("proxy.config.diags.debug.tags: rpc # default http|dns") - -# Test 3: Now same output test but with defaults, traffic_ctl supports adding default value -# when using --records. -traffic_ctl.config().get("proxy.config.diags.debug.tags").as_records().with_default().validate_with_goldfile("t2_yaml.gold") -# Test 4: -traffic_ctl.config().get( - "proxy.config.diags.debug.tags proxy.config.diags.debug.enabled proxy.config.diags.debug.throttling_interval_msec").as_records( - ).with_default().validate_with_goldfile("t3_yaml.gold") - -##### CONFIG MATCH -# Test 5: -traffic_ctl.config().match("threads").with_default().validate_with_goldfile("match.gold") - -# Test 6: The idea is to check the traffic_ctl yaml emitter when a value starts with the -# same prefix of a node like: -# diags: -# logfile: -# logfile_perm: rw-r--r-- -# -# traffic_ctl have a special logic to deal with cases like this, so better test it. -traffic_ctl.config().match("diags.logfile").as_records().validate_with_goldfile("t4_yaml.gold") - -##### CONFIG DIFF -# Test 7: -traffic_ctl.config().diff().validate_with_goldfile("diff.gold") -# Test 8: -traffic_ctl.config().diff().as_records().validate_with_goldfile("diff_yaml.gold") - -##### CONFIG DESCRIBE -# Test 9: don't really care about values, but just output and that the command actually went through -traffic_ctl.config().describe("proxy.config.http.server_ports").validate_with_goldfile("describe.gold") - -##### CONFIG RESET -# Test 10: Reset a single modified record (proxy.config.diags.debug.tags is set to "rpc" in records_yaml, -# default is "http|dns", so it should be reset) -traffic_ctl.config().reset("proxy.config.diags.debug.tags").validate_with_text( - "Set proxy.config.diags.debug.tags, please wait 10 seconds for traffic server to sync " - "configuration, restart is not required") -# Test 11: Validate the record was reset to its default value -traffic_ctl.config().get("proxy.config.diags.debug.tags").validate_with_text("proxy.config.diags.debug.tags: http|dns") - -# Test 12: Reset records matching a partial path (proxy.config.diags) -# First set the record back to non-default for this test -traffic_ctl.config().set("proxy.config.diags.debug.tags", "rpc").exec() -# Test 13: Resetting proxy.config.diags should reset all matching modified records under that path -traffic_ctl.config().reset("proxy.config.diags").validate_contains_all( - "Set proxy.config.diags.debug.tags", "Set proxy.config.diags.debug.enabled") -# Test 14: Validate the record was reset to its default value -traffic_ctl.config().get("proxy.config.diags.debug.tags").validate_with_text("proxy.config.diags.debug.tags: http|dns") - -# Test 15: Reset all records using "records" keyword -# First set the record back to non-default for this test -traffic_ctl.config().set("proxy.config.diags.debug.tags", "rpc").exec() -# Test 16: This will reset all modified records (including proxy.config.diags.debug.tags) -# Some may require restart, which is ok, we can use diff anyways as the records that needs -# restart will just change the value but won't have any effect. -traffic_ctl.config().reset("records").exec() -# Validate the diff -# Test 17: Validate the diff -traffic_ctl.config().diff().validate_with_text("") -# Test 18: Validate the record was reset to its default value -traffic_ctl.config().get("proxy.config.diags.debug.tags").validate_with_text("proxy.config.diags.debug.tags: http|dns") - -# # Test resetting when no records need resetting (all already at default) -# # Create a new instance with default values only -# traffic_ctl_default = Make_traffic_ctl(Test, None) -# traffic_ctl_default.config().reset("proxy.config.diags.debug.enabled").validate_with_text( -# "No records to reset (all matching records are already at default values)") - -##### CONFIG RESET with YAML-style paths (records.* format) -# Test 19: Set a record to non-default first -traffic_ctl.config().set("proxy.config.diags.debug.tags", "yaml_test").exec() -# Test 20: Reset using YAML-style path (records.diags.debug.tags instead of proxy.config.diags.debug.tags) -traffic_ctl.config().reset("records.diags.debug.tags").validate_with_text( - "Set proxy.config.diags.debug.tags, please wait 10 seconds for traffic server to sync " - "configuration, restart is not required") -# Test 21: Validate the record was reset to its default value -traffic_ctl.config().get("proxy.config.diags.debug.tags").validate_with_text("proxy.config.diags.debug.tags: http|dns") - -# Test 22: Reset using YAML-style partial path (records.diags) -traffic_ctl.config().set("proxy.config.diags.debug.tags", "yaml_partial_test").exec() -traffic_ctl.config().set("proxy.config.diags.debug.enabled", "1").exec() -# Test 23: Reset using records.diags (YAML format) -traffic_ctl.config().reset("records.diags").validate_contains_all( - "Set proxy.config.diags.debug.tags", "Set proxy.config.diags.debug.enabled") -# Test 24: Validate record was reset -traffic_ctl.config().get("proxy.config.diags.debug.tags").validate_with_text("proxy.config.diags.debug.tags: http|dns") - -# Test 25: Make sure that the command returns an exit code of 2 -traffic_ctl.config().get("invalid.should.set.the.exit.code.to.2").validate_with_exit_code(2) diff --git a/tests/gold_tests/traffic_ctl/traffic_ctl_config_reload.test.py b/tests/gold_tests/traffic_ctl/traffic_ctl_config_reload.test.py deleted file mode 100644 index 59f440b7223..00000000000 --- a/tests/gold_tests/traffic_ctl/traffic_ctl_config_reload.test.py +++ /dev/null @@ -1,192 +0,0 @@ -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. -import sys -import os - -# To include util classes -sys.path.insert(0, f'{Test.TestDirectory}') - -from traffic_ctl_test_utils import Make_traffic_ctl -# import ruamel.yaml Uncomment only when GoldFilePathFor is used. - - -def touch(fname, times=None): - with open(fname, 'a'): - os.utime(fname, times) - - -Test.Summary = ''' -Test traffic_ctl config reload. -''' - -Test.ContinueOnFail = True - -records_config = ''' - udp: - threads: 1 - diags: - debug: - enabled: 1 - tags: rpc|config - throttling_interval_msec: 0 - ''' - -traffic_ctl = Make_traffic_ctl(Test, records_config, Any(0, 2)) -# todo: we need to get the status just in json format - -#### CONFIG STATUS - -# Config status with no token, no reloads exist, should return error. -traffic_ctl.config().status().validate_with_text(""" -`` -Message: No reload tasks found, Code: 6005 -`` -""") - -traffic_ctl.config().status().token("test1").validate_with_text(""" -`` -Message: Token 'test1' not found, Code: 6001 -`` -""") - -traffic_ctl.config().status().count("all").validate_with_text(""" -`` -Message: No reload tasks found, Code: 6005 -`` -""") - -traffic_ctl.config().status().token("test1").count("all").validate_with_text( - """ -`` -You can't use both --token and --count options together. Ignoring --count -`` -Message: Token 'test1' not found, Code: 6001 -`` -""") -##### CONFIG RELOAD - -# basic reload, no params. no existing reload in progress, we expect this to start a new reload. -traffic_ctl.config().reload().validate_with_text( - "\u2714 Reload scheduled [``]\n\n Monitor : traffic_ctl config reload -t `` -m\n Details : traffic_ctl config reload -t `` -s -l" -) - -# basic reload, but traffic_ctl should create and wait for the details, showing the newly created -# reload and some details. -traffic_ctl.config().reload().show_details().validate_contains_all("Reload scheduled", "Waiting for details", "Reload [success]") - -# Now we try with a token, this should start a new reload with the given token. -token = "testtoken_1234" -traffic_ctl.config().reload().token(token).validate_with_text( - f"\u2714 Reload scheduled [{token}]\n\n Monitor : traffic_ctl config reload -t {token} -m\n Details : traffic_ctl config reload -t {token} -s -l" -) - -# traffic_ctl config status should show the last reload, same as the above. -traffic_ctl.config().status().token(token).validate_contains_all("success", "testtoken_1234") - -# Now we try again, with same token, this should fail as the token already exists. -traffic_ctl.config().reload().token(token).validate_with_text( - f"\u2717 Token '{token}' already in use\n\n Status : traffic_ctl config status -t {token}\n Retry : traffic_ctl config reload" -) - -# Modify ip_allow.yaml and validate the reload status. - -tr = Test.AddTestRun("touch file to trigger ip_allow reload") -tr.Processes.Default.Command = f"touch {os.path.join(traffic_ctl._ts.Variables.CONFIGDIR, 'ip_allow.yaml')} && sleep 1" -tr.Processes.Default.ReturnCode = 0 - -traffic_ctl.config().reload().token("reload_ip_allow").show_details().validate_contains_all( - "reload_ip_allow", "success", "ip_allow.yaml") - -##### FORCE RELOAD - -# Force reload should work even if we just did a reload -traffic_ctl.config().reload().force().validate_contains_all("Reload scheduled") - -##### INLINE DATA RELOAD - -# Test inline data with -d flag (config not registered, expect error but no stuck task) -# Use --force to avoid "reload in progress" conflict -tr = Test.AddTestRun("Inline data reload with unregistered config") -tr.DelayStart = 5 # Wait for previous reload to complete -tr.Processes.Default.Command = f'traffic_ctl config reload --force -d "unknown_cfg: {{foo: bar}}"' -tr.Processes.Default.Env = traffic_ctl._ts.Env -tr.Processes.Default.ReturnCode = Any(0, 1, 2) -tr.StillRunningAfter = traffic_ctl._ts -tr.Processes.Default.Streams.All.Content = Testers.ContainsExpression( - r'not registered|No configs were scheduled', "Should report config not registered") - -# Verify no stuck task - new reload should work immediately after -traffic_ctl.config().reload().token("after_inline_test").validate_with_text( - "\u2714 Reload scheduled [after_inline_test]\n\n Monitor : traffic_ctl config reload -t after_inline_test -m\n Details : traffic_ctl config reload -t after_inline_test -s -l" -) - -##### MULTI-KEY FILE RELOAD - -# Create a multi-key config file -tr = Test.AddTestRun("Create multi-key config file") -multi_config_path = os.path.join(traffic_ctl._ts.Variables.CONFIGDIR, 'multi_test.yaml') -tr.Processes.Default.Command = f'''cat > {multi_config_path} << 'EOF' -# Multiple config keys in one file -config_a: - foo: bar -config_b: - baz: qux -EOF''' -tr.Processes.Default.Env = traffic_ctl._ts.Env -tr.Processes.Default.ReturnCode = 0 -tr.StillRunningAfter = traffic_ctl._ts - -# Test reload with multi-key file using data_file() -tr = Test.AddTestRun("Multi-key file reload") -tr.DelayStart = 5 # Wait for previous reload to complete -tr.Processes.Default.Command = f'traffic_ctl config reload --force --data @{multi_config_path}' -tr.Processes.Default.Env = traffic_ctl._ts.Env -tr.Processes.Default.ReturnCode = Any(0, 1, 2) -tr.StillRunningAfter = traffic_ctl._ts -tr.Processes.Default.Streams.All.Content = Testers.ContainsExpression( - r'not registered|No configs were scheduled|error', "Should process multi-key file") - -##### FORCE WITH INLINE DATA - -# Force reload with inline data -tr = Test.AddTestRun("Force reload with inline data") -tr.DelayStart = 1 -tr.Processes.Default.Command = f'traffic_ctl config reload --force --data "test_config: {{key: value}}"' -tr.Processes.Default.Env = traffic_ctl._ts.Env -tr.Processes.Default.ReturnCode = Any(0, 1, 2) -tr.StillRunningAfter = traffic_ctl._ts -tr.Processes.Default.Streams.All.Content = Testers.ContainsExpression( - r'not registered|No configs were scheduled|scheduled', "Should handle force with inline data") - -##### EXIT CODE TESTS -# Exit codes: 0 = success, 2 = error, 75 = temporary failure / in-progress (EX_TEMPFAIL from sysexits.h) - -# Test: Successful reload should return exit code 0 -traffic_ctl.config().reload().token("exit_code_ok").validate_with_exit_code(0) - -# Test: Successful reload with --monitor should return exit code 0 -traffic_ctl.config().reload().token("exit_code_monitor_ok").monitor().initial_wait(0.5).validate_with_exit_code(0) - -# Test: Token already in use should return exit code 2 (CTRL_EX_ERROR) -traffic_ctl.config().reload().token("exit_code_ok").validate_with_exit_code(2) - -# NOTE: Exit code 75 (CTRL_EX_TEMPFAIL / RELOAD_IN_PROGRESS) is not tested here -# because the reload completes almost instantly in the test environment, making it -# impossible to reliably trigger the "in progress" window with a second concurrent -# request. Testing this path would require a test plugin that artificially delays -# reload processing. The exit code 75 paths are validated by code review: -# 1. Server returns RELOAD_IN_PROGRESS when a second reload is attempted while one is active. -# 2. Ctrl+C during --monitor sets CTRL_EX_TEMPFAIL when the reload hasn't finished. diff --git a/tests/gold_tests/traffic_ctl/traffic_ctl_server_debug.test.py b/tests/gold_tests/traffic_ctl/traffic_ctl_server_debug.test.py deleted file mode 100644 index f899f980708..00000000000 --- a/tests/gold_tests/traffic_ctl/traffic_ctl_server_debug.test.py +++ /dev/null @@ -1,80 +0,0 @@ -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import sys - -# To include util classes -sys.path.insert(0, f'{Test.TestDirectory}') - -from traffic_ctl_test_utils import Make_traffic_ctl - -Test.Summary = ''' -Test traffic_ctl server debug enable/disable commands. -''' - -Test.ContinueOnFail = True - -records_yaml = ''' -diags: - debug: - enabled: 0 - tags: xyz -''' - -traffic_ctl = Make_traffic_ctl(Test, records_yaml) - -###### -# Test 1: Enable debug with tags -traffic_ctl.server().debug().enable(tags="http").exec() -# Test 2: Verify debug is enabled and tags are set -traffic_ctl.config().get("proxy.config.diags.debug.enabled").validate_with_text("proxy.config.diags.debug.enabled: 1") -# Test 3: Verify tags are set -traffic_ctl.config().get("proxy.config.diags.debug.tags").validate_with_text("proxy.config.diags.debug.tags: http") - -# Test 4: Disable debug -traffic_ctl.server().debug().disable().exec() -# Test 5: Verify debug is disabled -traffic_ctl.config().get("proxy.config.diags.debug.enabled").validate_with_text("proxy.config.diags.debug.enabled: 0") - -# Test 6: Enable debug with new tags (replace mode) -traffic_ctl.server().debug().enable(tags="cache").exec() -# Test 7: Verify tags are replaced -traffic_ctl.config().get("proxy.config.diags.debug.tags").validate_with_text("proxy.config.diags.debug.tags: cache") - -# Test 8: Enable debug with append mode - should combine with existing tags -traffic_ctl.server().debug().enable(tags="http", append=True).exec() -# Test 9: Verify tags are appended -traffic_ctl.config().get("proxy.config.diags.debug.tags").validate_with_text("proxy.config.diags.debug.tags: cache|http") - -# Test 10: Append another tag -traffic_ctl.server().debug().enable(tags="dns", append=True).exec() -# Test 11: Verify all tags are present -traffic_ctl.config().get("proxy.config.diags.debug.tags").validate_with_text("proxy.config.diags.debug.tags: cache|http|dns") - -# Test 12: Disable and verify -traffic_ctl.server().debug().disable().exec() -# Test 13: Verify debug is disabled -traffic_ctl.config().get("proxy.config.diags.debug.enabled").validate_with_text("proxy.config.diags.debug.enabled: 0") - -# Test 14: Verify --append requires --tags (should fail with error) -# This tests the ArgParser requires() functionality -tr = Test.AddTestRun("test --append without --tags") -tr.Processes.Default.Env = traffic_ctl._ts.Env -tr.Processes.Default.Command = "traffic_ctl server debug enable --append" -tr.Processes.Default.ReturnCode = 64 # EX_USAGE - command line usage error -tr.Processes.Default.Streams.All = Testers.ContainsExpression( - "Option \'--append\' requires \'--tags\' to be specified", "Should show error that --append requires --tags") -tr.StillRunningAfter = traffic_ctl._ts diff --git a/tests/gold_tests/traffic_ctl/traffic_ctl_server_output.test.py b/tests/gold_tests/traffic_ctl/traffic_ctl_server_output.test.py deleted file mode 100644 index 237448d04ca..00000000000 --- a/tests/gold_tests/traffic_ctl/traffic_ctl_server_output.test.py +++ /dev/null @@ -1,65 +0,0 @@ -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import sys -import os - -# To include util classes -sys.path.insert(0, f'{Test.TestDirectory}') - -from traffic_ctl_test_utils import Make_traffic_ctl - -Test.Summary = ''' -Test traffic_ctl different commands. -''' - -Test.ContinueOnFail = True - -records_yaml = ''' - exec_thread: - autoconfig: - enabled: 0 - limit: 4 - ''' - -Test.Summary = 'Basic test for traffic_ctl server command features.' - -traffic_ctl = Make_traffic_ctl(Test, records_yaml) -###### -# traffic_ctl server status -traffic_ctl.server().status().validate_json_contains( - initialized_done='true', is_ssl_handshaking_stopped='false', is_draining='false', is_event_system_shut_down='false') -# Drain ats so we can check the output. -traffic_ctl.server().drain().exec() - -# After the drain, server status should reflect this change. -traffic_ctl.server().status().validate_json_contains(initialized_done='true', is_draining='true') - -# Get basic and empty connection tracker info. -traffic_ctl.rpc().invoke( - handler="get_connection_tracker_info", params='"table: both"').validate_result_with_text( - '{"outbound": {"count": "0", "list": []}, "inbound": {"count": "0", "list": []}}') -# default = outbound only -traffic_ctl.rpc().invoke( - handler="get_connection_tracker_info").validate_result_with_text('{"outbound": {"count": "0", "list": []}}') -# request inbound only -traffic_ctl.rpc().invoke( - handler="get_connection_tracker_info", - params='"table: inbound"').validate_result_with_text('{"inbound": {"count": "0", "list": []}}') -# request outbound only -traffic_ctl.rpc().invoke( - handler="get_connection_tracker_info", - params='"table: outbound"').validate_result_with_text('{"outbound": {"count": "0", "list": []}}') diff --git a/tests/gold_tests/traffic_ctl/traffic_ctl_set_read_only.test.py b/tests/gold_tests/traffic_ctl/traffic_ctl_set_read_only.test.py deleted file mode 100644 index b03a548d05a..00000000000 --- a/tests/gold_tests/traffic_ctl/traffic_ctl_set_read_only.test.py +++ /dev/null @@ -1,113 +0,0 @@ -''' -Verify that the management RPC refuses to write records registered as -RECA_READ_ONLY. -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -from jsonrpc import Request, Response - -Test.Summary = ''' -Verify that records registered with RECA_READ_ONLY cannot be modified through -the management RPC backing "traffic_ctl config set" -(admin_config_set_records). -''' - -Test.ContinueOnFail = True - -# proxy.config.thread.max_heartbeat_mseconds is registered as RECA_READ_ONLY -# in src/records/RecordsConfig.cc with a default of 60. RECA_READ_ONLY = 2 -# in the RecAccessT enum (see include/records/RecDefs.h). -READ_ONLY_RECORD = "proxy.config.thread.max_heartbeat_mseconds" -DEFAULT_VALUE = "60" -ATTEMPTED_VALUE = "999" -RECA_READ_ONLY = "2" -RECT_CONFIG_BIT = "1" # bit value in the rec_types filter - -# RecordError::RECORD_READ_ONLY in src/mgmt/rpc/handlers/common/RecordsUtils.h -# (assigned as Codes::RECORD + offset). This is the per-tier code that the -# JSONRPC response must surface in error.data[*].code so programmatic -# clients can branch on the access tier without parsing the message text. -RECORD_READ_ONLY_CODE = 2009 - -ts = Test.MakeATSProcess("ts") - - -def lookup_request(): - """Build a JSONRPC request that fetches the read-only record.""" - return Request.admin_lookup_records([{"record_name": READ_ONLY_RECORD, "rec_types": [RECT_CONFIG_BIT]}]) - - -def assert_record_at_default(resp: Response): - """Validate the looked-up record is RECA_READ_ONLY and at its default value.""" - if resp.is_error(): - return (False, f"unexpected error: {resp.error_as_str()}") - - records = resp.result.get('recordList', []) - if len(records) != 1: - return (False, f"expected exactly 1 record, got {len(records)}") - - rec = records[0]['record'] - if rec.get('record_name') != READ_ONLY_RECORD: - return (False, f"record_name {rec.get('record_name')!r} != {READ_ONLY_RECORD!r}") - if rec.get('current_value') != DEFAULT_VALUE: - return (False, f"current_value {rec.get('current_value')!r} != {DEFAULT_VALUE!r}") - - access = rec.get('config_meta', {}).get('access_type') - if str(access) != RECA_READ_ONLY: - return (False, f"access_type {access!r} != {RECA_READ_ONLY!r} (record is not RECA_READ_ONLY)") - - return (True, "record is RECA_READ_ONLY and at the registered default") - - -def assert_set_was_rejected(resp: Response): - """Validate the set attempt produced the per-tier not-writable error code.""" - if not resp.is_error(): - return (False, f"set should have failed but returned a result: {resp.result!r}") - # Validate the structured error code rather than the message text so the - # test stays meaningful if the error wording is ever rephrased and so - # that any regression to the generic Codes::RECORD (2000) is caught. - if not resp.contains_nested_error(code=RECORD_READ_ONLY_CODE): - return (False, f"expected nested error code {RECORD_READ_ONLY_CODE} (RECORD_READ_ONLY); got: {resp.error_as_str()}") - return (True, f"set was refused with code {RECORD_READ_ONLY_CODE} (RECORD_READ_ONLY)") - - -# Step 0: confirm the record is registered as RECA_READ_ONLY and starts at -# its registered default value. This anchors the rest of the test against -# the registration in RecordsConfig.cc -- if someone reclassifies the record -# the test fails noisily here instead of silently exercising a permissive -# write path. -tr = Test.AddTestRun("Confirm record is RECA_READ_ONLY and at default") -tr.Processes.Default.StartBefore(ts) -tr.AddJsonRPCClientRequest(ts, lookup_request()) -tr.Processes.Default.Streams.stdout = Testers.CustomJSONRPCResponse(assert_record_at_default) - -# Step 1: attempt to write the record via the management RPC. The handler -# must reject the request with the "Record is not writable" error. -tr = Test.AddTestRun("Attempt to set the RECA_READ_ONLY record (must be refused)") -tr.AddJsonRPCClientRequest( - ts, Request.admin_config_set_records([{ - "record_name": READ_ONLY_RECORD, - "record_value": ATTEMPTED_VALUE, - }])) -tr.Processes.Default.Streams.stdout = Testers.CustomJSONRPCResponse(assert_set_was_rejected) - -# Step 2: re-look-up the record. Even if step 1's response had been -# misleading, the record must still hold its default value -- this is the -# assertion that catches the underlying bug at the storage level. -tr = Test.AddTestRun("Confirm record was not modified") -tr.AddJsonRPCClientRequest(ts, lookup_request()) -tr.Processes.Default.Streams.stdout = Testers.CustomJSONRPCResponse(assert_record_at_default) diff --git a/tests/gold_tests/tunnel/tunnel_transform.test.py b/tests/gold_tests/tunnel/tunnel_transform.test.py deleted file mode 100644 index ecb4127156c..00000000000 --- a/tests/gold_tests/tunnel/tunnel_transform.test.py +++ /dev/null @@ -1,198 +0,0 @@ -''' -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import os -import subprocess -import sys -from ports import get_port -import re - -Test.Summary = ''' -Test the reported type of HTTP transactions and tunnels -''' - -Test.SkipIf(Condition.CurlUsingUnixDomainSocket()) - -# Define default ATS. Disable the cache to simplify the test. -ts = Test.MakeATSProcess("ts", enable_cache=False, enable_tls=True) -ts.addSSLfile("../tls/ssl/server.pem") -ts.addSSLfile("../tls/ssl/server.key") - -server = Test.MakeOriginServer("server", ssl=True) -server2 = Test.MakeOriginServer("server2") - -Test.testName = "" -request_tunnel_header = {"headers": "GET / HTTP/1.1\r\nHost: tunnel-test\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -# expected response from the origin server -response_tunnel_header = { - "headers": "HTTP/1.1 200 OK\r\nConnection: close\r\nContent-Length:0\r\n\r\n", - "timestamp": "1469733493.993", - "body": "" -} - -Test.PrepareTestPlugin(os.path.join(Test.Variables.AtsTestPluginsDir, 'tunnel_transform.so'), ts) - -# add response to the server dictionary -server.addResponse("sessionfile.log", request_tunnel_header, response_tunnel_header) -ts.Disk.records_config.update( - { - 'proxy.config.diags.debug.enabled': 0, - 'proxy.config.diags.debug.tags': 'http|test', - 'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir), - 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts.Variables.SSLDir), - 'proxy.config.ssl.client.verify.server.policy': 'PERMISSIVE', - 'proxy.config.http.connect_ports': '{0}'.format(server.Variables.SSL_Port) - }) - -ts.Disk.ssl_multicert_yaml.AddLines( - """ -ssl_multicert: - - dest_ip: "*" - ssl_cert_name: server.pem - ssl_key_name: server.key -""".split("\n")) - -ts.Disk.sni_yaml.AddLines([ - 'sni:', - '- fqdn: tunnel-test', - " tunnel_route: localhost:{0}".format(server.Variables.SSL_Port), -]) -ts.addPrivateConnectAllowYaml() - -# Set up simple forwarding proxy to keep track of TLS bytes for both -# directions -tr = Test.AddTestRun("Run dumb proxy and send tunnel request.") -tr.Setup.CopyAs('dumb_proxy.py', tr.RunDirectory) -dumb_proxy = tr.Processes.Process(f'dumb-proxy') -proxy_port = get_port(dumb_proxy, "listening_port") -dumb_proxy.Command = f'{sys.executable} dumb_proxy.py --listening_port {proxy_port} --forwarding_port {ts.Variables.ssl_port}' -dumb_proxy.StartBefore(Test.Processes.ts) -dumb_proxy.Ready = When.PortOpenv4(proxy_port) -dumb_proxy.ReturnCode = 0 -# Record the log file path for later verification. -proxy_output = dumb_proxy.Streams.stdout.AbsPath - -# Add connection close to ensure that the client connection closes promptly after completing the transaction -cmd_tunnel = '-k --http1.1 -H "Connection: close" -vs --resolve "tunnel-test:{0}:127.0.0.1" https://tunnel-test:{0}/'.format( - proxy_port) - -# Send the tunnel request -tr.Processes.Default.Env = ts.Env -tr.MakeCurlCommand(cmd_tunnel, ts=ts) -tr.Processes.Default.ReturnCode = 0 -tr.TimeOut = 10 -tr.Processes.Default.StartBefore(server, ready=When.PortOpen(server.Variables.SSL_Port)) -tr.Processes.Default.StartBefore(Test.Processes.ts) -tr.Processes.Default.StartBefore(dumb_proxy) -tr.StillRunningAfter = ts -tr.StillRunningAfter = server - -# Signal that all the curl processes have completed -tr = Test.AddTestRun("Curl Done") -tr.DelayStart = 2 # Delaying a couple seconds to make sure the global continuation's lock contention resolves. -tr.Processes.Default.Command = "traffic_ctl plugin msg done done" -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Env = ts.Env -tr.StillRunningAfter = ts -tr.StillRunningAfter = server - -# Parking this as a ready tester on a meaningless process -# To stall the test runs that check for the stats until the -# stats have propagated and are ready to read. - - -def make_done_stat_ready(tsenv): - - def done_stat_ready(process, hasRunFor, **kw): - retval = subprocess.run( - "traffic_ctl metric get tunnel_transform.test.done", - shell=True, - stdout=subprocess.PIPE, - stderr=subprocess.STDOUT, - env=tsenv) - return b'1' in retval.stdout - - return done_stat_ready - - -# number of sessions/transactions opened and closed are equal -tr = Test.AddTestRun("Check type errors") -server2.StartupTimeout = 60 -# Again, here the important thing is the ready function not the server2 process -tr.Processes.Default.StartBefore(server2, ready=make_done_stat_ready(ts.Env)) -tr.Processes.Default.Command = 'traffic_ctl metric get tunnel_transform.error' -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Env = ts.Env -tr.Processes.Default.Streams.All = Testers.ContainsExpression( - 'tunnel_transform.error 0', 'incorrect statistic return, or possible error.') -tr.StillRunningAfter = ts -tr.StillRunningAfter = server - - -def get_expected_bytes(path_to_proxy_output, key): - # Construct the regex pattern. - pattern = re.compile(rf'{re.escape(key)}:\s+(\d+)') - with open(path_to_proxy_output, 'r') as file: - log_content = file.read() - bytes_transferred = 0 - match = pattern.search(log_content) - if match: - bytes_transferred = int(match.group(1)) - return bytes_transferred - - -def check_byte_count(plugin_metric_path, proxy_output_path, key): - expected_bytes = get_expected_bytes(proxy_output_path, key) - f = open(plugin_metric_path, 'r') - content = f.read() - values = content.split() - f.close() - if len(values) == 2: - val = int(values[1]) - return val == expected_bytes, "Check byte count", "Byte count does not match the expected value" - else: - return False, "Check byte count", "Unexpected metrics output format" - - -tr = Test.AddTestRun("Fetch bytes sent") -tr.Processes.Default.Command = "traffic_ctl metric get tunnel_transform.ua.bytes_sent" -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Env = ts.Env -tr.StillRunningAfter = ts -tr.StillRunningAfter = server - -path1 = tr.Processes.Default.Streams.stdout.AbsPath - -tr2 = Test.AddTestRun("Check the input bytes sent and fetch outptut bytes sent") -tr2.Processes.Default.Command = 'traffic_ctl metric get tunnel_transform.os.bytes_sent' -tr2.Processes.Default.ReturnCode = 0 -tr2.Processes.Default.Env = ts.Env -tr2.StillRunningAfter = ts -tr2.StillRunningAfter = server -tr2.Processes.Default.Streams.stdout = Testers.Lambda( - lambda info, tester: check_byte_count(path1, proxy_output, 'client-to-server')) - -path2 = tr2.Processes.Default.Streams.stdout.AbsPath - -tr = Test.AddTestRun("Check that output bytes sent") -tr.Processes.Default.Command = 'echo foo' -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Env = ts.Env -tr.StillRunningAfter = ts -tr.StillRunningAfter = server -tr.Processes.Default.Streams.stdout = Testers.Lambda(lambda info, tester: check_byte_count(path2, proxy_output, 'server-to-client')) diff --git a/tests/gold_tests/tunnel/txn_type.test.py b/tests/gold_tests/tunnel/txn_type.test.py deleted file mode 100644 index 7cc0ed6cd80..00000000000 --- a/tests/gold_tests/tunnel/txn_type.test.py +++ /dev/null @@ -1,177 +0,0 @@ -''' -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -import os -import subprocess - -Test.Summary = ''' -Test the reported type of HTTP transactions and tunnels -''' - -Test.SkipIf(Condition.CurlUsingUnixDomainSocket()) - -# Define default ATS. Disable the cache to simplify the test. -ts = Test.MakeATSProcess("ts", enable_cache=False, enable_tls=True) -ts.addSSLfile("../tls/ssl/server.pem") -ts.addSSLfile("../tls/ssl/server.key") - -server = Test.MakeOriginServer("server", ssl=True) -server2 = Test.MakeOriginServer("server2") - -Test.testName = "" -request_header = {"headers": "GET / HTTP/1.1\r\nHost: http-test\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -# expected response from the origin server -response_header = { - "headers": "HTTP/1.1 200 OK\r\nConnection: close\r\nContent-Length:0\r\n\r\n", - "timestamp": "1469733493.993", - "body": "" -} -request_tunnel_header = {"headers": "GET / HTTP/1.1\r\nHost: tunnel-test\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -# expected response from the origin server -response_tunnel_header = { - "headers": "HTTP/1.1 200 OK\r\nConnection: close\r\nContent-Length:0\r\n\r\n", - "timestamp": "1469733493.993", - "body": "" -} - -Test.PrepareTestPlugin(os.path.join(Test.Variables.AtsTestPluginsDir, 'hook_tunnel_plugin.so'), ts) - -# add response to the server dictionary -server.addResponse("sessionfile.log", request_header, response_header) -server.addResponse("sessionfile.log", request_tunnel_header, response_tunnel_header) -ts.Disk.records_config.update( - { - 'proxy.config.diags.debug.enabled': 0, - 'proxy.config.diags.debug.tags': 'http|test', - 'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir), - 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts.Variables.SSLDir), - 'proxy.config.ssl.client.verify.server.policy': 'PERMISSIVE', - 'proxy.config.http.connect_ports': '{0}'.format(server.Variables.SSL_Port) - }) - -ts.Disk.ssl_multicert_yaml.AddLines( - """ -ssl_multicert: - - dest_ip: "*" - ssl_cert_name: server.pem - ssl_key_name: server.key -""".split("\n")) - -ts.Disk.remap_config.AddLine( - 'map https://http-test:{0}/ https://127.0.0.1:{1}/'.format(ts.Variables.ssl_port, server.Variables.SSL_Port)) - -ts.Disk.sni_yaml.AddLines([ - 'sni:', - '- fqdn: tunnel-test', - " tunnel_route: localhost:{0}".format(server.Variables.SSL_Port), -]) -ts.addPrivateConnectAllowYaml(methods='[ CONNECT, GET ]') - -# Add connection close to ensure that the client connection closes promptly after completing the transaction -cmd_http = '-k --http1.1 -H "Connection: close" -vs --resolve "http-test:{0}:127.0.0.1" https://http-test:{0}/'.format( - ts.Variables.ssl_port) -cmd_tunnel = '-k --http1.1 -H "Connection: close" -vs --resolve "tunnel-test:{0}:127.0.0.1" https://tunnel-test:{0}/'.format( - ts.Variables.ssl_port) -cmd_connect = '-k --http1.1 -H "Connection: close" -vs --resolve "connect-proxy:{0}:127.0.0.1" -x http://connect-proxy:{0} --resolve "http-test:{1}:127.0.0.1" https://http-test:{1}/'.format( - ts.Variables.port, ts.Variables.ssl_port) - -# Send the http request -tr = Test.AddTestRun("send http request") -tr.Processes.Default.Env = ts.Env -tr.MakeCurlCommand(cmd_http, ts=ts) -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.StartBefore(server, ready=When.PortOpen(server.Variables.SSL_Port)) -tr.Processes.Default.StartBefore(Test.Processes.ts) -tr.StillRunningAfter = ts -tr.StillRunningAfter = server - -# Send the tunnel request -tr = Test.AddTestRun("send tunnel request") -tr.Processes.Default.Env = ts.Env -tr.MakeCurlCommand(cmd_tunnel, ts=ts) -tr.Processes.Default.ReturnCode = 0 -tr.StillRunningAfter = ts -tr.StillRunningAfter = server - -# Send the connect request -# while the connect method will set up a tunnel, it is processed in ATS as a -# transaction rather than a blind tunnel directly. Plugs can differentiate on the -# method to determine whether a connect tunnel will be set up -tr = Test.AddTestRun("send connect request") -tr.Processes.Default.Env = ts.Env -tr.MakeCurlCommand(cmd_connect, ts=ts) -tr.Processes.Default.ReturnCode = 0 -tr.StillRunningAfter = ts -tr.StillRunningAfter = server - -# Signal that all the curl processes have completed -tr = Test.AddTestRun("Curl Done") -tr.DelayStart = 2 # Delaying a couple seconds to make sure the global continuation's lock contention resolves. -tr.Processes.Default.Command = "traffic_ctl plugin msg done done" -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Env = ts.Env -tr.StillRunningAfter = ts -tr.StillRunningAfter = server - -# Parking this as a ready tester on a meaningless process -# To stall the test runs that check for the stats until the -# stats have propagated and are ready to read. - - -def make_done_stat_ready(tsenv): - - def done_stat_ready(process, hasRunFor, **kw): - retval = subprocess.run( - "traffic_ctl metric get txn_type_verify.test.done", - shell=True, - stdout=subprocess.PIPE, - stderr=subprocess.STDOUT, - env=tsenv) - return b'1' in retval.stdout - - return done_stat_ready - - -# number of sessions/transactions opened and closed are equal -tr = Test.AddTestRun("Check type errors") -server2.StartupTimeout = 60 -# Again, here the important thing is the ready function not the server2 process -tr.Processes.Default.StartBefore(server2, ready=make_done_stat_ready(ts.Env)) -tr.Processes.Default.Command = 'traffic_ctl metric get txn_type_verify.error' -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Env = ts.Env -tr.Processes.Default.Streams.All = Testers.ContainsExpression( - 'txn_type_verify.error 0', 'incorrect statistic return, or possible error.') -tr.StillRunningAfter = ts -tr.StillRunningAfter = server - -tr = Test.AddTestRun("Check for tunnel start") -tr.Processes.Default.Command = 'traffic_ctl metric get txn_type_verify.tunnel.start' -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Env = ts.Env -tr.Processes.Default.Streams.stdout = Testers.ContainsExpression("txn_type_verify.tunnel.start 1", 'Should have a tunnel start.') -tr.StillRunningAfter = ts -tr.StillRunningAfter = server - -tr = Test.AddTestRun("Check for http request") -tr.Processes.Default.Command = 'traffic_ctl metric get txn_type_verify.http.req' -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Env = ts.Env -tr.Processes.Default.Streams.stdout = Testers.ContainsExpression("txn_type_verify.http.req 2", 'Should have two http requests.') -tr.StillRunningAfter = ts -tr.StillRunningAfter = server diff --git a/tests/gold_tests/uds/uds_socket_perm.test.py b/tests/gold_tests/uds/uds_socket_perm.test.py deleted file mode 100644 index 0aebcf15907..00000000000 --- a/tests/gold_tests/uds/uds_socket_perm.test.py +++ /dev/null @@ -1,57 +0,0 @@ -'''Verify the listening UDS socket mode honors uds-perm and defaults to 0666.''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -Test.Summary = ''' -Verify proxy.config.http.server_ports honors the uds-perm option on UDS -listeners and defaults to 0666. -''' - - -def assert_socket_mode(socket_path: str, expected_octal: str) -> str: - return ( - f'python3 -c "import os, stat, sys; ' - f'sys.exit(0 if stat.S_IMODE(os.stat(sys.argv[1]).st_mode) == int(sys.argv[2], 8) else 1)" ' - f'{socket_path} {expected_octal}') - - -# -# Default UDS permission should be 0666 (no uds-perm option specified). -# -ts_default = Test.MakeATSProcess("ts_default") -ts_default.Disk.records_config.update( - { - 'proxy.config.http.server_ports': f"{ts_default.Variables.port} {ts_default.Variables.uds_path}", - }) - -tr = Test.AddTestRun("UDS default permission is 0666") -tr.Processes.Default.Command = assert_socket_mode(ts_default.Variables.uds_path, '0666') -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.StartBefore(ts_default) - -# -# uds-perm=0660 should be honored. -# -ts_custom = Test.MakeATSProcess("ts_custom") -ts_custom.Disk.records_config.update( - { - 'proxy.config.http.server_ports': f"{ts_custom.Variables.port} {ts_custom.Variables.uds_path}:uds-perm=0660", - }) - -tr = Test.AddTestRun("UDS custom permission 0660 honored") -tr.Processes.Default.Command = assert_socket_mode(ts_custom.Variables.uds_path, '0660') -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.StartBefore(ts_custom) diff --git a/tests/gold_tests/url/uri.test.py b/tests/gold_tests/url/uri.test.py deleted file mode 100644 index d315ea98cb9..00000000000 --- a/tests/gold_tests/url/uri.test.py +++ /dev/null @@ -1,35 +0,0 @@ -''' -Verify correct URI parsing behavior. -''' -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. - -Test.Summary = ''' -Verify correct URI parsing behavior. -''' - -ts = Test.MakeATSProcess("ts") -replay_file = "uri.replay.yaml" -server = Test.MakeVerifierServerProcess("server", replay_file) -ts.Disk.records_config.update({ - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'http|cache|url', -}) -ts.Disk.remap_config.AddLine('map / http://127.0.0.1:{0}'.format(server.Variables.http_port)) -tr = Test.AddTestRun("Verify correct URI parsing behavior.") -tr.Processes.Default.StartBefore(server) -tr.Processes.Default.StartBefore(ts) -tr.AddVerifierClientProcess("client", replay_file, http_ports=[ts.Variables.port]) diff --git a/tests/pyproject.toml b/tests/pyproject.toml index 3d7237dea03..b87963b1287 100644 --- a/tests/pyproject.toml +++ b/tests/pyproject.toml @@ -15,15 +15,12 @@ # limitations under the License. [project] -name = "ats-autest" +name = "ats-urtest" version = "0.1.0" -description = "AuTest dependencies for Apache Traffic Server testing" +description = "Pytest dependencies for Apache Traffic Server Uranium tests" requires-python = ">=3.11" dependencies = [ - # Keep init.cli.ext updated with this required autest version. - "autest==1.10.6", - # This should install TRLib, MicroServer, MicroDNS, Traffic-Replay. "traffic-replay", @@ -46,8 +43,8 @@ dependencies = [ "python-jose", "pyyaml", - # Pytest drives replay-based end-to-end tests. xdist gives each worker an - # independent sandbox and lets CI run those tests concurrently. + # Pytest schedules the full Uranium test inventory. xdist gives each worker + # an independent sandbox and lets CI run those tests concurrently. "pytest>=8.0", "pytest-xdist>=3.6", diff --git a/tests/pytest_tests/test_config.py b/tests/pytest_tests/test_config.py index 42d6a1fc974..2de281a142b 100644 --- a/tests/pytest_tests/test_config.py +++ b/tests/pytest_tests/test_config.py @@ -20,33 +20,40 @@ import pytest import yaml -from ats_testkit.config import ReplayConfigError, ReplaySpec, merge_flat_records, replace_server_ports +from uranium_testkit.config import ReplayConfigError, ReplaySpec, merge_flat_records, replace_server_ports def test_all_migrated_replays_are_valid() -> None: """Verify every directly collected replay has valid test metadata.""" - gold_tests = Path(__file__).parents[1] / "gold_tests" - replay_files = list(gold_tests.rglob("*.test.yaml")) - assert len(replay_files) == 96 + uranium_tests = Path(__file__).parents[1] / "uranium_tests" + replay_files = list(uranium_tests.rglob("*.test.yaml")) + assert len(replay_files) >= 96 for path in replay_files: ReplaySpec.load(path) -def test_no_gold_test_registers_an_ats_replay() -> None: +def test_no_uranium_test_registers_an_uranium_replay() -> None: """Keep replay ownership out of legacy Python wrappers.""" - gold_tests = Path(__file__).parents[1] / "gold_tests" - registrations = [path for path in gold_tests.rglob("*.test.py") if "Test.ATSReplayTest(" in path.read_text()] + uranium_tests = Path(__file__).parents[1] / "uranium_tests" + registrations = [path for path in uranium_tests.rglob("test_*.py") if "Test.ATSReplayTest(" in path.read_text()] assert registrations == [] -def test_replay_requires_autest_metadata(tmp_path: Path) -> None: +def test_all_bespoke_tests_are_available_to_pytest() -> None: + """Keep the compatibility inventory explicit while definitions migrate.""" + + uranium_tests = Path(__file__).parents[1] / "uranium_tests" + assert len(list(uranium_tests.rglob("test_*.py"))) == 505 + + +def test_replay_requires_urtest_metadata(tmp_path: Path) -> None: """Reject a Proxy Verifier file that has not opted into direct collection.""" path = tmp_path / "missing.test.yaml" path.write_text(yaml.safe_dump({"meta": {"version": "1.0"}, "sessions": []})) - with pytest.raises(ReplayConfigError, match="'autest' mapping"): + with pytest.raises(ReplayConfigError, match="'urtest' mapping"): ReplaySpec.load(path) @@ -56,7 +63,7 @@ def test_replay_accepts_summary_as_description(tmp_path: Path) -> None: path = tmp_path / "summary.test.yaml" path.write_text( yaml.safe_dump({ - "autest": { + "urtest": { "summary": "Summary-only replay", "server": {}, "client": {}, diff --git a/tests/pytest_tests/test_process.py b/tests/pytest_tests/test_process.py index 48afaebdbc6..1492723c78c 100644 --- a/tests/pytest_tests/test_process.py +++ b/tests/pytest_tests/test_process.py @@ -20,8 +20,8 @@ import pytest -from ats_testkit.process import ManagedProcess, ProcessError -from ats_testkit.replay import ReplayTest +from uranium_testkit.process import ManagedProcess, ProcessError +from uranium_testkit.replay import ReplayTest def test_managed_process_captures_output(tmp_path: Path) -> None: diff --git a/tests/pytest_tests/test_runner.py b/tests/pytest_tests/test_runner.py new file mode 100644 index 00000000000..cad42e3f88f --- /dev/null +++ b/tests/pytest_tests/test_runner.py @@ -0,0 +1,158 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. +"""Unit tests for the unified ATS test command.""" + +from pathlib import Path +import os +import subprocess + +import pytest + +from uranium_testkit.runner import ( + RunnerError, + _cmake_cache_value, + _copy_sandbox_artifacts, + _short_sandbox, + choose_docker_mode, + is_official_test_container, + launch_docker, + translate_arguments, +) + + +def test_explicit_container_mode_is_removed_from_test_arguments() -> None: + """Do not leak container-only flags into pytest.""" + + assert choose_docker_mode(["--run-in-docker", "-k", "cache"]) == (True, ["-k", "cache"]) + assert choose_docker_mode(["--no-run-in-docker", "-k", "cache"]) == (False, ["-k", "cache"]) + + +def test_conflicting_container_modes_fail() -> None: + """Reject an ambiguous explicit execution environment.""" + + with pytest.raises(RunnerError, match="conflicts"): + choose_docker_mode(["--run-in-docker", "--no-run-in-docker"]) + + +def test_official_container_requires_fedora_44_and_container_marker(tmp_path: Path) -> None: + """Do not mistake a Fedora host or a different container image for CI.""" + + os_release = tmp_path / "etc" / "os-release" + os_release.parent.mkdir() + os_release.write_text('ID="fedora"\nVERSION_ID="44"\n') + assert not is_official_test_container(tmp_path) + + (tmp_path / ".dockerenv").touch() + assert is_official_test_container(tmp_path) + + os_release.write_text('ID="fedora"\nVERSION_ID="45"\n') + assert not is_official_test_container(tmp_path) + + +def test_urtest_arguments_translate_to_pytest() -> None: + """Preserve the common established command-line spellings.""" + + translated = translate_arguments( + ["-j2", "-v", "--sandbox", "/tmp/sb", "--clean=none", "-f", "cache-*", "tls-basic"], + {}, + ) + assert translated == [ + "-n", + "2", + "--dist", + "loadgroup", + "-v", + "--sandbox", + "/tmp/sb", + "--legacy-clean", + "none", + "--urtest-filter", + "cache-*", + "--urtest-filter", + "tls-basic", + ] + + +def test_ci_sharding_replaces_old_python_only_filter_list() -> None: + """Let pytest shard its full inventory when old Jenkins passes Python names.""" + + translated = translate_arguments(["-f", "legacy-one", "legacy-two"], {"SHARD": "3", "SHARDCNT": "12"}) + assert translated == ["--urtest-shard-index", "3", "--urtest-shard-count", "12"] + + +def test_invalid_ci_shard_fails() -> None: + """Prevent a typo from silently collecting no tests.""" + + with pytest.raises(RunnerError, match="SHARD"): + translate_arguments([], {"SHARD": "12", "SHARDCNT": "12"}) + + +def test_docker_launch_forwards_ci_sharding(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None: + """Keep the stable pytest shard when the source launcher enters Docker.""" + + commands: list[list[str]] = [] + + def record_run(command: list[str], *, check: bool) -> subprocess.CompletedProcess[str]: + assert not check + commands.append(command) + return subprocess.CompletedProcess(command, 0) + + def find_executable(executable: str) -> str: + return f"/usr/bin/{executable}" + + monkeypatch.setattr("uranium_testkit.runner.shutil.which", find_executable) + monkeypatch.setattr("uranium_testkit.runner.subprocess.run", record_run) + monkeypatch.setenv("SHARD", "3") + monkeypatch.setenv("SHARDCNT", "12") + + assert launch_docker(tmp_path, ["-j2", "-q"]) == 0 + command = commands[0] + assert command[command.index("SHARD=3") - 1] == "--env" + assert command[command.index("SHARDCNT=12") - 1] == "--env" + assert command[-4:] == [str(tmp_path / "tests" / "urtest.sh"), "--no-run-in-docker", "-j2", "-q"] + + +def test_source_runner_keeps_a_short_stable_sandbox(tmp_path: Path) -> None: + """Leave enough path space for ATS and AuTest Unix sockets.""" + + assert _short_sandbox(tmp_path) == _short_sandbox(tmp_path) + assert _short_sandbox(tmp_path).parent == Path("/tmp") + assert len(str(_short_sandbox(tmp_path))) < 32 + + +def test_cmake_cache_value_detects_a_required_reconfigure(tmp_path: Path) -> None: + """Update the configured wrapper when its short sandbox changes.""" + + cache = tmp_path / "CMakeCache.txt" + cache.write_text("IGNORED:BOOL=ON\nURTEST_SANDBOX:STRING=/tmp/ats-urtest-12345678\n") + assert _cmake_cache_value(cache, "URTEST_SANDBOX") == "/tmp/ats-urtest-12345678" + assert _cmake_cache_value(cache, "MISSING") is None + + +def test_sandbox_copy_omits_container_special_files(tmp_path: Path) -> None: + """Preserve diagnostics without copying Unix sockets or runroot symlinks.""" + + source = tmp_path / "source" + destination = tmp_path / "destination" + source.mkdir() + (source / "diags.log").write_text("diagnostic") + (source / "binary").symlink_to("/container/install/traffic_server") + os.mkfifo(source / "manager.fifo") + _copy_sandbox_artifacts(source, destination) + + assert (destination / "diags.log").read_text() == "diagnostic" + assert not (destination / "binary").is_symlink() + assert not (destination / "manager.fifo").exists() diff --git a/tests/pytest_tests/test_services.py b/tests/pytest_tests/test_services.py new file mode 100644 index 00000000000..fabb50c49db --- /dev/null +++ b/tests/pytest_tests/test_services.py @@ -0,0 +1,109 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. +"""Unit tests for procedural Uranium service objects.""" + +from pathlib import Path +import subprocess +from types import SimpleNamespace +from typing import Any + +import pytest + +from uranium_testkit.services import ATS, Curl + + +class FakeProcess: + """Record ATS lifecycle calls without starting a process.""" + + def __init__(self) -> None: + self.Variables = type("Variables", (), {"port": 12345})() + self.Disk = SimpleNamespace(records_config=FakeRecords()) + self.was_started = False + self.was_stopped = False + self.was_validated = False + + def start(self) -> None: + self.was_started = True + + def stop(self) -> None: + self.was_stopped = True + + def validate(self) -> None: + self.was_validated = True + + def is_running(self) -> bool: + return self.was_started and not self.was_stopped + + +class FakeRecords: + """Record staged records.yaml updates.""" + + def __init__(self) -> None: + self.values: dict[str, Any] = {} + + def update(self, values: dict[str, Any]) -> None: + self.values.update(values) + + +class FakeScenario: + """Return a single fake ATS process.""" + + def __init__(self) -> None: + self.process = FakeProcess() + + def MakeATSProcess(self, name: str) -> FakeProcess: # noqa: N802 + assert name == "ats" + return self.process + + +def test_ats_owns_process_lifecycle() -> None: + """Start, stop, and validate the fixture-owned ATS process.""" + + scenario = FakeScenario() + ats = ATS(scenario) # type: ignore[arg-type] + + assert ats.http_port == 12345 + assert not ats.is_running + + ats.records.update({"proxy.config.http.server_ports": "12345"}) + assert scenario.process.Disk.records_config.values == {"proxy.config.http.server_ports": "12345"} + + ats.start() + assert ats.is_running + + ats.close() + assert scenario.process.was_stopped + assert scenario.process.was_validated + + +def test_curl_returns_a_plain_command_result(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None: + """Expose curl status and output for ordinary pytest assertions.""" + + observed: dict[str, Any] = {} + + def run(command: tuple[str, ...], **kwargs: Any) -> subprocess.CompletedProcess[str]: + observed["command"] = command + observed.update(kwargs) + return subprocess.CompletedProcess(command, 7, "response", "diagnostic") + + monkeypatch.setattr("uranium_testkit.services.subprocess.run", run) + + result = Curl(tmp_path).get("http://127.0.0.1:12345", headers={"X-Test": "value"}) + + assert observed["command"] == ("curl", "--header", "X-Test: value", "http://127.0.0.1:12345") + assert observed["cwd"] == tmp_path + assert result.returncode == 7 + assert result.output == "responsediagnostic" diff --git a/tests/serial_tests.txt b/tests/serial_tests.txt index 6fa665bf61d..ebb87f905b0 100644 --- a/tests/serial_tests.txt +++ b/tests/serial_tests.txt @@ -1,12 +1,14 @@ # Tests that must run serially (not in parallel) due to resource conflicts # or other constraints that prevent parallel execution. # -# Format: One test file path per line, relative to tests/gold_tests/ +# Format: One test file path per line, relative to tests/uranium_tests/ # Lines starting with # are comments and ignored. # Spins up 12 ATS instances with varying thread configs; fails under parallel load -thread_config/thread_config.test.py +thread_config/test_thread_config.py -# Each must run after tls_conn_timeout and starts 14 ATS instances at once. -next_hop/zzz_strategies_peer/zzz_strategies_peer.test.py -next_hop/zzz_strategies_peer2/zzz_strategies_peer2.test.py +# These must retain this order. The peer tests each start 14 ATS instances and +# must run after tls_conn_timeout. +timeout/test_tls_conn_timeout.py +next_hop/zzz_strategies_peer/test_zzz_strategies_peer.py +next_hop/zzz_strategies_peer2/test_zzz_strategies_peer2.py diff --git a/tests/tools/mock_origin.py b/tests/tools/mock_origin.py index f2390ed7fdc..06b7ea145b4 100644 --- a/tests/tools/mock_origin.py +++ b/tests/tools/mock_origin.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 ''' -A reusable mock origin server for ATS autests. +A reusable mock origin server for ATS Uranium tests. Replaces the various ad-hoc nc-based shell scripts (post/server1.sh, chunked_encoding/server2..4.sh, post_slow_server/server.sh) with a single @@ -145,7 +145,7 @@ def serve_one(args): def main(): parser = argparse.ArgumentParser( - description='Mock origin server for ATS autests. ' + description='Mock origin server for ATS Uranium tests. ' 'Listens on PORT, serves one HTTP transaction, then exits. ' 'Compatible with When.PortOpen() readiness probes.') diff --git a/tests/tools/plugins/CMakeLists.txt b/tests/tools/plugins/CMakeLists.txt index b7f18109ef0..ebdec561be0 100644 --- a/tests/tools/plugins/CMakeLists.txt +++ b/tests/tools/plugins/CMakeLists.txt @@ -15,33 +15,33 @@ # ####################### -add_autest_plugin(client_packet_mark client_packet_mark.cc packet_mark_common.cc) -add_autest_plugin(conf_remap_stripped conf_remap_stripped.cc) -add_autest_plugin(continuations_verify continuations_verify.cc) -add_autest_plugin(cont_schedule cont_schedule.cc) -add_autest_plugin(custom204plugin custom204plugin.cc) -add_autest_plugin(delay_txn_start delay_txn_start.cc) -add_autest_plugin(emergency_shutdown emergency_shutdown.cc) -add_autest_plugin(fatal_shutdown fatal_shutdown.cc) -add_autest_plugin(hook_add_plugin hook_add_plugin.cc) -add_autest_plugin(http_alt_info_quality http_alt_info_quality.cc) -add_autest_plugin(missing_mangled_definition missing_mangled_definition_c.c missing_mangled_definition_cpp.cc) -add_autest_plugin(missing_ts_plugin_init missing_ts_plugin_init.cc) -add_autest_plugin(server_packet_mark server_packet_mark.cc packet_mark_common.cc) -add_autest_plugin(ssl_client_verify_test ssl_client_verify_test.cc) -add_autest_plugin(ssl_hook_test ssl_hook_test.cc) -add_autest_plugin(ssl_secret_load_test ssl_secret_load_test.cc) -add_autest_plugin(ssl_verify_test ssl_verify_test.cc) -add_autest_plugin(ssntxnorder_verify ssntxnorder_verify.cc) -add_autest_plugin(test_cppapi test_cppapi.cc) +add_urtest_plugin(client_packet_mark client_packet_mark.cc packet_mark_common.cc) +add_urtest_plugin(conf_remap_stripped conf_remap_stripped.cc) +add_urtest_plugin(continuations_verify continuations_verify.cc) +add_urtest_plugin(cont_schedule cont_schedule.cc) +add_urtest_plugin(custom204plugin custom204plugin.cc) +add_urtest_plugin(delay_txn_start delay_txn_start.cc) +add_urtest_plugin(emergency_shutdown emergency_shutdown.cc) +add_urtest_plugin(fatal_shutdown fatal_shutdown.cc) +add_urtest_plugin(hook_add_plugin hook_add_plugin.cc) +add_urtest_plugin(http_alt_info_quality http_alt_info_quality.cc) +add_urtest_plugin(missing_mangled_definition missing_mangled_definition_c.c missing_mangled_definition_cpp.cc) +add_urtest_plugin(missing_ts_plugin_init missing_ts_plugin_init.cc) +add_urtest_plugin(server_packet_mark server_packet_mark.cc packet_mark_common.cc) +add_urtest_plugin(ssl_client_verify_test ssl_client_verify_test.cc) +add_urtest_plugin(ssl_hook_test ssl_hook_test.cc) +add_urtest_plugin(ssl_secret_load_test ssl_secret_load_test.cc) +add_urtest_plugin(ssl_verify_test ssl_verify_test.cc) +add_urtest_plugin(ssntxnorder_verify ssntxnorder_verify.cc) +add_urtest_plugin(test_cppapi test_cppapi.cc) target_link_libraries(test_cppapi PRIVATE ts::tscppapi) -add_autest_plugin(test_hooks test_hooks.cc) -add_autest_plugin(test_log_interface test_log_interface.cc) -add_autest_plugin(user_args user_args.cc) -add_autest_plugin(hook_tunnel_plugin hook_tunnel_plugin.cc) -add_autest_plugin(tunnel_transform tunnel_transform.cc) -add_autest_plugin(http2_close_connection http2_close_connection.cc) -add_autest_plugin(redirect_rearm redirect_rearm.cc) +add_urtest_plugin(test_hooks test_hooks.cc) +add_urtest_plugin(test_log_interface test_log_interface.cc) +add_urtest_plugin(user_args user_args.cc) +add_urtest_plugin(hook_tunnel_plugin hook_tunnel_plugin.cc) +add_urtest_plugin(tunnel_transform tunnel_transform.cc) +add_urtest_plugin(http2_close_connection http2_close_connection.cc) +add_urtest_plugin(redirect_rearm redirect_rearm.cc) target_link_libraries(continuations_verify PRIVATE OpenSSL::SSL) target_link_libraries(ssl_client_verify_test PRIVATE OpenSSL::SSL) @@ -49,6 +49,6 @@ target_link_libraries(ssl_hook_test PRIVATE OpenSSL::SSL) target_link_libraries(ssl_secret_load_test PRIVATE OpenSSL::SSL) target_link_libraries(ssl_verify_test PRIVATE OpenSSL::SSL) if(TS_USE_TLS_ASYNC) - add_autest_plugin(async_handshake async_handshake.cc) + add_urtest_plugin(async_handshake async_handshake.cc) target_link_libraries(async_handshake PRIVATE OpenSSL::SSL) endif() diff --git a/tests/tools/plugins/test_hooks.cc b/tests/tools/plugins/test_hooks.cc index e9a53abd193..5d51ee0dff0 100644 --- a/tests/tools/plugins/test_hooks.cc +++ b/tests/tools/plugins/test_hooks.cc @@ -41,7 +41,7 @@ char PIName[] = PINAME; DbgCtl dbg_ctl{PIName}; -// NOTE: It's important to flush this after writing so that a gold test using this plugin can examine the log before TS +// NOTE: It's important to flush this after writing so that a Uranium test using this plugin can examine the log before TS // terminates. // std::fstream logFile; diff --git a/tests/ats_testkit/config.py b/tests/uranium_testkit/config.py similarity index 84% rename from tests/ats_testkit/config.py rename to tests/uranium_testkit/config.py index 19fef0e25f3..ba98d430160 100644 --- a/tests/ats_testkit/config.py +++ b/tests/uranium_testkit/config.py @@ -37,14 +37,14 @@ class ReplaySpec: path: Path document: dict[str, Any] - autest: dict[str, Any] + urtest: dict[str, Any] @property def description(self) -> str: """Return the human-readable test description.""" - key = "description" if "description" in self.autest else "summary" - return str(self.autest[key]) + key = "description" if "description" in self.urtest else "summary" + return str(self.urtest[key]) @classmethod def load(cls, path: Path) -> "ReplaySpec": @@ -56,18 +56,18 @@ def load(cls, path: Path) -> "ReplaySpec": raise ReplayConfigError(f"Could not load {path}: {error}") from error if not isinstance(document, dict): raise ReplayConfigError(f"{path} must contain a YAML mapping") - autest = document.get("autest") - if not isinstance(autest, dict): - raise ReplayConfigError(f"{path} must contain an 'autest' mapping") - if "description" not in autest and "summary" not in autest: - raise ReplayConfigError(f"{path} is missing 'autest.description' or 'autest.summary'") + urtest = document.get("urtest") + if not isinstance(urtest, dict): + raise ReplayConfigError(f"{path} must contain a 'urtest' mapping") + if "description" not in urtest and "summary" not in urtest: + raise ReplayConfigError(f"{path} is missing 'urtest.description' or 'urtest.summary'") for required in ("server", "client", "ats"): - if required not in autest: - raise ReplayConfigError(f"{path} is missing 'autest.{required}'") + if required not in urtest: + raise ReplayConfigError(f"{path} is missing 'urtest.{required}'") for process in ("server", "client", "ats"): - if not isinstance(autest[process], dict): - raise ReplayConfigError(f"{path}: 'autest.{process}' must be a mapping") - return cls(path=path, document=document, autest=autest) + if not isinstance(urtest[process], dict): + raise ReplayConfigError(f"{path}: 'urtest.{process}' must be a mapping") + return cls(path=path, document=document, urtest=urtest) def merge_flat_records(records: Mapping[str, Any], defaults: Mapping[str, Any] | None = None) -> dict[str, Any]: diff --git a/tests/gold_tests/autest-site/min_cfg/ip_allow.yaml b/tests/uranium_testkit/min_cfg/ip_allow.yaml similarity index 100% rename from tests/gold_tests/autest-site/min_cfg/ip_allow.yaml rename to tests/uranium_testkit/min_cfg/ip_allow.yaml diff --git a/tests/gold_tests/autest-site/min_cfg/readme.txt b/tests/uranium_testkit/min_cfg/readme.txt similarity index 100% rename from tests/gold_tests/autest-site/min_cfg/readme.txt rename to tests/uranium_testkit/min_cfg/readme.txt diff --git a/tests/gold_tests/autest-site/min_cfg/storage.yaml b/tests/uranium_testkit/min_cfg/storage.yaml similarity index 100% rename from tests/gold_tests/autest-site/min_cfg/storage.yaml rename to tests/uranium_testkit/min_cfg/storage.yaml diff --git a/tests/uranium_testkit/plugin.py b/tests/uranium_testkit/plugin.py new file mode 100644 index 00000000000..aedb6de7934 --- /dev/null +++ b/tests/uranium_testkit/plugin.py @@ -0,0 +1,297 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. +"""Collect ``*.test.yaml`` Proxy Verifier files as pytest items.""" + +from __future__ import annotations + +from collections.abc import Callable, Iterator +from pathlib import Path +from typing import Any +import fnmatch +import os + +import pytest + +from .config import ReplayConfigError, ReplaySpec +from .process import ProcessError +from .replay import ReplaySkip, ReplayTest +from .runtime import TestRuntime +from .scenario import UraniumTest +from .services import ATS, Curl + + +def pytest_addoption(parser: pytest.Parser) -> None: + """Register ATS Uranium test runtime paths.""" + + group = parser.getgroup("ATS Uranium tests") + group.addoption("--ats-bin", help="Directory containing installed ATS executables") + group.addoption("--proxy-verifier-bin", help="Directory containing verifier-client and verifier-server") + group.addoption("--build-root", help="ATS build directory containing test plugins") + group.addoption("--sandbox", help="Directory for isolated test process trees") + group.addoption("--urtest-filter", action="append", default=[], help="Glob selecting Uranium test names") + group.addoption("--urtest-shard-index", type=int, help="Zero-based CI shard to collect") + group.addoption("--urtest-shard-count", type=int, help="Total number of CI shards") + group.addoption("--curl-uds", action="store_true", help="Run supported Uranium tests with curl Unix sockets") + + +def pytest_configure(config: pytest.Config) -> None: + """Register the replay marker used for selection.""" + + config.addinivalue_line("markers", "uranium_replay: Uranium test driven by a Proxy Verifier replay file") + config.addinivalue_line("markers", "uranium_procedural: Uranium test written as a pytest function") + config.addinivalue_line("markers", "serial: Uranium test that must run without parallel peers") + if getattr(config.option, "numprocesses", None) and getattr(config.option, "dist", None) == "load": + config.option.dist = "loadgroup" + + +def pytest_collect_file(file_path: Path, parent: pytest.Collector) -> pytest.File | None: + """Collect direct replay files; pytest collects procedural Python normally.""" + + if file_path.name.endswith((".test.yaml", ".test.yml")): + return ReplayFile.from_parent(parent, path=file_path) + return None + + +def pytest_collection_modifyitems(config: pytest.Config, items: list[pytest.Item]) -> None: + """Apply stable name filtering and zero-based CI sharding.""" + + for item in items: + if item.path.name.startswith("test_") and "uranium_tests" in item.path.parts: + item.add_marker("uranium_procedural") + if _is_serial_test(Path(item.path)): + item.add_marker("serial") + item.add_marker(pytest.mark.xdist_group("ats_serial")) + + selected = items + deselected: list[pytest.Item] = [] + patterns = config.getoption("urtest_filter") + if patterns: + selected, rejected = _partition_items(selected, lambda item: _matches_filter(item, patterns)) + deselected.extend(rejected) + + if config.getoption("curl_uds"): + unsupported = {"h2", "tls", "tls_hooks"} + selected, rejected = _partition_items( + selected, + lambda item: not _is_below_uranium_directory(item, unsupported), + ) + deselected.extend(rejected) + + shard_index = config.getoption("urtest_shard_index") + shard_count = config.getoption("urtest_shard_count") + if (shard_index is None) != (shard_count is None): + raise pytest.UsageError("--urtest-shard-index and --urtest-shard-count must be provided together") + if shard_count is not None: + if shard_count <= 0 or shard_index < 0 or shard_index >= shard_count: + raise pytest.UsageError("ATS shard values must satisfy 0 <= index < count") + shard_nodeids = { + item.nodeid + for position, item in enumerate(sorted(selected, key=lambda candidate: candidate.nodeid)) + if position % shard_count == shard_index + } + selected, rejected = _partition_items(selected, lambda item: item.nodeid in shard_nodeids) + deselected.extend(rejected) + + if deselected: + config.hook.pytest_deselected(items=deselected) + regular_items = [item for item in selected if item.get_closest_marker("serial") is None] + serial_items = sorted( + (item for item in selected if item.get_closest_marker("serial") is not None), + key=lambda item: ("tls_conn_timeout" not in item.name, item.nodeid), + ) + items[:] = regular_items + serial_items + + +class ReplayFile(pytest.File): + """Represent one directly collected replay file.""" + + def collect(self) -> Iterator[pytest.Item]: + """Load collection metadata and yield one test item.""" + + try: + spec = ReplaySpec.load(Path(self.path)) + except ReplayConfigError as error: + raise self.CollectError(str(error)) from error + suffix = ".test.yaml" if spec.path.name.endswith(".test.yaml") else ".test.yml" + item = ReplayItem.from_parent(self, name=spec.path.name.removesuffix(suffix), spec=spec) + item.add_marker("uranium_replay") + yield item + + +class ReplayItem(pytest.Item): + """Execute one replay file as a pytest test item.""" + + def __init__(self, *, spec: ReplaySpec, **kwargs: Any) -> None: + super().__init__(**kwargs) + self.spec = spec + + def runtest(self) -> None: + """Run this item's replay lifecycle.""" + + runtime = get_runtime(self.config) + with runtime.execution_lock(is_exclusive=False): + try: + ReplayTest(self.spec, runtime, self.nodeid).run() + except ReplaySkip as reason: + pytest.skip(str(reason)) + + def repr_failure(self, excinfo: pytest.ExceptionInfo[BaseException], style: str | None = None) -> str: + """Present system-test failures without pytest's internal collector frames.""" + + if isinstance(excinfo.value, (AssertionError, ProcessError, ReplayConfigError)): + return str(excinfo.value) + return super().repr_failure(excinfo, style=style) + + def reportinfo(self) -> tuple[Path, int, str]: + """Identify the replay file in pytest reports.""" + + return self.spec.path, 0, self.spec.description + + +def get_runtime(config: pytest.Config) -> TestRuntime: + """Build and cache the runtime selected by pytest command-line options.""" + + cache_name = "_uranium_test_runtime" + cached = getattr(config, cache_name, None) + if cached is not None: + return cached + + repository_root = Path(__file__).resolve().parents[2] + values = { + "ats_bin": config.getoption("ats_bin") or os.environ.get("ATS_BIN"), + "verifier_bin": config.getoption("proxy_verifier_bin") or os.environ.get("PROXY_VERIFIER_BIN"), + "build_root": config.getoption("build_root") or os.environ.get("ATS_BUILD_ROOT"), + "sandbox": config.getoption("sandbox") or os.environ.get("ATS_URTEST_SANDBOX"), + } + missing = [name for name, value in values.items() if not value] + if missing: + raise pytest.UsageError("ATS Uranium tests require " + ", ".join(f"--{name.replace('_', '-')}" for name in missing)) + worker = os.environ.get("PYTEST_XDIST_WORKER", "main") + runtime = TestRuntime.create( + repository_root=repository_root, + build_root=Path(values["build_root"]), + ats_bin=Path(values["ats_bin"]), + verifier_bin=Path(values["verifier_bin"]), + sandbox_root=Path(values["sandbox"]) / worker, + ) + setattr(config, cache_name, runtime) + return runtime + + +@pytest.fixture(scope="session") +def uranium_test_runtime(pytestconfig: pytest.Config) -> TestRuntime: + """Provide installed ATS paths and capabilities to handwritten tests.""" + + return get_runtime(pytestconfig) + + +@pytest.fixture +def uranium_replay(uranium_test_runtime: TestRuntime, request: pytest.FixtureRequest) -> Callable[[Path], None]: + """Return a helper that executes a replay file from a handwritten pytest test.""" + + def run(path: Path) -> None: + spec = ReplaySpec.load(path) + ReplayTest(spec, uranium_test_runtime, request.node.nodeid).run() + + return run + + +@pytest.fixture +def uranium_scenario(uranium_test_runtime: TestRuntime, request: pytest.FixtureRequest) -> Iterator[UraniumTest]: + """Own the sandbox and processes shared by procedural service fixtures.""" + + is_serial = request.node.get_closest_marker("serial") is not None + with uranium_test_runtime.execution_lock(is_exclusive=is_serial): + scenario = UraniumTest( + uranium_test_runtime, + request.node.nodeid, + Path(request.node.path), + curl_uds=request.config.getoption("curl_uds"), + ) + try: + yield scenario + finally: + if not scenario._executed: + scenario.cleanup() + + +@pytest.fixture +def urtest(uranium_scenario: UraniumTest) -> UraniumTest: + """Provide the transitional scenario API to tests not yet rewritten.""" + + return uranium_scenario + + +@pytest.fixture +def ats(uranium_scenario: UraniumTest) -> Iterator[ATS]: + """Provide one configured Traffic Server with fixture-owned cleanup.""" + + service = ATS(uranium_scenario) + try: + yield service + finally: + service.close() + + +@pytest.fixture +def curl(uranium_scenario: UraniumTest) -> Curl: + """Provide a curl client rooted in this test's sandbox.""" + + return Curl(Path(uranium_scenario.RunDirectory)) + + +def _partition_items(items: list[pytest.Item], predicate: Callable[[pytest.Item], + bool]) -> tuple[list[pytest.Item], list[pytest.Item]]: + """Split collected items while preserving their order.""" + + selected = [] + rejected = [] + for item in items: + (selected if predicate(item) else rejected).append(item) + return selected, rejected + + +def _matches_filter(item: pytest.Item, patterns: list[str]) -> bool: + """Match procedural basenames, direct replay names, and pytest node IDs.""" + + names = {item.name, item.path.name, item.nodeid} + names.update({name.removesuffix(suffix) for name in names for suffix in (".py", ".test.yaml", ".test.yml")}) + names.update({name.removeprefix("test_") for name in names}) + return any(fnmatch.fnmatch(name, pattern) for name in names for pattern in patterns) + + +def _is_below_uranium_directory(item: pytest.Item, directories: set[str]) -> bool: + """Return whether an item is in a curl-UDS-incompatible directory.""" + + parts = item.path.parts + try: + uranium_index = parts.index("uranium_tests") + except ValueError: + return False + return uranium_index + 1 < len(parts) and parts[uranium_index + 1] in directories + + +def _is_serial_test(test_path: Path) -> bool: + """Read the existing list of tests that require exclusive execution.""" + + tests_root = Path(__file__).resolve().parents[1] + try: + relative = test_path.resolve().relative_to(tests_root / "uranium_tests").as_posix() + except ValueError: + return False + serial_file = tests_root / "serial_tests.txt" + entries = {line.strip() for line in serial_file.read_text().splitlines() if line.strip() and not line.lstrip().startswith("#")} + return relative in entries diff --git a/tests/ats_testkit/process.py b/tests/uranium_testkit/process.py similarity index 98% rename from tests/ats_testkit/process.py rename to tests/uranium_testkit/process.py index 567c4b0754d..9a342587a18 100644 --- a/tests/ats_testkit/process.py +++ b/tests/uranium_testkit/process.py @@ -13,7 +13,7 @@ # 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. -"""Process lifecycle helpers for ATS end-to-end tests.""" +"""Process lifecycle helpers for ATS Uranium tests.""" from __future__ import annotations diff --git a/tests/ats_testkit/replay.py b/tests/uranium_testkit/replay.py similarity index 97% rename from tests/ats_testkit/replay.py rename to tests/uranium_testkit/replay.py index b32a7ab5cb3..7f8f81d2bf7 100644 --- a/tests/ats_testkit/replay.py +++ b/tests/uranium_testkit/replay.py @@ -74,7 +74,7 @@ def test_directory(self) -> Path: def run(self) -> None: """Execute the replay and validate all declared outputs.""" - reason = self.runtime.requirement_failure(self.spec.autest.get("requires", {})) + reason = self.runtime.requirement_failure(self.spec.urtest.get("requires", {})) if reason: raise ReplaySkip(reason) self.runtime.prepare_sandbox(self.sandbox) @@ -96,14 +96,14 @@ def run(self) -> None: shutil.rmtree(directory, ignore_errors=True) if client is not None: - self._validate_process_output(client, self.spec.autest["client"], "Violation|Invalid status") + self._validate_process_output(client, self.spec.urtest["client"], "Violation|Invalid status") if server is not None: - self._validate_process_output(server, self.spec.autest["server"], "Violation") + self._validate_process_output(server, self.spec.urtest["server"], "Violation") if ats is not None: self._validate_ats_logs() def _start_dns(self) -> ManagedProcess | None: - config = self.spec.autest.get("dns") + config = self.spec.urtest.get("dns") if config is None: return None self.dns_port = self.runtime.allocate_port(socket.SOCK_DGRAM) @@ -133,7 +133,7 @@ def responds() -> bool: return process def _start_verifier_server(self) -> ManagedProcess: - config = self.spec.autest["server"] + config = self.spec.urtest["server"] process_config = dict(config.get("process_config", {})) name = str(config.get("name", "server")) directory = self.sandbox / name @@ -173,7 +173,7 @@ def _start_verifier_server(self) -> ManagedProcess: return process def _start_ats(self) -> ManagedProcess: - config = self.spec.autest["ats"] + config = self.spec.urtest["ats"] process_config = dict(config.get("process_config", {})) name = str(config.get("name", "ts")) ts_root = self.sandbox / name @@ -253,12 +253,12 @@ def _start_ats(self) -> ManagedProcess: return process def _run_verifier_client(self) -> ManagedProcess: - config = self.spec.autest["client"] + config = self.spec.urtest["client"] process_config = dict(config.get("process_config", {})) name = str(config.get("name", "client")) directory = self.sandbox / name directory.mkdir() - ats_process_config = self.spec.autest["ats"].get("process_config", {}) + ats_process_config = self.spec.urtest["ats"].get("process_config", {}) enable_tls = bool(ats_process_config.get("enable_tls", False)) enable_quic = bool(ats_process_config.get("enable_quic", False)) ssl_dir = self.runtime.test_tools / "proxy-verifier" / "ssl" @@ -317,7 +317,7 @@ def _prepare_ats_tree(self, root: Path) -> dict[str, Path]: body_factory = Path(self.runtime.layout["SYSCONFDIR"]) / "body_factory" if body_factory.is_dir(): shutil.copytree(body_factory, paths["body_factory"], dirs_exist_ok=True) - min_config = self.runtime.repository_root / "tests" / "gold_tests" / "autest-site" / "min_cfg" + min_config = self.runtime.repository_root / "tests" / "uranium_testkit" / "min_cfg" for source in min_config.iterdir(): if source.is_file() and source.name != "readme.txt": shutil.copy2(source, paths["config"] / source.name) @@ -402,7 +402,7 @@ def _write_ats_configs(self, config: Mapping[str, Any], paths: dict[str, Path]) shutil.copy2(source, destination) def _check_metrics(self) -> None: - checks = self.spec.autest["ats"].get("metric_checks", []) + checks = self.spec.urtest["ats"].get("metric_checks", []) for check in checks: time.sleep(float(check.get("delay", 2))) metric = str(check["metric"]) @@ -441,7 +441,7 @@ def _validate_process_output(self, process: ManagedProcess, config: Mapping[str, self._validate_text(output, config.get("log_validation", {}), process.name) def _validate_ats_logs(self) -> None: - config = self.spec.autest["ats"] + config = self.spec.urtest["ats"] if not config.get("process_config", {}).get("disable_log_checks", False): diags = (self.ats_paths["log"] / "diags.log").read_text(errors="replace") for expression in ("ERROR:", "FATAL:", "Unrecognized configuration value"): diff --git a/tests/uranium_testkit/runner.py b/tests/uranium_testkit/runner.py new file mode 100644 index 00000000000..7c40566630d --- /dev/null +++ b/tests/uranium_testkit/runner.py @@ -0,0 +1,479 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. +"""Single entry point and container launcher for ATS Uranium tests.""" + +from __future__ import annotations + +from collections.abc import Mapping, Sequence +from dataclasses import dataclass +from pathlib import Path +import hashlib +import os +import shlex +import shutil +import stat +import subprocess +import sys + +DEFAULT_IMAGE = "ci.trafficserver.apache.org/ats/fedora:44" + + +class RunnerError(RuntimeError): + """Report invalid runner arguments or an unavailable execution environment.""" + + +@dataclass(frozen=True) +class ConfiguredBuild: + """Paths substituted into the generated build-tree wrapper.""" + + source_root: Path + build_root: Path + install_prefix: Path + verifier_bin: Path + sandbox: Path + project_directory: Path + curl_uds: bool = False + + +def main(arguments: Sequence[str] | None = None) -> int: + """Dispatch a source-tree or configured-build invocation.""" + + args = list(sys.argv[1:] if arguments is None else arguments) + try: + if not args: + raise RunnerError("The ATS test runner requires an internal source or configured mode") + entry_mode = args.pop(0) + if entry_mode == "source": + if not args: + raise RunnerError("The source runner requires the repository root") + return run_from_source(Path(args.pop(0)), args) + if entry_mode == "configured": + config, args = _parse_configured_build(args) + return run_configured(config, args) + raise RunnerError(f"Unknown ATS test runner mode: {entry_mode}") + except RunnerError as error: + print(f"urtest.sh: {error}", file=sys.stderr) + return 2 + except OSError as error: + print(f"urtest.sh: {error}", file=sys.stderr) + return 2 + except KeyboardInterrupt: + print("urtest.sh: interrupted", file=sys.stderr) + return 130 + + +def run_from_source(source_root: Path, arguments: Sequence[str]) -> int: + """Build ATS in the official environment, then invoke the configured runner.""" + + source_root = source_root.resolve() + should_use_docker, test_arguments = choose_docker_mode(arguments) + if should_use_docker: + return launch_docker(source_root, test_arguments) + + build_root = Path(os.environ.get("ATS_URTEST_CONTAINER_BUILD", source_root / "build-urtest-container")).resolve() + install_prefix = build_root / "install" + sandbox = _short_sandbox(source_root) + default_build_jobs = str(min(8, os.cpu_count() or 4)) + build_jobs = os.environ.get("ATS_URTEST_BUILD_JOBS", default_build_jobs) + configure_command = [ + "cmake", + "--preset", + "urtest", + "-B", + str(build_root), + "-DBUILD_TESTING=OFF", + f"-DCMAKE_INSTALL_PREFIX={install_prefix}", + f"-DURTEST_SANDBOX={sandbox}", + ] + commands = [] + cache_path = build_root / "CMakeCache.txt" + if _cmake_cache_value(cache_path, "URTEST_SANDBOX") != str(sandbox): + commands.append(configure_command) + commands.extend([ + ["cmake", "--build", str(build_root), "-j", build_jobs], + ["cmake", "--install", str(build_root)], + ]) + try: + for command in commands: + is_install = command[0:2] == ["cmake", "--install"] + result = subprocess.run(command, cwd=source_root, capture_output=is_install, text=is_install, check=False) + if result.returncode != 0: + if is_install: + print(result.stdout, end="") + print(result.stderr, end="", file=sys.stderr) + return result.returncode + + wrapper = build_root / "tests" / "urtest.sh" + result = subprocess.run([wrapper, "--no-run-in-docker", *test_arguments], cwd=wrapper.parent, check=False) + return result.returncode + finally: + _copy_sandbox_artifacts(sandbox, build_root / "sandbox") + _restore_container_artifact_ownership(source_root, build_root) + + +def run_configured(config: ConfiguredBuild, arguments: Sequence[str]) -> int: + """Run pytest for every Uranium test in an already configured build.""" + + should_use_docker, test_arguments = choose_docker_mode(arguments) + if should_use_docker: + return launch_docker(config.source_root, test_arguments) + + pytest_arguments = translate_arguments(test_arguments, os.environ) + if config.curl_uds and "--curl-uds" not in pytest_arguments: + pytest_arguments.append("--curl-uds") + command = [ + "uv", + "--project", + str(config.project_directory), + "run", + "pytest", + "--import-mode=importlib", + "-p", + "uranium_testkit.plugin", + str(config.source_root / "tests" / "pytest_tests"), + str(config.source_root / "tests" / "uranium_tests"), + f"--ats-bin={config.install_prefix / 'bin'}", + f"--proxy-verifier-bin={config.verifier_bin}", + f"--build-root={config.build_root}", + f"--sandbox={config.sandbox}", + "-o", + f"cache_dir={config.project_directory / '.pytest_cache'}", + *pytest_arguments, + ] + environment = os.environ.copy() + python_paths = [ + config.source_root / "tests", + config.source_root / "tests" / "uranium_tests" / "remap", + config.source_root / "tests" / "uranium_tests" / "remap_yaml", + config.source_root / "tests" / "uranium_tests" / "lib", + ] + if existing := environment.get("PYTHONPATH"): + environment["PYTHONPATH"] = os.pathsep.join([*(str(path) for path in python_paths), existing]) + else: + environment["PYTHONPATH"] = os.pathsep.join(str(path) for path in python_paths) + environment["LD_LIBRARY_PATH"] = _prepend_path(config.install_prefix / "lib", environment.get("LD_LIBRARY_PATH")) + environment["PYTHONDONTWRITEBYTECODE"] = "1" + for proxy_variable in ("HTTP_PROXY", "HTTPS_PROXY", "NO_PROXY", "http_proxy", "https_proxy", "no_proxy"): + environment[proxy_variable] = "" + + result = subprocess.run(command, cwd=config.source_root, env=environment, check=False) + return result.returncode + + +def choose_docker_mode(arguments: Sequence[str]) -> tuple[bool, list[str]]: + """Apply explicit Docker flags, then the official-container default.""" + + requested: bool | None = None + remaining = [] + for argument in arguments: + if argument == "--run-in-docker": + if requested is False: + raise RunnerError("--run-in-docker conflicts with --no-run-in-docker") + requested = True + elif argument == "--no-run-in-docker": + if requested is True: + raise RunnerError("--no-run-in-docker conflicts with --run-in-docker") + requested = False + else: + remaining.append(argument) + + if requested is not None: + return requested, remaining + return not is_official_test_container(), remaining + + +def is_official_test_container(root: Path = Path("/")) -> bool: + """Recognize Fedora 44 only when the process is also containerized.""" + + if not is_container(root): + return False + release = read_os_release(root / "etc" / "os-release") + return release.get("ID") == "fedora" and release.get("VERSION_ID") == "44" + + +def is_container(root: Path = Path("/")) -> bool: + """Detect Docker, Podman, containerd, and Kubernetes environments.""" + + if (root / ".dockerenv").exists() or (root / "run" / ".containerenv").exists(): + return True + cgroup = root / "proc" / "1" / "cgroup" + if not cgroup.is_file(): + return False + try: + content = cgroup.read_text(errors="replace").lower() + except OSError: + return False + return any(marker in content for marker in ("docker", "containerd", "kubepods", "libpod")) + + +def read_os_release(path: Path) -> dict[str, str]: + """Parse the simple key/value fields needed from ``os-release``.""" + + values = {} + try: + lines = path.read_text().splitlines() + except OSError: + return values + for line in lines: + if not line or line.lstrip().startswith("#") or "=" not in line: + continue + key, value = line.split("=", 1) + values[key] = value.strip().strip("\"'") + return values + + +def launch_docker(source_root: Path, arguments: Sequence[str]) -> int: + """Run the source entry point once in the official Fedora test image.""" + + docker = shutil.which("docker") + if docker is None: + raise RunnerError( + "Docker is required by the default execution mode. Install Docker, run inside the Fedora 44 test " + "container, or pass --no-run-in-docker to use the current environment.") + image = os.environ.get("ATS_URTEST_DOCKER_IMAGE", DEFAULT_IMAGE) + source_root = source_root.resolve() + command = [ + docker, + "run", + "--rm", + "--init", + "--cap-add=SYS_PTRACE", + "--network=host", + "--volume", + f"{source_root}:{source_root}", + "--workdir", + str(source_root), + "--env", + f"ATS_URTEST_HOST_UID={os.getuid()}", + "--env", + f"ATS_URTEST_HOST_GID={os.getgid()}", + "--env", + f"ATS_URTEST_HOST_OS={sys.platform}", + ] + for variable in ("ATS_URTEST_BUILD_JOBS", "RUN_CACHE_CONTENTION_TEST", "SHARD", "SHARDCNT"): + if variable in os.environ: + command.extend(["--env", f"{variable}={os.environ[variable]}"]) + if extra_arguments := os.environ.get("ATS_URTEST_DOCKER_ARGS"): + command.extend(shlex.split(extra_arguments)) + command.extend([ + image, + str(source_root / "tests" / "urtest.sh"), + "--no-run-in-docker", + *arguments, + ]) + result = subprocess.run(command, check=False) + return result.returncode + + +def translate_arguments(arguments: Sequence[str], environment: Mapping[str, str]) -> list[str]: + """Translate the established ``urtest.sh`` CLI into pytest options.""" + + translated = [] + filters = [] + index = 0 + while index < len(arguments): + argument = arguments[index] + if argument in ("-j", "--jobs"): + value, index = _required_value(arguments, index, argument) + translated.extend(["-n", value, "--dist", "loadgroup"]) + elif argument.startswith("-j") and argument != "-j": + translated.extend(["-n", argument[2:], "--dist", "loadgroup"]) + elif argument.startswith("--jobs="): + translated.extend(["-n", argument.split("=", 1)[1], "--dist", "loadgroup"]) + elif argument in ("-f", "--filter", "--filters"): + index += 1 + start = index + while index < len(arguments) and not arguments[index].startswith("-"): + filters.append(arguments[index]) + index += 1 + if start == index: + raise RunnerError(f"{argument} requires at least one test name") + continue + elif argument.startswith(("--filter=", "--filters=")): + filters.append(argument.split("=", 1)[1]) + elif argument in ("-C", "--clean"): + value, index = _required_value(arguments, index, argument) + translated.extend(["--legacy-clean", value]) + elif argument.startswith("--clean="): + translated.extend(["--legacy-clean", argument.split("=", 1)[1]]) + elif argument == "--list": + translated.extend(["--collect-only", "-q"]) + else: + translated.append(argument) + index += 1 + + shard_count = _nonnegative_integer(environment.get("SHARDCNT")) + shard_index = _nonnegative_integer(environment.get("SHARD")) + if shard_count is not None and shard_count > 0: + if shard_index is None or shard_index >= shard_count: + raise RunnerError("SHARD and SHARDCNT must satisfy 0 <= SHARD < SHARDCNT") + translated.extend(["--urtest-shard-index", str(shard_index), "--urtest-shard-count", str(shard_count)]) + else: + for pattern in filters: + translated.extend(["--urtest-filter", pattern]) + return translated + + +def _parse_configured_build(arguments: list[str]) -> tuple[ConfiguredBuild, list[str]]: + """Consume the fixed path arguments supplied by ``configure_file``.""" + + if "--" not in arguments: + raise RunnerError("The configured runner is missing its argument separator") + separator = arguments.index("--") + fixed = arguments[:separator] + test_arguments = arguments[separator + 1:] + values: dict[str, str] = {} + curl_uds = False + for argument in fixed: + if argument == "--configured-curl-uds": + curl_uds = True + continue + if not argument.startswith("--") or "=" not in argument: + raise RunnerError(f"Invalid configured runner argument: {argument}") + key, value = argument[2:].split("=", 1) + values[key] = value + required = {"source-root", "build-root", "install-prefix", "verifier-bin", "sandbox", "project-directory"} + if missing := sorted(required - values.keys()): + raise RunnerError("Missing configured runner values: " + ", ".join(missing)) + return ConfiguredBuild( + source_root=Path(values["source-root"]), + build_root=Path(values["build-root"]), + install_prefix=Path(values["install-prefix"]), + verifier_bin=Path(values["verifier-bin"]), + sandbox=Path(values["sandbox"]), + project_directory=Path(values["project-directory"]), + curl_uds=curl_uds, + ), test_arguments + + +def _required_value(arguments: Sequence[str], index: int, option: str) -> tuple[str, int]: + """Return the following CLI value and its index.""" + + value_index = index + 1 + if value_index >= len(arguments): + raise RunnerError(f"{option} requires a value") + return arguments[value_index], value_index + + +def _nonnegative_integer(value: str | None) -> int | None: + """Parse a nonnegative environment integer.""" + + if value is None or value == "": + return None + try: + parsed = int(value) + except ValueError as error: + raise RunnerError(f"Expected an integer, got {value!r}") from error + if parsed < 0: + raise RunnerError(f"Expected a nonnegative integer, got {value!r}") + return parsed + + +def _prepend_path(path: Path, existing: str | None) -> str: + """Prepend one filesystem path to an optional path-list value.""" + + return str(path) if not existing else os.pathsep.join((str(path), existing)) + + +def _short_sandbox(source_root: Path) -> Path: + """Choose a short, deterministic path that leaves room for Unix sockets.""" + + digest = hashlib.sha256(str(source_root.resolve()).encode()).hexdigest()[:8] + return Path("/tmp") / f"ats-urtest-{digest}" + + +def _cmake_cache_value(cache_path: Path, key: str) -> str | None: + """Read one value from a CMake cache without invoking configuration.""" + + try: + lines = cache_path.read_text().splitlines() + except OSError: + return None + prefix = f"{key}:" + for line in lines: + if line.startswith(prefix) and "=" in line: + return line.split("=", 1)[1] + return None + + +def _copy_sandbox_artifacts(source: Path, destination: Path) -> None: + """Preserve test output from container-local ``/tmp`` without its sockets.""" + + if not source.is_dir() or source.resolve() == destination.resolve(): + return + + def ignore_special_files(directory: str, names: list[str]) -> list[str]: + ignored = [] + for name in names: + try: + mode = (Path(directory) / name).lstat().st_mode + except OSError: + ignored.append(name) + continue + if not (stat.S_ISREG(mode) or stat.S_ISDIR(mode)): + ignored.append(name) + return ignored + + try: + shutil.copytree(source, destination, dirs_exist_ok=True, ignore=ignore_special_files) + except OSError as error: + print(f"urtest.sh: could not preserve sandbox artifacts: {error}", file=sys.stderr) + + +def _restore_host_ownership(root: Path) -> None: + """Avoid leaving root-owned container build artifacts in a bind mount.""" + + if os.geteuid() != 0 or not root.exists(): + return + if os.environ.get("ATS_URTEST_HOST_OS") == "darwin": + # Docker Desktop presents bind mounts as root-owned in the VM while + # preserving the macOS owner's access on the host. + return + try: + uid = int(os.environ.get("ATS_URTEST_HOST_UID", "0")) + gid = int(os.environ.get("ATS_URTEST_HOST_GID", "0")) + except ValueError: + return + if uid == 0 and gid == 0: + return + + def restore(path: Path) -> None: + try: + status = path.stat(follow_symlinks=False) + if status.st_uid != uid or status.st_gid != gid: + os.chown(path, uid, gid, follow_symlinks=False) + except OSError: + pass + + for directory, directories, files in os.walk(root): + for name in [*directories, *files]: + restore(Path(directory) / name) + restore(root) + + +def _restore_container_artifact_ownership(source_root: Path, build_root: Path) -> None: + """Return bind-mounted build and Proxy Verifier caches to the host user.""" + + _restore_host_ownership(build_root) + git_directory = source_root / ".git" + if git_directory.is_dir(): + _restore_host_ownership(git_directory / "proxy-verifier") + for verifier_directory in git_directory.glob("proxy-verifier-*"): + _restore_host_ownership(verifier_directory) + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tests/ats_testkit/runtime.py b/tests/uranium_testkit/runtime.py similarity index 82% rename from tests/ats_testkit/runtime.py rename to tests/uranium_testkit/runtime.py index a11378464a1..ca9fa3dacae 100644 --- a/tests/ats_testkit/runtime.py +++ b/tests/uranium_testkit/runtime.py @@ -17,6 +17,8 @@ from __future__ import annotations +from collections.abc import Iterator +from contextlib import contextmanager from dataclasses import dataclass from pathlib import Path from typing import Any @@ -34,7 +36,7 @@ class RuntimeError(ValueError): - """Report an invalid end-to-end test runtime.""" + """Report an invalid Uranium test runtime.""" @dataclass(frozen=True) @@ -97,10 +99,10 @@ def test_tools(self) -> Path: return self.repository_root / "tests" / "tools" @property - def build_gold_tests(self) -> Path: - """Return the build-tree mirror of gold_tests.""" + def build_uranium_tests(self) -> Path: + """Return the build-tree mirror of uranium_tests.""" - return self.build_root / "tests" / "gold_tests" + return self.build_root / "tests" / "uranium_tests" @property def test_plugins(self) -> Path: @@ -119,8 +121,8 @@ def allocate_port(self, socket_type: int = socket.SOCK_STREAM) -> int: state.seek(0) content = state.read().strip() candidate = int(content) if content else 10000 - for _ in range(50000): - candidate = 10000 if candidate >= 60000 else candidate + 1 + for _ in range(20000): + candidate = 10000 if candidate >= 30000 else candidate + 1 with socket.socket(socket.AF_INET, socket_type) as probe: try: probe.bind(("127.0.0.1", candidate)) @@ -135,11 +137,25 @@ def allocate_port(self, socket_type: int = socket.SOCK_STREAM) -> int: fcntl.flock(state, fcntl.LOCK_UN) raise RuntimeError("Could not allocate a test port") + @contextmanager + def execution_lock(self, is_exclusive: bool) -> Iterator[None]: + """Coordinate tests that must run without any parallel peers.""" + + common_sandbox = self.sandbox_root.parent + common_sandbox.mkdir(parents=True, exist_ok=True) + with (common_sandbox / ".execution-lock").open("a+") as lock: + operation = fcntl.LOCK_EX if is_exclusive else fcntl.LOCK_SH + fcntl.flock(lock, operation) + try: + yield + finally: + fcntl.flock(lock, fcntl.LOCK_UN) + def item_sandbox(self, replay_path: Path, node_name: str) -> Path: """Return a deterministic, path-length-conscious sandbox for one item.""" try: - relative = replay_path.relative_to(self.repository_root / "tests" / "gold_tests") + relative = replay_path.relative_to(self.repository_root / "tests" / "uranium_tests") except ValueError: relative = replay_path.resolve() identity = f"{relative}:{node_name}" @@ -147,6 +163,12 @@ def item_sandbox(self, replay_path: Path, node_name: str) -> Path: stem = re.sub(r"[^A-Za-z0-9_.-]+", "_", replay_path.stem)[:48] return self.sandbox_root / f"{stem}-{digest}" + def compatibility_sandbox(self, node_name: str) -> Path: + """Return an especially short sandbox for AuTest's deeper process tree.""" + + digest = hashlib.sha256(node_name.encode()).hexdigest()[:10] + return self.sandbox_root / f"l-{digest}" + def prepare_sandbox(self, path: Path) -> None: """Create an empty item sandbox without allowing a broad deletion target.""" @@ -195,7 +217,7 @@ def resolve_artifact(self, test_directory: Path, value: str) -> Path: variables = { "AtsTestPluginsDir": self.test_plugins, "ATS_TEST_PLUGINS_DIR": self.test_plugins, - "AtsBuildGoldTestsDir": self.build_gold_tests, + "AtsBuildUraniumTestsDir": self.build_uranium_tests, "RepoDir": self.repository_root, } expanded = re.sub( @@ -207,8 +229,8 @@ def resolve_artifact(self, test_directory: Path, value: str) -> Path: if candidate.is_absolute(): return candidate candidates = [test_directory / candidate] - gold_source = self.repository_root / "tests" / "gold_tests" - if test_directory.is_relative_to(gold_source): - candidates.append(self.build_gold_tests / test_directory.relative_to(gold_source) / candidate) - candidates.append(self.build_gold_tests / candidate) + uranium_source = self.repository_root / "tests" / "uranium_tests" + if test_directory.is_relative_to(uranium_source): + candidates.append(self.build_uranium_tests / test_directory.relative_to(uranium_source) / candidate) + candidates.append(self.build_uranium_tests / candidate) return next((path for path in candidates if path.exists()), candidates[-1]) diff --git a/tests/uranium_testkit/scenario.py b/tests/uranium_testkit/scenario.py new file mode 100644 index 00000000000..1887c76ac02 --- /dev/null +++ b/tests/uranium_testkit/scenario.py @@ -0,0 +1,1684 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. +"""Pytest-native orchestration for procedural ATS Uranium tests.""" + +from __future__ import annotations + +from collections.abc import Callable, Iterable, Iterator, Mapping, Sequence +from dataclasses import dataclass +from pathlib import Path +from string import Template +from typing import Any as TypingAny +import contextvars +import copy +import difflib +import json +import os +import re +import shlex +import shutil +import signal +import socket +import ssl +import subprocess +import sys +import tempfile +import time + +import pytest +import yaml + +from .config import merge_flat_records +from .runtime import TestRuntime + + +class ScenarioError(RuntimeError): + """Report a pytest-native Uranium scenario failure.""" + + +class Namespace(dict[str, TypingAny]): + """Provide dictionary and attribute access for test runtime values.""" + + def __getattr__(self, name: str) -> TypingAny: + try: + return self[name] + except KeyError as error: + raise AttributeError(name) from error + + def __setattr__(self, name: str, value: TypingAny) -> None: + self[name] = value + + +@dataclass(frozen=True) +class Choice: + """Represent a set of acceptable values or alternatives.""" + + values: tuple[TypingAny, ...] + + +def Any(*values: TypingAny) -> Choice: # noqa: N802 - retained while test sources migrate + """Accept any of @a values.""" + + return Choice(values) + + +@dataclass(frozen=True) +class Condition: + """A lazily evaluated prerequisite for a pytest test.""" + + predicate: Callable[[], bool] + reason: str + expected: bool = True + + def __call__(self) -> bool: + return bool(self.predicate()) is self.expected + + @classmethod + def true(cls, value: TypingAny) -> "Condition": + return cls(lambda: bool(value), f"Expected {value!r} to be true") + + @classmethod + def HasProgram(cls, program: str, reason: str | None = None) -> "Condition": # noqa: N802 + return cls(lambda: shutil.which(program) is not None, reason or f"{program} is required") + + @classmethod + def PluginExists(cls, plugin: str) -> "Condition": # noqa: N802 + scenario = _scenario() + name = plugin if plugin.endswith(".so") else f"{plugin}.so" + path = Path(scenario.runtime.layout["PLUGINDIR"]) / name + return cls(path.is_file, f"ATS plugin is not installed: {name}") + + @classmethod + def HasATSFeature(cls, feature: str) -> "Condition": # noqa: N802 + scenario = _scenario() + return cls(lambda: bool(scenario.runtime.features.get(feature)), f"ATS was built without {feature}") + + @classmethod + def HasCurlFeature(cls, feature: str) -> "Condition": # noqa: N802 + return cls(lambda: feature.lower() in _command_output(["curl", "--version"]).lower(), f"curl lacks {feature}") + + @classmethod + def HasCurlOption(cls, option: str) -> "Condition": # noqa: N802 + return cls(lambda: option in _command_output(["curl", "--help", "all"]), f"curl lacks {option}") + + @classmethod + def HasCurlVersion(cls, version: str) -> "Condition": # noqa: N802 + return cls(lambda: _version(_command_output(["curl", "--version"])) >= _version(version), f"curl {version} is required") + + @classmethod + def HasProxyVerifierVersion(cls, version: str) -> "Condition": # noqa: N802 + scenario = _scenario() + binary = scenario.runtime.verifier_bin / "verifier-client" + return cls( + lambda: _version(_command_output([str(binary), "--version"])) >= _version(version), + f"Proxy Verifier {version} is required") + + @classmethod + def HasOpenSSLVersion(cls, version: str) -> "Condition": # noqa: N802 + return cls(lambda: _version(_command_output(["openssl", "version"])) >= _version(version), f"OpenSSL {version} is required") + + @classmethod + def HasOpenSSLQuicClient(cls) -> "Condition": # noqa: N802 + return cls(lambda: "-quic" in _command_output(["openssl", "s_client", "-help"]), "OpenSSL CLI must support s_client -quic") + + @classmethod + def IsPlatform(cls, platform: str) -> "Condition": # noqa: N802 + return cls(lambda: sys.platform.startswith(platform.lower()), f"Test requires {platform}") + + @classmethod + def IsOpenSSL(cls) -> "Condition": # noqa: N802 + return cls(lambda: "boringssl" not in _command_output(["openssl", "version"]).lower(), "Test requires OpenSSL") + + @classmethod + def IsBoringSSL(cls) -> "Condition": # noqa: N802 + return cls(lambda: "boringssl" in _command_output(["openssl", "version"]).lower(), "Test requires BoringSSL") + + @classmethod + def CurlUsingUnixDomainSocket(cls) -> "Condition": # noqa: N802 + scenario = _scenario() + return cls(lambda: bool(scenario.Variables.get("CurlUds", False)), "Test requires curl over a Unix socket") + + @classmethod + def HasLegacyTLSSupport(cls, *_args: TypingAny, **_kwargs: TypingAny) -> "Condition": # noqa: N802 + return cls(lambda: "tlsv1" in _command_output(["openssl", "ciphers", "-v"]).lower(), "Legacy TLS is unavailable") + + @classmethod + def HasCurlTLSVersionSupport(cls, version: str) -> "Condition": # noqa: N802 + option = "--tlsv" + version.replace(".", ".") + return cls(lambda: option in _command_output(["curl", "--help", "all"]), f"curl lacks TLS {version}") + + @classmethod + def HasGoVersion(cls, version: str) -> "Condition": # noqa: N802 + return cls(lambda: _version(_command_output(["go", "version"])) >= _version(version), f"Go {version} is required") + + +def _condition_all(*conditions: TypingAny) -> Condition: + normalized = [_as_condition(condition) for condition in conditions] + return Condition(lambda: all(condition() for condition in normalized), "; ".join(condition.reason for condition in normalized)) + + +def _as_condition(value: TypingAny) -> Condition: + if isinstance(value, Condition): + return value + if isinstance(value, Choice): + normalized = [_as_condition(item) for item in value.values] + return Condition(lambda: any(item() for item in normalized), "; ".join(item.reason for item in normalized)) + if callable(value): + return Condition(value, "condition was not satisfied") + return Condition(lambda: bool(value), f"Expected {value!r} to be true") + + +class ReadyCheck: + """Poll a process-readiness predicate.""" + + def __init__(self, predicate: Callable[[], bool], description: str) -> None: + self.predicate = predicate + self.description = description + + def __call__(self) -> bool: + try: + return bool(self.predicate()) + except (OSError, ValueError): + return False + + +class When: + """Factories for process-readiness predicates.""" + + @staticmethod + def PortOpen(port: int, address: str = "127.0.0.1") -> ReadyCheck: # noqa: N802 + return ReadyCheck(lambda: _port_open(address, int(port)), f"{address}:{port} to accept connections") + + PortOpenv4 = PortOpen + + @staticmethod + def PortOpenv6(port: int, address: str = "::1") -> ReadyCheck: # noqa: N802 + return ReadyCheck(lambda: _port_open(address, int(port)), f"[{address}]:{port} to accept connections") + + @staticmethod + def PortReady(port: int, address: str = "127.0.0.1") -> ReadyCheck: # noqa: N802 + return When.PortOpen(port, address) + + @staticmethod + def FileExists(path: str | Path) -> ReadyCheck: # noqa: N802 + return ReadyCheck(Path(path).exists, f"{path} to exist") + + @staticmethod + def FileContains(path: str | Path, expression: str, desired_count: int = 1) -> ReadyCheck: # noqa: N802 + target = Path(path) + + def contains() -> bool: + if not target.is_file(): + return False + return len(re.findall(expression, target.read_text(errors="replace"), re.MULTILINE)) >= desired_count + + return ReadyCheck(contains, f"{path} to contain {expression!r} {desired_count} time(s)") + + @staticmethod + def ReloadCompleted(*_args: TypingAny, **_kwargs: TypingAny) -> ReadyCheck: # noqa: N802 + return ReadyCheck(lambda: True, "configuration reload to complete") + + +class Validator: + """Validate captured output or a declared file.""" + + def validate(self, text: str, path: Path | None, test_directory: Path) -> None: + raise NotImplementedError + + def __add__(self, other: TypingAny) -> "ValidatorGroup": + return ValidatorGroup([self]) + other + + +class ValidatorGroup(Validator): + """Apply every contained output validator.""" + + def __init__(self, validators: Iterable[TypingAny] = ()) -> None: + self.validators: list[TypingAny] = [] + for validator in validators: + self += validator + + def __iadd__(self, validator: TypingAny) -> "ValidatorGroup": + if validator is None: + return self + if isinstance(validator, ValidatorGroup): + self.validators.extend(validator.validators) + elif isinstance(validator, list): + self.validators.extend(validator) + else: + self.validators.append(validator) + return self + + def __add__(self, validator: TypingAny) -> "ValidatorGroup": + result = ValidatorGroup(self.validators) + result += validator + return result + + def validate(self, text: str, path: Path | None, test_directory: Path) -> None: + for validator in self.validators: + _validate(validator, text, path, test_directory) + + +def All(*validators: TypingAny) -> ValidatorGroup: # noqa: N802 - retained while test sources migrate + """Require all provided validators.""" + + return ValidatorGroup(validators) + + +class ExpressionValidator(Validator): + + def __init__(self, expression: str, description: str = "", *, present: bool, literal: bool = False, reflags: int = 0) -> None: + self.expression = expression + self.description = description or expression + self.present = present + self.literal = literal + self.reflags = reflags + + def validate(self, text: str, path: Path | None, test_directory: Path) -> None: + found = self.expression in text if self.literal else re.search( + self.expression, text, self.reflags | re.MULTILINE) is not None + if found is not self.present: + expectation = "contain" if self.present else "exclude" + raise AssertionError(f"Expected {path or 'process output'} to {expectation} {self.expression!r}: {self.description}") + + +class GoldValidator(Validator): + + def __init__(self, expected: str | Path) -> None: + self.expected = Path(expected) + + def validate(self, text: str, path: Path | None, test_directory: Path) -> None: + expected = self.expected if self.expected.is_absolute() else test_directory / self.expected + expected_text = expected.read_text(errors="replace") + pattern = re.escape(expected_text).replace(re.escape("``"), ".*?").replace(re.escape("{}"), ".*?") + if re.fullmatch(pattern, text, re.DOTALL) is None: + difference = "".join( + difflib.unified_diff( + expected_text.splitlines(True), text.splitlines(True), fromfile=str(expected), tofile=str(path or "output"))) + raise AssertionError(f"{path or 'process output'} did not match {expected}:\n{difference}") + + +class LambdaValidator(Validator): + + def __init__(self, function: Callable[..., TypingAny], description: str = "") -> None: + self.function = function + self.description = description or getattr(function, "__name__", "lambda validator") + + def validate(self, text: str, path: Path | None, test_directory: Path) -> None: + info = Namespace(AbsPath=str(path) if path else "", Content=text) + try: + result = self.function(info, self) + except TypeError: + result = self.function(info) + if isinstance(result, tuple): + passed, *reason = result + if not passed: + raise AssertionError(str(reason[0] if reason else self.description)) + elif result is False: + raise AssertionError(self.description) + + +class CallbackValidator(Validator): + + def __init__(self, function: Callable[[Path], TypingAny], description: str = "") -> None: + self.function = function + self.description = description or getattr(function, "__name__", "file callback") + + def validate(self, text: str, path: Path | None, test_directory: Path) -> None: + result = self.function(path) + if isinstance(result, tuple) and not result[0]: + raise AssertionError(str(result[1] if len(result) > 1 else self.description)) + if result is False: + raise AssertionError(self.description) + + +class JSONRPCValidator(Validator): + + def __init__(self, function: Callable[[TypingAny], tuple[bool, str]]) -> None: + self.function = function + + def validate(self, text: str, path: Path | None, test_directory: Path) -> None: + try: + from jsonrpc import Response + response = Response(text=text) + except (ImportError, TypeError): + response = json.loads(text) + passed, reason = self.function(response) + if not passed: + raise AssertionError(reason) + + +class Testers: + """Factories for pytest-native output assertions.""" + + @staticmethod + def ContainsExpression( + expression: str, description: str = "", reflags: int = 0, **_kwargs: TypingAny) -> Validator: # noqa: N802 + return ExpressionValidator(expression, description, present=True, reflags=reflags) + + @staticmethod + def ExcludesExpression(expression: str, description: str = "", **_kwargs: TypingAny) -> Validator: # noqa: N802 + return ExpressionValidator(expression, description, present=False) + + @staticmethod + def IncludesExpression(expression: str, description: str = "", **_kwargs: TypingAny) -> Validator: # noqa: N802 + return ExpressionValidator(expression, description, present=True, literal=True) + + @staticmethod + def GoldFile(path: str | Path, *_args: TypingAny, **_kwargs: TypingAny) -> Validator: # noqa: N802 + return GoldValidator(path) + + @staticmethod + def Lambda(function: Callable[..., TypingAny], description: str = "", **_kwargs: TypingAny) -> Validator: # noqa: N802 + return LambdaValidator(function, description) + + @staticmethod + def FileContentCallback(function: Callable[[Path], TypingAny], description: str = "") -> Validator: # noqa: N802 + return CallbackValidator(function, description) + + @staticmethod + def CustomJSONRPCResponse(function: Callable[[TypingAny], tuple[bool, str]], **_kwargs: TypingAny) -> Validator: # noqa: N802 + return JSONRPCValidator(function) + + @staticmethod + def GreaterThan(value: float, description: str = "") -> Validator: # noqa: N802 + return LambdaValidator(lambda info, _tester: (float(info.Content.strip()) > value, description)) + + CurlHeader = GoldFile + JSONRPCResponseSchemaValidator = GoldFile + + +class OutputTarget: + """A captured stream or file with deferred assertions.""" + + def __init__(self, path: Path | None = None, combined: Sequence["OutputTarget"] = ()) -> None: + self.path = path + self.combined = tuple(combined) + self.Content: TypingAny = ValidatorGroup() + + @property + def Name(self) -> str: + return str(self.path or "") + + @property + def AbsPath(self) -> str: + return self.Name + + def read(self) -> str: + if self.combined: + return "".join(target.read() for target in self.combined) + return self.path.read_text(errors="replace") if self.path and self.path.is_file() else "" + + def __iadd__(self, validator: TypingAny) -> "OutputTarget": + if not isinstance(self.Content, ValidatorGroup): + self.Content = ValidatorGroup([self.Content]) + self.Content += validator + return self + + def validate(self, test_directory: Path) -> None: + _validate(self.Content, self.read(), self.path, test_directory) + + +class Streams: + """Captured stdout and stderr targets for one process.""" + + def __init__(self, directory: Path, name: str) -> None: + self._stdout = OutputTarget(directory / f"{name}.stdout") + self._stderr = OutputTarget(directory / f"{name}.stderr") + self._all = OutputTarget(combined=(self._stdout, self._stderr)) + + @property + def stdout(self) -> OutputTarget: + return self._stdout + + @stdout.setter + def stdout(self, value: TypingAny) -> None: + if value is not self._stdout: + self._stdout.Content = value + + @property + def stderr(self) -> OutputTarget: + return self._stderr + + @stderr.setter + def stderr(self, value: TypingAny) -> None: + if value is not self._stderr: + self._stderr.Content = value + + @property + def All(self) -> OutputTarget: # noqa: N802 + return self._all + + @All.setter + def All(self, value: TypingAny) -> None: # noqa: N802 + if value is not self._all: + self._all.Content = value + + @property + def all(self) -> OutputTarget: + return self._all + + @all.setter + def all(self, value: TypingAny) -> None: + if value is not self._all: + self._all.Content = value + + +class FileNode(OutputTarget): + """A file staged before execution or asserted afterward.""" + + def __init__(self, path: Path, *, exists: bool | None = None, typename: str | None = None) -> None: + super().__init__(path) + self.Exists = exists + self.typename = typename or "" + self.content: TypingAny = None + self._lines: list[str] = [] + self._documents: list[dict[str, TypingAny]] = [{}] + self._write_callbacks: list[Callable[[str], TypingAny]] = [] + + def AddLine(self, line: TypingAny) -> "FileNode": # noqa: N802 + self._lines.append(str(line).rstrip("\n") + "\n") + return self + + def AddLines(self, lines: Iterable[TypingAny] | str) -> "FileNode": # noqa: N802 + values = lines.splitlines() if isinstance(lines, str) else lines + for line in values: + self.AddLine(line) + return self + + def WriteOn(self, content: str) -> "FileNode": # noqa: N802 + self.content = content + return self + + def WriteCustomOn(self, callback: Callable[[str], TypingAny]) -> "FileNode": # noqa: N802 + self._write_callbacks.append(callback) + return self + + def update(self, values: Mapping[str, TypingAny] | str) -> None: + parsed = yaml.safe_load(values) if isinstance(values, str) else copy.deepcopy(dict(values)) + if "records" in self.typename: + parsed = merge_flat_records(parsed)["records"] + _deep_update(self._documents[0], parsed or {}) + + def append_to_document(self, values: Mapping[str, TypingAny] | str) -> None: + parsed = yaml.safe_load(values) if isinstance(values, str) else copy.deepcopy(dict(values)) + if "records" in self.typename: + parsed = merge_flat_records(parsed)["records"] + document: dict[str, TypingAny] = {} + _deep_update(document, parsed or {}) + self._documents.append(document) + + def materialize(self) -> None: + path = Path(self.Name) + path.parent.mkdir(parents=True, exist_ok=True) + if self.content is not None: + path.write_text(str(self.content)) + elif self._lines: + path.write_text("".join(self._lines)) + elif any(self._documents): + documents: list[TypingAny] = self._documents + if "records" in self.typename: + documents = [{"records": document} for document in self._documents] + path.write_text(yaml.safe_dump_all(documents, sort_keys=False)) + for callback in self._write_callbacks: + callback(str(path)) + + def validate(self, test_directory: Path) -> None: + path = Path(self.Name) + if self.Exists is not None and path.exists() is not self.Exists: + raise AssertionError(f"Expected {path} existence to be {self.Exists}") + super().validate(test_directory) + + +class Disk: + """Registry of files associated with a scenario entity.""" + + def __init__(self, owner: "Entity") -> None: + self.owner = owner + self._files: list[FileNode] = [] + + def File( + self, + path: str | Path, + id: str | None = None, + exists: bool | None = None, # noqa: A002, N802 + typename: str | None = None, + content: TypingAny = None, + **_kwargs: TypingAny) -> FileNode: + node = FileNode(Path(path), exists=exists, typename=typename) + if content is not None: + node.Content = content + self._files.append(node) + if id: + setattr(self, id, node) + return node + + def Directory(self, path: str | Path, exists: bool | None = None, **_kwargs: TypingAny) -> FileNode: # noqa: N802 + return self.File(path, exists=exists) + + def MakeConfigFile(self, name: str) -> FileNode: # noqa: N802 + config = Path(self.owner.Variables.CONFIGDIR) / name + return self.File( + config, id=_identifier(name), typename="ats:config:yaml" if name.endswith((".yaml", ".yml")) else "ats:config") + + def materialize(self) -> None: + for node in self._files: + node.materialize() + + def validate(self, test_directory: Path) -> None: + for node in self._files: + node.validate(test_directory) + + +class Setup: + """Immediate filesystem setup helpers scoped to one pytest sandbox.""" + + def __init__(self, owner: "Entity") -> None: + self.owner = owner + self._actions: list[Callable[[], None]] = [] + self._was_applied = False + + def MakeDir(self, path: str | Path) -> None: # noqa: N802 + Path(path).mkdir(parents=True, exist_ok=True) + + def Copy(self, source: str | Path, destination: str | Path | None = None, *_args: TypingAny) -> None: # noqa: N802 + self._actions.append(lambda: self._copy(source, destination, preserve_name=True)) + + def CopyAs(self, source: str | Path, destination: str | Path | None = None, *_args: TypingAny) -> None: # noqa: N802 + self._actions.append(lambda: self._copy(source, destination, preserve_name=False)) + + def Chown(self, path: str | Path, *_args: TypingAny, **_kwargs: TypingAny) -> None: # noqa: N802 + + def chown() -> None: + try: + shutil.chown(path, user="nobody") + except (LookupError, OSError): + pass + + self._actions.append(chown) + + def Lambda(self, function: Callable[..., TypingAny] | None = None, *args: TypingAny, **kwargs: TypingAny) -> None: # noqa: N802 + callback = function or kwargs.pop("func", None) + if callback is not None: + self._actions.append(lambda: callback(*args, **kwargs)) + + def apply(self) -> None: + if self._was_applied: + return + self._was_applied = True + for action in self._actions: + action() + + def _copy(self, source: str | Path, destination: str | Path | None, *, preserve_name: bool) -> None: + source_path = self.owner.resolve_path(source) + destination_path = Path(destination) if destination is not None else Path(self.owner.RunDirectory) + if not destination_path.is_absolute(): + destination_path = Path(self.owner.RunDirectory) / destination_path + if destination_path.is_dir() or destination is None or str(destination).endswith(os.sep): + destination_path /= source_path.name + elif preserve_name and not destination_path.suffix and not destination_path.exists(): + destination_path.mkdir(parents=True, exist_ok=True) + destination_path /= source_path.name + destination_path.parent.mkdir(parents=True, exist_ok=True) + if source_path.is_dir(): + target = destination_path / source_path.name if preserve_name and destination_path.exists() and destination_path.is_dir( + ) else destination_path + shutil.copytree(source_path, target, dirs_exist_ok=True, symlinks=True) + else: + shutil.copy2(source_path, destination_path) + + +class Entity: + """Shared paths and helpers for tests, runs, and processes.""" + + def __init__(self, scenario: "UraniumTest", name: str, run_directory: Path) -> None: + self.scenario = scenario + self.Name = name + self.RunDirectory = str(run_directory) + self.TestDirectory = str(scenario.test_directory) + self.Variables = Namespace(scenario.Variables) + self.Setup = Setup(self) + self.Disk = Disk(self) + + def resolve_path(self, value: str | Path) -> Path: + text = str(value) + text = re.sub(r"\{([A-Za-z_][A-Za-z0-9_]*)\}", lambda match: str(self.Variables.get(match.group(1), match.group(0))), text) + path = Path(os.path.expandvars(text)) + if path.is_absolute(): + return path + candidates = [ + Path(self.TestDirectory) / path, + Path(self.RunDirectory) / path, self.scenario.runtime.build_uranium_tests / path + ] + return next((candidate for candidate in candidates if candidate.exists()), candidates[0]) + + +class Process(Entity): + """One subprocess configured and executed by pytest.""" + + def __init__( + self, + scenario: "UraniumTest", + name: str, + run_directory: Path, + command: str = "true", + return_code: TypingAny = 0) -> None: + directory = run_directory / name + directory.mkdir(parents=True, exist_ok=True) + super().__init__(scenario, name, directory) + self.Command = command + self.ReturnCode = return_code + self.Ready: ReadyCheck | Callable[[], bool] | None = None + self.StartupTimeout = 10.0 + self.TimeOut = 60.0 + self.Timeout = 60.0 + self.DelayStart = 0.0 + self.Env: dict[str, str] = {} + self.Streams = Streams(directory, name) + self.dependencies: list[Process] = [] + self._process: subprocess.Popen[bytes] | None = None + self._stdout: TypingAny = None + self._stderr: TypingAny = None + self._started = False + self._validated = False + + def ComposeEnv(self) -> dict[str, str]: # noqa: N802 + environment = os.environ.copy() + environment.update({key: str(value) for key, value in self.Env.items()}) + return environment + + def __add__(self, other: TypingAny) -> "ProcessGroup": + return ProcessGroup([self]) + other + + def __iadd__(self, other: TypingAny) -> "ProcessGroup": + return self + other + + def StartBefore(self, *processes: "Process", ready: ReadyCheck | Callable[[], bool] | None = None) -> "Process": # noqa: N802 + for process in processes: + if ready is not None: + process.Ready = ready + if process not in self.dependencies: + self.dependencies.append(process) + return self + + def StartAfter(self, *processes: "Process", ready: ReadyCheck | Callable[[], bool] | None = None) -> "Process": # noqa: N802 + for process in processes: + if ready is not None: + self.Ready = ready + if self not in process.dependencies: + process.dependencies.append(self) + return self + + @property + def return_code(self) -> int | None: + return self._process.poll() if self._process else None + + def is_running(self) -> bool: + return self._process is not None and self._process.poll() is None + + def start(self) -> None: + if self.is_running(): + return + for dependency in self.dependencies: + dependency.start() + if self.DelayStart: + time.sleep(float(self.DelayStart)) + self.Setup.apply() + self.Disk.materialize() + command = self.Command or "true" + self._stdout = Path(self.Streams.stdout.Name).open("wb") + self._stderr = Path(self.Streams.stderr.Name).open("wb") + self._process = subprocess.Popen( + ["/bin/bash", "-o", "pipefail", "-c", command], + cwd=self.RunDirectory, + env=self.ComposeEnv(), + stdout=self._stdout, + stderr=self._stderr, + start_new_session=True, + ) + self._started = True + if self.Ready is not None: + self._wait_ready() + + def wait(self) -> None: + if self._process is None: + raise ScenarioError(f"{self.Name} was not started") + timeout = float(self.TimeOut if self.TimeOut is not None else self.Timeout) + try: + code = self._process.wait(timeout=timeout) + except subprocess.TimeoutExpired as error: + self.stop() + raise ScenarioError(f"{self.Name} timed out after {timeout:g}s\n{self.output()}") from error + finally: + self._close_streams() + if not _return_code_matches(code, self.ReturnCode): + raise ScenarioError(f"{self.Name} exited with {code}; expected {self.ReturnCode!r}\n{self.output()}") + + def validate(self) -> None: + if self._validated: + return + self._close_streams() + self.Streams.stdout.validate(self.scenario.test_directory) + self.Streams.stderr.validate(self.scenario.test_directory) + self.Streams.All.validate(self.scenario.test_directory) + self.Disk.validate(self.scenario.test_directory) + self._validated = True + + def stop(self) -> None: + if self._process and self._process.poll() is None: + try: + os.killpg(self._process.pid, signal.SIGTERM) + self._process.wait(timeout=5) + except ProcessLookupError: + pass + except subprocess.TimeoutExpired: + os.killpg(self._process.pid, signal.SIGKILL) + self._process.wait(timeout=5) + self._close_streams() + + def output(self) -> str: + self._flush_streams() + return self.Streams.All.read() + + def _wait_ready(self) -> None: + assert self._process is not None + deadline = time.monotonic() + float(self.StartupTimeout) + description = getattr(self.Ready, "description", "readiness condition") + while time.monotonic() < deadline: + code = self._process.poll() + if code is not None: + self._close_streams() + if _return_code_matches(code, self.ReturnCode) and self.Ready(): + return + raise ScenarioError(f"{self.Name} exited with {code} while waiting for {description}\n{self.output()}") + if self.Ready(): + return + time.sleep(0.05) + raise ScenarioError(f"Timed out waiting for {self.Name}: {description}\n{self.output()}") + + def _flush_streams(self) -> None: + for stream in (self._stdout, self._stderr): + if stream is not None and not stream.closed: + stream.flush() + + def _close_streams(self) -> None: + for stream in (self._stdout, self._stderr): + if stream is not None and not stream.closed: + stream.close() + + +class ProcessGroup(list[Process]): + """Support additive process sets used by persistent-run declarations.""" + + def __add__(self, value: TypingAny) -> "ProcessGroup": + result = ProcessGroup(self) + result += value + return result + + def __iadd__(self, value: TypingAny) -> "ProcessGroup": + if isinstance(value, Process): + if value not in self: + self.append(value) + elif isinstance(value, (list, tuple, set, ProcessGroup)): + for process in value: + self += process + return self + + +class ProcessRegistry: + """Create and expose named processes for a test or run.""" + + def __init__(self, owner: "UraniumTest | TestRun", create_default: bool) -> None: + self.owner = owner + self._processes: dict[str, Process] = {} + if create_default: + self.Default = self.Process("default") + + def Process( + self, name: str, cmdstr: str | None = None, returncode: TypingAny = 0, **kwargs: TypingAny) -> Process: # noqa: N802 + if name in self._processes: + return self._processes[name] + process = Process( + self.owner.scenario if isinstance(self.owner, TestRun) else self.owner, name, Path(self.owner.RunDirectory), cmdstr or + kwargs.get("command", "true"), returncode) + self._processes[name] = process + setattr(self, name, process) + self.owner.scenario._register_process(process) + return process + + def __iter__(self) -> Iterator[Process]: + return iter(self._processes.values()) + + +class TestRun(Entity): + """A sequential pytest scenario step containing one or more processes.""" + + def __init__(self, scenario: "UraniumTest", name: str, index: int) -> None: + directory = scenario.sandbox / f"run-{index:03d}" + directory.mkdir(parents=True, exist_ok=True) + super().__init__(scenario, name, directory) + self.Processes = ProcessRegistry(self, create_default=True) + self.StillRunningAfter: TypingAny = None + self.StillRunningBefore: TypingAny = None + self.DelayStart = 0.0 + self.ContinueOnFail = False + + @property + def Command(self) -> str: + return self.Processes.Default.Command + + @Command.setter + def Command(self, value: str) -> None: + self.Processes.Default.Command = value + + @property + def ReturnCode(self) -> TypingAny: + return self.Processes.Default.ReturnCode + + @ReturnCode.setter + def ReturnCode(self, value: TypingAny) -> None: + self.Processes.Default.ReturnCode = value + + @property + def Streams(self) -> Streams: + return self.Processes.Default.Streams + + @property + def Env(self) -> dict[str, str]: + return self.Processes.Default.Env + + @Env.setter + def Env(self, value: Mapping[str, str]) -> None: + self.Processes.Default.Env = dict(value) + + @property + def TimeOut(self) -> float: + return self.Processes.Default.TimeOut + + @TimeOut.setter + def TimeOut(self, value: float) -> None: + self.Processes.Default.TimeOut = value + + def MakeCurlCommand(self, command: str, ts: Process | None = None, p: Process | None = None) -> Process: # noqa: N802 + process = p or self.Processes.Default + if self.scenario.Variables.get("CurlUds", False) and ts is not None: + process.Command = f"curl --unix-socket {shlex.quote(str(ts.Variables.uds_path))} {command}" + else: + process.Command = f"curl {command}" + return process + + def MakeCurlCommandMulti(self, command: str, ts: Process | None = None) -> Process: # noqa: N802 + curl = "curl" + if self.scenario.Variables.get("CurlUds", False) and ts is not None: + curl = f"curl --unix-socket {shlex.quote(str(ts.Variables.uds_path))}" + self.Processes.Default.Command = command.format(curl=curl, curl_base="curl") + return self.Processes.Default + + def SpawnCurlCommands( + self, + command: str, + count: int, + ts: Process | None = None, + retcode: TypingAny = 0, # noqa: N802 + use_default: bool = True) -> list[Process]: + processes = [] + for index in range(int(count) - (1 if use_default else 0)): + process = self.Processes.Process(f"curl-{index}", returncode=retcode) + self.MakeCurlCommand(command, ts, process) + processes.append(process) + if use_default: + self.MakeCurlCommand(command, ts) + self.Processes.Default.ReturnCode = retcode + self.Processes.Default.StartBefore(*processes) + return processes + + SpawnCommands = SpawnCurlCommands + + def MakeATSProcess(self, name: str, **kwargs: TypingAny) -> Process: # noqa: N802 + return self.scenario._make_ats_process(self.Processes, self, name, **kwargs) + + def MakeDNServer(self, name: str, **kwargs: TypingAny) -> Process: # noqa: N802 + return self.scenario._make_dns(self.Processes, self, name, **kwargs) + + MakeDNS = MakeDNServer + + def MakeHttpBinServer(self, name: str, **kwargs: TypingAny) -> Process: # noqa: N802 + return self.scenario._make_httpbin(self.Processes, self, name, **kwargs) + + def AddVerifierServerProcess(self, name: str, replay_path: str | Path, **kwargs: TypingAny) -> Process: # noqa: N802 + server = self.scenario._make_verifier_server(self.Processes, self, name, replay_path, **kwargs) + self.Processes.Default.StartBefore(server) + return server + + def AddVerifierClientProcess(self, name: str, replay_path: str | Path, **kwargs: TypingAny) -> Process: # noqa: N802 + return self.scenario._configure_verifier_client(self.Processes.Default, self, name, replay_path, **kwargs) + + def AddJsonRPCClientRequest( + self, + ts: Process, + request: TypingAny = "", + file: str | None = None, + **_kwargs: TypingAny) -> Process: # noqa: A002, N802 + process = self.Processes.Default + request_path = Path(file) if file else Path(process.RunDirectory) / f"request-{self.scenario._request_counter}.json" + self.scenario._request_counter += 1 + if file is None: + request_path.write_text(str(request)) + process.Command = f"{shlex.quote(str(ts.Variables.BINDIR))}/traffic_ctl rpc file {shlex.quote(str(request_path))} " \ + f"--run-root {shlex.quote(ts.Disk.runroot_yaml.Name)} --format json" + process.ReturnCode = 0 + process.Env = dict(ts.Env) + return process + + def AddJsonRPCShowRegisterHandlerRequest(self, ts: Process) -> Process: # noqa: N802 + from jsonrpc import Request + return self.AddJsonRPCClientRequest(ts, Request.show_registered_handlers()) + + +_active_scenario: contextvars.ContextVar[UraniumTest | None] = contextvars.ContextVar("active_uranium_scenario", default=None) + + +class UraniumTest(Entity): + """Build and execute one procedural Uranium test inside pytest.""" + + def __init__(self, runtime: TestRuntime, nodeid: str, test_path: Path, *, curl_uds: bool = False) -> None: + self.runtime = runtime + self.test_path = test_path + self.test_directory = test_path.parent + self.sandbox = runtime.item_sandbox(test_path, nodeid) + runtime.prepare_sandbox(self.sandbox) + self.scenario = self + self.Variables = Namespace( + RepoDir=str(runtime.repository_root), + BINDIR=runtime.layout["BINDIR"], + PREFIX=runtime.layout["PREFIX"], + SYSCONFDIR=runtime.layout["SYSCONFDIR"], + PLUGINDIR=runtime.layout["PLUGINDIR"], + AtsTestToolsDir=str(runtime.test_tools), + AtsTestPluginsDir=str(runtime.test_plugins), + AtsBuildUraniumTestsDir=str(runtime.build_uranium_tests), + CurlUds=curl_uds, + Autest=Namespace(Process=Namespace(TimeOut=60)), + ) + self.Name = test_path.stem + self.RunDirectory = str(self.sandbox) + self.TestDirectory = str(self.test_directory) + self.Setup = Setup(self) + self.Disk = Disk(self) + self.Processes = ProcessRegistry(self, create_default=False) + self.Summary = "" + self.ContinueOnFail = False + self._runs: list[TestRun] = [] + self._all_processes: list[Process] = [] + self._executed = False + self._request_counter = 0 + self._context_token = _active_scenario.set(self) + + def close_context(self) -> None: + try: + _active_scenario.reset(self._context_token) + except ValueError: + pass + + def AddTestRun(self, name: str = "") -> TestRun: # noqa: N802 + run = TestRun(self, name or f"run {len(self._runs) + 1}", len(self._runs)) + self._runs.append(run) + return run + + def MakeATSProcess(self, name: str, **kwargs: TypingAny) -> Process: # noqa: N802 + return self._make_ats_process(self.Processes, self, name, **kwargs) + + def MakeOriginServer(self, name: str, **kwargs: TypingAny) -> Process: # noqa: N802 + return self._make_origin(self.Processes, self, name, **kwargs) + + MakeOrigin = MakeOriginServer + + def MakeDNServer(self, name: str, **kwargs: TypingAny) -> Process: # noqa: N802 + return self._make_dns(self.Processes, self, name, **kwargs) + + MakeDNS = MakeDNServer + + def MakeHttpBinServer(self, name: str, **kwargs: TypingAny) -> Process: # noqa: N802 + return self._make_httpbin(self.Processes, self, name, **kwargs) + + def MakeVerifierServerProcess(self, name: str, replay_path: str | Path, **kwargs: TypingAny) -> Process: # noqa: N802 + return self._make_verifier_server(self.Processes, self, name, replay_path, **kwargs) + + def MakeCurlCommand(self, command: str, ts: Process | None = None, p: Process | None = None) -> Process: # noqa: N802 + if not self._runs: + self.AddTestRun("curl") + return self._runs[-1].MakeCurlCommand(command, ts, p) + + def MakeCurlCommandMulti(self, command: str, ts: Process | None = None) -> Process: # noqa: N802 + if not self._runs: + self.AddTestRun("curl") + return self._runs[-1].MakeCurlCommandMulti(command, ts) + + def GetTcpPort(self, name: str | None = None) -> int: # noqa: N802 + port = self.runtime.allocate_port() + if name is not None: + self.Variables[name] = port + return port + + def SkipUnless(self, *conditions: TypingAny) -> None: # noqa: N802 + condition = _condition_all(*conditions) + if not condition(): + pytest.skip(condition.reason) + + def SkipIf(self, *conditions: TypingAny) -> None: # noqa: N802 + normalized = [_as_condition(condition) for condition in conditions] + for condition in normalized: + if condition(): + pytest.skip(condition.reason) + + def PrepareTestPlugin(self, path: str | Path, ts: Process, plugin_args: str = "") -> None: # noqa: N802 + source = self.resolve_path(path) + ts.Setup.Copy(source, ts.Env["PROXY_CONFIG_PLUGIN_PLUGIN_DIR"]) + ts.Disk.plugin_config.AddLine(f"{source.name} {plugin_args}".rstrip()) + + def PrepareInstalledPlugin(self, name: str, ts: Process, plugin_args: str = "") -> None: # noqa: N802 + ts.Disk.plugin_config.AddLine(f"{name} {plugin_args}".rstrip()) + + def AddAwaitFileContainsTestRun( + self, name: str, path: str | Path, expression: str, desired_count: int = 1) -> TestRun: # noqa: N802 + run = self.AddTestRun(name) + waiter = run.Processes.Process("await", "sleep 60") + waiter.Ready = When.FileContains(path, expression, desired_count) + waiter.StartupTimeout = 30 + run.Processes.Default.StartBefore(waiter) + return run + + def AddConfigReload( + self, + ts: Process, + expect: str = "success", + token: str | None = None, + data: str | None = None, # noqa: N802 + force: bool = False, + timeout: str | None = "30s", + initial_wait: float = 1.0, + refresh_int: float = 0.5, + delay_start: float | None = None, + description: str | None = None, + **_kwargs: TypingAny) -> TestRun: + token = token or f"urtest-reload-{len(self._runs)}" + run = self.AddTestRun(description or f"Reload config [{token}]") + run.Processes.Default.Env = dict(ts.Env) + command = f"traffic_ctl config reload -m -t {shlex.quote(token)} -w {initial_wait} -r {refresh_int}" + if timeout is not None: + command += f" -T {shlex.quote(timeout)}" + if force: + command += " --force" + if data is not None: + command += f" --data {shlex.quote(data)}" + run.Command = command + run.ReturnCode = Choice((0, 2)) if expect == "any" else {"success": 0, "fail": 2, "timeout": 75}[expect] + run.StillRunningAfter = ts + if delay_start is not None: + run.DelayStart = delay_start + return run + + def execute(self) -> None: + """Execute every configured step, then validate and clean up processes.""" + + if self._executed: + raise ScenarioError("A Uranium scenario can only execute once") + self._executed = True + failures: list[BaseException] = [] + try: + self.Setup.apply() + for run in self._runs: + try: + self._execute_run(run) + except BaseException as error: + failures.append(error) + if not (run.ContinueOnFail or self.ContinueOnFail): + break + finally: + for process in reversed(self._all_processes): + process.stop() + for process in self._all_processes: + if process._started: + try: + process.validate() + except BaseException as error: + failures.append(error) + try: + self.Disk.validate(self.test_directory) + except BaseException as error: + failures.append(error) + self.close_context() + if failures: + messages = [f"{type(error).__name__}: {error}" for error in failures] + raise ScenarioError("\n\n".join(messages)) + + def cleanup(self) -> None: + for process in reversed(self._all_processes): + process.stop() + self.close_context() + + def _execute_run(self, run: TestRun) -> None: + if run.DelayStart: + time.sleep(float(run.DelayStart)) + run.Setup.apply() + for process in _process_values(run.StillRunningBefore): + if not process.is_running(): + raise ScenarioError(f"{process.Name} was expected to be running before {run.Name}") + processes = list(run.Processes) + for process in processes: + process.start() + keep = set(_process_values(run.StillRunningAfter)) + for process in processes: + if process in keep or process.is_running() and (process.Ready is not None or + None in _return_code_values(process.ReturnCode)): + continue + process.wait() + for process in processes: + if process not in keep and process.is_running(): + process.stop() + if process._started and not process.is_running(): + process.validate() + for process in keep: + if not process.is_running(): + raise ScenarioError(f"{process.Name} was expected to remain running after {run.Name}") + + def _register_process(self, process: Process) -> None: + if process not in self._all_processes: + self._all_processes.append(process) + + def _make_ats_process( + self, + registry: ProcessRegistry, + owner: Entity, + name: str, + command: str = "traffic_server", + select_ports: bool = True, + enable_tls: bool = False, + enable_cache: bool = True, + enable_quic: bool = False, + enable_uds: bool = True, + enable_cripts: bool = False, + block_for_debug: bool = False, + use_traffic_out: bool = True, + disable_log_checks: bool = False, + enable_proxy_protocol: bool = False, + enable_proxy_protocol_cp_src: bool = False, + **_kwargs: TypingAny) -> Process: + process = registry.Process(name) + root = Path(process.RunDirectory) + paths = {key: root / key for key in ("bin", "config", "plugin", "log", "runtime", "ssl", "storage", "cache")} + for path in paths.values(): + path.mkdir(parents=True, exist_ok=True) + for path in (paths["log"], paths["runtime"], paths["ssl"], paths["storage"], paths["cache"]): + path.chmod(0o777) + body_factory = paths["config"] / "body_factory" + body_factory.mkdir(exist_ok=True) + _link_directory(Path(self.runtime.layout["BINDIR"]), paths["bin"]) + _link_directory(Path(self.runtime.layout["PLUGINDIR"]), paths["plugin"]) + installed_body_factory = Path(self.runtime.layout["SYSCONFDIR"]) / "body_factory" + if installed_body_factory.is_dir(): + shutil.copytree(installed_body_factory, body_factory, dirs_exist_ok=True) + min_config = self.runtime.repository_root / "tests" / "uranium_testkit" / "min_cfg" + if min_config.is_dir(): + for source in min_config.iterdir(): + if source.is_file() and source.name != "readme.txt": + shutil.copy2(source, paths["config"] / source.name) + process.Variables.update( + CONFIGDIR=str(paths["config"]), + BODY_FACTORY_TEMPLATE_DIR=str(body_factory), + CACHEDIR=str(paths["cache"]), + LOGDIR=str(paths["log"]), + RUNTIMEDIR=str(paths["runtime"]), + LOCALSTATEDIR=str(paths["runtime"]), + SSLDir=str(paths["ssl"]), + STORAGEDIR=str(paths["storage"]), + BINDIR=str(paths["bin"]), + ) + process.Env.update( + TS_ROOT=str(root), + TS_RUNROOT=str(paths["config"] / "runroot.yaml"), + PROXY_CONFIG_BIN_PATH=str(paths["bin"]), + PROXY_CONFIG_CONFIG_DIR=str(paths["config"]), + PROXY_CONFIG_BODY_FACTORY_TEMPLATE_SETS_DIR=str(body_factory), + PROXY_CONFIG_CACHE_DIR=str(paths["cache"]), + PROXY_CONFIG_PLUGIN_PLUGIN_DIR=str(paths["plugin"]), + PROXY_CONFIG_LOG_LOGFILE_DIR=str(paths["log"]), + PROXY_CONFIG_LOCAL_STATE_DIR=str(paths["runtime"]), + PROXY_CONFIG_SSL_DIR=str(paths["ssl"]), + PROXY_CONFIG_STORAGE_DIR=str(paths["storage"]), + PATH=str(paths["bin"]) + os.pathsep + os.environ.get("PATH", ""), + ) + files = { + "records_config": ("records.yaml", "ats:config:records"), + "cache_config": ("cache.config", "ats:config"), + "hosting_config": ("hosting.config", "ats:config"), + "ip_allow_yaml": ("ip_allow.yaml", "ats:config:yaml"), + "logging_yaml": ("logging.yaml", "ats:config:yaml"), + "parent_config": ("parent.config", "ats:config"), + "plugin_config": ("plugin.config", "ats:config"), + "remap_config": ("remap.config", "ats:config"), + "remap_yaml": ("remap.yaml", "ats:config:yaml"), + "splitdns_config": ("splitdns.config", "ats:config"), + "ssl_multicert_yaml": ("ssl_multicert.yaml", "ats:config:yaml"), + "sni_yaml": ("sni.yaml", "ats:config:yaml"), + "storage_yaml": ("storage.yaml", "ats:config:yaml"), + "runroot_yaml": ("runroot.yaml", "ats:config:yaml"), + } + for identifier, (filename, typename) in files.items(): + process.Disk.File(paths["config"] / filename, id=identifier, typename=typename) + for identifier, filename in (("traffic_out", "traffic.out"), ("diags_log", "diags.log"), ("error_log", "error.log"), + ("squid_log", "squid.log")): + process.Disk.File(paths["log"] / filename, id=identifier) + process.Disk.runroot_yaml.AddLines( + [ + f"runtimedir: {paths['runtime']}", + f"cachedir: {paths['cache']}", + f"localstatedir: {paths['runtime']}", + f"bindir: {paths['bin']}", + f"prefix: {root}", + f"logdir: {paths['log']}", + f"sysconfdir: {paths['config']}", + ]) + if select_ports: + process.Variables.port = self.runtime.allocate_port() + process.Variables.portv6 = self.runtime.allocate_port() + if enable_tls or enable_quic: + process.Variables.ssl_port = self.runtime.allocate_port() + process.Variables.ssl_portv6 = self.runtime.allocate_port() + else: + process.Variables.port = 8080 + process.Variables.portv6 = 8080 + if enable_tls or enable_quic: + process.Variables.ssl_port = 4443 + process.Variables.ssl_portv6 = 4444 + process.Variables.manager_port = self.runtime.allocate_port() + process.Variables.admin_port = self.runtime.allocate_port() + process.Variables.uds_path = str(paths["runtime"] / "uds.socket") + ports = f"{process.Variables.port} {process.Variables.portv6}:ipv6" + if enable_tls: + ports += f" {process.Variables.ssl_port}:ssl {process.Variables.ssl_portv6}:ssl:ipv6" + if enable_quic: + ports += f" {process.Variables.ssl_port}:quic {process.Variables.ssl_portv6}:quic:ipv6" + if enable_uds: + ports += f" {process.Variables.uds_path}" + process.Disk.records_config.update( + { + "proxy.config.config_update_interval_ms": 20, + "proxy.config.http.server_ports": ports, + "proxy.config.http.wait_for_cache": 1 if enable_cache else 0, + }) + if not enable_cache: + process.Disk.records_config.update({"proxy.config.http.cache.http": 0}) + if enable_quic: + process.Disk.records_config.update({"proxy.config.udp.threads": 1}) + if enable_cripts: + compiler = paths["bin"] / "cripts_compiler.sh" + shutil.copy2(self.runtime.repository_root / "tools" / "cripts" / "compiler.sh", compiler) + compiler.chmod(0o755) + process.Variables.cripts_compiler = str(compiler) + process.Env["ATS_ROOT"] = self.runtime.layout["PREFIX"] + process.Disk.records_config.update({"proxy.config.plugin.compiler_path": str(compiler)}) + process.StartupTimeout = 60 + process.Env.update( + PROXY_CONFIG_PROCESS_MANAGER_MGMT_PORT=str(process.Variables.manager_port), + PROXY_CONFIG_ADMIN_SYNTHETIC_PORT=str(process.Variables.admin_port), + PROXY_CONFIG_ADMIN_AUTOCONF_PORT=str(process.Variables.admin_port), + ) + traffic_out = paths["log"] / "traffic.out" + arguments = "" + if use_traffic_out: + arguments += f" --bind_stdout {shlex.quote(str(traffic_out))} --bind_stderr {shlex.quote(str(traffic_out))}" + if block_for_debug: + arguments += " --block" + process.StartupTimeout = 36000 + process.Command = f"{shlex.quote(str(paths['bin'] / command))}{arguments}" + process.Ready = When.FileContains(process.Disk.diags_log.AbsPath, "NOTE: Traffic Server is fully initialized") + process.ReturnCode = 0 + if not disable_log_checks: + process.Disk.diags_log.Content += Testers.ExcludesExpression("FATAL:", "ATS diagnostics must not contain fatal errors") + + def add_ssl_file(filename: str | Path) -> None: + shutil.copy2(owner.resolve_path(filename), paths["ssl"] / Path(filename).name) + + process.addSSLfile = add_ssl_file + process.addDefaultSSLFiles = lambda: [ + add_ssl_file(self.runtime.test_tools / "ssl" / filename) for filename in ("server.pem", "server.key") + ] + process.addSSLFileFromDefaultTestFolder = lambda filename: add_ssl_file(self.runtime.test_tools / "ssl" / filename) + process.addPrivateConnectAllowYaml = lambda methods="CONNECT": None + process.chownForATSProcess = lambda path: process.Setup.Chown(path) + return process + + def _make_origin( + self, + registry: ProcessRegistry, + owner: Entity, + name: str, + port: int | None = None, + s_port: int | None = None, + ip: str = "INADDR_LOOPBACK", + delay: TypingAny = None, + ssl: bool = False, + lookup_key: str = "{PATH}", + clientcert: str = "", + clientkey: str = "", + both: bool = False, + options: Mapping[str, TypingAny] | None = None, + **_kwargs: TypingAny) -> Process: + process = registry.Process(name) + address = {"INADDR_LOOPBACK": "127.0.0.1", "IN6ADDR_LOOPBACK": "::1"}.get(ip, ip) + data_dir = Path(process.RunDirectory) / "data" + data_dir.mkdir(parents=True, exist_ok=True) + process.Variables.DataDir = str(data_dir) + process.Variables.lookup_key = lookup_key + command = f"microserver --data-dir {shlex.quote(str(data_dir))} --ip_address {shlex.quote(address)} --lookupkey {shlex.quote(lookup_key)}" + if delay: + command += f" --delay {delay}" + if not ssl: + port = port or self.runtime.allocate_port() + process.Variables.Port = port + command += f" --port {port}" + if ssl or both: + s_port = s_port or self.runtime.allocate_port() + process.Variables.SSL_Port = s_port + command += " --both" if both else " --ssl" + key = clientkey or str(self.runtime.test_tools / "microserver" / "ssl" / "server.pem") + cert = clientcert or str(self.runtime.test_tools / "microserver" / "ssl" / "server.crt") + command += f" --key {shlex.quote(key)} --cert {shlex.quote(cert)} --s_port {s_port}" + for flag, value in (options or {}).items(): + command += f" {flag} {value or ''}" + process.Command = command + process.Ready = When.PortOpen(s_port if ssl else port, address) + process.ReturnCode = Any(None, 0) + + def add_response(filename: str, request_header: Mapping[str, TypingAny], response_header: Mapping[str, TypingAny]) -> None: + try: + from trlib import Request, Response, Session, Transaction + request = Request.fromRequestLine( + request_header["headers"], request_header.get("body", ""), request_header.get("options")) + response = Response.fromRequestLine( + response_header["headers"], response_header.get("body", ""), response_header.get("options")) + transaction = Transaction(request, None, response, None, None, None) + path = data_dir / filename + if path.exists(): + document = json.loads(path.read_text()) + document["sessions"][0]["transactions"].append(transaction.toJSON()) + else: + document = {"sessions": [Session(filename, None, None, [transaction]).toJSON()], "meta": {"version": "1.0"}} + path.write_text(json.dumps(document)) + except ImportError as error: + raise ScenarioError("traffic-replay is required for microserver tests") from error + + process.addResponse = add_response + process.addSessionFromFiles = lambda directory: shutil.copytree(owner.resolve_path(directory), data_dir, dirs_exist_ok=True) + health_request = {"headers": f"GET /ruok HTTP/1.1\r\nHost: {address}\r\n\r\n", "body": ""} + health_response = { + "headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", + "body": "imok", + "options": { + "skipHooks": None + } + } + add_response("healthcheck.json", health_request, health_response) + return process + + def _make_dns( + self, + registry: ProcessRegistry, + owner: Entity, + name: str, + filename: str = "dns_file.json", + port: int | bool = False, + ip: str = "INADDR_LOOPBACK", + rr: bool = False, + default: TypingAny = None, + options: Mapping[str, TypingAny] | None = None, + **_kwargs: TypingAny) -> Process: + process = registry.Process(name) + data_dir = Path(process.RunDirectory) / "data" + data_dir.mkdir(parents=True, exist_ok=True) + zone = data_dir / filename + otherwise = [default] if isinstance(default, str) else default + zone.write_text(json.dumps({"mappings": [], **({"otherwise": otherwise} if otherwise else {})})) + port = int(port) if port else self.runtime.allocate_port(socket.SOCK_DGRAM) + process.Variables.update(Port=port, DataDir=str(data_dir), zone_file=str(zone)) + owner.Variables.zone_file = str(zone) + command = f"microdns {ip} {port} {shlex.quote(str(zone))}" + if rr: + command += " --rr" + for flag, value in (options or {}).items(): + command += f" {flag} {value or ''}" + process.Command = command + process.Ready = ReadyCheck(lambda: _dns_ready(port), f"DNS on port {port}") + + def add_records(records: Mapping[str, Sequence[str]] | None = None, jsonFile: str | None = None) -> None: # noqa: N803 + document = json.loads(zone.read_text()) + for hostname, addresses in (records or {}).items(): + document["mappings"].append({hostname if hostname.endswith(".") else hostname + ".": list(addresses)}) + if jsonFile: + extra = json.loads(owner.resolve_path(jsonFile).read_text()) + document["mappings"].extend(extra.get("mappings", [])) + zone.write_text(json.dumps(document)) + + process.addRecords = add_records + return process + + def _make_httpbin( + self, + registry: ProcessRegistry, + owner: Entity, + name: str, + ip: str = "127.0.0.1", + port: int | None = None, + options: Mapping[str, TypingAny] | None = None, + **_kwargs: TypingAny) -> Process: + self.SkipUnless(Condition.HasProgram("go-httpbin", "go-httpbin is required")) + process = registry.Process(name) + port = port or self.runtime.allocate_port() + process.Variables.Port = port + process.Command = f"go-httpbin -host {ip} -port {port}" + "".join( + f" {flag} {value}" for flag, value in (options or {}).items()) + process.Ready = When.PortOpen(port, ip) + process.ReturnCode = Any(None, 0) + return process + + def _make_verifier_server( + self, + registry: ProcessRegistry, + owner: Entity, + name: str, + replay_path: str | Path, + http_ports: Sequence[int] | None = None, + https_ports: Sequence[int] | None = None, + http3_ports: Sequence[int] | None = None, + ssl_cert: str = "", + ca_cert: str = "", + verbose: bool = True, + other_args: str = "", + context: Mapping[str, TypingAny] | None = None, + **_kwargs: TypingAny) -> Process: + process = registry.Process(name) + http_ports = list(http_ports) if http_ports is not None else [self.runtime.allocate_port()] + https_ports = list(https_ports) if https_ports is not None else [self.runtime.allocate_port()] + process.Variables.http_port = http_ports[0] if http_ports else 0 + process.Variables.https_port = https_ports[0] if https_ports else 0 + process.Variables.http3_port = (http3_ports or [0])[0] + replay = self._prepare_replay(owner, process, replay_path, context) + command = [str(self.runtime.verifier_bin / "verifier-server"), "run"] + if http_ports: + command.extend(["--listen-http", _addresses(http_ports)]) + if https_ports: + command.extend(["--listen-https", _addresses(https_ports)]) + if https_ports or http3_ports: + ssl_cert = ssl_cert or str(self.runtime.test_tools / "proxy-verifier" / "ssl" / "server.pem") + ca_cert = ca_cert or str(self.runtime.test_tools / "proxy-verifier" / "ssl" / "ca.pem") + command.extend(["--server-cert", ssl_cert, "--ca-certs", ca_cert]) + command.append(str(replay)) + if verbose: + command.extend(["--verbose", "diag"]) + command.extend(shlex.split(other_args)) + process.Command = shlex.join(command) + process.Ready = When.PortOpen(http_ports[0]) if http_ports else When.PortOpen(https_ports[0]) + process.Streams.stdout.Content = Testers.ExcludesExpression("Violation", "Proxy Verifier server violations") + return process + + def _configure_verifier_client( + self, + process: Process, + owner: Entity, + name: str, + replay_path: str | Path, + http_ports: Sequence[int] | None = None, + https_ports: Sequence[int] | None = None, + http3_ports: Sequence[int] | None = None, + keys: str | None = None, + ssl_cert: str = "", + ca_cert: str = "", + verbose: bool = True, + other_args: str = "", + run_parallel: bool = False, + context: Mapping[str, TypingAny] | None = None, + poll_timeout: int | None = None, + **_kwargs: TypingAny) -> Process: + process.Name = name + replay = self._prepare_replay(owner, process, replay_path, context) + command = [str(self.runtime.verifier_bin / "verifier-client"), "run", str(replay)] + if http_ports: + command.extend(["--connect-http", _addresses(http_ports)]) + if https_ports: + command.extend(["--connect-https", _addresses(https_ports)]) + if http3_ports: + command.extend(["--connect-http3", _addresses(http3_ports)]) + if https_ports or http3_ports: + ssl_cert = ssl_cert or str(self.runtime.test_tools / "proxy-verifier" / "ssl" / "client.pem") + ca_cert = ca_cert or str(self.runtime.test_tools / "proxy-verifier" / "ssl" / "ca.pem") + command.extend(["--client-cert", ssl_cert, "--ca-certs", ca_cert]) + if keys: + command.extend(["--keys", keys]) + if verbose: + command.extend(["--verbose", "diag"]) + if poll_timeout is not None: + command.extend(["--poll-timeout", str(poll_timeout)]) + command.extend(shlex.split(other_args)) + if not run_parallel and "thread-limit" not in other_args: + command.extend(["--thread-limit", "1"]) + process.Command = shlex.join(command) + process.Streams.stdout.Content = Testers.ExcludesExpression("Violation|Invalid status", "Proxy Verifier client violations") + return process + + def _prepare_replay( + self, owner: Entity, process: Process, replay_path: str | Path, context: Mapping[str, TypingAny] | None) -> Path: + source = owner.resolve_path(replay_path) + destination = Path(process.RunDirectory) / source.name + if context and source.is_file(): + destination.write_text(Template(source.read_text()).substitute(context)) + elif source.is_dir(): + shutil.copytree(source, destination, dirs_exist_ok=True) + else: + shutil.copy2(source, destination) + return destination + + +def _scenario() -> UraniumTest: + scenario = _active_scenario.get() + if scenario is None: + raise ScenarioError("Uranium helpers must be used inside the urtest pytest fixture") + return scenario + + +def _validate(value: TypingAny, text: str, path: Path | None, test_directory: Path) -> None: + if value is None or value == []: + return + if isinstance(value, Validator): + value.validate(text, path, test_directory) + elif isinstance(value, str): + GoldValidator(value).validate(text, path, test_directory) + elif isinstance(value, list): + ValidatorGroup(value).validate(text, path, test_directory) + else: + raise ScenarioError(f"Unsupported output validator: {value!r}") + + +def _process_values(value: TypingAny) -> list[Process]: + if value is None: + return [] + if isinstance(value, Process): + return [value] + if isinstance(value, Choice): + return [item for item in value.values if isinstance(item, Process)] + if isinstance(value, (list, tuple, set, ProcessGroup)): + return [item for item in value if isinstance(item, Process)] + return [] + + +def _return_code_values(value: TypingAny) -> tuple[TypingAny, ...]: + return value.values if isinstance(value, Choice) else (value,) + + +def _return_code_matches(actual: int, expected: TypingAny) -> bool: + return actual in _return_code_values(expected) + + +def _identifier(name: str) -> str: + return name.replace(".", "_").replace("-", "_") + + +def _deep_update(destination: dict[str, TypingAny], source: Mapping[str, TypingAny]) -> None: + for key, value in source.items(): + if isinstance(value, Mapping) and isinstance(destination.get(key), dict): + _deep_update(destination[key], value) + else: + destination[key] = copy.deepcopy(value) + + +def _link_directory(source: Path, destination: Path) -> None: + destination.mkdir(parents=True, exist_ok=True) + for entry in source.iterdir(): + target = destination / entry.name + if not target.exists() and not target.is_symlink(): + target.symlink_to(entry) + + +def _port_open(address: str, port: int) -> bool: + family = socket.AF_INET6 if ":" in address else socket.AF_INET + with socket.socket(family, socket.SOCK_STREAM) as probe: + probe.settimeout(0.1) + return probe.connect_ex((address, port)) == 0 + + +def _dns_ready(port: int) -> bool: + try: + from dnslib import DNSRecord + DNSRecord.question("urtest-readiness.invalid").send("127.0.0.1", port, timeout=0.1) + return True + except (OSError, socket.timeout): + return False + + +def _addresses(ports: Sequence[int]) -> str: + return ",".join(f"127.0.0.1:{port}" for port in ports) + + +def _command_output(command: Sequence[str]) -> str: + try: + result = subprocess.run(command, capture_output=True, text=True, timeout=10, check=False) + except (OSError, subprocess.SubprocessError): + return "" + return result.stdout + result.stderr + + +def _version(value: str) -> tuple[int, ...]: + match = re.search(r"\d+(?:\.\d+)+", value) + return tuple(int(part) for part in match.group().split(".")) if match else () diff --git a/tests/uranium_testkit/services.py b/tests/uranium_testkit/services.py new file mode 100644 index 00000000000..059a1321369 --- /dev/null +++ b/tests/uranium_testkit/services.py @@ -0,0 +1,124 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. +"""Procedural service objects exposed by Uranium pytest fixtures.""" + +from __future__ import annotations + +from collections.abc import Mapping, Sequence +from dataclasses import dataclass +from pathlib import Path +import subprocess + +from .scenario import FileNode, Process, UraniumTest + + +@dataclass(frozen=True) +class CommandResult: + """The observable result of a client command.""" + + command: tuple[str, ...] + returncode: int + stdout: str + stderr: str + + @property + def output(self) -> str: + """Return stdout and stderr together for assertion diagnostics.""" + + return self.stdout + self.stderr + + +class RecordsConfig: + """Stage records.yaml updates without exposing scenario internals.""" + + def __init__(self, config_file: FileNode) -> None: + self._config_file = config_file + + def update(self, values: Mapping[str, object]) -> None: + """Merge configuration records into the staged records.yaml file.""" + + self._config_file.update(values) + + +class ATS: + """A fixture-owned Traffic Server process.""" + + def __init__(self, scenario: UraniumTest, name: str = "ats") -> None: + self._process: Process = scenario.MakeATSProcess(name) + self._was_started = False + self.records = RecordsConfig(self._process.Disk.records_config) + + @property + def http_port(self) -> int: + """Return the IPv4 HTTP listening port selected for this process.""" + + return int(self._process.Variables.port) + + @property + def is_running(self) -> bool: + """Return whether Traffic Server is still running.""" + + return self._process.is_running() + + def start(self) -> None: + """Materialize configuration and wait until Traffic Server is ready.""" + + self._process.start() + self._was_started = True + + def close(self) -> None: + """Stop Traffic Server and validate its diagnostics.""" + + self._process.stop() + if self._was_started: + self._process.validate() + + +class Curl: + """Run curl requests and return ordinary Python result objects.""" + + def __init__(self, working_directory: Path) -> None: + self._working_directory = working_directory + + def get( + self, + url: str, + *, + headers: Mapping[str, str] | None = None, + options: Sequence[str] = (), + timeout: float = 30, + ) -> CommandResult: + """Issue a GET request to @a url.""" + + arguments = [*options] + for name, value in (headers or {}).items(): + arguments.extend(["--header", f"{name}: {value}"]) + arguments.append(url) + return self.run(*arguments, timeout=timeout) + + def run(self, *arguments: str, timeout: float = 30) -> CommandResult: + """Run curl with @a arguments without interpreting the response.""" + + command = ("curl", *arguments) + completed = subprocess.run( + command, + cwd=self._working_directory, + capture_output=True, + text=True, + timeout=timeout, + check=False, + ) + return CommandResult(command, completed.returncode, completed.stdout, completed.stderr) diff --git a/tests/gold_tests/at_headers/plugins/CMakeLists.txt b/tests/uranium_tests/at_headers/plugins/CMakeLists.txt similarity index 93% rename from tests/gold_tests/at_headers/plugins/CMakeLists.txt rename to tests/uranium_tests/at_headers/plugins/CMakeLists.txt index 67501089475..da6c1aefa06 100644 --- a/tests/gold_tests/at_headers/plugins/CMakeLists.txt +++ b/tests/uranium_tests/at_headers/plugins/CMakeLists.txt @@ -15,4 +15,4 @@ # ####################### -add_autest_plugin(at_header_probe at_header_probe.cc) +add_urtest_plugin(at_header_probe at_header_probe.cc) diff --git a/tests/gold_tests/at_headers/plugins/at_header_probe.cc b/tests/uranium_tests/at_headers/plugins/at_header_probe.cc similarity index 100% rename from tests/gold_tests/at_headers/plugins/at_header_probe.cc rename to tests/uranium_tests/at_headers/plugins/at_header_probe.cc diff --git a/tests/gold_tests/at_headers/replay/at_headers.test.yaml b/tests/uranium_tests/at_headers/replay/at_headers.test.yaml similarity index 99% rename from tests/gold_tests/at_headers/replay/at_headers.test.yaml rename to tests/uranium_tests/at_headers/replay/at_headers.test.yaml index 095406c4994..dd3d8d52a7c 100644 --- a/tests/gold_tests/at_headers/replay/at_headers.test.yaml +++ b/tests/uranium_tests/at_headers/replay/at_headers.test.yaml @@ -17,7 +17,7 @@ meta: version: "1.0" -autest: +urtest: description: "Verify client-supplied @ headers are stripped before global plugin and remap hooks run" at_headers: diff --git a/tests/gold_tests/ats_probe/ats_probe.bt b/tests/uranium_tests/ats_probe/ats_probe.bt similarity index 100% rename from tests/gold_tests/ats_probe/ats_probe.bt rename to tests/uranium_tests/ats_probe/ats_probe.bt diff --git a/tests/gold_tests/ats_probe/ats_probe.replay.yaml b/tests/uranium_tests/ats_probe/ats_probe.replay.yaml similarity index 100% rename from tests/gold_tests/ats_probe/ats_probe.replay.yaml rename to tests/uranium_tests/ats_probe/ats_probe.replay.yaml diff --git a/tests/gold_tests/ats_probe/cache_dir_probe.bt b/tests/uranium_tests/ats_probe/cache_dir_probe.bt similarity index 100% rename from tests/gold_tests/ats_probe/cache_dir_probe.bt rename to tests/uranium_tests/ats_probe/cache_dir_probe.bt diff --git a/tests/uranium_tests/ats_probe/test_ats_probe.py b/tests/uranium_tests/ats_probe/test_ats_probe.py new file mode 100644 index 00000000000..8d35ffedbdb --- /dev/null +++ b/tests/uranium_tests/ats_probe/test_ats_probe.py @@ -0,0 +1,131 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_ats_probe(urtest: UraniumTest) -> None: + '''Verify SystemTap ATS probe behavior.''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import os + + urtest.Summary = '''Verify SystemTap ATS probes.''' + + # Skipping this test generally because it requires privilege. Thus most CI + # systems will fail this test. Comment out the following line to run in your + # privileged environment. + urtest.SkipUnless(Condition(lambda: False, "Test requires privilege", True)) + urtest.SkipUnless(Condition.HasProgram("bpftrace", "Need bpftrace to verify the probe.")) + + class TestATSProbe: + '''Verify SystemTap ATS probes.''' + replay_file: str = 'ats_probe.replay.yaml' + bt_script: str = 'ats_probe.bt' + + def __init__(self): + '''Configure the TestRun.''' + tr = urtest.AddTestRun('Verify ATS probes.') + self._configure_dns(tr) + self._configure_server(tr) + self._configure_traffic_server(tr) + self._configure_bpftrace(tr) + self._configure_client(tr) + + def _configure_dns(self, tr: 'TestRun') -> 'Process': + '''Configure the DNS process. + + :param tr: The TestRun to add the DNS process to. + :return: The DNS process. + ''' + dns = tr.MakeDNServer('dns', default='127.0.0.1') + self._dns = dns + return dns + + def _configure_server(self, tr: 'TestRun') -> 'Process': + '''Configure the server process. + + :param tr: The TestRun to add the server process to. + :return: The server process. + ''' + server = tr.AddVerifierServerProcess('server', self.replay_file) + self._server = server + return server + + def _configure_traffic_server(self, tr: 'TestRun') -> 'Process': + '''Configure the Traffic Server process. + + :param tr: The TestRun to add the Traffic Server process to. + :return: The Traffic Server process. + ''' + ts = tr.MakeATSProcess("ts", enable_cache=False) + self._ts = ts + + ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'http', + 'proxy.config.dns.nameservers': f"127.0.0.1:{self._dns.Variables.Port}", + 'proxy.config.dns.resolv_conf': 'NULL' + }) + server_port = self._server.Variables.http_port + ts.Disk.remap_config.AddLine(f'map / http://backend.server.com:{server_port}') + return ts + + def _configure_bpftrace(self, tr: 'TestRun') -> 'Process': + '''Configure the bpftrace process for the ATS probe. + + :param tr: The TestRun to add the bpftrace process to. + :return: The bpftrace process. + ''' + bpftrace = tr.Processes.Process('bpftrace') + self._bpftrace = bpftrace + tr.Setup.Copy(self.bt_script) + tr_script = os.path.join(tr.RunDirectory, self.bt_script) + bpftrace.Command = f'sudo bpftrace {tr_script}' + bpftrace.ReturnCode = 0 + bpftrace.Streams.All += Testers.ContainsExpression( + 'backend.server.com', 'The probe correctly printed the origin servername.') + return bpftrace + + def _configure_client(self, tr: 'TestRun') -> 'Process': + '''Configure the client process. + + :param tr: The TestRun to add the client process to. + :return: The client process. + ''' + client = tr.AddVerifierClientProcess("client", self.replay_file, http_ports=[self._ts.Variables.port]) + self._ts.StartBefore(self._dns) + self._ts.StartBefore(self._server) + self._bpftrace.StartBefore(self._ts) + client.StartBefore(self._bpftrace) + + TestATSProbe() + urtest.execute() diff --git a/tests/uranium_tests/ats_probe/test_cache_dir_probe.py b/tests/uranium_tests/ats_probe/test_cache_dir_probe.py new file mode 100644 index 00000000000..c2ba2c0891f --- /dev/null +++ b/tests/uranium_tests/ats_probe/test_cache_dir_probe.py @@ -0,0 +1,141 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_cache_dir_probe(urtest: UraniumTest) -> None: + '''Verify cache directory SystemTap probes fire on insert and remove.''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import os + + urtest.Summary = '''Verify cache directory SystemTap probes fire on cache fill and PURGE.''' + + # Skipping this test generally because it requires privilege. Thus most CI systems will skip it. + urtest.SkipUnless( + Condition(lambda: os.geteuid() == 0, "Test requires privilege", True), + Condition.HasProgram("bpftrace", "Need bpftrace to verify the probe.")) + + class CacheDirProbeTest: + '''Verify cache directory SystemTap probes.''' + bt_script: str = 'cache_dir_probe.bt' + _cache_path: str = '/cacheable' + + def __init__(self): + tr = urtest.AddTestRun('Cache directory probes should trigger on insert and purge.') + self._configure_origin(tr) + self._configure_traffic_server(tr) + self._configure_bpftrace(tr) + self._configure_client(tr) + + def _configure_origin(self, tr: 'TestRun') -> 'Process': + '''Configure the origin microserver.''' + origin = urtest.MakeOriginServer('origin') + self._origin = origin + + cache_request = { + "headers": f"GET {self._cache_path} HTTP/1.1\r\nHost: cache-probe.test\r\n\r\n", + "timestamp": "1469733493.993", + "body": "" + } + cache_response = { + "headers": "HTTP/1.1 200 OK\r\nConnection: close\r\nCache-Control: max-age=120\r\nContent-Length: 5\r\n\r\n", + "timestamp": "1469733493.993", + "body": "hello" + } + origin.addResponse("sessionlog.json", cache_request, cache_response) + origin.addResponse("sessionlog.json", cache_request, cache_response) + + purge_request = { + "headers": f"PURGE {self._cache_path} HTTP/1.1\r\nHost: cache-probe.test\r\n\r\n", + "timestamp": "1469733493.993", + "body": "" + } + purge_response = { + "headers": "HTTP/1.1 200 OK\r\nConnection: close\r\nContent-Length: 0\r\n\r\n", + "timestamp": "1469733493.993", + "body": "" + } + origin.addResponse("sessionlog.json", purge_request, purge_response) + return origin + + def _configure_traffic_server(self, tr: 'TestRun') -> 'Process': + '''Configure the Traffic Server process.''' + ts = tr.MakeATSProcess("ts_cache_dir_probe", enable_cache=True) + self._ts = ts + ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'http|cache', + 'proxy.config.http.cache.required_headers': 0, + # Keep ATS running as the invoking user inside sudo (no privilege drop). + 'proxy.config.admin.user_id': '#-1', + }) + ts.Disk.remap_config.AddLine(f'map / http://127.0.0.1:{self._origin.Variables.Port}') + return ts + + def _configure_bpftrace(self, tr: 'TestRun') -> 'Process': + '''Configure the bpftrace process for the cache directory probes.''' + bpftrace = tr.Processes.Process('bpftrace') + self._bpftrace = bpftrace + + tr.Setup.Copy(self.bt_script) + tr_script = os.path.join(tr.RunDirectory, self.bt_script) + + # fan out output so AuTest stream checks still work + tee_path = os.path.join(tr.RunDirectory, 'bpftrace.out') + bpftrace.Command = f"bpftrace {tr_script}" + bpftrace.ReturnCode = 0 + bpftrace.Streams.All += Testers.ContainsExpression('cache_dir_insert', 'cache_dir_insert probe fired.') + bpftrace.Streams.All += Testers.ContainsExpression('cache_dir_remove', 'cache_dir_remove probe fired.') + + return bpftrace + + def _configure_client(self, tr: 'TestRun') -> 'Process': + '''Configure the client traffic to exercise cache insert and purge.''' + client = tr.Processes.Default + self._client = client + cache_url = f"http://127.0.0.1:{self._ts.Variables.port}{self._cache_path}" + # Ideally we don't need this "sleep 1", but I haven't been able to get it to work with the Ready = When.FileContains(...) approach. + client.Command = ( + f"sleep 1 && curl -sSf -o /dev/null -H 'Host: cache-probe.test' {cache_url} && " + f"curl -sSf -o /dev/null -X PURGE -H 'Host: cache-probe.test' {cache_url} && " + f"curl -sSf -o /dev/null -H 'Host: cache-probe.test' {cache_url}") + client.ReturnCode = 0 + client.Env = self._ts.Env + + self._ts.StartBefore(self._origin) # origin before ts + self._bpftrace.StartBefore(self._ts) # ts before bpftrace + client.StartBefore(self._bpftrace) # bpftrace before client + return client + + CacheDirProbeTest() + urtest.execute() diff --git a/tests/gold_tests/bad_http_fmt/client.gold b/tests/uranium_tests/bad_http_fmt/client.gold similarity index 100% rename from tests/gold_tests/bad_http_fmt/client.gold rename to tests/uranium_tests/bad_http_fmt/client.gold diff --git a/tests/gold_tests/bad_http_fmt/method-server.sh b/tests/uranium_tests/bad_http_fmt/method-server.sh similarity index 100% rename from tests/gold_tests/bad_http_fmt/method-server.sh rename to tests/uranium_tests/bad_http_fmt/method-server.sh diff --git a/tests/gold_tests/bad_http_fmt/server.gold b/tests/uranium_tests/bad_http_fmt/server.gold similarity index 100% rename from tests/gold_tests/bad_http_fmt/server.gold rename to tests/uranium_tests/bad_http_fmt/server.gold diff --git a/tests/uranium_tests/bad_http_fmt/test_bad_http_fmt.py b/tests/uranium_tests/bad_http_fmt/test_bad_http_fmt.py new file mode 100644 index 00000000000..100df0d84da --- /dev/null +++ b/tests/uranium_tests/bad_http_fmt/test_bad_http_fmt.py @@ -0,0 +1,207 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_bad_http_fmt(urtest: UraniumTest) -> None: + ''' + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + urtest.Summary = ''' + Test requests with bad HTTP formats + ''' + + ArbitraryTimestamp = '12345678' + + random_path = "/sdfsdf" + + random_method = "xyzxyz" + + ts = urtest.MakeATSProcess("ts") + + urtest.GetTcpPort("upstream_port") + + method_server = urtest.Processes.Process( + "method-server", "bash -c '" + urtest.TestDirectory + f"/method-server.sh {urtest.Variables.upstream_port} outserver'") + + server = urtest.MakeOriginServer("server", ssl=False) + request_header = { + "headers": "GET {}/0 HTTP/1.1\r\nX-Req-Id: 0\r\nHost: example.com\r\n\r\n".format(random_path), + "timestamp": "1469733493.993", + "body": "" + } + response_header = { + "headers": "HTTP/1.1 200 OK\r\nX-Resp-Id: 1\r\nConnection: close\r\n\r\n", + "timestamp": "1469733493.993", + "body": "" + } + server.addResponse("sessionlog.json", request_header, response_header) + + request_header = { + "headers": "GET {}/1 HTTP/1.1\r\nX-Req-Id: 1\r\nHost: example.com\r\n\r\n".format(random_path), + "timestamp": "1469733493.993", + "body": "" + } + response_header = { + "headers": "HTTP/1.1 200 OK\r\nX-Resp-Id: 2\r\nConnection: close\r\n\r\n", + "timestamp": "1469733493.993", + "body": "" + } + server.addResponse("sessionlog.json", request_header, response_header) + + request_header = { + "headers": "GET /example/1 HTTP/1.1\r\nX-Req-Id: 6\r\nHost: example.com\r\n\r\n", + "timestamp": "1469733493.993", + "body": "" + } + response_header = { + "headers": "HTTP/1.1 200 OK\r\nX-Resp-Id: 3\r\nConnection: close\r\n\r\n", + "timestamp": "1469733493.993", + "body": "" + } + server.addResponse("sessionlog.json", request_header, response_header) + + request_header = { + "headers": "GET /example/2 HTTP/1.1\r\nX-Req-Id: 7\r\nHost: example.com\r\n\r\n", + "timestamp": "1469733493.993", + "body": "" + } + response_header = { + "headers": "HTTP/1.1 200 OK\r\nX-Resp-Id: 4\r\nConnection: close\r\n\r\n", + "timestamp": "1469733493.993", + "body": "" + } + server.addResponse("sessionlog.json", request_header, response_header) + + ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'http|dns', + 'proxy.config.url_remap.remap_required': 0, + 'proxy.config.http.strict_uri_parsing': 1, + 'proxy.config.http.cache.http': 0, + }) + + # Allow random_method for 128.0.0.1, do not allow it for ::1. + # + ts.Disk.ip_allow_yaml.AddLines( + [ + 'ip_allow:', + ' - apply: in', + ' ip_addrs: 127.0.0.1', + ' action: allow', + ' methods:', + ' - GET', + f' - {random_method}', + ]) + ts.Disk.ip_allow_yaml.AddLines([ + ' - apply: in', + ' ip_addrs: ::1', + ' action: allow', + ' methods:', + ' - GET', + ]) + + ts.Disk.remap_config.AddLine('map /add-method http://127.0.0.1:{0}/'.format(urtest.Variables.upstream_port)) + ts.Disk.remap_config.AddLine('map / http://127.0.0.1:{0}/'.format(server.Variables.Port)) + + # Even if the request from the client is HTTP version 1.0, ATS's request to server will be HTTP version 1.1. + # + tr = urtest.AddTestRun("success-1.0") + tr.Processes.Default.StartBefore(ts) + tr.Processes.Default.StartBefore(server) + tr.Processes.Default.Command = ( + f"printf 'GET {random_path}/0HTTP/1.0\r\n" + "Host: example.com\r\n" + "Connection: close\r\n" + "X-Req-Id: 1\r\n\r\n'" + + f" | nc localhost {ts.Variables.port} >> client.log" + " ; echo '======' >> client.log") + tr.Processes.Default.ReturnCode = 0 + + tr = urtest.AddTestRun("success-1.1") + tr.Processes.Default.Command = ( + f"printf 'GET {random_path}/1HTTP/1.1\r\n" + "Host: example.com\r\n" + "Connection: close\r\n" + "X-Req-Id: 2\r\n\r\n'" + + f" | nc localhost {ts.Variables.port} >> client.log" + " ; echo '======' >> client.log") + tr.Processes.Default.ReturnCode = 0 + + tr = urtest.AddTestRun("invalid-url-line") + tr.Processes.Default.Command = ( + f"printf 'GET {random_path}> client.log" + " ; echo '======' >> client.log") + tr.Processes.Default.ReturnCode = 0 + + tr = urtest.AddTestRun("bad-http-version") + tr.Processes.Default.Command = ( + f"printf 'GET {random_path} HTTP/1.2\r\n" + "Host: example.com\r\n" + "Connection: close\r\n" + "X-Req-Id: 3\r\n\r\n'" + + f" | nc localhost {ts.Variables.port} >> client.log" + " ; echo '======' >> client.log") + tr.Processes.Default.ReturnCode = 0 + + tr = urtest.AddTestRun("bad-request-maybe-http-version") + tr.Processes.Default.Command = ( + f"printf 'GET {random_path} HTTP/0.9\r\n" + "Host: example.com\r\n" + "Connection: close\r\n" + "X-Req-Id: 4\r\n\r\n'" + + f" | nc localhost {ts.Variables.port} >> client.log" + " ; echo '======' >> client.log") + tr.Processes.Default.ReturnCode = 0 + + tr = urtest.AddTestRun("same-as-last-case?") + tr.Processes.Default.Command = ( + fr"printf 'GET {random_path} HTTP/0.9\r\n" + "Host: example.com\r\n" + "Connection: close\r\n" + "X-Req-Id: 5\r\n\r\n'" + + fr"| nc localhost {ts.Variables.port} >> client.log" + " ; echo '======' >> client.log") + tr.Processes.Default.ReturnCode = 0 + + tr = urtest.AddTestRun("allowed-random-method") + tr.Processes.Default.StartBefore(method_server) + tr.Processes.Default.Command = ( + fr"printf '{random_method} /add-method HTTP/1.1\r\n" + "Host: example.com\r\n" + "Connection: close\r\n" + + "X-Req-Id: 6\r\n\r\n'" + f" | nc localhost {ts.Variables.port} >> client.log" + " ; echo '======' >> client.log") + tr.Processes.Default.ReturnCode = 0 + + tr = urtest.AddTestRun("valid-ipv6") + tr.Processes.Default.Command = ( + f"printf 'GET /example/2 HTTP/1.1\r\n" + "Host: example.com\r\n" + "Connection: close\r\n" + "X-Req-Id: 7\r\n\r\n'" + + f" | nc ::1 {ts.Variables.portv6} >> client.log" + " ; echo '======' >> client.log") + tr.Processes.Default.ReturnCode = 0 + + tr = urtest.AddTestRun("unallowed-method-v6") + tr.Processes.Default.Command = ( + f"printf '{random_method} /example/1 HTTP/1.1\r\n" + "Host: example.com\r\n" + "Connection: close\r\n\r\n'" + + f" | nc ::1 {ts.Variables.portv6} >> client.log" + " ; echo '======' >> client.log") + tr.Processes.Default.ReturnCode = 0 + + tr = urtest.AddTestRun() + tr.Processes.Default.Command = "grep -e '^===' -e '^HTTP/' -e 'X-Resp-Id:' -e '' client.log" + tr.Processes.Default.Streams.stdout = 'client.gold' + tr.Processes.Default.ReturnCode = 0 + + server.Streams.All += Testers.ContainsExpression("Serving GET /sdfsdf/0... Finished", "Served 1") + server.Streams.All += Testers.ContainsExpression("Serving GET /sdfsdf/1... Finished", "Served 2") + server.Streams.All += Testers.ContainsExpression("Serving GET /example/2... Finished", "Served 3") + + outserver = urtest.Disk.File("outserver") + outserver.Content = "server.gold" + urtest.execute() diff --git a/tests/gold_tests/basic/.gitignore b/tests/uranium_tests/basic/.gitignore similarity index 100% rename from tests/gold_tests/basic/.gitignore rename to tests/uranium_tests/basic/.gitignore diff --git a/tests/gold_tests/basic/basic.test.py b/tests/uranium_tests/basic/test_basic.py similarity index 68% rename from tests/gold_tests/basic/basic.test.py rename to tests/uranium_tests/basic/test_basic.py index d812a38a36e..e37dcd69cd3 100644 --- a/tests/gold_tests/basic/basic.test.py +++ b/tests/uranium_tests/basic/test_basic.py @@ -1,5 +1,3 @@ -''' -''' # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information @@ -16,15 +14,14 @@ # See the License for the specific language governing permissions and # limitations under the License. -Test.Summary = ''' -Test that Trafficserver starts with default configurations. -''' +from uranium_testkit.services import ATS, Curl -ts = Test.MakeATSProcess("ts") -t = Test.AddTestRun("Test traffic server started properly") -t.StillRunningAfter = Test.Processes.ts -p = t.Processes.Default -t.MakeCurlCommand("http://127.0.0.1:{0}".format(ts.Variables.port), ts=ts) -p.ReturnCode = 0 -p.StartBefore(Test.Processes.ts) +def test_traffic_server_starts(ats: ATS, curl: Curl) -> None: + """Verify that Traffic Server starts with its default test configuration.""" + + ats.start() + + result = curl.get(f"http://127.0.0.1:{ats.http_port}") + + assert result.returncode == 0, result.output diff --git a/tests/gold_tests/pluginTest/cppapi/cppapi.test.py b/tests/uranium_tests/basic/test_config.py similarity index 65% rename from tests/gold_tests/pluginTest/cppapi/cppapi.test.py rename to tests/uranium_tests/basic/test_config.py index c0d68af0490..38a41588915 100644 --- a/tests/gold_tests/pluginTest/cppapi/cppapi.test.py +++ b/tests/uranium_tests/basic/test_config.py @@ -14,18 +14,15 @@ # See the License for the specific language governing permissions and # limitations under the License. -import os +from uranium_testkit.services import ATS, Curl -Test.Summary = ''' -Execute plugin with cppapi tests. -''' -ts = Test.MakeATSProcess("ts") +def test_traffic_server_starts_with_custom_listener(ats: ATS, curl: Curl) -> None: + """Verify that records.yaml can replace the default listener configuration.""" -Test.PrepareTestPlugin(os.path.join(Test.Variables.AtsTestPluginsDir, 'test_cppapi.so'), ts) + ats.records.update({"proxy.config.http.server_ports": str(ats.http_port)}) + ats.start() -tr = Test.AddTestRun() -tr.Processes.Default.StartBefore(Test.Processes.ts) -tr.Processes.Default.Command = "echo run test_cppapi plugin" -tr.Processes.Default.ReturnCode = 0 -tr.Processes.StillRunningAfter = ts + result = curl.get(f"http://127.0.0.1:{ats.http_port}") + + assert result.returncode == 0, result.output diff --git a/tests/uranium_tests/basic/test_copy_config.py b/tests/uranium_tests/basic/test_copy_config.py new file mode 100644 index 00000000000..f53a1354743 --- /dev/null +++ b/tests/uranium_tests/basic/test_copy_config.py @@ -0,0 +1,69 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_copy_config(urtest: UraniumTest) -> None: + ''' + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + urtest.Summary = "Test start up of Traffic server with configuration modification of starting port of different servers at the same time" + + # set up some ATS processes with dynamic port selection + ts1 = urtest.MakeATSProcess("ts1") + ts1.Disk.records_config.update({ + 'proxy.config.http.server_ports': str(ts1.Variables.port) + f" {ts1.Variables.uds_path}", + }) + ts1.Ready = When.PortOpen(ts1.Variables.port) + + ts2 = urtest.MakeATSProcess("ts2", enable_uds=False) + ts2.Disk.records_config.update({ + 'proxy.config.http.server_ports': str(ts2.Variables.port), + }) + ts2.Ready = When.PortOpen(ts2.Variables.port) + + # setup a testrun + t = urtest.AddTestRun("Talk to ts1") + t.Processes.Default.StartBefore(ts1) + t.Processes.Default.StartBefore(ts2) + t.MakeCurlCommand("127.0.0.1:{port}".format(port=ts1.Variables.port), ts=ts1) + t.ReturnCode = 0 + t.StillRunningAfter = ts1 + t.StillRunningAfter += ts2 + + # setup a testrun + t = urtest.AddTestRun("Talk to ts2") + t.MakeCurlCommandMulti("{{curl_base}} 127.0.0.1:{port}".format(port=ts2.Variables.port), ts=ts2) + t.ReturnCode = 0 + t.StillRunningAfter = ts1 + t.StillRunningAfter += ts2 + urtest.execute() diff --git a/tests/uranium_tests/basic/test_copy_config2.py b/tests/uranium_tests/basic/test_copy_config2.py new file mode 100644 index 00000000000..e1a4b9bd280 --- /dev/null +++ b/tests/uranium_tests/basic/test_copy_config2.py @@ -0,0 +1,64 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_copy_config2(urtest: UraniumTest) -> None: + ''' + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + urtest.Summary = "Test start up of Traffic server with generated ports of more than one servers at the same time" + + # set up some ATS processes + ts1 = urtest.MakeATSProcess("ts1") + ts2 = urtest.MakeATSProcess("ts2") + + # setup a testrun + t = urtest.AddTestRun("Talk to ts1") + t.StillRunningAfter = ts1 + t.StillRunningAfter += ts2 + p = t.Processes.Default + t.MakeCurlCommand("127.0.0.1:{0}".format(ts1.Variables.port), ts=ts1) + p.ReturnCode = 0 + p.StartBefore(urtest.Processes.ts1) + p.StartBefore(urtest.Processes.ts2) + + # setup a testrun + t = urtest.AddTestRun("Talk to ts2") + t.StillRunningBefore = ts1 + t.StillRunningBefore += ts2 + t.StillRunningAfter = ts1 + t.StillRunningAfter += ts2 + p = t.Processes.Default + t.MakeCurlCommand("127.0.0.1:{0}".format(ts2.Variables.port), ts=ts2) + p.ReturnCode = 0 + urtest.execute() diff --git a/tests/uranium_tests/basic/test_deny0.py b/tests/uranium_tests/basic/test_deny0.py new file mode 100644 index 00000000000..dc0be1dc945 --- /dev/null +++ b/tests/uranium_tests/basic/test_deny0.py @@ -0,0 +1,133 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_deny0(urtest: UraniumTest) -> None: + ''' + Test that Trafficserver rejects requests for host 0 + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import os + import sys + + urtest.Summary = ''' + Test that Trafficserver rejects requests for host 0 + ''' + + urtest.ContinueOnFail = True + + HOST1 = 'redirect.test' + + redirect_serv = urtest.MakeOriginServer("redirect_serv", ip='0.0.0.0') + + dns = urtest.MakeDNServer("dns") + dns.addRecords(records={HOST1: ['127.0.0.1']}) + + ts = urtest.MakeATSProcess("ts", enable_cache=False) + ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'http|dns|redirect', + 'proxy.config.http.number_of_redirections': 1, + 'proxy.config.dns.nameservers': '127.0.0.1:{0}'.format(dns.Variables.Port), + 'proxy.config.dns.resolv_conf': 'NULL', + 'proxy.config.url_remap.remap_required': 0 # need this so the domain gets a chance to be evaluated through DNS + }) + + urtest.Setup.Copy(os.path.join(urtest.Variables.AtsTestToolsDir, 'tcp_client.py')) + + data_dirname = 'generated_test_data' + data_path = os.path.join(urtest.TestDirectory, data_dirname) + os.makedirs(data_path, exist_ok=True) + gold_filepath = os.path.join(data_path, 'deny0.gold') + with open(os.path.join(data_path, 'deny0.gold'), 'w') as f: + f.write('HTTP/1.1 400 Bad Destination Address\r\n') + + isFirstTest = True + + def buildMetaTest(testName, requestString): + tr = urtest.AddTestRun(testName) + nonlocal isFirstTest + if isFirstTest: + isFirstTest = False + tr.Processes.Default.StartBefore(ts) + tr.Processes.Default.StartBefore(redirect_serv, ready=When.PortOpen(redirect_serv.Variables.Port)) + tr.Processes.Default.StartBefore(dns) + + requestCommandPath = os.path.join(data_path, tr.Name) + with open(requestCommandPath, 'w') as f: + f.write(requestString) + tr.Processes.Default.Command = \ + (f"{sys.executable} tcp_client.py 127.0.0.1 {ts.Variables.port} " + f"{requestCommandPath} | head -1") + tr.ReturnCode = 0 + tr.Processes.Default.Streams.stdout = gold_filepath + tr.StillRunningAfter = ts + tr.StillRunningAfter = redirect_serv + tr.StillRunningAfter = dns + + buildMetaTest( + 'RejectInterfaceAnyIpv4', 'GET / HTTP/1.1\r\nHost: 0:{port}\r\nConnection: close\r\n\r\n'.format(port=ts.Variables.port)) + + buildMetaTest( + 'RejectInterfaceAnyIpv6', + 'GET / HTTP/1.1\r\nHost: [::]:{port}\r\nConnection: close\r\n\r\n'.format(port=ts.Variables.portv6)) + + # Sets up redirect to IPv4 ANY address + redirect_request_header = {"headers": "GET /redirect-0 HTTP/1.1\r\nHost: *\r\n\r\n", "timestamp": "5678", "body": ""} + redirect_response_header = { + "headers": "HTTP/1.1 302 Found\r\nLocation: http://0:{0}/\r\nConnection: close\r\n\r\n".format(ts.Variables.port), + "timestamp": "5678", + "body": "" + } + redirect_serv.addResponse("sessionfile.log", redirect_request_header, redirect_response_header) + + buildMetaTest( + 'RejectRedirectToInterfaceAnyIpv4', + 'GET /redirect-0 HTTP/1.1\r\nHost: {host}:{port}\r\n\r\n'.format(host=HOST1, port=redirect_serv.Variables.Port)) + + # Sets up redirect to IPv6 ANY address + redirect_request_header = {"headers": "GET /redirect-0v6 HTTP/1.1\r\nHost: *\r\n\r\n", "timestamp": "5678", "body": ""} + redirect_response_header = { + "headers": "HTTP/1.1 302 Found\r\nLocation: http://[::]:{0}/\r\nConnection: close\r\n\r\n".format(ts.Variables.port), + "timestamp": "5678", + "body": "" + } + redirect_serv.addResponse("sessionfile.log", redirect_request_header, redirect_response_header) + + buildMetaTest( + 'RejectRedirectToInterfaceAnyIpv6', + 'GET /redirect-0v6 HTTP/1.1\r\nHost: {host}:{port}\r\n\r\n'.format(host=HOST1, port=redirect_serv.Variables.Port)) + + urtest.Setup.Copy(data_path) + urtest.execute() diff --git a/tests/uranium_tests/bigobj/test_bigobj.py b/tests/uranium_tests/bigobj/test_bigobj.py new file mode 100644 index 00000000000..f9228753d5d --- /dev/null +++ b/tests/uranium_tests/bigobj/test_bigobj.py @@ -0,0 +1,168 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_bigobj(urtest: UraniumTest) -> None: + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import os + + urtest.Summary = ''' + Test PUSHing an object into the cache and the GETting it with a few variations on the client connection protocol. + ''' + + # NOTE: You can also use this to test client-side communication when GET-ing very large (multi-GB) objects + # by increasing the value of the obj_kilobytes variable below. (But do not increase it on any shared branch + # that we do CI runs on.) + + urtest.SkipUnless(Condition.HasCurlFeature('http2')) + + ts = urtest.MakeATSProcess("ts1", enable_tls=True) + ts.addDefaultSSLFiles() + + ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'http|dns|cache', + 'proxy.config.http.cache.required_headers': 0, # No required headers for caching + 'proxy.config.http.push_method_enabled': 1, + 'proxy.config.proxy_name': 'Poxy_Proxy', # This will be the server name. + 'proxy.config.ssl.server.cert.path': ts.Variables.SSLDir, + 'proxy.config.ssl.server.private_key.path': ts.Variables.SSLDir, + 'proxy.config.url_remap.remap_required': 0 + }) + + ts.Disk.ssl_multicert_yaml.AddLines( + """ + ssl_multicert: + - dest_ip: "*" + ssl_cert_name: server.pem + ssl_key_name: server.key + """.split("\n")) + + ts.Disk.remap_config.AddLine(f'map https://localhost:{ts.Variables.ssl_port} http://localhost:{ts.Variables.port}') + ts.Disk.remap_config.AddLine(f'map https://localhost:{ts.Variables.ssl_portv6} http://localhost:{ts.Variables.port}') + + # Size of object to get. (NOTE: If you increase this significantly you may also have to increase cache + # capacity in tests/uranium_tests/autest-size/min_cfg/storage.yaml. Also, for very large objects, if + # proxy.config.diags.debug.enabled is 1, the PUSH request will timeout and fail.) + # + obj_kilobytes = 10 * 1024 + obj_bytes = obj_kilobytes * 10 + header = "HTTP/1.1 200 OK\r\nContent-length: {}\r\n\r\n".format(obj_bytes) + + def create_pushfile(): + f = open(urtest.RunDirectory + "/objfile", "w") + f.write(header) + f.write("x" * obj_bytes) + f.close() + return True + + ipv4flag = "" + if not Condition.CurlUsingUnixDomainSocket(): + ipv4flag = "--ipv4" + + tr = urtest.AddTestRun("PUSH an object to the cache") + # Delay on readiness of TS IPv4 ssl port + tr.Processes.Default.StartBefore(ts, ready=lambda: create_pushfile()) + # Put object with URL http://localhost/bigobj in cache using PUSH request. + tr.MakeCurlCommand( + "-v -H 'Content-Type: application/octet-stream' --data-binary @{}/objfile -X PUSH http://localhost:{}/bigobj -H 'Content-Length:{}'" + .format(urtest.RunDirectory, ts.Variables.port, + len(header) + obj_bytes), + ts=ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.All = Testers.ContainsExpression("HTTP/1.1 201 Created", "The PUSH request should have succeeded") + + tr = urtest.AddTestRun("GET bigobj: cleartext, HTTP/1.1, IPv4") + tr.MakeCurlCommand(f'--verbose {ipv4flag} --http1.1 http://localhost:{ts.Variables.port}/bigobj', ts=ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.All = Testers.ContainsExpression("HTTP/1.1 200 OK", "Should fetch pushed object") + tr.Processes.Default.Streams.All = Testers.ContainsExpression("Content-length: 102400", "Content size should be accurate") + + if not Condition.CurlUsingUnixDomainSocket(): + tr = urtest.AddTestRun("GET bigobj: TLS, HTTP/1.1, IPv4") + tr.MakeCurlCommand(f'--verbose --ipv4 --http1.1 --insecure https://localhost:{ts.Variables.ssl_port}/bigobj', ts=ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.All = Testers.ContainsExpression("HTTP/1.1 200 OK", "Should fetch pushed object") + tr.Processes.Default.Streams.All = Testers.ContainsExpression("Content-length: 102400", "Content size should be accurate") + + tr = urtest.AddTestRun("GET bigobj: TLS, HTTP/2, IPv4") + tr.MakeCurlCommand(f'--verbose --ipv4 --http2 --insecure https://localhost:{ts.Variables.ssl_port}/bigobj', ts=ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.All = Testers.ContainsExpression("HTTP/2 200", "Should fetch pushed object") + tr.Processes.Default.Streams.All = Testers.ContainsExpression("content-length: 102400", "Content size should be accurate") + + tr = urtest.AddTestRun("GET bigobj: TLS, HTTP/2, IPv6") + tr.MakeCurlCommand(f'--verbose --ipv6 --http2 --insecure https://localhost:{ts.Variables.ssl_portv6}/bigobj', ts=ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.All = Testers.ContainsExpression("HTTP/2 200", "Should fetch pushed object") + tr.Processes.Default.Streams.All = Testers.ContainsExpression("content-length: 102400", "Content size should be accurate") + + # Verify that PUSH requests are rejected when push_method_enabled is 0 (the + # default configuration). + ts = urtest.MakeATSProcess("ts2", enable_tls=True) + ts.addDefaultSSLFiles() + + ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'http|dns|cache', + 'proxy.config.http.cache.required_headers': 0, # No required headers for caching + 'proxy.config.proxy_name': 'Poxy_Proxy', # This will be the server name. + 'proxy.config.ssl.server.cert.path': ts.Variables.SSLDir, + 'proxy.config.ssl.server.private_key.path': ts.Variables.SSLDir, + 'proxy.config.url_remap.remap_required': 0 + }) + + ts.Disk.ssl_multicert_yaml.AddLines( + """ + ssl_multicert: + - dest_ip: "*" + ssl_cert_name: server.pem + ssl_key_name: server.key + """.split("\n")) + + ts.Disk.remap_config.AddLine(f'map https://localhost:{ts.Variables.ssl_port} http://localhost:{ts.Variables.port}') + ts.Disk.remap_config.AddLine(f'map https://localhost:{ts.Variables.ssl_portv6} http://localhost:{ts.Variables.port}') + + tr = urtest.AddTestRun("PUSH request is rejected when push_method_enabled is 0") + tr.Processes.Default.StartBefore(ts) + tr.MakeCurlCommand( + "-v -H 'Content-Type: application/octet-stream' --data-binary @{}/objfile -X PUSH http://localhost:{}/bigobj -H 'Content-Length:{}'" + .format(urtest.RunDirectory, ts.Variables.port, + len(header) + obj_bytes), + ts=ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.All = Testers.ContainsExpression( + "403 Access Denied", "The PUSH request should have received a 403 response.") + urtest.execute() diff --git a/tests/gold_tests/body_factory/config/remap.config b/tests/uranium_tests/body_factory/config/remap.config similarity index 100% rename from tests/gold_tests/body_factory/config/remap.config rename to tests/uranium_tests/body_factory/config/remap.config diff --git a/tests/gold_tests/body_factory/data/www.customplugin204.test_get.txt b/tests/uranium_tests/body_factory/data/www.customplugin204.test_get.txt similarity index 100% rename from tests/gold_tests/body_factory/data/www.customplugin204.test_get.txt rename to tests/uranium_tests/body_factory/data/www.customplugin204.test_get.txt diff --git a/tests/gold_tests/body_factory/data/www.customtemplate204.test_get.txt b/tests/uranium_tests/body_factory/data/www.customtemplate204.test_get.txt similarity index 100% rename from tests/gold_tests/body_factory/data/www.customtemplate204.test_get.txt rename to tests/uranium_tests/body_factory/data/www.customtemplate204.test_get.txt diff --git a/tests/gold_tests/body_factory/data/www.default204.test_get.txt b/tests/uranium_tests/body_factory/data/www.default204.test_get.txt similarity index 100% rename from tests/gold_tests/body_factory/data/www.default204.test_get.txt rename to tests/uranium_tests/body_factory/data/www.default204.test_get.txt diff --git a/tests/gold_tests/body_factory/data/www.default304.test_get.txt b/tests/uranium_tests/body_factory/data/www.default304.test_get.txt similarity index 100% rename from tests/gold_tests/body_factory/data/www.default304.test_get.txt rename to tests/uranium_tests/body_factory/data/www.default304.test_get.txt diff --git a/tests/gold_tests/body_factory/data/www.example.test_get_200.txt b/tests/uranium_tests/body_factory/data/www.example.test_get_200.txt similarity index 100% rename from tests/gold_tests/body_factory/data/www.example.test_get_200.txt rename to tests/uranium_tests/body_factory/data/www.example.test_get_200.txt diff --git a/tests/gold_tests/body_factory/data/www.example.test_get_304.txt b/tests/uranium_tests/body_factory/data/www.example.test_get_304.txt similarity index 100% rename from tests/gold_tests/body_factory/data/www.example.test_get_304.txt rename to tests/uranium_tests/body_factory/data/www.example.test_get_304.txt diff --git a/tests/gold_tests/body_factory/data/www.example.test_head.txt b/tests/uranium_tests/body_factory/data/www.example.test_head.txt similarity index 100% rename from tests/gold_tests/body_factory/data/www.example.test_head.txt rename to tests/uranium_tests/body_factory/data/www.example.test_head.txt diff --git a/tests/gold_tests/body_factory/data/www.example.test_head_200.txt b/tests/uranium_tests/body_factory/data/www.example.test_head_200.txt similarity index 100% rename from tests/gold_tests/body_factory/data/www.example.test_head_200.txt rename to tests/uranium_tests/body_factory/data/www.example.test_head_200.txt diff --git a/tests/gold_tests/body_factory/gold/http-204-custom-plugin.gold b/tests/uranium_tests/body_factory/gold/http-204-custom-plugin.gold similarity index 100% rename from tests/gold_tests/body_factory/gold/http-204-custom-plugin.gold rename to tests/uranium_tests/body_factory/gold/http-204-custom-plugin.gold diff --git a/tests/gold_tests/body_factory/gold/http-204-custom.gold b/tests/uranium_tests/body_factory/gold/http-204-custom.gold similarity index 100% rename from tests/gold_tests/body_factory/gold/http-204-custom.gold rename to tests/uranium_tests/body_factory/gold/http-204-custom.gold diff --git a/tests/gold_tests/body_factory/gold/http-204.gold b/tests/uranium_tests/body_factory/gold/http-204.gold similarity index 100% rename from tests/gold_tests/body_factory/gold/http-204.gold rename to tests/uranium_tests/body_factory/gold/http-204.gold diff --git a/tests/gold_tests/body_factory/gold/http-304.gold b/tests/uranium_tests/body_factory/gold/http-304.gold similarity index 100% rename from tests/gold_tests/body_factory/gold/http-304.gold rename to tests/uranium_tests/body_factory/gold/http-304.gold diff --git a/tests/gold_tests/body_factory/gold/http-get-200.gold b/tests/uranium_tests/body_factory/gold/http-get-200.gold similarity index 100% rename from tests/gold_tests/body_factory/gold/http-get-200.gold rename to tests/uranium_tests/body_factory/gold/http-get-200.gold diff --git a/tests/gold_tests/body_factory/gold/http-get-304.gold b/tests/uranium_tests/body_factory/gold/http-get-304.gold similarity index 100% rename from tests/gold_tests/body_factory/gold/http-get-304.gold rename to tests/uranium_tests/body_factory/gold/http-get-304.gold diff --git a/tests/gold_tests/body_factory/gold/http-head-200.gold b/tests/uranium_tests/body_factory/gold/http-head-200.gold similarity index 100% rename from tests/gold_tests/body_factory/gold/http-head-200.gold rename to tests/uranium_tests/body_factory/gold/http-head-200.gold diff --git a/tests/gold_tests/body_factory/gold/http-head-no-origin.gold b/tests/uranium_tests/body_factory/gold/http-head-no-origin.gold similarity index 100% rename from tests/gold_tests/body_factory/gold/http-head-no-origin.gold rename to tests/uranium_tests/body_factory/gold/http-head-no-origin.gold diff --git a/tests/uranium_tests/body_factory/test_http204_response.py b/tests/uranium_tests/body_factory/test_http204_response.py new file mode 100644 index 00000000000..1e715d5d412 --- /dev/null +++ b/tests/uranium_tests/body_factory/test_http204_response.py @@ -0,0 +1,107 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_http204_response(urtest: UraniumTest) -> None: + ''' + Tests that 204 responses conform to rfc2616, unless custom templates override. + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import os + import sys + + urtest.Summary = ''' + Tests that 204 responses conform to rfc2616, unless custom templates override. + ''' + + ts = urtest.MakeATSProcess("ts") + server = urtest.MakeOriginServer("server") + + DEFAULT_204_HOST = 'www.default204.test' + CUSTOM_TEMPLATE_204_HOST = 'www.customtemplate204.test' + + ts.Disk.records_config.update({ + # enable domain specific body factory + 'proxy.config.body_factory.enable_customizations': 3, + }) + + # Create a template body for a 204. + body_factory_dir = ts.Variables.BODY_FACTORY_TEMPLATE_DIR + ts.Disk.File(os.path.join(body_factory_dir, 'default', CUSTOM_TEMPLATE_204_HOST + '_default')).\ + WriteOn( + """ + + Spec-breaking 204! + + + +

This is body content for a 204.

+
+ + + Description: According to rfc7231 I should not have been sent to you! + +
+ + """) + + regex_remap_conf_file = "maps.reg" + + ts.Disk.remap_config.AddLine( + 'map http://{0} http://127.0.0.1:{1} @plugin=regex_remap.so @pparam={2} @pparam=no-query-string @pparam=host'.format( + DEFAULT_204_HOST, server.Variables.Port, regex_remap_conf_file)) + ts.Disk.remap_config.AddLine( + 'map http://{0} http://127.0.0.1:{1} @plugin=regex_remap.so @pparam={2} @pparam=no-query-string @pparam=host @plugin=conf_remap.so @pparam=proxy.config.body_factory.template_base={0}' + .format(CUSTOM_TEMPLATE_204_HOST, server.Variables.Port, regex_remap_conf_file)) + ts.Disk.MakeConfigFile(regex_remap_conf_file).AddLine('//.*/ http://127.0.0.1:{0} @status=204'.format(server.Variables.Port)) + + urtest.Setup.Copy(os.path.join(os.pardir, os.pardir, 'tools', 'tcp_client.py')) + urtest.Setup.Copy('data') + + defaultTr = urtest.AddTestRun("Test domain {0}".format(DEFAULT_204_HOST)) + defaultTr.Processes.Default.StartBefore(urtest.Processes.ts) + defaultTr.StillRunningAfter = ts + + defaultTr.Processes.Default.Command = f"{sys.executable} tcp_client.py 127.0.0.1 {ts.Variables.port} data/{DEFAULT_204_HOST}_get.txt" + defaultTr.Processes.Default.TimeOut = 5 # seconds + defaultTr.Processes.Default.ReturnCode = 0 + defaultTr.Processes.Default.Streams.stdout = "gold/http-204.gold" + + customTemplateTr = urtest.AddTestRun(f"Test domain {CUSTOM_TEMPLATE_204_HOST}") + customTemplateTr.StillRunningBefore = ts + customTemplateTr.StillRunningAfter = ts + customTemplateTr.Processes.Default.Command = f"{sys.executable} tcp_client.py 127.0.0.1 {ts.Variables.port} data/{CUSTOM_TEMPLATE_204_HOST}_get.txt" + customTemplateTr.Processes.Default.TimeOut = 5 # seconds + customTemplateTr.Processes.Default.ReturnCode = 0 + customTemplateTr.Processes.Default.Streams.stdout = "gold/http-204-custom.gold" + urtest.execute() diff --git a/tests/uranium_tests/body_factory/test_http204_response_plugin.py b/tests/uranium_tests/body_factory/test_http204_response_plugin.py new file mode 100644 index 00000000000..35dd52b02da --- /dev/null +++ b/tests/uranium_tests/body_factory/test_http204_response_plugin.py @@ -0,0 +1,72 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_http204_response_plugin(urtest: UraniumTest) -> None: + ''' + Tests that plugins may break HTTP by sending 204 response bodies + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import os + import sys + + urtest.Summary = ''' + Tests that plugins may break HTTP by sending 204 response bodies + ''' + + ts = urtest.MakeATSProcess("ts") + server = urtest.MakeOriginServer("server") + + CUSTOM_PLUGIN_204_HOST = 'www.customplugin204.test' + + regex_remap_conf_file = "maps.reg" + + ts.Disk.remap_config.AddLine( + f'map http://{CUSTOM_PLUGIN_204_HOST} http://127.0.0.1:{server.Variables.Port} @plugin=regex_remap.so @pparam={regex_remap_conf_file} @pparam=no-query-string @pparam=host' + ) + ts.Disk.MakeConfigFile(regex_remap_conf_file).AddLine('//.*/ http://donotcare.test @status=204') + + urtest.PrepareTestPlugin(os.path.join(urtest.Variables.AtsTestPluginsDir, 'custom204plugin.so'), ts) + + urtest.Setup.Copy(os.path.join(os.pardir, os.pardir, 'tools', 'tcp_client.py')) + urtest.Setup.Copy('data') + + tr = urtest.AddTestRun("Test domain {0}".format(CUSTOM_PLUGIN_204_HOST)) + tr.Processes.Default.StartBefore(urtest.Processes.ts) + tr.StillRunningAfter = ts + + tr.Processes.Default.Command = f"{sys.executable} tcp_client.py 127.0.0.1 {ts.Variables.port} data/{CUSTOM_PLUGIN_204_HOST}_get.txt" + tr.Processes.Default.TimeOut = 5 # seconds + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.stdout = "gold/http-204-custom-plugin.gold" + urtest.execute() diff --git a/tests/uranium_tests/body_factory/test_http304_response.py b/tests/uranium_tests/body_factory/test_http304_response.py new file mode 100644 index 00000000000..03de8daa1cf --- /dev/null +++ b/tests/uranium_tests/body_factory/test_http304_response.py @@ -0,0 +1,71 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_http304_response(urtest: UraniumTest) -> None: + ''' + Tests 304 responses + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import os + import sys + + urtest.Summary = ''' + Tests 304 responses + ''' + + ts = urtest.MakeATSProcess("ts") + server = urtest.MakeOriginServer("server") + + DEFAULT_304_HOST = 'www.default304.test' + + regex_remap_conf_file = "maps.reg" + + ts.Disk.remap_config.AddLine( + 'map http://{0} http://127.0.0.1:{1} @plugin=regex_remap.so @pparam={2} @pparam=no-query-string @pparam=host'.format( + DEFAULT_304_HOST, server.Variables.Port, regex_remap_conf_file)) + ts.Disk.MakeConfigFile(regex_remap_conf_file).AddLine('//.*/ http://127.0.0.1:{0} @status=304'.format(server.Variables.Port)) + + urtest.Setup.Copy(os.path.join(os.pardir, os.pardir, 'tools', 'tcp_client.py')) + urtest.Setup.Copy('data') + + tr = urtest.AddTestRun(f"Test domain {DEFAULT_304_HOST}") + tr.Processes.Default.StartBefore(urtest.Processes.ts) + tr.StillRunningAfter = ts + + cmd_tpl = f"{sys.executable} tcp_client.py 127.0.0.1 {ts.Variables.port} data/{DEFAULT_304_HOST}_get.txt" + tr.Processes.Default.Command = cmd_tpl + tr.Processes.Default.TimeOut = 5 # seconds + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.stdout = "gold/http-304.gold" + urtest.execute() diff --git a/tests/uranium_tests/body_factory/test_http_head_no_origin.py b/tests/uranium_tests/body_factory/test_http_head_no_origin.py new file mode 100644 index 00000000000..a529be7bb1c --- /dev/null +++ b/tests/uranium_tests/body_factory/test_http_head_no_origin.py @@ -0,0 +1,63 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_http_head_no_origin(urtest: UraniumTest) -> None: + ''' + Tests that HEAD requests return proper responses when origin fails + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import os + import sys + + urtest.Summary = ''' + Tests that HEAD requests return proper responses when origin fails + ''' + + ts = urtest.MakeATSProcess("ts") + server = urtest.MakeOriginServer("server") + + HOST = 'www.example.test' + + urtest.Setup.Copy(os.path.join(os.pardir, os.pardir, 'tools', 'tcp_client.py')) + urtest.Setup.Copy('data') + + tr = urtest.AddTestRun("Test domain {0}".format(HOST)) + tr.Processes.Default.StartBefore(urtest.Processes.ts) + tr.StillRunningAfter = ts + + tr.Processes.Default.Command = f"{sys.executable} tcp_client.py 127.0.0.1 {ts.Variables.port} data/{HOST}_head.txt" + tr.Processes.Default.TimeOut = 5 # seconds + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.stdout = "gold/http-head-no-origin.gold" + urtest.execute() diff --git a/tests/uranium_tests/body_factory/test_http_with_origin.py b/tests/uranium_tests/body_factory/test_http_with_origin.py new file mode 100644 index 00000000000..4dca582ba88 --- /dev/null +++ b/tests/uranium_tests/body_factory/test_http_with_origin.py @@ -0,0 +1,124 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_http_with_origin(urtest: UraniumTest) -> None: + ''' + Tests that HEAD requests return proper responses + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import os + import sys + + urtest.Summary = ''' + Tests that HEAD requests return proper responses + ''' + + ts = urtest.MakeATSProcess("ts") + + HOST = 'www.example.test' + + server = urtest.MakeOriginServer("server") + + ts.Disk.remap_config.AddLine('map http://{0} http://127.0.0.1:{1}'.format(HOST, server.Variables.Port)) + + server.addResponse( + "sessionfile.log", { + "headers": "HEAD /head200 HTTP/1.1\r\nHost: {0}\r\n\r\n".format(HOST), + "timestamp": "1469733493.993", + "body": "" + }, { + "headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", + "timestamp": "1469733493.993", + "body": "This body should not be returned for a HEAD request." + }) + + server.addResponse( + "sessionfile.log", { + "headers": "GET /get200 HTTP/1.1\r\nHost: {0}\r\n\r\n".format(HOST), + "timestamp": "1469733493.993", + "body": "" + }, { + "headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", + "timestamp": "1469733493.993", + "body": "This body should be returned for a GET request." + }) + + server.addResponse( + "sessionfile.log", { + "headers": "GET /get304 HTTP/1.1\r\nHost: {0}\r\n\r\n".format(HOST), + "timestamp": "1469733493.993", + "body": "" + }, { + "headers": "HTTP/1.1 304 Not Modified\r\nConnection: close\r\n\r\n", + "timestamp": "1469733493.993", + "body": "" + }) + + urtest.Setup.Copy(os.path.join(os.pardir, os.pardir, 'tools', 'tcp_client.py')) + urtest.Setup.Copy('data') + + trhead200 = urtest.AddTestRun("Test domain {0}".format(HOST)) + trhead200.Processes.Default.StartBefore(urtest.Processes.ts) + trhead200.Processes.Default.StartBefore(server, ready=When.PortOpen(server.Variables.Port)) + trhead200.StillRunningAfter = ts + trhead200.StillRunningAfter = server + + trhead200.Processes.Default.Command = f"{sys.executable} tcp_client.py 127.0.0.1 {ts.Variables.port} data/{HOST}_head_200.txt" + trhead200.Processes.Default.TimeOut = 5 # seconds + trhead200.Processes.Default.ReturnCode = 0 + trhead200.Processes.Default.Streams.stdout = "gold/http-head-200.gold" + + trget200 = urtest.AddTestRun("Test domain {0}".format(HOST)) + trget200.StillRunningBefore = ts + trget200.StillRunningBefore = server + trget200.StillRunningAfter = ts + trget200.StillRunningAfter = server + + trget200.Processes.Default.Command = f"{sys.executable} tcp_client.py 127.0.0.1 {ts.Variables.port} data/{HOST}_get_200.txt" + trget200.Processes.Default.TimeOut = 5 # seconds + trget200.Processes.Default.ReturnCode = 0 + trget200.Processes.Default.Streams.stdout = "gold/http-get-200.gold" + + trget304 = urtest.AddTestRun("Test domain {0}".format(HOST)) + trget304.StillRunningBefore = ts + trget304.StillRunningBefore = server + trget304.StillRunningAfter = ts + trget304.StillRunningAfter = server + + cmd_tpl = f"{sys.executable} tcp_client.py 127.0.0.1 {ts.Variables.port} data/{HOST}_get_304.txt" + trget304.Processes.Default.Command = cmd_tpl + trget304.Processes.Default.TimeOut = 5 # seconds + trget304.Processes.Default.ReturnCode = 0 + trget304.Processes.Default.Streams.stdout = "gold/http-get-304.gold" + urtest.execute() diff --git a/tests/gold_tests/cache/cache-write-lock-fail-write.conf b/tests/uranium_tests/cache/cache-write-lock-fail-write.conf similarity index 100% rename from tests/gold_tests/cache/cache-write-lock-fail-write.conf rename to tests/uranium_tests/cache/cache-write-lock-fail-write.conf diff --git a/tests/gold_tests/cache/cache_volume_defaults.test.yaml b/tests/uranium_tests/cache/cache_volume_defaults.test.yaml similarity index 99% rename from tests/gold_tests/cache/cache_volume_defaults.test.yaml rename to tests/uranium_tests/cache/cache_volume_defaults.test.yaml index a828f8efd4a..288f3d51a2e 100644 --- a/tests/gold_tests/cache/cache_volume_defaults.test.yaml +++ b/tests/uranium_tests/cache/cache_volume_defaults.test.yaml @@ -20,7 +20,7 @@ meta: # Test that proxy.config.cache.default_volumes and @volume= work correctly. # Uses a single volume at 100% so it fits in the 256MB min_cfg storage # (128MB stripe rounding requires at least 128MB per volume). -autest: +urtest: description: "Test proxy.config.cache.default_volumes configuration" dns: diff --git a/tests/gold_tests/cache/cache_volume_features.test.yaml b/tests/uranium_tests/cache/cache_volume_features.test.yaml similarity index 99% rename from tests/gold_tests/cache/cache_volume_features.test.yaml rename to tests/uranium_tests/cache/cache_volume_features.test.yaml index cb69a36ff59..8e2067bae46 100644 --- a/tests/gold_tests/cache/cache_volume_features.test.yaml +++ b/tests/uranium_tests/cache/cache_volume_features.test.yaml @@ -44,7 +44,7 @@ meta: # These exercise two distinct downstream paths (the RAM cache sizing in # cacheInitialized() and the directory sizing in the StripeSM constructor). On a # build that ignores per-volume settings on first start, none of them match. -autest: +urtest: description: "Test cache volume features: per-volume RAM cache and @volume= directive" dns: diff --git a/tests/gold_tests/cache/gold/background_fill_0_stderr_H.gold b/tests/uranium_tests/cache/gold/background_fill_0_stderr_H.gold similarity index 100% rename from tests/gold_tests/cache/gold/background_fill_0_stderr_H.gold rename to tests/uranium_tests/cache/gold/background_fill_0_stderr_H.gold diff --git a/tests/gold_tests/cache/gold/background_fill_1_stderr_H.gold b/tests/uranium_tests/cache/gold/background_fill_1_stderr_H.gold similarity index 100% rename from tests/gold_tests/cache/gold/background_fill_1_stderr_H.gold rename to tests/uranium_tests/cache/gold/background_fill_1_stderr_H.gold diff --git a/tests/gold_tests/cache/gold/background_fill_2_stderr_H.gold b/tests/uranium_tests/cache/gold/background_fill_2_stderr_H.gold similarity index 100% rename from tests/gold_tests/cache/gold/background_fill_2_stderr_H.gold rename to tests/uranium_tests/cache/gold/background_fill_2_stderr_H.gold diff --git a/tests/gold_tests/cache/gold/background_fill_3_stdout.gold b/tests/uranium_tests/cache/gold/background_fill_3_stdout.gold similarity index 100% rename from tests/gold_tests/cache/gold/background_fill_3_stdout.gold rename to tests/uranium_tests/cache/gold/background_fill_3_stdout.gold diff --git a/tests/gold_tests/cache/gold/cache_shm_state_after_shutdown.gold b/tests/uranium_tests/cache/gold/cache_shm_state_after_shutdown.gold similarity index 100% rename from tests/gold_tests/cache/gold/cache_shm_state_after_shutdown.gold rename to tests/uranium_tests/cache/gold/cache_shm_state_after_shutdown.gold diff --git a/tests/gold_tests/cache/gold/hit_default-1.gold b/tests/uranium_tests/cache/gold/hit_default-1.gold similarity index 100% rename from tests/gold_tests/cache/gold/hit_default-1.gold rename to tests/uranium_tests/cache/gold/hit_default-1.gold diff --git a/tests/gold_tests/cache/gold/hit_default0.gold b/tests/uranium_tests/cache/gold/hit_default0.gold similarity index 100% rename from tests/gold_tests/cache/gold/hit_default0.gold rename to tests/uranium_tests/cache/gold/hit_default0.gold diff --git a/tests/gold_tests/cache/gold/hit_gen1.gold b/tests/uranium_tests/cache/gold/hit_gen1.gold similarity index 100% rename from tests/gold_tests/cache/gold/hit_gen1.gold rename to tests/uranium_tests/cache/gold/hit_gen1.gold diff --git a/tests/gold_tests/cache/gold/hit_gen2.gold b/tests/uranium_tests/cache/gold/hit_gen2.gold similarity index 100% rename from tests/gold_tests/cache/gold/hit_gen2.gold rename to tests/uranium_tests/cache/gold/hit_gen2.gold diff --git a/tests/gold_tests/cache/gold/miss_default-1.gold b/tests/uranium_tests/cache/gold/miss_default-1.gold similarity index 100% rename from tests/gold_tests/cache/gold/miss_default-1.gold rename to tests/uranium_tests/cache/gold/miss_default-1.gold diff --git a/tests/gold_tests/cache/gold/miss_default0.gold b/tests/uranium_tests/cache/gold/miss_default0.gold similarity index 100% rename from tests/gold_tests/cache/gold/miss_default0.gold rename to tests/uranium_tests/cache/gold/miss_default0.gold diff --git a/tests/gold_tests/cache/gold/miss_gen1.gold b/tests/uranium_tests/cache/gold/miss_gen1.gold similarity index 100% rename from tests/gold_tests/cache/gold/miss_gen1.gold rename to tests/uranium_tests/cache/gold/miss_gen1.gold diff --git a/tests/gold_tests/cache/gold/miss_gen2.gold b/tests/uranium_tests/cache/gold/miss_gen2.gold similarity index 100% rename from tests/gold_tests/cache/gold/miss_gen2.gold rename to tests/uranium_tests/cache/gold/miss_gen2.gold diff --git a/tests/gold_tests/cache/gold/negative-revalidating-enabled-access.gold b/tests/uranium_tests/cache/gold/negative-revalidating-enabled-access.gold similarity index 100% rename from tests/gold_tests/cache/gold/negative-revalidating-enabled-access.gold rename to tests/uranium_tests/cache/gold/negative-revalidating-enabled-access.gold diff --git a/tests/gold_tests/cache/gold/serve_stale_dns_fail.gold b/tests/uranium_tests/cache/gold/serve_stale_dns_fail.gold similarity index 100% rename from tests/gold_tests/cache/gold/serve_stale_dns_fail.gold rename to tests/uranium_tests/cache/gold/serve_stale_dns_fail.gold diff --git a/tests/gold_tests/cache/gold/storage_metrics_0_stdout.gold b/tests/uranium_tests/cache/gold/storage_metrics_0_stdout.gold similarity index 100% rename from tests/gold_tests/cache/gold/storage_metrics_0_stdout.gold rename to tests/uranium_tests/cache/gold/storage_metrics_0_stdout.gold diff --git a/tests/gold_tests/cache/gold/storage_metrics_1_stdout.gold b/tests/uranium_tests/cache/gold/storage_metrics_1_stdout.gold similarity index 100% rename from tests/gold_tests/cache/gold/storage_metrics_1_stdout.gold rename to tests/uranium_tests/cache/gold/storage_metrics_1_stdout.gold diff --git a/tests/gold_tests/cache/gold/storage_metrics_2_stdout.gold b/tests/uranium_tests/cache/gold/storage_metrics_2_stdout.gold similarity index 100% rename from tests/gold_tests/cache/gold/storage_metrics_2_stdout.gold rename to tests/uranium_tests/cache/gold/storage_metrics_2_stdout.gold diff --git a/tests/gold_tests/cache/gold/storage_metrics_3_stdout.gold b/tests/uranium_tests/cache/gold/storage_metrics_3_stdout.gold similarity index 100% rename from tests/gold_tests/cache/gold/storage_metrics_3_stdout.gold rename to tests/uranium_tests/cache/gold/storage_metrics_3_stdout.gold diff --git a/tests/gold_tests/cache/gold/two_alternates_evicted.gold b/tests/uranium_tests/cache/gold/two_alternates_evicted.gold similarity index 100% rename from tests/gold_tests/cache/gold/two_alternates_evicted.gold rename to tests/uranium_tests/cache/gold/two_alternates_evicted.gold diff --git a/tests/gold_tests/cache/replay/alternate-caching-quality.test.yaml b/tests/uranium_tests/cache/replay/alternate-caching-quality.test.yaml similarity index 99% rename from tests/gold_tests/cache/replay/alternate-caching-quality.test.yaml rename to tests/uranium_tests/cache/replay/alternate-caching-quality.test.yaml index f37e482da9a..e0507fadae0 100644 --- a/tests/gold_tests/cache/replay/alternate-caching-quality.test.yaml +++ b/tests/uranium_tests/cache/replay/alternate-caching-quality.test.yaml @@ -18,7 +18,7 @@ meta: version: "1.0" -autest: +urtest: description: 'Verify TSHttpAltInfoQualitySet affects alternate selection' dns: diff --git a/tests/gold_tests/cache/replay/alternate-caching-update-size.test.yaml b/tests/uranium_tests/cache/replay/alternate-caching-update-size.test.yaml similarity index 99% rename from tests/gold_tests/cache/replay/alternate-caching-update-size.test.yaml rename to tests/uranium_tests/cache/replay/alternate-caching-update-size.test.yaml index ad7fdda5938..350975cb557 100644 --- a/tests/gold_tests/cache/replay/alternate-caching-update-size.test.yaml +++ b/tests/uranium_tests/cache/replay/alternate-caching-update-size.test.yaml @@ -50,7 +50,7 @@ meta: - [ Accept-Language, es_US ] delay: 100ms -autest: +urtest: description: 'Verify disabled negative revalidating behavior' dns: diff --git a/tests/gold_tests/cache/replay/auth-s-maxage.test.yaml b/tests/uranium_tests/cache/replay/auth-s-maxage.test.yaml similarity index 99% rename from tests/gold_tests/cache/replay/auth-s-maxage.test.yaml rename to tests/uranium_tests/cache/replay/auth-s-maxage.test.yaml index c0234d51f48..92af11f21f1 100644 --- a/tests/gold_tests/cache/replay/auth-s-maxage.test.yaml +++ b/tests/uranium_tests/cache/replay/auth-s-maxage.test.yaml @@ -26,8 +26,8 @@ meta: version: "1.0" -# Configuration section for autest integration -autest: +# Configuration for the Uranium test runner +urtest: description: 'Test s-maxage allows serving cached responses to requests with Authorization headers per RFC 7234 section 3.2' # Server configuration diff --git a/tests/gold_tests/cache/replay/bg_fill.yaml b/tests/uranium_tests/cache/replay/bg_fill.yaml similarity index 100% rename from tests/gold_tests/cache/replay/bg_fill.yaml rename to tests/uranium_tests/cache/replay/bg_fill.yaml diff --git a/tests/gold_tests/cache/replay/cache-control-basic.test.yaml b/tests/uranium_tests/cache/replay/cache-control-basic.test.yaml similarity index 99% rename from tests/gold_tests/cache/replay/cache-control-basic.test.yaml rename to tests/uranium_tests/cache/replay/cache-control-basic.test.yaml index 02a6264256f..bbed985375f 100644 --- a/tests/gold_tests/cache/replay/cache-control-basic.test.yaml +++ b/tests/uranium_tests/cache/replay/cache-control-basic.test.yaml @@ -17,8 +17,8 @@ meta: version: "1.0" -# Configuration section for autest integration -autest: +# Configuration for the Uranium test runner +urtest: description: 'Test basic cache operations: miss, hit, no-cache-control, stale, and only-if-cached' dns: diff --git a/tests/gold_tests/cache/replay/cache-control-max-age.test.yaml b/tests/uranium_tests/cache/replay/cache-control-max-age.test.yaml similarity index 99% rename from tests/gold_tests/cache/replay/cache-control-max-age.test.yaml rename to tests/uranium_tests/cache/replay/cache-control-max-age.test.yaml index 7024c571589..6197c3dbc23 100644 --- a/tests/gold_tests/cache/replay/cache-control-max-age.test.yaml +++ b/tests/uranium_tests/cache/replay/cache-control-max-age.test.yaml @@ -23,8 +23,8 @@ meta: version: "1.0" -# Configuration section for autest integration -autest: +# Configuration for the Uranium test runner +urtest: description: 'Verify correct handling of various max-age directives in both clients and responses' dns: diff --git a/tests/gold_tests/cache/replay/cache-control-pragma.test.yaml b/tests/uranium_tests/cache/replay/cache-control-pragma.test.yaml similarity index 98% rename from tests/gold_tests/cache/replay/cache-control-pragma.test.yaml rename to tests/uranium_tests/cache/replay/cache-control-pragma.test.yaml index a691977d091..6614dd3850c 100644 --- a/tests/gold_tests/cache/replay/cache-control-pragma.test.yaml +++ b/tests/uranium_tests/cache/replay/cache-control-pragma.test.yaml @@ -23,8 +23,8 @@ meta: version: "1.0" -# Configuration section for autest integration -autest: +# Configuration for the Uranium test runner +urtest: description: 'Verify correct interaction between cache-control no-cache and pragma header' dns: diff --git a/tests/gold_tests/cache/replay/cache-control-s-maxage.test.yaml b/tests/uranium_tests/cache/replay/cache-control-s-maxage.test.yaml similarity index 99% rename from tests/gold_tests/cache/replay/cache-control-s-maxage.test.yaml rename to tests/uranium_tests/cache/replay/cache-control-s-maxage.test.yaml index 97ea21933a1..b344daeffdc 100644 --- a/tests/gold_tests/cache/replay/cache-control-s-maxage.test.yaml +++ b/tests/uranium_tests/cache/replay/cache-control-s-maxage.test.yaml @@ -21,8 +21,8 @@ meta: version: "1.0" -# Configuration section for autest integration -autest: +# Configuration for the Uranium test runner +urtest: description: 'Verify correct handling of various s-maxage directives in responses' dns: diff --git a/tests/gold_tests/cache/replay/cache-exact-url.test.yaml b/tests/uranium_tests/cache/replay/cache-exact-url.test.yaml similarity index 99% rename from tests/gold_tests/cache/replay/cache-exact-url.test.yaml rename to tests/uranium_tests/cache/replay/cache-exact-url.test.yaml index da0efda6714..aea0d401439 100644 --- a/tests/gold_tests/cache/replay/cache-exact-url.test.yaml +++ b/tests/uranium_tests/cache/replay/cache-exact-url.test.yaml @@ -17,7 +17,7 @@ meta: version: "1.0" -autest: +urtest: description: 'Verify exact URL cache.config matching' server: diff --git a/tests/gold_tests/cache/replay/cache-heuristic-status.test.yaml b/tests/uranium_tests/cache/replay/cache-heuristic-status.test.yaml similarity index 99% rename from tests/gold_tests/cache/replay/cache-heuristic-status.test.yaml rename to tests/uranium_tests/cache/replay/cache-heuristic-status.test.yaml index fd6e9b204b9..b9768675234 100644 --- a/tests/gold_tests/cache/replay/cache-heuristic-status.test.yaml +++ b/tests/uranium_tests/cache/replay/cache-heuristic-status.test.yaml @@ -26,7 +26,7 @@ meta: version: "1.0" -autest: +urtest: description: 'Verify heuristic caching by status code per RFC 9110 Section 15.1' dns: diff --git a/tests/gold_tests/cache/replay/cache-range-response.test.yaml b/tests/uranium_tests/cache/replay/cache-range-response.test.yaml similarity index 99% rename from tests/gold_tests/cache/replay/cache-range-response.test.yaml rename to tests/uranium_tests/cache/replay/cache-range-response.test.yaml index cfebcd0d64b..048273a87ef 100644 --- a/tests/gold_tests/cache/replay/cache-range-response.test.yaml +++ b/tests/uranium_tests/cache/replay/cache-range-response.test.yaml @@ -18,7 +18,7 @@ meta: version: "1.0" -autest: +urtest: description: 'Verify correct caching behavior for range requests' dns: diff --git a/tests/gold_tests/cache/replay/cache-read-retry-stale.test.yaml b/tests/uranium_tests/cache/replay/cache-read-retry-stale.test.yaml similarity index 99% rename from tests/gold_tests/cache/replay/cache-read-retry-stale.test.yaml rename to tests/uranium_tests/cache/replay/cache-read-retry-stale.test.yaml index 61e36c6791c..f413951c755 100644 --- a/tests/gold_tests/cache/replay/cache-read-retry-stale.test.yaml +++ b/tests/uranium_tests/cache/replay/cache-read-retry-stale.test.yaml @@ -30,7 +30,7 @@ meta: version: "1.0" -autest: +urtest: description: 'Test cache_open_write_fail_action = 6 (READ_RETRY with stale fallback) smoke test' dns: name: 'dns-read-retry-stale' diff --git a/tests/gold_tests/cache/replay/cache-read-retry.test.yaml b/tests/uranium_tests/cache/replay/cache-read-retry.test.yaml similarity index 99% rename from tests/gold_tests/cache/replay/cache-read-retry.test.yaml rename to tests/uranium_tests/cache/replay/cache-read-retry.test.yaml index a488418d740..70e8cbb2502 100644 --- a/tests/gold_tests/cache/replay/cache-read-retry.test.yaml +++ b/tests/uranium_tests/cache/replay/cache-read-retry.test.yaml @@ -36,7 +36,7 @@ meta: version: "1.0" -autest: +urtest: description: 'Test cache_open_write_fail_action = 5 (READ_RETRY) with slow origin and concurrent requests to verify request collapsing and stability' dns: name: 'dns-read-retry-exhausted' diff --git a/tests/gold_tests/cache/replay/cache-shm-dir-invalid.replay.yaml b/tests/uranium_tests/cache/replay/cache-shm-dir-invalid.replay.yaml similarity index 100% rename from tests/gold_tests/cache/replay/cache-shm-dir-invalid.replay.yaml rename to tests/uranium_tests/cache/replay/cache-shm-dir-invalid.replay.yaml diff --git a/tests/gold_tests/cache/replay/cache-shm-fast-restart.replay.yaml b/tests/uranium_tests/cache/replay/cache-shm-fast-restart.replay.yaml similarity index 100% rename from tests/gold_tests/cache/replay/cache-shm-fast-restart.replay.yaml rename to tests/uranium_tests/cache/replay/cache-shm-fast-restart.replay.yaml diff --git a/tests/gold_tests/cache/replay/cache-write-lock-stale-retry.test.yaml b/tests/uranium_tests/cache/replay/cache-write-lock-stale-retry.test.yaml similarity index 99% rename from tests/gold_tests/cache/replay/cache-write-lock-stale-retry.test.yaml rename to tests/uranium_tests/cache/replay/cache-write-lock-stale-retry.test.yaml index 6fe5975b61d..8e67e2151c1 100644 --- a/tests/gold_tests/cache/replay/cache-write-lock-stale-retry.test.yaml +++ b/tests/uranium_tests/cache/replay/cache-write-lock-stale-retry.test.yaml @@ -41,7 +41,7 @@ meta: - [ Content-Length, 16 ] - [ X-Response, origin ] -autest: +urtest: description: 'Verify a lost write lock on revalidation can retry the cache read' dns: diff --git a/tests/gold_tests/cache/replay/cache-write-lock-stale-serve.test.yaml b/tests/uranium_tests/cache/replay/cache-write-lock-stale-serve.test.yaml similarity index 99% rename from tests/gold_tests/cache/replay/cache-write-lock-stale-serve.test.yaml rename to tests/uranium_tests/cache/replay/cache-write-lock-stale-serve.test.yaml index 1b4924e0a09..b61358f32f0 100644 --- a/tests/gold_tests/cache/replay/cache-write-lock-stale-serve.test.yaml +++ b/tests/uranium_tests/cache/replay/cache-write-lock-stale-serve.test.yaml @@ -39,7 +39,7 @@ meta: - [ Content-Length, 16 ] - [ X-Response, origin ] -autest: +urtest: description: 'Verify a lost write lock on revalidation serves the stale object' dns: diff --git a/tests/gold_tests/cache/replay/cache-write-retry-callback.test.yaml b/tests/uranium_tests/cache/replay/cache-write-retry-callback.test.yaml similarity index 99% rename from tests/gold_tests/cache/replay/cache-write-retry-callback.test.yaml rename to tests/uranium_tests/cache/replay/cache-write-retry-callback.test.yaml index 9e3627803b7..f19f0e9bff6 100644 --- a/tests/gold_tests/cache/replay/cache-write-retry-callback.test.yaml +++ b/tests/uranium_tests/cache/replay/cache-write-retry-callback.test.yaml @@ -28,7 +28,7 @@ meta: version: "1.0" -autest: +urtest: description: 'Verify a cache write retry preserves the cache read callback that follows it' dns: name: 'dns-write-retry-callback' diff --git a/tests/gold_tests/cache/replay/conditional-get-cache-hit.test.yaml b/tests/uranium_tests/cache/replay/conditional-get-cache-hit.test.yaml similarity index 99% rename from tests/gold_tests/cache/replay/conditional-get-cache-hit.test.yaml rename to tests/uranium_tests/cache/replay/conditional-get-cache-hit.test.yaml index f5e75382a9d..9a48ee3b646 100644 --- a/tests/gold_tests/cache/replay/conditional-get-cache-hit.test.yaml +++ b/tests/uranium_tests/cache/replay/conditional-get-cache-hit.test.yaml @@ -17,7 +17,7 @@ meta: version: "1.0" -autest: +urtest: description: 'Verify conditional get with cache hit drains client body' dns: diff --git a/tests/gold_tests/cache/replay/cookie-all-but-text-with-excp.test.yaml b/tests/uranium_tests/cache/replay/cookie-all-but-text-with-excp.test.yaml similarity index 99% rename from tests/gold_tests/cache/replay/cookie-all-but-text-with-excp.test.yaml rename to tests/uranium_tests/cache/replay/cookie-all-but-text-with-excp.test.yaml index b8fb5ba0b64..4255c7d8456 100644 --- a/tests/gold_tests/cache/replay/cookie-all-but-text-with-excp.test.yaml +++ b/tests/uranium_tests/cache/replay/cookie-all-but-text-with-excp.test.yaml @@ -23,8 +23,8 @@ meta: version: "1.0" -# Configuration section for autest integration -autest: +# Configuration for the Uranium test runner +urtest: dns: name: 'dns-cookie_all_but_text_with_excp' description: 'Verify the correct caching behavior when ATS is configured to cache all content types but text, but with a few exceptions for text types which would also be cached' diff --git a/tests/gold_tests/cache/replay/cookie-all-but-text.test.yaml b/tests/uranium_tests/cache/replay/cookie-all-but-text.test.yaml similarity index 99% rename from tests/gold_tests/cache/replay/cookie-all-but-text.test.yaml rename to tests/uranium_tests/cache/replay/cookie-all-but-text.test.yaml index 82005644154..dad47f004f3 100644 --- a/tests/gold_tests/cache/replay/cookie-all-but-text.test.yaml +++ b/tests/uranium_tests/cache/replay/cookie-all-but-text.test.yaml @@ -22,8 +22,8 @@ meta: version: "1.0" -# Configuration section for autest integration -autest: +# Configuration for the Uranium test runner +urtest: dns: name: 'dns-cookie_all_but_text' description: 'Verify the correct caching behavior when ATS is configured to cache response to cookie for all but text types' diff --git a/tests/gold_tests/cache/replay/cookie-bypass-cache.test.yaml b/tests/uranium_tests/cache/replay/cookie-bypass-cache.test.yaml similarity index 99% rename from tests/gold_tests/cache/replay/cookie-bypass-cache.test.yaml rename to tests/uranium_tests/cache/replay/cookie-bypass-cache.test.yaml index 9e0ab697ac0..c6a2dc09c2c 100644 --- a/tests/gold_tests/cache/replay/cookie-bypass-cache.test.yaml +++ b/tests/uranium_tests/cache/replay/cookie-bypass-cache.test.yaml @@ -22,8 +22,8 @@ meta: version: "1.0" -# Configuration section for autest integration -autest: +# Configuration for the Uranium test runner +urtest: dns: name: 'dns-cookie_bypass_cache' description: 'Verify the correct caching behavior when ATS is configured to not cache response to cookie for any content type' diff --git a/tests/gold_tests/cache/replay/cookie-cache-img-only.test.yaml b/tests/uranium_tests/cache/replay/cookie-cache-img-only.test.yaml similarity index 99% rename from tests/gold_tests/cache/replay/cookie-cache-img-only.test.yaml rename to tests/uranium_tests/cache/replay/cookie-cache-img-only.test.yaml index ac3502a0340..c29a273019f 100644 --- a/tests/gold_tests/cache/replay/cookie-cache-img-only.test.yaml +++ b/tests/uranium_tests/cache/replay/cookie-cache-img-only.test.yaml @@ -22,8 +22,8 @@ meta: version: "1.0" -# Configuration section for autest integration -autest: +# Configuration for the Uranium test runner +urtest: dns: name: 'dns-cookie_cache_img_only' description: 'Verify the correct caching behavior when ATS is configured to cache response to cookie only for image content type' diff --git a/tests/gold_tests/cache/replay/cookie-default.test.yaml b/tests/uranium_tests/cache/replay/cookie-default.test.yaml similarity index 99% rename from tests/gold_tests/cache/replay/cookie-default.test.yaml rename to tests/uranium_tests/cache/replay/cookie-default.test.yaml index c07ac6a8db5..52bf8d11404 100644 --- a/tests/gold_tests/cache/replay/cookie-default.test.yaml +++ b/tests/uranium_tests/cache/replay/cookie-default.test.yaml @@ -22,8 +22,8 @@ meta: version: "1.0" -# Configuration section for autest integration -autest: +# Configuration for the Uranium test runner +urtest: dns: name: 'dns-cookie_default' description: 'Verify the correct caching behavior when ATS is in default configuration' diff --git a/tests/gold_tests/cache/replay/delete_cached.test.yaml b/tests/uranium_tests/cache/replay/delete_cached.test.yaml similarity index 98% rename from tests/gold_tests/cache/replay/delete_cached.test.yaml rename to tests/uranium_tests/cache/replay/delete_cached.test.yaml index f6cb62a422f..ec9152ee832 100644 --- a/tests/gold_tests/cache/replay/delete_cached.test.yaml +++ b/tests/uranium_tests/cache/replay/delete_cached.test.yaml @@ -17,8 +17,8 @@ meta: version: "1.0" -# Configuration section for autest integration -autest: +# Configuration for the Uranium test runner +urtest: description: 'Verify DELETE method handling' dns: diff --git a/tests/gold_tests/cache/replay/get_then_post.replay.yaml b/tests/uranium_tests/cache/replay/get_then_post.replay.yaml similarity index 100% rename from tests/gold_tests/cache/replay/get_then_post.replay.yaml rename to tests/uranium_tests/cache/replay/get_then_post.replay.yaml diff --git a/tests/gold_tests/cache/replay/head_with_get_cached.test.yaml b/tests/uranium_tests/cache/replay/head_with_get_cached.test.yaml similarity index 99% rename from tests/gold_tests/cache/replay/head_with_get_cached.test.yaml rename to tests/uranium_tests/cache/replay/head_with_get_cached.test.yaml index b109857ae9c..1bb91a4f991 100644 --- a/tests/gold_tests/cache/replay/head_with_get_cached.test.yaml +++ b/tests/uranium_tests/cache/replay/head_with_get_cached.test.yaml @@ -17,8 +17,8 @@ meta: version: "1.0" -# Configuration section for autest integration -autest: +# Configuration for the Uranium test runner +urtest: dns: name: 'dns-head_with_get_cached' description: 'Verify correct HEAD response handling with cached GET response' diff --git a/tests/gold_tests/cache/replay/host_down_range_recursion_prime.replay.yaml b/tests/uranium_tests/cache/replay/host_down_range_recursion_prime.replay.yaml similarity index 100% rename from tests/gold_tests/cache/replay/host_down_range_recursion_prime.replay.yaml rename to tests/uranium_tests/cache/replay/host_down_range_recursion_prime.replay.yaml diff --git a/tests/gold_tests/cache/replay/host_down_range_recursion_range.replay.yaml b/tests/uranium_tests/cache/replay/host_down_range_recursion_range.replay.yaml similarity index 100% rename from tests/gold_tests/cache/replay/host_down_range_recursion_range.replay.yaml rename to tests/uranium_tests/cache/replay/host_down_range_recursion_range.replay.yaml diff --git a/tests/gold_tests/cache/replay/ignore_authentication.test.yaml b/tests/uranium_tests/cache/replay/ignore_authentication.test.yaml similarity index 98% rename from tests/gold_tests/cache/replay/ignore_authentication.test.yaml rename to tests/uranium_tests/cache/replay/ignore_authentication.test.yaml index eb04957d2ab..4f5ee841817 100644 --- a/tests/gold_tests/cache/replay/ignore_authentication.test.yaml +++ b/tests/uranium_tests/cache/replay/ignore_authentication.test.yaml @@ -21,8 +21,8 @@ meta: version: "1.0" -# Configuration section for autest integration -autest: +# Configuration for the Uranium test runner +urtest: dns: name: 'dns-auth' description: 'Verify the proper caching behavior for request/response containing auth-related fields in both default and ignore_authentication configurations' diff --git a/tests/uranium_tests/cache/replay/max-stale-age-percent.test.yaml b/tests/uranium_tests/cache/replay/max-stale-age-percent.test.yaml new file mode 100644 index 00000000000..8c83099b706 --- /dev/null +++ b/tests/uranium_tests/cache/replay/max-stale-age-percent.test.yaml @@ -0,0 +1,117 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +meta: + version: "1.0" + + blocks: + + - origin_response: &origin_response + server-response: + status: 500 + reason: "Internal Server Error" + headers: + fields: + - [ Content-Length, 16 ] + - [ X-Response, should_not_see ] + +urtest: + description: 'Verify the percentage maximum stale age limit' + + dns: + name: 'dns-max-stale-age-percent' + + server: + name: 'server-max-stale-age-percent' + + client: + name: 'client-max-stale-age-percent' + + ats: + name: 'ts-max-stale-age-percent' + process_config: + enable_cache: true + + records_config: + proxy.config.http.push_method_enabled: 1 + proxy.config.http.parent_proxy.fail_threshold: 1 + proxy.config.http.parent_proxy.total_connect_attempts: 1 + proxy.config.http.cache.max_stale_age: 100 + proxy.config.http.cache.max_stale_age_percent: 50 + proxy.config.http.parent_proxy.self_detect: 0 + + parent_config: + - 'dest_domain=. parent="localhost:82" round_robin=consistent_hash go_direct=false' + + remap_config: + - from: "http://example.com/" + to: "http://localhost:{SERVER_HTTP_PORT}/" + +sessions: +- transactions: + + # Populate the cache with a four-second freshness lifetime. + - client-request: + method: "PUSH" + version: "1.1" + url: /percentage + headers: + fields: + - [ Host, example.com ] + - [ uuid, push ] + - [ Content-Length, 73 ] + content: + encoding: plain + data: "HTTP/1.1 200 OK\nContent-Length: 6\nCache-Control: public,max-age=4\n\nCACHED" + + <<: *origin_response + + proxy-response: + status: 201 + + # At five seconds old, the object is stale but remains within the two-second + # percentage stale window. + - client-request: + delay: 5s + method: "GET" + version: "1.1" + url: /percentage + headers: + fields: + - [ Host, example.com ] + - [ uuid, within_percentage_limit ] + + <<: *origin_response + + proxy-response: + status: 200 + + # At eight seconds old, the object exceeds max-age (4 seconds) plus 50% + # (2 seconds), even though the absolute stale age limit is 100 seconds. + - client-request: + delay: 3s + method: "GET" + version: "1.1" + url: /percentage + headers: + fields: + - [ Host, example.com ] + - [ uuid, past_percentage_limit ] + + <<: *origin_response + + proxy-response: + status: 502 diff --git a/tests/gold_tests/cache/replay/negative-caching-300-second-timeout.replay.yaml b/tests/uranium_tests/cache/replay/negative-caching-300-second-timeout.replay.yaml similarity index 100% rename from tests/gold_tests/cache/replay/negative-caching-300-second-timeout.replay.yaml rename to tests/uranium_tests/cache/replay/negative-caching-300-second-timeout.replay.yaml diff --git a/tests/gold_tests/cache/replay/negative-caching-customized.test.yaml b/tests/uranium_tests/cache/replay/negative-caching-customized.test.yaml similarity index 98% rename from tests/gold_tests/cache/replay/negative-caching-customized.test.yaml rename to tests/uranium_tests/cache/replay/negative-caching-customized.test.yaml index be84a14db06..f45d2bb3607 100644 --- a/tests/gold_tests/cache/replay/negative-caching-customized.test.yaml +++ b/tests/uranium_tests/cache/replay/negative-caching-customized.test.yaml @@ -23,8 +23,8 @@ meta: version: "1.0" -# Configuration section for autest integration -autest: +# Configuration for the Uranium test runner +urtest: dns: name: 'dns-neg_caching_customized' description: 'Verify customized negative caching list' diff --git a/tests/gold_tests/cache/replay/negative-caching-default.test.yaml b/tests/uranium_tests/cache/replay/negative-caching-default.test.yaml similarity index 99% rename from tests/gold_tests/cache/replay/negative-caching-default.test.yaml rename to tests/uranium_tests/cache/replay/negative-caching-default.test.yaml index b2c48d0a96c..9825a07d542 100644 --- a/tests/gold_tests/cache/replay/negative-caching-default.test.yaml +++ b/tests/uranium_tests/cache/replay/negative-caching-default.test.yaml @@ -22,8 +22,8 @@ meta: version: "1.0" -# Configuration section for autest integration -autest: +# Configuration for the Uranium test runner +urtest: dns: name: 'dns-neg_caching_default' description: 'Verify default negative caching behavior' diff --git a/tests/gold_tests/cache/replay/negative-caching-disabled.test.yaml b/tests/uranium_tests/cache/replay/negative-caching-disabled.test.yaml similarity index 99% rename from tests/gold_tests/cache/replay/negative-caching-disabled.test.yaml rename to tests/uranium_tests/cache/replay/negative-caching-disabled.test.yaml index 7f91ee40f2f..7b3665543ca 100644 --- a/tests/gold_tests/cache/replay/negative-caching-disabled.test.yaml +++ b/tests/uranium_tests/cache/replay/negative-caching-disabled.test.yaml @@ -21,8 +21,8 @@ meta: version: "1.0" -# Configuration section for autest integration -autest: +# Configuration for the Uranium test runner +urtest: dns: name: 'dns-neg_caching_disabled' description: 'Verify correct behavior without negative caching enabled' diff --git a/tests/gold_tests/cache/replay/negative-caching-malformed-cc.test.yaml b/tests/uranium_tests/cache/replay/negative-caching-malformed-cc.test.yaml similarity index 99% rename from tests/gold_tests/cache/replay/negative-caching-malformed-cc.test.yaml rename to tests/uranium_tests/cache/replay/negative-caching-malformed-cc.test.yaml index dccee65e2b4..452e154c4bc 100644 --- a/tests/gold_tests/cache/replay/negative-caching-malformed-cc.test.yaml +++ b/tests/uranium_tests/cache/replay/negative-caching-malformed-cc.test.yaml @@ -22,8 +22,8 @@ meta: version: "1.0" -# Configuration section for autest integration -autest: +# Configuration for the Uranium test runner +urtest: description: 'Test malformed Cache-Control header with semicolons instead of commas (issue #12029)' # Server configuration diff --git a/tests/gold_tests/cache/replay/negative-caching-no-timeout.replay.yaml b/tests/uranium_tests/cache/replay/negative-caching-no-timeout.replay.yaml similarity index 100% rename from tests/gold_tests/cache/replay/negative-caching-no-timeout.replay.yaml rename to tests/uranium_tests/cache/replay/negative-caching-no-timeout.replay.yaml diff --git a/tests/gold_tests/cache/replay/negative-caching-timeout.replay.yaml b/tests/uranium_tests/cache/replay/negative-caching-timeout.replay.yaml similarity index 100% rename from tests/gold_tests/cache/replay/negative-caching-timeout.replay.yaml rename to tests/uranium_tests/cache/replay/negative-caching-timeout.replay.yaml diff --git a/tests/gold_tests/cache/replay/negative-caching-ttl-in-cache.replay.yaml b/tests/uranium_tests/cache/replay/negative-caching-ttl-in-cache.replay.yaml similarity index 100% rename from tests/gold_tests/cache/replay/negative-caching-ttl-in-cache.replay.yaml rename to tests/uranium_tests/cache/replay/negative-caching-ttl-in-cache.replay.yaml diff --git a/tests/gold_tests/cache/replay/negative-revalidating-disabled.test.yaml b/tests/uranium_tests/cache/replay/negative-revalidating-disabled.test.yaml similarity index 98% rename from tests/gold_tests/cache/replay/negative-revalidating-disabled.test.yaml rename to tests/uranium_tests/cache/replay/negative-revalidating-disabled.test.yaml index 93e3a71a483..4ced8f146ad 100644 --- a/tests/gold_tests/cache/replay/negative-revalidating-disabled.test.yaml +++ b/tests/uranium_tests/cache/replay/negative-revalidating-disabled.test.yaml @@ -22,8 +22,8 @@ meta: version: "1.0" -# Configuration section for autest integration -autest: +# Configuration for the Uranium test runner +urtest: dns: name: 'dns-neg_revalidating_disabled' description: 'Verify negative revalidating disabled' diff --git a/tests/gold_tests/cache/replay/negative-revalidating-enabled.test.yaml b/tests/uranium_tests/cache/replay/negative-revalidating-enabled.test.yaml similarity index 99% rename from tests/gold_tests/cache/replay/negative-revalidating-enabled.test.yaml rename to tests/uranium_tests/cache/replay/negative-revalidating-enabled.test.yaml index fdf7fca6fc6..a29161ecadd 100644 --- a/tests/gold_tests/cache/replay/negative-revalidating-enabled.test.yaml +++ b/tests/uranium_tests/cache/replay/negative-revalidating-enabled.test.yaml @@ -23,8 +23,8 @@ meta: version: "1.0" -# Configuration section for autest integration -autest: +# Configuration for the Uranium test runner +urtest: dns: name: 'dns-neg_revalidating_enabled' description: 'Verify negative revalidating enabled' diff --git a/tests/gold_tests/cache/replay/negative-revalidating-list.test.yaml b/tests/uranium_tests/cache/replay/negative-revalidating-list.test.yaml similarity index 98% rename from tests/gold_tests/cache/replay/negative-revalidating-list.test.yaml rename to tests/uranium_tests/cache/replay/negative-revalidating-list.test.yaml index af23465b161..cdd3cae1801 100644 --- a/tests/gold_tests/cache/replay/negative-revalidating-list.test.yaml +++ b/tests/uranium_tests/cache/replay/negative-revalidating-list.test.yaml @@ -24,8 +24,8 @@ meta: version: "1.0" -# Configuration section for autest integration -autest: +# Configuration for the Uranium test runner +urtest: dns: name: 'dns-neg_revalidating_list' description: 'Verify negative_revalidating_list behavior' diff --git a/tests/gold_tests/cache/replay/post_with_post_caching_disabled.test.yaml b/tests/uranium_tests/cache/replay/post_with_post_caching_disabled.test.yaml similarity index 99% rename from tests/gold_tests/cache/replay/post_with_post_caching_disabled.test.yaml rename to tests/uranium_tests/cache/replay/post_with_post_caching_disabled.test.yaml index bd16681e34f..93df57f71f4 100644 --- a/tests/gold_tests/cache/replay/post_with_post_caching_disabled.test.yaml +++ b/tests/uranium_tests/cache/replay/post_with_post_caching_disabled.test.yaml @@ -17,8 +17,8 @@ meta: version: "1.0" -# Configuration section for autest integration -autest: +# Configuration for the Uranium test runner +urtest: dns: name: 'dns-post_with_post_caching_disabled' dns: diff --git a/tests/gold_tests/cache/replay/post_with_post_caching_enabled.test.yaml b/tests/uranium_tests/cache/replay/post_with_post_caching_enabled.test.yaml similarity index 99% rename from tests/gold_tests/cache/replay/post_with_post_caching_enabled.test.yaml rename to tests/uranium_tests/cache/replay/post_with_post_caching_enabled.test.yaml index e56e5cc01b2..94e4a2d3db0 100644 --- a/tests/gold_tests/cache/replay/post_with_post_caching_enabled.test.yaml +++ b/tests/uranium_tests/cache/replay/post_with_post_caching_enabled.test.yaml @@ -17,8 +17,8 @@ meta: version: "1.0" -# Configuration section for autest integration -autest: +# Configuration for the Uranium test runner +urtest: dns: name: 'dns-post_with_post_caching_enabled' dns: diff --git a/tests/gold_tests/cache/replay/post_with_post_caching_override.test.yaml b/tests/uranium_tests/cache/replay/post_with_post_caching_override.test.yaml similarity index 98% rename from tests/gold_tests/cache/replay/post_with_post_caching_override.test.yaml rename to tests/uranium_tests/cache/replay/post_with_post_caching_override.test.yaml index 0ee3b194168..12ab5460925 100644 --- a/tests/gold_tests/cache/replay/post_with_post_caching_override.test.yaml +++ b/tests/uranium_tests/cache/replay/post_with_post_caching_override.test.yaml @@ -17,8 +17,8 @@ meta: version: "1.0" -# Configuration section for autest integration -autest: +# Configuration for the Uranium test runner +urtest: dns: name: 'dns-post_with_post_caching_override' dns: diff --git a/tests/gold_tests/cache/replay/proxy_serve_stale.test.yaml b/tests/uranium_tests/cache/replay/proxy_serve_stale.test.yaml similarity index 99% rename from tests/gold_tests/cache/replay/proxy_serve_stale.test.yaml rename to tests/uranium_tests/cache/replay/proxy_serve_stale.test.yaml index b3a84d1b571..828c51aeb3f 100644 --- a/tests/gold_tests/cache/replay/proxy_serve_stale.test.yaml +++ b/tests/uranium_tests/cache/replay/proxy_serve_stale.test.yaml @@ -36,7 +36,7 @@ meta: - [ Content-Length, 16 ] - [ X-Response, should_not_see ] -autest: +urtest: description: 'Verify that stale content is served when the parent is down' dns: diff --git a/tests/gold_tests/cache/replay/proxy_serve_stale_origin_down.test.yaml b/tests/uranium_tests/cache/replay/proxy_serve_stale_origin_down.test.yaml similarity index 99% rename from tests/gold_tests/cache/replay/proxy_serve_stale_origin_down.test.yaml rename to tests/uranium_tests/cache/replay/proxy_serve_stale_origin_down.test.yaml index c1ca7e90eaa..ed5d23efa30 100644 --- a/tests/gold_tests/cache/replay/proxy_serve_stale_origin_down.test.yaml +++ b/tests/uranium_tests/cache/replay/proxy_serve_stale_origin_down.test.yaml @@ -17,7 +17,7 @@ meta: version: "1.0" -autest: +urtest: description: 'Verify that stale content is served when the origin server is down' dns: diff --git a/tests/gold_tests/cache/replay/request-cache-control-default.test.yaml b/tests/uranium_tests/cache/replay/request-cache-control-default.test.yaml similarity index 98% rename from tests/gold_tests/cache/replay/request-cache-control-default.test.yaml rename to tests/uranium_tests/cache/replay/request-cache-control-default.test.yaml index e0a0d91f1ad..f0685761a11 100644 --- a/tests/gold_tests/cache/replay/request-cache-control-default.test.yaml +++ b/tests/uranium_tests/cache/replay/request-cache-control-default.test.yaml @@ -21,8 +21,8 @@ meta: version: "1.0" -# Configuration section for autest integration -autest: +# Configuration for the Uranium test runner +urtest: description: 'Verify the proper handling of cache-control directives in requests in default configuration' dns: diff --git a/tests/gold_tests/cache/replay/request-cache-control-honor-client.test.yaml b/tests/uranium_tests/cache/replay/request-cache-control-honor-client.test.yaml similarity index 99% rename from tests/gold_tests/cache/replay/request-cache-control-honor-client.test.yaml rename to tests/uranium_tests/cache/replay/request-cache-control-honor-client.test.yaml index a5e8faf3153..28f49fec849 100644 --- a/tests/gold_tests/cache/replay/request-cache-control-honor-client.test.yaml +++ b/tests/uranium_tests/cache/replay/request-cache-control-honor-client.test.yaml @@ -22,8 +22,8 @@ meta: version: "1.0" -# Configuration section for autest integration -autest: +# Configuration for the Uranium test runner +urtest: description: 'Verify the proper handling of cache-control directives in requests when ATS is configured to honor client requests to bypass the cache' dns: diff --git a/tests/gold_tests/cache/replay/response-cache-control-default.test.yaml b/tests/uranium_tests/cache/replay/response-cache-control-default.test.yaml similarity index 98% rename from tests/gold_tests/cache/replay/response-cache-control-default.test.yaml rename to tests/uranium_tests/cache/replay/response-cache-control-default.test.yaml index 3ecfa831314..49d08db2b8e 100644 --- a/tests/gold_tests/cache/replay/response-cache-control-default.test.yaml +++ b/tests/uranium_tests/cache/replay/response-cache-control-default.test.yaml @@ -22,8 +22,8 @@ meta: version: "1.0" -# Configuration section for autest integration -autest: +# Configuration for the Uranium test runner +urtest: description: 'Verify the proper handling of cache-control directives in responses in default configuration' dns: diff --git a/tests/gold_tests/cache/replay/response-cache-control-ignored.test.yaml b/tests/uranium_tests/cache/replay/response-cache-control-ignored.test.yaml similarity index 99% rename from tests/gold_tests/cache/replay/response-cache-control-ignored.test.yaml rename to tests/uranium_tests/cache/replay/response-cache-control-ignored.test.yaml index f97c54bd966..a2c9cbe4e8b 100644 --- a/tests/gold_tests/cache/replay/response-cache-control-ignored.test.yaml +++ b/tests/uranium_tests/cache/replay/response-cache-control-ignored.test.yaml @@ -21,8 +21,8 @@ meta: version: "1.0" -# Configuration section for autest integration -autest: +# Configuration for the Uranium test runner +urtest: description: 'Verify the proper handling of cache-control directives in responses when ATS is configured to ignore server requests to bypass the cache' dns: diff --git a/tests/gold_tests/cache/replay/targeted-cache-control.test.yaml b/tests/uranium_tests/cache/replay/targeted-cache-control.test.yaml similarity index 99% rename from tests/gold_tests/cache/replay/targeted-cache-control.test.yaml rename to tests/uranium_tests/cache/replay/targeted-cache-control.test.yaml index bd51379e898..d49c96a9717 100644 --- a/tests/gold_tests/cache/replay/targeted-cache-control.test.yaml +++ b/tests/uranium_tests/cache/replay/targeted-cache-control.test.yaml @@ -20,8 +20,8 @@ meta: version: "1.0" -# Configuration section for autest integration -autest: +# Configuration for the Uranium test runner +urtest: description: 'Test targeted cache control headers per RFC 9213' dns: diff --git a/tests/gold_tests/cache/replay/varied_transactions.test.yaml b/tests/uranium_tests/cache/replay/varied_transactions.test.yaml similarity index 99% rename from tests/gold_tests/cache/replay/varied_transactions.test.yaml rename to tests/uranium_tests/cache/replay/varied_transactions.test.yaml index 31e13279e24..87295829955 100644 --- a/tests/gold_tests/cache/replay/varied_transactions.test.yaml +++ b/tests/uranium_tests/cache/replay/varied_transactions.test.yaml @@ -33,7 +33,7 @@ meta: fields: - [ Content-Length, 0 ] -autest: +urtest: description: 'Run traffic with max_alts behavior when set to 4' dns: diff --git a/tests/gold_tests/cache/shm_poke.py b/tests/uranium_tests/cache/shm_poke.py similarity index 99% rename from tests/gold_tests/cache/shm_poke.py rename to tests/uranium_tests/cache/shm_poke.py index a4ed38c0485..53dd02b061c 100644 --- a/tests/gold_tests/cache/shm_poke.py +++ b/tests/uranium_tests/cache/shm_poke.py @@ -13,7 +13,7 @@ # 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. -"""Flip raw bytes in a cache shm segment file, for shm trust-gate autests. +"""Flip raw bytes in a cache shm segment file, for shm trust-gate Uranium tests. On Linux the POSIX shm segments are plain files under /dev/shm, so a segment left behind by a clean shutdown can be tampered with between runs to drive the diff --git a/tests/uranium_tests/cache/test_background_fill.py b/tests/uranium_tests/cache/test_background_fill.py new file mode 100644 index 00000000000..248e5c16640 --- /dev/null +++ b/tests/uranium_tests/cache/test_background_fill.py @@ -0,0 +1,208 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_background_fill(urtest: UraniumTest) -> None: + ''' + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + from enum import Enum + + urtest.Summary = 'Exercise Background Fill' + urtest.SkipUnless(Condition.HasCurlFeature('http2'), Condition.HasProxyVerifierVersion('2.8.0')) + urtest.ContinueOnFail = True + + class BackgroundFillTest: + """ + https://docs.trafficserver.apache.org/en/latest/admin-guide/files/records.yaml.en.html#proxy-config-http-background-fill-completed-threshold + """ + + class State(Enum): + """ + State of process + """ + INIT = 0 + RUNNING = 1 + + def __init__(self): + self.state = self.State.INIT + self.ts = {} + self.__setupOriginServer() + self.__setupTS(['for_httpbin', 'for_pv']) + + def __setupOriginServer(self): + self.httpbin = urtest.MakeHttpBinServer("httpbin") + self.pv_server = urtest.MakeVerifierServerProcess("server0", "replay/bg_fill.yaml") + + def __setupTS(self, ts_names=['default']): + for name in ts_names: + self.ts[name] = urtest.MakeATSProcess(name, select_ports=True, enable_tls=True, enable_cache=True) + + self.ts[name].addDefaultSSLFiles() + self.ts[name].Disk.ssl_multicert_yaml.AddLines( + """ + ssl_multicert: + - dest_ip: "*" + ssl_cert_name: server.pem + ssl_key_name: server.key + """.split("\n")) + + self.ts[name].Disk.records_config.update( + { + "proxy.config.http.server_ports": + f"{self.ts[name].Variables.port} {self.ts[name].Variables.ssl_port}:ssl {self.ts[name].Variables.uds_path}", + "proxy.config.http.background_fill_active_timeout": "0", + "proxy.config.http.background_fill_completed_threshold": "0.0", + "proxy.config.http.cache.required_headers": 0, # Force cache + "proxy.config.http.insert_response_via_str": 2, + 'proxy.config.http.server_session_sharing.pool': 'thread', + 'proxy.config.http.server_session_sharing.match': 'ip,sni,cert', + 'proxy.config.exec_thread.autoconfig.enabled': 0, + 'proxy.config.exec_thread.limit': 1, + 'proxy.config.ssl.server.cert.path': f"{self.ts[name].Variables.SSLDir}", + 'proxy.config.ssl.server.private_key.path': f"{self.ts[name].Variables.SSLDir}", + 'proxy.config.ssl.client.alpn_protocols': 'h2,http/1.1', + 'proxy.config.ssl.client.verify.server.policy': 'PERMISSIVE', + "proxy.config.diags.debug.enabled": 3, + "proxy.config.diags.debug.tags": "http", + }) + + self.ts[name].Disk.plugin_config.AddLine('xdebug.so --enable=x-cache') + + if name == 'for_httpbin' or name == 'default': + self.ts[name].Disk.remap_config.AddLines([ + f"map / http://127.0.0.1:{self.httpbin.Variables.Port}", + ]) + else: + self.ts[name].Disk.remap_config.AddLines([ + f'map / https://127.0.0.1:{self.pv_server.Variables.https_port}', + ]) + + def __checkProcessBefore(self, tr): + if self.state == self.State.RUNNING: + tr.StillRunningBefore = self.httpbin + tr.StillRunningBefore = self.pv_server + tr.StillRunningBefore = self.ts['for_httpbin'] + tr.StillRunningBefore = self.ts['for_pv'] + else: + tr.Processes.Default.StartBefore(self.httpbin) + tr.Processes.Default.StartBefore(self.pv_server) + tr.Processes.Default.StartBefore(self.ts['for_httpbin']) + tr.Processes.Default.StartBefore(self.ts['for_pv']) + self.state = self.State.RUNNING + + def __checkProcessAfter(self, tr): + assert (self.state == self.State.RUNNING) + tr.StillRunningAfter = self.httpbin + tr.StillRunningAfter = self.pv_server + tr.StillRunningAfter = self.ts['for_httpbin'] + tr.StillRunningAfter = self.ts['for_pv'] + + def __testCase0(self): + """ + HTTP/1.1 over TCP + """ + tr = urtest.AddTestRun() + self.__checkProcessBefore(tr) + tr.MakeCurlCommandMulti( + f""" + {{curl}} -X PURGE --http1.1 -vs http://127.0.0.1:{self.ts['for_httpbin'].Variables.port}/drip?duration=4; + timeout 1 {{curl}} --http1.1 -vs http://127.0.0.1:{self.ts['for_httpbin'].Variables.port}/drip?duration=4; + sleep 5; + {{curl}} --http1.1 -vs http://127.0.0.1:{self.ts['for_httpbin'].Variables.port}/drip?duration=4 -H "x-debug: x-cache" + """, + ts=self.ts['for_httpbin']) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.stderr = "gold/background_fill_0_stderr_H.gold" + self.__checkProcessAfter(tr) + + def __testCase1(self): + """ + HTTP/1.1 over TLS + """ + tr = urtest.AddTestRun() + self.__checkProcessBefore(tr) + tr.MakeCurlCommandMulti( + f""" + {{curl}} -X PURGE --http1.1 -vsk https://127.0.0.1:{self.ts['for_httpbin'].Variables.ssl_port}/drip?duration=4; + timeout 1 {{curl}} --http1.1 -vsk https://127.0.0.1:{self.ts['for_httpbin'].Variables.ssl_port}/drip?duration=4; + sleep 5; + {{curl}} --http1.1 -vsk https://127.0.0.1:{self.ts['for_httpbin'].Variables.ssl_port}/drip?duration=4 -H "x-debug: x-cache" + """, + ts=self.ts['for_httpbin']) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.stderr = "gold/background_fill_1_stderr_H.gold" + self.__checkProcessAfter(tr) + + def __testCase2(self): + """ + HTTP/2 over TLS + """ + tr = urtest.AddTestRun() + self.__checkProcessBefore(tr) + tr.MakeCurlCommandMulti( + f""" + {{curl}} -X PURGE --http2 -vsk https://127.0.0.1:{self.ts['for_httpbin'].Variables.ssl_port}/drip?duration=4; + timeout 1 {{curl}} --http2 -vsk https://127.0.0.1:{self.ts['for_httpbin'].Variables.ssl_port}/drip?duration=4; + sleep 5; + {{curl}} --http2 -vsk https://127.0.0.1:{self.ts['for_httpbin'].Variables.ssl_port}/drip?duration=4 -H "x-debug: x-cache" + """, + ts=self.ts['for_httpbin']) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.stderr = "gold/background_fill_2_stderr_H.gold" + self.__checkProcessAfter(tr) + + def __testCase3(self): + """ + HTTP/2 over TLS using ProxyVerifier + """ + tr = urtest.AddTestRun() + self.__checkProcessBefore(tr) + tr.AddVerifierClientProcess( + "pv_client", + "replay/bg_fill.yaml", + http_ports=[self.ts['for_pv'].Variables.port], + https_ports=[self.ts['for_pv'].Variables.ssl_port]) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.stdout = "gold/background_fill_3_stdout.gold" + self.__checkProcessAfter(tr) + + def run(self): + self.__testCase0() + if not Condition.CurlUsingUnixDomainSocket(): + self.__testCase1() + self.__testCase2() + self.__testCase3() + + BackgroundFillTest().run() + urtest.execute() diff --git a/tests/uranium_tests/cache/test_cache_config_reload.py b/tests/uranium_tests/cache/test_cache_config_reload.py new file mode 100644 index 00000000000..d8cdafaf072 --- /dev/null +++ b/tests/uranium_tests/cache/test_cache_config_reload.py @@ -0,0 +1,89 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_cache_config_reload(urtest: UraniumTest) -> None: + ''' + Test cache.config and hosting.config reload via ConfigRegistry. + + Verifies that: + 1. cache.config reload works after file touch + 2. hosting.config reload works after file touch (requires cache to be initialized) + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import os + + urtest.Summary = ''' + Test cache.config and hosting.config reload via ConfigRegistry. + ''' + + urtest.ContinueOnFail = True + + # Create ATS with cache enabled (needed for hosting.config registration in open_done) + ts = urtest.MakeATSProcess("ts", enable_cache=True) + ts.Disk.records_config.update({ + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'rpc|config', + }) + + # Set up initial cache.config with a caching rule + ts.Disk.cache_config.AddLine('dest_domain=example.com ttl-in-cache=30d') + + config_dir = ts.Variables.CONFIGDIR + + # --- Test 1: Touch cache.config and reload --- + + tr = urtest.AddTestRun("Touch cache.config to trigger change detection") + tr.Processes.Default.StartBefore(ts) + tr.Processes.Default.Command = f"touch {os.path.join(config_dir, 'cache.config')} && sleep 2" + tr.Processes.Default.ReturnCode = 0 + tr.StillRunningAfter = ts + + tr = urtest.AddConfigReload( + ts, expect="any", expect_tasks=["cache.config"], token="reload_cache_test", description="Reload after cache.config touch") + + # --- Test 2: Touch hosting.config and reload --- + + tr = urtest.AddTestRun("Touch hosting.config to trigger change detection") + tr.DelayStart = 3 + tr.Processes.Default.Command = f"touch {os.path.join(config_dir, 'hosting.config')} && sleep 2" + tr.Processes.Default.ReturnCode = 0 + tr.StillRunningAfter = ts + + tr = urtest.AddConfigReload( + ts, + expect="any", + expect_tasks=["hosting.config"], + token="reload_hosting_test", + description="Reload after hosting.config touch") + urtest.execute() diff --git a/tests/uranium_tests/cache/test_cache_corrupt_recursive_uaf.py b/tests/uranium_tests/cache/test_cache_corrupt_recursive_uaf.py new file mode 100644 index 00000000000..2517dbc70e6 --- /dev/null +++ b/tests/uranium_tests/cache/test_cache_corrupt_recursive_uaf.py @@ -0,0 +1,149 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_cache_corrupt_recursive_uaf(urtest: UraniumTest) -> None: + ''' + Recursive cache-read tracking must not use-after-free on failure. + + This autest sets ATS_TEST_FORCE_CORRUPT_DOC=1 in the ATS environment, which + makes openReadStartEarliest treat every doc magic as corrupt. Reading a + multi-fragment object then drives the recursive earliest-read path: each level + re-enters openReadStartEarliest via do_read_call, and when an inner level falls + into free_CacheVC the outer frame -- on the *unfixed* code -- decrements the + recursion counter through the now-freed CacheVC member. A sanitizer build + catches that as a use-after-free. Note the depth limit ("Too many recursive + calls") is NOT required to trigger the bug: the dangling decrement happens at + whatever level the inner call frees `this`. + + With the fix in place the counter lives in a thread_local that outlives any + freed CacheVC, so the recursive run completes cleanly and the proxy stays up. + + NOTE ON COVERAGE: the use-after-free is only *caught* when ATS is built with a + memory sanitizer (e.g. the ASan preset the security CI uses). On a non-sanitized + build the dangling decrement is typically a silent no-op, so this test would + pass even on the unfixed sources. What the test verifies unconditionally is that + the forced-corrupt earliest-read path actually runs ("Doc magic does not match") + and that the proxy never crashes (no FATAL). Run it under a sanitizer build to + get the regression-catching guarantee. + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + urtest.Summary = 'Recursive cache read must not use-after-free when every doc magic is corrupt' + + # The corrupt-doc env hook is compiled in only when TS_HAS_TESTS is enabled; + # without it the hook is a constexpr false and this test would silently exercise + # a normal cache hit instead of the recursive corrupt-doc path. + urtest.SkipUnless(Condition.HasATSFeature('TS_HAS_TESTS')) + + urtest.ContinueOnFail = True + + server = urtest.MakeOriginServer("server") + + # The corrupt-doc hook lives in openReadStartEarliest, which is only reached when + # the cached object spans more than one fragment (a single-fragment object is + # served entirely from openReadStartHead and never enters the recursive earliest + # read path). Use a multi-fragment body together with a small target_fragment_size + # below so the second read actually drives openReadStartEarliest. 64 KiB over an + # 8 KiB fragment size is several fragments while keeping the test lightweight. + body = "x" * (64 * 1024) + + server.addResponse( + "sessionlog.json", { + "headers": "GET /obj HTTP/1.1\r\nHost: *\r\n\r\n", + "timestamp": "1", + "body": "" + }, { + "headers": "HTTP/1.1 200 OK\r\nContent-Length: {0}\r\nCache-Control: max-age=3600\r\n\r\n".format(len(body)), + "timestamp": "1", + "body": body + }) + + ts = urtest.MakeATSProcess("ts", enable_cache=True) + # Force the corrupt-doc path inside openReadStartEarliest for every cache read. + ts.Env['ATS_TEST_FORCE_CORRUPT_DOC'] = '1' + + ts.Disk.records_config.update( + { + 'proxy.config.http.wait_for_cache': 1, + # Force the object to be written as several small fragments so the read + # path enters openReadStartEarliest (sizeof(Doc) < value <= cap). Debug + # logging is intentionally left off: the corruption/recursion diagnostics + # asserted below are Warning/Error level and always reach diags.log, and + # cache_read debug over many fragments would bloat the run's disk use. + 'proxy.config.cache.target_fragment_size': 8192, + }) + + ts.Disk.remap_config.AddLine('map http://example.com/ http://127.0.0.1:{0}/'.format(server.Variables.Port)) + + # Assert the forced-corrupt earliest-read path actually ran. The depth-limit + # diagnostic ("Too many recursive calls") is deliberately NOT required: the + # recursion frees `this` at whatever level the inner read fails, which is the + # use-after-free locus, and it does not need to reach MAX_READ_RECURSION_DEPTH. + # So the reliable, sanitizer-independent signal that the vulnerable path was + # exercised is the corruption warning itself. The real regression guard is a + # sanitizer build catching the UAF on the unfixed sources (see module docstring). + # Assigning Content with `=` replaces the framework's default ERROR:/FATAL: + # guards, so re-add a FATAL: guard to keep asserting the proxy never crashed. + ts.Disk.diags_log.Content = Testers.ContainsExpression( + "Doc magic does not match", "the forced-corrupt earliest-read path must run") + ts.Disk.diags_log.Content += Testers.ExcludesExpression("FATAL:", "ATS must not crash on the recursive corrupt-doc path") + + # First request: cache miss, populate cache from origin. + tr1 = urtest.AddTestRun() + tr1.MakeCurlCommandMulti( + '{curl} -sS -i -x 127.0.0.1:TSPORT http://example.com/obj'.replace('TSPORT', str(ts.Variables.port)), ts=ts) + tr1.Processes.Default.StartBefore(ts) + tr1.Processes.Default.StartBefore(server) + tr1.Processes.Default.ReturnCode = 0 + # The populate read also walks the forced-corrupt earliest path; assert ATS and + # origin survive it (a UAF crash here would otherwise look like a later failure). + tr1.StillRunningAfter = ts + tr1.StillRunningAfter = server + + # Second request: cache hit; openReadStartEarliest treats the doc as corrupt + # (env var) and drives the recursive earliest-read path. With the fix ATS + # survives; the unfixed code would be caught by a sanitizer as UAF on the + # recursion counter. + tr2 = urtest.AddTestRun() + tr2.MakeCurlCommandMulti( + '{curl} -sS -i -x 127.0.0.1:TSPORT http://example.com/obj'.replace('TSPORT', str(ts.Variables.port)), ts=ts) + tr2.Processes.Default.ReturnCode = 0 + # Distinguishing assertion: ATS must remain up and respond. Body content + # doesn't matter; what matters is the proxy didn't crash. + tr2.Processes.Default.Streams.stdout = Testers.ContainsExpression("HTTP/1.1", "ATS must respond, not crash") + # Belt-and-suspenders: the proxy (and origin) must still be alive after serving + # the recursive corrupt-doc read -- catches a crash that lands right after the + # response is written. + tr2.StillRunningAfter = ts + tr2.StillRunningAfter = server + urtest.execute() diff --git a/tests/uranium_tests/cache/test_cache_generation_clear.py b/tests/uranium_tests/cache/test_cache_generation_clear.py new file mode 100644 index 00000000000..4b90efdc42a --- /dev/null +++ b/tests/uranium_tests/cache/test_cache_generation_clear.py @@ -0,0 +1,122 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_cache_generation_clear(urtest: UraniumTest) -> None: + ''' + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import uuid + + urtest.Summary = ''' + Test that incrementing the cache generation acts like a cache clear + ''' + urtest.ContinueOnFail = True + # Define default ATS + ts = urtest.MakeATSProcess("ts") + + # setup some config file for this server + ts.Disk.records_config.update( + { + 'proxy.config.body_factory.enable_customizations': 3, # enable domain specific body factory + 'proxy.config.http.cache.generation': -1, # Start with cache turned off + }) + + ts.Disk.plugin_config.AddLine('xdebug.so --enable=x-cache,x-cache-key,via,x-cache-generation') + ts.Disk.remap_config.AddLines( + [ + 'map /default/ http://127.0.0.1/ @plugin=generator.so', + # line 2 + 'map /generation1/ http://127.0.0.1/' + ' @plugin=conf_remap.so @pparam=proxy.config.http.cache.generation=1' + + ' @plugin=generator.so', + # line 3 + 'map /generation2/ http://127.0.0.1/' + ' @plugin=conf_remap.so @pparam=proxy.config.http.cache.generation=2' + + ' @plugin=generator.so', + ]) + + objectid = uuid.uuid4() + # first test is a miss for default + tr = urtest.AddTestRun() + tr.MakeCurlCommand( + '"http://127.0.0.1:{0}/default/cache/10/{1}" -H "x-debug: x-cache,x-cache-key,via,x-cache-generation" --verbose'.format( + ts.Variables.port, objectid), + ts=ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.StartBefore(urtest.Processes.ts) + tr.Processes.Default.Streams.All = "gold/miss_default-1.gold" + + # Second touch is a HIT for default. + tr = urtest.AddTestRun() + tr.MakeCurlCommand( + '"http://127.0.0.1:{0}/default/cache/10/{1}" -H "x-debug: x-cache,x-cache-key,via,x-cache-generation" --verbose'.format( + ts.Variables.port, objectid), + ts=ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.All = "gold/hit_default-1.gold" + + # Clear the cache through traffic_ctl. The command advances the initial generation from -1 to 0. + tr = urtest.AddTestRun() + tr.Processes.Default.Command = 'traffic_ctl cache clear' + tr.Processes.Default.ForceUseShell = False + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Env = ts.Env # set the environment for traffic_control to run in + + # new generation should first be a miss. + tr = urtest.AddTestRun() + tr.DelayStart = 15 # delay start of test run to allow previous command to take effect + # create a new traffic_ctrl call and the environment + tr.MakeCurlCommand( + '"http://127.0.0.1:{0}/default/cache/10/{1}" -H "x-debug: x-cache,x-cache-key,via,x-cache-generation" --verbose'.format( + ts.Variables.port, objectid), + ts=ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.All = "gold/miss_default0.gold" + + # The new generation should now hit. + tr = urtest.AddTestRun() + tr.MakeCurlCommand( + '"http://127.0.0.1:{0}/default/cache/10/{1}" -H "x-debug: x-cache,x-cache-key,via,x-cache-generation" --verbose'.format( + ts.Variables.port, objectid), + ts=ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.All = "gold/hit_default0.gold" + + # should still hit. + tr = urtest.AddTestRun() + tr.MakeCurlCommand( + '"http://127.0.0.1:{0}/default/cache/10/{1}" -H "x-debug: x-cache,x-cache-key,via,x-cache-generation" --verbose'.format( + ts.Variables.port, objectid), + ts=ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.All = "gold/hit_default0.gold" + urtest.execute() diff --git a/tests/uranium_tests/cache/test_cache_generation_disjoint.py b/tests/uranium_tests/cache/test_cache_generation_disjoint.py new file mode 100644 index 00000000000..c975fff71c7 --- /dev/null +++ b/tests/uranium_tests/cache/test_cache_generation_disjoint.py @@ -0,0 +1,122 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_cache_generation_disjoint(urtest: UraniumTest) -> None: + ''' + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import uuid + + urtest.Summary = ''' + Test that the same URL path in different cache generations creates disjoint objects + ''' + + urtest.ContinueOnFail = True + # Define default ATS + ts = urtest.MakeATSProcess("ts") + + # setup some config file for this server + ts.Disk.records_config.update( + { + 'proxy.config.body_factory.enable_customizations': 3, # enable domain specific body factory + 'proxy.config.http.cache.generation': -1, # Start with cache turned off + }) + ts.Disk.plugin_config.AddLine('xdebug.so --enable=x-cache,x-cache-key,via,x-cache-generation') + ts.Disk.remap_config.AddLines( + [ + 'map /default/ http://127.0.0.1/ @plugin=generator.so', + # line 2 + 'map /generation1/ http://127.0.0.1/' + ' @plugin=conf_remap.so @pparam=proxy.config.http.cache.generation=1' + + ' @plugin=generator.so', + # line 3 + 'map /generation2/ http://127.0.0.1/' + ' @plugin=conf_remap.so @pparam=proxy.config.http.cache.generation=2' + + ' @plugin=generator.so', + ]) + + objectid = uuid.uuid4() + # first test is a miss for default + tr = urtest.AddTestRun() + tr.MakeCurlCommand( + '"http://127.0.0.1:{0}/default/cache/10/{1}" -H "x-debug: x-cache,x-cache-key,via,x-cache-generation" --verbose'.format( + ts.Variables.port, objectid), + ts=ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.StartBefore(urtest.Processes.ts) + tr.Processes.Default.Streams.All = "gold/miss_default-1.gold" + + # Same URL in generation 1 is a MISS. + tr = urtest.AddTestRun() + tr.MakeCurlCommand( + '"http://127.0.0.1:{0}/generation1/cache/10/{1}" -H "x-debug: x-cache,x-cache-key,via,x-cache-generation" --verbose'.format( + ts.Variables.port, objectid), + ts=ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.All = "gold/miss_gen1.gold" + + # Same URL in generation 2 is still a MISS. + tr = urtest.AddTestRun() + tr.MakeCurlCommand( + '"http://127.0.0.1:{0}/generation2/cache/10/{1}" -H "x-debug: x-cache,x-cache-key,via,x-cache-generation" --verbose'.format( + ts.Variables.port, objectid), + ts=ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.All = "gold/miss_gen2.gold" + + # Second touch is a HIT for default. + tr = urtest.AddTestRun() + tr.MakeCurlCommand( + '"http://127.0.0.1:{0}/default/cache/10/{1}" -H "x-debug: x-cache,x-cache-key,via,x-cache-generation" --verbose'.format( + ts.Variables.port, objectid), + ts=ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.All = "gold/hit_default-1.gold" + + # Second touch is a HIT for generation1. + tr = urtest.AddTestRun() + tr.MakeCurlCommand( + '"http://127.0.0.1:{0}/generation1/cache/10/{1}" -H "x-debug: x-cache,x-cache-key,via,x-cache-generation" --verbose'.format( + ts.Variables.port, objectid), + ts=ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.All = "gold/hit_gen1.gold" + + # Second touch is a HIT for generation2. + tr = urtest.AddTestRun() + tr.MakeCurlCommand( + '"http://127.0.0.1:{0}/generation2/cache/10/{1}" -H "x-debug: x-cache,x-cache-key,via,x-cache-generation" --verbose'.format( + ts.Variables.port, objectid), + ts=ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.All = "gold/hit_gen2.gold" + urtest.execute() diff --git a/tests/uranium_tests/cache/test_cache_shm_bad_disk_dropped.py b/tests/uranium_tests/cache/test_cache_shm_bad_disk_dropped.py new file mode 100644 index 00000000000..2072bfb68c2 --- /dev/null +++ b/tests/uranium_tests/cache/test_cache_shm_bad_disk_dropped.py @@ -0,0 +1,251 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_cache_shm_bad_disk_dropped(urtest: UraniumTest) -> None: + ''' + Verify per-stripe partial attach when a disk is dropped from storage.yaml + (the "bad disk" case). A storage change no longer cold-starts every stripe: + the stripes on healthy, unchanged disks fast-attach their prior shm segments + while the segment left behind by the removed disk is reclaimed. + + ts1 caches an object across two disks and clean-shuts-down (marking the shm + clean). ts2 starts against the *same* shm prefix but with the second disk + removed from storage.yaml -- simulating a bad disk dropped by the operator. + ts2 must: + - keep the existing control segment (partial attach, not a full recreate), + - fast-attach the surviving disk's stripe by its stable identity, + - reclaim the orphaned stripe segment of the removed disk, + - and still serve traffic. + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import os + import sys + import uuid + + urtest.Summary = ''' + Dropping a disk from storage.yaml fast-attaches the surviving stripes from + shm and reclaims the orphaned stripe segment of the removed disk. + ''' + urtest.ContinueOnFail = True + + class CacheShmBadDiskDroppedTest: + """ + A stripe's shm identity is its hash_text -- the disk seed (path or + hash_base_string) plus that disk's own dir_skip:blocks, read from the + disk's persisted header. None of those depend on the other disks, so when + one disk is removed from storage.yaml the surviving disks compute the + same hash_text as before and re-attach their prior shm segments. The + removed disk's stripe is no longer present, so its control entry is never + claimed and finalize_attach() reclaims the orphaned segment. + + ts1 starts cold across disk_a + disk_b, populates the cache, and clean-shuts + down. ts2 starts against disk_a only (disk_b "fails"/dropped) sharing the + shm prefix, and asserts ts2: + - enters partial-attach mode (storage signature changed) keeping the + control segment rather than recreating it, + - fast-attaches the surviving disk_a stripe from shm, + - reclaims exactly the orphaned disk_b stripe segment, + - reports neither an unclean shutdown nor a schema/ABI mismatch, + - and serves a request (200). + """ + + TS_PID_SCRIPT = 'ts_process_handler.py' + + SHARED_DISK_SIZE_BYTES = 256 * 1024 * 1024 # 256 MiB per disk + + def __init__(self): + self._setup_shared_state() + # ts1 sees both disks; ts2 sees only disk_a (disk_b dropped). + self.ts1 = self._configure_ts('shmbd_ts1', [self._storage_path_a, self._storage_path_b]) + self.ts2 = self._configure_ts('shmbd_ts2', [self._storage_path_a]) + self._add_diags_log_assertions() + self._url_path = f'/cache/40/{uuid.uuid4()}' + + def _setup_shared_state(self): + urtest.Setup.Copy(os.path.join(urtest.TestDirectory, '..', 'logging', self.TS_PID_SCRIPT)) + + shared_storage_dir = os.path.join(urtest.RunDirectory, 'shared-storage') + os.makedirs(shared_storage_dir, exist_ok=True) + # Absolute paths keep the spans independent of MakeATSProcess's + # per-instance STORAGEDIR so disk_a has identical geometry for ts1 and + # ts2 (hence identical stripe identity -> fast attach). + self._storage_path_a = os.path.join(shared_storage_dir, 'disk_a.img') + self._storage_path_b = os.path.join(shared_storage_dir, 'disk_b.img') + for path in (self._storage_path_a, self._storage_path_b): + with open(path, 'ab') as f: + f.truncate(self.SHARED_DISK_SIZE_BYTES) + + # macOS PSHMNAMLEN is 31 chars incl. '/'; 'bd' = bad-disk-dropped variant. + self._shm_prefix = f'/cshmbd-{os.getpid() % 100000}-' + + def _configure_ts(self, name, storage_paths): + ts = urtest.MakeATSProcess(name) + storage_lines = ['cache:', ' spans:'] + for i, storage_path in enumerate(storage_paths): + storage_lines += [ + f' - name: disk.{i}', + f' path: {storage_path}', + f' size: {self.SHARED_DISK_SIZE_BYTES}', + ] + storage_lines += [' volumes:', ' - id: 1', ' scheme: http', ' size: 100%'] + ts.Disk.storage_yaml.AddLines(storage_lines) + ts.Disk.records_config.update( + { + 'proxy.config.cache.shm.enabled': 1, + 'proxy.config.cache.shm.name_prefix': self._shm_prefix, + 'proxy.config.cache.shm.use_hugepages': 0, + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'cache_shm|cache_init', + 'proxy.config.diags.output.diag': 'L', + 'proxy.config.http.wait_for_cache': 1, + }) + ts.Disk.plugin_config.AddLine('xdebug.so --enable=x-cache,via') + ts.Disk.remap_config.AddLine('map / http://127.0.0.1/ @plugin=generator.so') + return ts + + def _add_diags_log_assertions(self): + # ts1 cold start across both disks, clean shutdown. + self.ts1.Disk.diags_log.Content += Testers.ContainsExpression( + r'cache shm: creating fresh control segment', 'ts1 should create a fresh shm control segment on first start') + self.ts1.Disk.diags_log.Content += Testers.ContainsExpression( + r'created stripe \S+ \(\d+ bytes\) for key=', 'ts1 should create the shm-backed stripe segments') + self.ts1.Disk.diags_log.Content += Testers.ContainsExpression( + r'cache shm: marking clean shutdown', 'ts1 should mark the shm clean before exit') + + # ts2 warm start with disk_b dropped: partial attach -- the surviving + # disk_a stripe attaches, the orphaned disk_b segment is reclaimed. + self.ts2.Disk.diags_log.Content += Testers.ContainsExpression( + r'attaching up to \d+ stripes \(fast restart, partial -- storage changed\)', + 'ts2 must enter partial-attach mode after the disk was dropped') + self.ts2.Disk.diags_log.Content += Testers.ContainsExpression( + r'attached stripe \S+ \(\d+ bytes\) for key=', 'ts2 must fast-attach the surviving disk_a stripe from shm') + self.ts2.Disk.diags_log.Content += Testers.ContainsExpression( + r'cache shm: reclaiming orphaned stripe segment', 'ts2 must reclaim the dropped disk_b stripe segment') + self.ts2.Disk.diags_log.Content += Testers.ContainsExpression( + r'reclaimed \d+ orphaned stripe segment\(s\) after attach', 'ts2 must report the reclaim summary') + self.ts2.Disk.diags_log.Content += Testers.ExcludesExpression( + r'cache shm: creating fresh control segment', 'ts2 must keep the control segment across the disk drop') + self.ts2.Disk.diags_log.Content += Testers.ExcludesExpression( + r'cache shm: previous run did not shutdown cleanly', + 'the partial attach must be due to the disk drop, not an unclean shutdown') + self.ts2.Disk.diags_log.Content += Testers.ExcludesExpression( + r'cache shm: (schema|ABI) mismatch', 'the partial attach must be due to the disk drop, not schema/ABI') + + def _populate_cache(self): + tr = urtest.AddTestRun('Populate cache via ts1 (disk_a + disk_b)') + tr.Processes.Default.StartBefore(self.ts1) + tr.MakeCurlCommand( + f'-s -o /dev/null -w "%{{http_code}}\\n" ' + f'-H "x-debug: x-cache,via" ' + f'http://127.0.0.1:{self.ts1.Variables.port}{self._url_path}') + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.stdout = Testers.ContainsExpression('200', 'ts1 first GET should return 200') + tr.StillRunningAfter = self.ts1 + + def _clean_shutdown_ts1(self): + tr = urtest.AddTestRun('Drain and clean-shutdown ts1') + tr.Processes.Default.Env = self.ts1.Env + tr.Processes.Default.Command = ( + f'traffic_ctl server drain && sleep 1 && ' + f'{sys.executable} ./{self.TS_PID_SCRIPT} shmbd_ts1 --signal TERM && sleep 3') + tr.Processes.Default.ReturnCode = 0 + + def _dump_shm_state(self): + # Between ts1's clean shutdown and ts2's start the control segment is + # marked clean and records both stripes (nothing reclaimed yet). Capture + # it with `traffic_ctl cache shm status` and compare against a gold file. + # The gold masks the run-specific names, the ABI/storage hashes, and the + # page-rounded sizes with the `` wildcard, so what is asserted literally + # is the meaningful state: valid magic/schema, clean_shutdown=1, + # stripe_count=2, both stripe segments present, and a retained owner_pid + # that reads as stale (a clean shutdown keeps the pid so a platform + # without flock cannot attach into the shutdown window). + tr = urtest.AddTestRun('Dump shm control state after ts1 clean shutdown') + # Use ts1's Env: it has been started, so the per-instance bin dir is on + # PATH (ts2's Env only gains it once ts2 starts, which is the next step). + # `cache shm status` reads the segment directly and needs no live server. + tr.Processes.Default.Env = self.ts1.Env + tr.Processes.Default.Command = f'traffic_ctl cache shm status --prefix {self._shm_prefix}' + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.stdout = 'gold/cache_shm_state_after_shutdown.gold' + # The gold's stripe rows end at `present`. A stripe marked untrusted at shutdown appends to that line, so say so + # explicitly -- otherwise a stray mark (e.g. an AIO write still in flight) surfaces as an opaque gold diff. + tr.Processes.Default.Streams.stdout += Testers.ExcludesExpression( + 'untrusted', 'this cache was shut down cleanly, so no stripe should be marked untrusted') + + def _verify_survivor_attach_and_reclaim(self): + tr = urtest.AddTestRun('Start ts2 (disk_b dropped); verify survivor fast-attach + orphan reclaim') + tr.Processes.Default.StartBefore(self.ts2) + tr.MakeCurlCommand( + f'-s -o /dev/null -w "%{{http_code}}\\n" ' + f'-H "x-debug: x-cache,via" ' + f'http://127.0.0.1:{self.ts2.Variables.port}{self._url_path}') + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.stdout = Testers.ContainsExpression( + '200', 'ts2 should serve correctly after the partial attach') + tr.StillRunningAfter = self.ts2 + + def _clean_shutdown_ts2(self): + # Stop ts2 before clearing the shm: `cache shm clear` refuses to unlink a + # segment a live traffic_server still owns, so the owner must be gone + # (its retained owner_pid then reads as dead) before cleanup runs. + tr = urtest.AddTestRun('Drain and clean-shutdown ts2') + tr.Processes.Default.Env = self.ts2.Env + tr.Processes.Default.Command = ( + f'traffic_ctl server drain && sleep 1 && ' + f'{sys.executable} ./{self.TS_PID_SCRIPT} shmbd_ts2 --signal TERM && sleep 3') + tr.Processes.Default.ReturnCode = 0 + + def _cleanup_shm(self): + # A clean shutdown deliberately keeps the control + live stripe segments + # for the next fast restart, so they outlive the test. Unlink them by + # prefix to avoid leaking POSIX shm across repeated local runs (macOS has + # no /dev/shm to clear out of band). + tr = urtest.AddTestRun('Unlink the test shm segments') + tr.Processes.Default.Env = self.ts2.Env + tr.Processes.Default.Command = f'traffic_ctl cache shm clear --prefix {self._shm_prefix}' + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.stderr = Testers.ExcludesExpression( + 'Invalid argument', 'clear must skip tombstoned slots, not fail on them') + + def run(self): + self._populate_cache() + self._clean_shutdown_ts1() + self._dump_shm_state() + self._verify_survivor_attach_and_reclaim() + self._clean_shutdown_ts2() + self._cleanup_shm() + + CacheShmBadDiskDroppedTest().run() + urtest.execute() diff --git a/tests/uranium_tests/cache/test_cache_shm_concurrent_attach.py b/tests/uranium_tests/cache/test_cache_shm_concurrent_attach.py new file mode 100644 index 00000000000..e2ac81f9757 --- /dev/null +++ b/tests/uranium_tests/cache/test_cache_shm_concurrent_attach.py @@ -0,0 +1,204 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_cache_shm_concurrent_attach(urtest: UraniumTest) -> None: + ''' + Verify the concurrent-attach guard: a second traffic_server must never map the + shm directory read-write underneath a live owner. ts1 cold-starts and becomes + the owner of the control segment (it sets owner_pid and, on Linux, holds an + exclusive flock for its lifetime). While ts1 is still running, ts2 starts + against the *same* shm prefix; it must refuse shm for this run, disable it, and + come up on its own disk cache without touching ts1's segment. ts1 keeps serving + throughout. + + The two instances use *separate* on-disk cache files so the test isolates the + shm concurrent-attach guard from any contention over a shared cache file. + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import os + import sys + import uuid + + urtest.Summary = ''' + A second traffic_server refuses to attach the shm directory while a live owner + holds it, disabling shm for its run instead of attaching concurrently. + ''' + urtest.ContinueOnFail = True + + class CacheShmConcurrentAttachTest: + """ + The concurrent-attach guard (P0). A live owner is still mapping the Dir + read-write; a second writer would corrupt it, and clean_shutdown is no + protection against a concurrent *live* run. The guard fires from either of + two mechanisms, so this test asserts on the shared tail of both messages: + - Linux: ts1 holds an exclusive flock on the control segment for its + lifetime; ts2's lock attempt returns HeldByOther ("... is locked by a + live owner ..."). + - macOS (flock unsupported): the owner_pid liveness backstop fires + instead ("... claims a live owner ..."). + Both end in "disabling shm this run to avoid concurrent attach" and set the + run to shm-disabled, which is what this test pins -- so it runs on every + platform. + + ts2 must: + - log the concurrent-attach refusal, + - NOT create or attach a control segment (it bails before either), + - still serve a request (200) from its own disk cache. + ts1 must keep running and serving the whole time. + """ + + TS_PID_SCRIPT = 'ts_process_handler.py' + + SHARED_DISK_SIZE_BYTES = 256 * 1024 * 1024 # 256 MiB + + def __init__(self): + self._setup_shared_state() + # Same shm prefix, different storage files: the collision under test is + # purely on the shm control segment, not the on-disk cache. + self.ts1 = self._configure_ts('shmc_ts1', self._storage_path_a) + self.ts2 = self._configure_ts('shmc_ts2', self._storage_path_b) + self._add_diags_log_assertions() + self._url_path = f'/cache/40/{uuid.uuid4()}' + + def _setup_shared_state(self): + urtest.Setup.Copy(os.path.join(urtest.TestDirectory, '..', 'logging', self.TS_PID_SCRIPT)) + + shared_storage_dir = os.path.join(urtest.RunDirectory, 'shared-storage') + os.makedirs(shared_storage_dir, exist_ok=True) + self._storage_path_a = os.path.join(shared_storage_dir, 'disk_a.img') + self._storage_path_b = os.path.join(shared_storage_dir, 'disk_b.img') + for path in (self._storage_path_a, self._storage_path_b): + with open(path, 'ab') as f: + f.truncate(self.SHARED_DISK_SIZE_BYTES) + + # macOS PSHMNAMLEN is 31 chars incl. '/'; 'c' = concurrent-attach variant. + self._shm_prefix = f'/cshmc-{os.getpid() % 100000}-' + + def _configure_ts(self, name, storage_path): + ts = urtest.MakeATSProcess(name) + ts.Disk.storage_yaml.AddLines( + [ + 'cache:', + ' spans:', + ' - name: disk.0', + f' path: {storage_path}', + f' size: {self.SHARED_DISK_SIZE_BYTES}', + ' volumes:', + ' - id: 1', + ' scheme: http', + ' size: 100%', + ]) + ts.Disk.records_config.update( + { + 'proxy.config.cache.shm.enabled': 1, + 'proxy.config.cache.shm.name_prefix': self._shm_prefix, + 'proxy.config.cache.shm.use_hugepages': 0, + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'cache_shm', + 'proxy.config.diags.output.diag': 'L', + 'proxy.config.http.wait_for_cache': 1, + }) + ts.Disk.plugin_config.AddLine('xdebug.so --enable=x-cache,via') + ts.Disk.remap_config.AddLine('map / http://127.0.0.1/ @plugin=generator.so') + return ts + + def _add_diags_log_assertions(self): + # ts1 is the owner: it creates the fresh control segment. + self.ts1.Disk.diags_log.Content += Testers.ContainsExpression( + r'cache shm: creating fresh control segment', 'ts1 should create and own the shm control segment') + + # ts2 starts while ts1 owns the segment: it must refuse and disable shm. + # The message head differs by platform (flock vs owner_pid backstop); the + # tail is common, so anchor on it. + self.ts2.Disk.diags_log.Content += Testers.ContainsExpression( + r'disabling shm this run to avoid concurrent attach', 'ts2 must refuse to attach while ts1 owns the segment') + self.ts2.Disk.diags_log.Content += Testers.ExcludesExpression( + r'cache shm: creating fresh control segment', 'ts2 must not create a control segment when it refuses shm') + self.ts2.Disk.diags_log.Content += Testers.ExcludesExpression( + r'cache shm: attaching up to \d+ stripes \(fast restart', "ts2 must not attach ts1's live control segment") + + def _start_owner(self): + tr = urtest.AddTestRun('Cold-start ts1 (becomes the shm owner)') + tr.Processes.Default.StartBefore(self.ts1) + tr.MakeCurlCommand( + f'-s -o /dev/null -w "%{{http_code}}\\n" ' + f'-H "x-debug: x-cache,via" ' + f'http://127.0.0.1:{self.ts1.Variables.port}{self._url_path}') + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.stdout = Testers.ContainsExpression('200', 'ts1 GET should return 200') + tr.StillRunningAfter = self.ts1 + + def _start_second_refused(self): + # ts1 is still running (kept alive by StillRunningAfter above), so ts2's + # start hits the concurrent-attach guard. + tr = urtest.AddTestRun('Start ts2 while ts1 is live; ts2 must refuse shm and serve from disk') + tr.Processes.Default.StartBefore(self.ts2) + tr.MakeCurlCommand( + f'-s -o /dev/null -w "%{{http_code}}\\n" ' + f'-H "x-debug: x-cache,via" ' + f'http://127.0.0.1:{self.ts2.Variables.port}{self._url_path}') + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.stdout = Testers.ContainsExpression( + '200', 'ts2 should serve from its own disk cache with shm disabled') + tr.StillRunningAfter = self.ts1 + tr.StillRunningAfter = self.ts2 + + def _clean_shutdown(self, ts, name): + tr = urtest.AddTestRun(f'Drain and clean-shutdown {name}') + tr.Processes.Default.Env = ts.Env + tr.Processes.Default.Command = ( + f'traffic_ctl server drain && sleep 1 && ' + f'{sys.executable} ./{self.TS_PID_SCRIPT} {name} --signal TERM && sleep 3') + tr.Processes.Default.ReturnCode = 0 + + def _cleanup_shm(self): + # ts1 (the owner) is stopped before this so clean_shutdown clears + # owner_pid; otherwise `cache shm clear` refuses a live owner. + tr = urtest.AddTestRun('Unlink the test shm segments') + tr.Processes.Default.Env = self.ts1.Env + tr.Processes.Default.Command = f'traffic_ctl cache shm clear --prefix {self._shm_prefix}' + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.stderr = Testers.ExcludesExpression( + 'Invalid argument', 'clear must skip tombstoned slots, not fail on them') + + def run(self): + self._start_owner() + self._start_second_refused() + # Stop the non-owner first, then the owner, then clear: its retained owner_pid reads as dead once it exits. + self._clean_shutdown(self.ts2, 'shmc_ts2') + self._clean_shutdown(self.ts1, 'shmc_ts1') + self._cleanup_shm() + + CacheShmConcurrentAttachTest().run() + urtest.execute() diff --git a/tests/uranium_tests/cache/test_cache_shm_control_size_mismatch.py b/tests/uranium_tests/cache/test_cache_shm_control_size_mismatch.py new file mode 100644 index 00000000000..f0b92b6002a --- /dev/null +++ b/tests/uranium_tests/cache/test_cache_shm_control_size_mismatch.py @@ -0,0 +1,222 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_cache_shm_control_size_mismatch(urtest: UraniumTest) -> None: + ''' + Verify a control segment whose size is not this build's sizeof(CacheShmControl) is + dropped and recreated rather than wedging shm off. Such a segment is what an upgrade + that changes the control layout (a MAX_STRIPES bump, a longer shm_name, a new + StripeEntry field) leaves behind: the size-checked attach cannot map it, and without + the frozen-header drop path the O_EXCL create would then fail with EEXIST on every + restart until an operator ran `traffic_ctl cache shm clear`. + + ts1 cold-starts, caches an object, and clean-shuts-down. The control segment file + under /dev/shm is then grown past any size this build could have written, so ts2 sees + a foreign-size segment: it must drop it, create a fresh one, and rebuild from disk. + ts3 then starts against what ts2 left behind and must fast-attach normally, which is + what proves the drop actually healed rather than deferring the wedge by one restart. + + Linux-only: it resizes the /dev/shm segment file, which exists only on Linux (macOS + POSIX shm segments are not path-addressable). + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import os + import platform + import sys + import uuid + + urtest.Summary = ''' + A control segment written with a different sizeof(CacheShmControl) is dropped and + recreated, never left to wedge the create path. + ''' + urtest.ContinueOnFail = True + + # The poke drives the gate by editing /dev/shm directly, which is a Linux facility; + # macOS POSIX shm is not exposed as a file. There is no Condition for the platform, + # so gate with a lambda (ports.py branches on platform the same way). + urtest.SkipUnless(Condition(lambda: platform.system() == 'Linux', "shm byte-poke gates need Linux /dev/shm")) + + class CacheShmControlSizeMismatchTest: + """ + The control-segment size gate. Every field the attach path needs to identify a + segment (magic, schema_version, abi_hash) and to guard dropping it (owner_pid, + clean_shutdown) lives in a frozen prefix, so a segment of any other size is still + readable that far: it is guarded, dropped, and recreated in one start. + + Sequence: ts1 creates a clean segment; the segment file is grown past this build's + size; ts2 must report the size mismatch, drop, recreate, and serve; ts3 must then + fast-attach the segment ts2 created. + """ + + TS_PID_SCRIPT = 'ts_process_handler.py' + POKE_SCRIPT = 'shm_poke.py' + + SHARED_DISK_SIZE_BYTES = 256 * 1024 * 1024 # 256 MiB + + # Writing one byte at this offset grows the (sparse) segment file well past + # sizeof(CacheShmControl) rounded up to a page, which is what the attach path + # accepts. MAX_CONTROL_SEGMENT_BYTES caps the struct at 32 KiB, so 1 MiB is + # beyond reach of any layout this build could compile. The header itself is left + # intact, so the size is the only thing that makes the segment foreign. + GROW_OFFSET = 1024 * 1024 + GROW_BYTE_HEX = '00' + + def __init__(self): + self._setup_shared_state() + self.ts1 = self._configure_ts('shmz_ts1') + self.ts2 = self._configure_ts('shmz_ts2') + self.ts3 = self._configure_ts('shmz_ts3') + self._add_diags_log_assertions() + self._url_path = f'/cache/40/{uuid.uuid4()}' + + def _setup_shared_state(self): + urtest.Setup.Copy(os.path.join(urtest.TestDirectory, '..', 'logging', self.TS_PID_SCRIPT)) + urtest.Setup.Copy(os.path.join(urtest.TestDirectory, self.POKE_SCRIPT)) + + shared_storage_dir = os.path.join(urtest.RunDirectory, 'shared-storage') + os.makedirs(shared_storage_dir, exist_ok=True) + self._shared_storage_path = os.path.join(shared_storage_dir, 'disk.img') + with open(self._shared_storage_path, 'ab') as f: + f.truncate(self.SHARED_DISK_SIZE_BYTES) + + # macOS PSHMNAMLEN is 31 chars incl. '/'; 'z' = size-mismatch variant. + # (This test is Linux-only, but keep the prefix short for consistency.) + self._shm_prefix = f'/cshmz-{os.getpid() % 100000}-' + self._control_file = '/dev/shm/' + self._shm_prefix.lstrip('/') + 'control' + + def _configure_ts(self, name): + ts = urtest.MakeATSProcess(name) + ts.Disk.storage_yaml.AddLines( + [ + 'cache:', + ' spans:', + ' - name: disk.0', + f' path: {self._shared_storage_path}', + f' size: {self.SHARED_DISK_SIZE_BYTES}', + ' volumes:', + ' - id: 1', + ' scheme: http', + ' size: 100%', + ]) + ts.Disk.records_config.update( + { + 'proxy.config.cache.shm.enabled': 1, + 'proxy.config.cache.shm.name_prefix': self._shm_prefix, + 'proxy.config.cache.shm.use_hugepages': 0, + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'cache_shm', + 'proxy.config.diags.output.diag': 'L', + 'proxy.config.http.wait_for_cache': 1, + }) + ts.Disk.plugin_config.AddLine('xdebug.so --enable=x-cache,via') + ts.Disk.remap_config.AddLine('map / http://127.0.0.1/ @plugin=generator.so') + return ts + + def _add_diags_log_assertions(self): + # ts1 cold start, clean shutdown -- a valid, clean segment to resize. + self.ts1.Disk.diags_log.Content += Testers.ContainsExpression( + r'cache shm: creating fresh control segment', 'ts1 should create a fresh shm control segment on first start') + self.ts1.Disk.diags_log.Content += Testers.ContainsExpression( + r'cache shm: marking clean shutdown', 'ts1 should mark the shm clean before exit') + + # ts2 start against the resized segment: report, drop, recreate, rebuild. + self.ts2.Disk.diags_log.Content += Testers.ContainsExpression( + r"cache shm: control segment \S+ is \d+ bytes, not this build's \d+; dropping it", + 'ts2 must report the control segment size mismatch') + self.ts2.Disk.diags_log.Content += Testers.ContainsExpression( + r'cache shm: creating fresh control segment', 'ts2 must recreate the control segment after the drop') + self.ts2.Disk.diags_log.Content += Testers.ExcludesExpression( + r'cache shm: failed to create control segment', + 'the drop must leave the name free, so the O_EXCL create cannot fail with EEXIST') + self.ts2.Disk.diags_log.Content += Testers.ExcludesExpression( + r'\(fast restart, recovery skipped\)', 'ts2 must rebuild from disk, never fast-attach a foreign-size segment') + + # ts3 fast-attaches what ts2 created: the drop healed, it did not just defer. + self.ts3.Disk.diags_log.Content += Testers.ContainsExpression( + r'cache shm: attaching up to \d+ stripes \(fast restart', 'ts3 should attach the segment ts2 created') + self.ts3.Disk.diags_log.Content += Testers.ContainsExpression( + r"attaching cached directory from shm for '.+' \(fast restart", + 'ts3 should reuse the per-stripe directory from shm') + self.ts3.Disk.diags_log.Content += Testers.ExcludesExpression( + r'cache shm: control segment \S+ is \d+ bytes', 'ts3 should see a segment of the expected size') + self.ts3.Disk.diags_log.Content += Testers.ExcludesExpression( + r'cache shm: creating fresh control segment', 'ts3 should not have to create another control segment') + + def _get(self, ts, description): + tr = urtest.AddTestRun(description) + tr.Processes.Default.StartBefore(ts) + tr.MakeCurlCommand( + f'-s -o /dev/null -w "%{{http_code}}\\n" ' + f'-H "x-debug: x-cache,via" ' + f'http://127.0.0.1:{ts.Variables.port}{self._url_path}') + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.stdout = Testers.ContainsExpression('200', f'{description}: should return 200') + tr.StillRunningAfter = ts + + def _clean_shutdown(self, ts, name): + tr = urtest.AddTestRun(f'Drain and clean-shutdown {name}') + tr.Processes.Default.Env = ts.Env + tr.Processes.Default.Command = ( + f'traffic_ctl server drain && sleep 1 && ' + f'{sys.executable} ./{self.TS_PID_SCRIPT} {name} --signal TERM && sleep 3') + tr.Processes.Default.ReturnCode = 0 + + def _grow_control_segment(self): + # ts1 is dead; the segment is just a file now. Extend it without touching the + # header, so only its size makes it foreign to this build. + tr = urtest.AddTestRun('Grow the shm control segment past this build\'s size') + tr.Processes.Default.Command = ( + f'{sys.executable} ./{self.POKE_SCRIPT} {self._control_file} {self.GROW_OFFSET} {self.GROW_BYTE_HEX}') + tr.Processes.Default.ReturnCode = 0 + + def _cleanup_shm(self): + tr = urtest.AddTestRun('Unlink the test shm segments') + tr.Processes.Default.Env = self.ts3.Env + tr.Processes.Default.Command = f'traffic_ctl cache shm clear --prefix {self._shm_prefix}' + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.stderr = Testers.ExcludesExpression( + 'Invalid argument', 'clear must skip tombstoned slots, not fail on them') + + def run(self): + self._get(self.ts1, 'Cold-start ts1 and cache an object') + self._clean_shutdown(self.ts1, 'shmz_ts1') + self._grow_control_segment() + self._get(self.ts2, 'Start ts2; the foreign-size segment is dropped and recreated') + self._clean_shutdown(self.ts2, 'shmz_ts2') + self._get(self.ts3, 'Start ts3; the recreated segment fast-attaches') + self._clean_shutdown(self.ts3, 'shmz_ts3') + self._cleanup_shm() + + CacheShmControlSizeMismatchTest().run() + urtest.execute() diff --git a/tests/uranium_tests/cache/test_cache_shm_dir_invalid.py b/tests/uranium_tests/cache/test_cache_shm_dir_invalid.py new file mode 100644 index 00000000000..f76158781f0 --- /dev/null +++ b/tests/uranium_tests/cache/test_cache_shm_dir_invalid.py @@ -0,0 +1,261 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_cache_shm_dir_invalid(urtest: UraniumTest) -> None: + ''' + Verify the per-stripe shm directory trust gates: an in-shm directory whose header + fields are out of range is rejected and rebuilt from disk, never fast-attached. + These branches (Stripe::_shm_directory_is_valid) are what stand between a stale or + torn shm directory and out-of-bounds disk I/O, so each is driven directly. + + ts1 cold-starts, caches an object, and clean-shuts-down, which leaves the control + segment marked clean and the stripe segment holding a valid directory. The stripe + segment file under /dev/shm is then tampered with between runs: + + * ts2 sees write_pos pushed past the end of the stripe. + * ts3 sees freelist[0] pushed past the segment's entry count. + + Each instance must attach the shm segments, reject the directory, fall back to the + disk read + recover_data(), and still serve the object out of cache. + + Linux-only: it pokes raw bytes in the /dev/shm segment files, which exist only on + Linux (macOS POSIX shm segments are not path-addressable). + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import os + import platform + import sys + + urtest.Summary = ''' + An in-shm stripe directory with out-of-range header fields is rejected and rebuilt + from disk, never fast-attached. + ''' + urtest.ContinueOnFail = True + + # The byte-poke drives the gate by editing /dev/shm directly, which is a Linux + # facility; macOS POSIX shm is not exposed as a file. There is no Condition for + # the platform, so gate with a lambda (ports.py branches on platform the same way). + urtest.SkipUnless(Condition(lambda: platform.system() == 'Linux', "shm byte-poke gates need Linux /dev/shm")) + + class CacheShmDirInvalidTest: + """ + The per-stripe directory gates. On a fast restart the in-shm directory is used + verbatim -- the disk read and recover_data() are both skipped -- so every header + field the cache later trusts is range-checked first. A rejection must be safe and + silent to clients: the stripe falls back to the disk read, recovers, and serves + the same object. + + Sequence, per tampered field: + - poke the field in the /dev/shm stripe segment left by a clean shutdown, + - start the next ts, which must log "shm directory invalid ... falling back to + disk read" and must NOT log the fast-attach line, + - replay the cache-hit transaction, which must be served from cache. + """ + + TS_PID_SCRIPT = 'ts_process_handler.py' + POKE_SCRIPT = 'shm_poke.py' + + REPLAY_FILE = 'replay/cache-shm-dir-invalid.replay.yaml' + + SHARED_DISK_SIZE_BYTES = 256 * 1024 * 1024 # 256 MiB + + # StripeHeaderFooter layout (P_CacheDir.h) at the head of the stripe segment: + # magic @0, version @4, create_time @8, write_pos @16, last_write_pos @24, + # agg_pos @32, ... sector_size @64, unused @68, freelist[0] @72. + WRITE_POS_OFFSET = 16 + FREELIST_0_OFFSET = 72 + + # Little-endian off_t 0x0000FFFFFFFFFFFF: far beyond skip + len for a 256 MiB + # stripe, so the write_pos range check rejects it. Leaving the top two bytes + # zero keeps the value positive, which exercises the upper-bound branch rather + # than the negative-offset one. + BOGUS_WRITE_POS_LE_HEX = 'ffffffffffff0000' + # Little-endian uint16 65535. The largest value a Dir next/prev field can hold, + # and past the entry count of any segment with fewer than 16384 buckets -- which + # a 256 MiB stripe is. + BOGUS_FREELIST_LE_HEX = 'ffff' + + def __init__(self): + self._setup_shared_state() + # A single verifier-server is the origin for every ts, started before ts1 + # and kept running for the whole test. + self.server = urtest.MakeVerifierServerProcess('shmd-origin', self.REPLAY_FILE) + self.ts1 = self._configure_ts('shmd_ts1') + self.ts2 = self._configure_ts('shmd_ts2') + self.ts3 = self._configure_ts('shmd_ts3') + self._add_diags_log_assertions() + + def _setup_shared_state(self): + urtest.Setup.Copy(os.path.join(urtest.TestDirectory, '..', 'logging', self.TS_PID_SCRIPT)) + urtest.Setup.Copy(os.path.join(urtest.TestDirectory, self.POKE_SCRIPT)) + + shared_storage_dir = os.path.join(urtest.RunDirectory, 'shared-storage') + os.makedirs(shared_storage_dir, exist_ok=True) + self._shared_storage_path = os.path.join(shared_storage_dir, 'disk.img') + with open(self._shared_storage_path, 'ab') as f: + f.truncate(self.SHARED_DISK_SIZE_BYTES) + + # macOS PSHMNAMLEN is 31 chars incl. '/'; 'd' = dir-invalid variant. + # (This test is Linux-only, but keep the prefix short for consistency.) + self._shm_prefix = f'/cshmd-{os.getpid() % 100000}-' + # A single span with a single volume yields exactly one stripe, so its + # segment is index 0. On Linux each segment is a file under /dev/shm by the + # same name (sans the leading '/'). + self._stripe_file = '/dev/shm/' + self._shm_prefix.lstrip('/') + 's0' + + def _configure_ts(self, name): + ts = urtest.MakeATSProcess(name) + # An absolute span path keeps the span independent of MakeATSProcess's + # per-instance STORAGEDIR, so every ts shares the same on-disk cache and + # therefore the same stripe geometry (hence the same shm identity). + ts.Disk.storage_yaml.AddLines( + [ + 'cache:', + ' spans:', + ' - name: disk.0', + f' path: {self._shared_storage_path}', + f' size: {self.SHARED_DISK_SIZE_BYTES}', + ' volumes:', + ' - id: 1', + ' scheme: http', + ' size: 100%', + ]) + ts.Disk.records_config.update( + { + 'proxy.config.cache.shm.enabled': 1, + 'proxy.config.cache.shm.name_prefix': self._shm_prefix, + 'proxy.config.cache.shm.use_hugepages': 0, + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'cache_shm', + 'proxy.config.diags.output.diag': 'L', + 'proxy.config.http.wait_for_cache': 1, + }) + ts.Disk.plugin_config.AddLine('xdebug.so --enable=x-cache,via') + ts.Disk.remap_config.AddLine(f'map / http://127.0.0.1:{self.server.Variables.http_port}/') + return ts + + def _add_reject_assertions(self, ts, label): + # The shm segments themselves are still attached -- the control segment is + # untouched and clean, so this is specifically the per-stripe directory gate. + ts.Disk.diags_log.Content += Testers.ContainsExpression( + r'cache shm: attaching up to \d+ stripes \(fast restart', f'{label} should attach the existing control segment') + ts.Disk.diags_log.Content += Testers.ContainsExpression( + r'cache shm: attached stripe \S+ \(\d+ bytes\) for key=', f'{label} should attach the existing stripe segment') + ts.Disk.diags_log.Content += Testers.ContainsExpression( + r"shm directory invalid for '.+'; falling back to disk read", f'{label} must reject the tampered shm directory') + ts.Disk.diags_log.Content += Testers.ExcludesExpression( + r'attaching cached directory from shm for', f'{label} must not fast-attach the tampered directory') + # The rejection is per-stripe: the control segment stays valid, so none of + # the whole-segment drop reasons should appear. + ts.Disk.diags_log.Content += Testers.ExcludesExpression( + r'cache shm: (schema|ABI) mismatch', f'{label} should reject the directory, not the control segment') + ts.Disk.diags_log.Content += Testers.ExcludesExpression( + r'cache shm: previous run did not shutdown cleanly', f'{label} should see the shm marked clean') + + def _add_diags_log_assertions(self): + # ts1 cold start, clean shutdown -- a valid, clean stripe segment to tamper with. + self.ts1.Disk.diags_log.Content += Testers.ContainsExpression( + r'cache shm: creating fresh control segment', 'ts1 should create a fresh shm control segment on first start') + self.ts1.Disk.diags_log.Content += Testers.ContainsExpression( + r'cache shm: created stripe \S+ \(\d+ bytes\) for key=', 'ts1 should create the shm-backed stripe segment') + self.ts1.Disk.diags_log.Content += Testers.ContainsExpression( + r'cache shm: marking clean shutdown', 'ts1 should mark the shm clean before exit') + self.ts1.Disk.diags_log.Content += Testers.ExcludesExpression( + r'shm directory invalid for', 'ts1 has no shm directory to reject on cold start') + + self._add_reject_assertions(self.ts2, 'ts2 (write_pos)') + self._add_reject_assertions(self.ts3, 'ts3 (freelist[0])') + + def _fill(self): + tr = urtest.AddTestRun('Cold-start ts1 and cache an object') + tr.AddVerifierClientProcess( + 'shmd-fill-client', + self.REPLAY_FILE, + http_ports=[self.ts1.Variables.port], + keys='fill', + other_args='--thread-limit 1') + tr.Processes.Default.StartBefore(self.server) + tr.Processes.Default.StartBefore(self.ts1) + tr.StillRunningAfter = self.server + tr.StillRunningAfter = self.ts1 + + def _clean_shutdown(self, ts, name): + tr = urtest.AddTestRun(f'Drain and clean-shutdown {name}') + tr.Processes.Default.Env = ts.Env + tr.Processes.Default.Command = ( + f'traffic_ctl server drain && sleep 1 && ' + f'{sys.executable} ./{self.TS_PID_SCRIPT} {name} --signal TERM && sleep 3') + tr.Processes.Default.ReturnCode = 0 + tr.StillRunningAfter = self.server + + def _poke(self, description, offset, hex_bytes): + # The previous ts is dead; the stripe segment is just a file now. + tr = urtest.AddTestRun(description) + tr.Processes.Default.Command = (f'{sys.executable} ./{self.POKE_SCRIPT} {self._stripe_file} {offset} {hex_bytes}') + tr.Processes.Default.ReturnCode = 0 + tr.StillRunningAfter = self.server + + def _verify_reject(self, ts, key, description): + tr = urtest.AddTestRun(description) + tr.AddVerifierClientProcess( + f'shmd-{key}-client', self.REPLAY_FILE, http_ports=[ts.Variables.port], keys=key, other_args='--thread-limit 1') + tr.Processes.Default.StartBefore(ts) + tr.StillRunningAfter = self.server + tr.StillRunningAfter = ts + + def _cleanup_shm(self): + tr = urtest.AddTestRun('Unlink the test shm segments') + tr.Processes.Default.Env = self.ts3.Env + tr.Processes.Default.Command = f'traffic_ctl cache shm clear --prefix {self._shm_prefix}' + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.stderr = Testers.ExcludesExpression( + 'Invalid argument', 'clear must skip tombstoned slots, not fail on them') + + def run(self): + self._fill() + self._clean_shutdown(self.ts1, 'shmd_ts1') + + self._poke('Tamper write_pos in the in-shm stripe header', self.WRITE_POS_OFFSET, self.BOGUS_WRITE_POS_LE_HEX) + self._verify_reject(self.ts2, 'hit_write_pos', 'Start ts2; an out-of-range write_pos is rejected and rebuilt from disk') + self._clean_shutdown(self.ts2, 'shmd_ts2') + + self._poke('Tamper freelist[0] in the in-shm stripe header', self.FREELIST_0_OFFSET, self.BOGUS_FREELIST_LE_HEX) + self._verify_reject( + self.ts3, 'hit_freelist', 'Start ts3; an out-of-range freelist head is rejected and rebuilt from disk') + self._clean_shutdown(self.ts3, 'shmd_ts3') + + self._cleanup_shm() + + CacheShmDirInvalidTest().run() + urtest.execute() diff --git a/tests/uranium_tests/cache/test_cache_shm_fast_restart.py b/tests/uranium_tests/cache/test_cache_shm_fast_restart.py new file mode 100644 index 00000000000..27704a7d93a --- /dev/null +++ b/tests/uranium_tests/cache/test_cache_shm_fast_restart.py @@ -0,0 +1,272 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_cache_shm_fast_restart(urtest: UraniumTest) -> None: + ''' + Verify the cache directory survives a clean shutdown via shared memory and is + attached on the next start (fast restart). Two ATS instances share an on-disk + cache file and a POSIX shm name prefix; ts1 populates the cache and is shut + down via traffic_ctl drain + SIGTERM, then ts2 starts and serves the same URL + out of cache without re-fetching from the origin. + + Traffic is driven with Proxy Verifier: a single verifier-server acts as the + origin and the verifier-client replays cache-shm-fast-restart.replay.yaml -- + the "fill" transaction against ts1 and the "hit" transaction against ts2. + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import os + import sys + + urtest.Summary = ''' + Cache directory survives clean shutdown via POSIX shared memory. + ''' + urtest.ContinueOnFail = True + + class CacheShmFastRestartTest: + """ + Cover the cache shm fast-restart scenario end-to-end. + + Sequence: + 1. ts1 cold-start: creates a fresh shm control segment and per-stripe + segments; populates the cache via the "fill" transaction (cache miss, + fetched from the verifier-server origin). + 2. ts1 is drained and SIGTERM'd. The shutdown hook flushes the directory + and marks the shm clean. + 3. ts2 starts against the same on-disk file and shm prefix: attaches the + existing control segment, attaches per-stripe segments, and reuses the + cached directory without re-reading it from disk. + 4. ts2 serves the same URL out of cache via the "hit" transaction + (X-Cache: hit-fresh). The transaction's origin response is a 502 + sentinel, so any forward to the origin would fail the run. + + Each step is verified both at the response level (proxy-verifier) and via + diags-log assertions on the cache_shm / cache_dir_init code paths. + """ + + # Helper script for sending signals to a traffic_server process by command-line + # identifier match. Reused from uranium_tests/logging. + TS_PID_SCRIPT = 'ts_process_handler.py' + + # The replay file driving both the populate ("fill") and verify ("hit") + # transactions. They share a cache key and differ only by uuid. + REPLAY_FILE = 'replay/cache-shm-fast-restart.replay.yaml' + + # Stripe size for the shared cache. Must be large enough that the directory + # contains real entries; small enough that the disk.img is cheap to create. + SHARED_DISK_SIZE_BYTES = 256 * 1024 * 1024 # 256 MiB + + def __init__(self): + self._setup_shared_state() + # A single verifier-server is the origin for both ts1 and ts2. It is + # started before ts1 and kept running across the whole test. + self.server = urtest.MakeVerifierServerProcess('shm-origin', self.REPLAY_FILE) + self.ts1 = self._configure_ts('shm_ts1') + self.ts2 = self._configure_ts('shm_ts2') + self._add_diags_log_assertions() + + def _setup_shared_state(self): + urtest.Setup.Copy(os.path.join(urtest.TestDirectory, '..', 'logging', self.TS_PID_SCRIPT)) + + # Shared storage file used by both ts1 and ts2. The absolute path makes + # storage.yaml independent of MakeATSProcess's per-instance STORAGEDIR. + # ATS opens regular-file spans with O_RDONLY first to stat them -- it + # does not auto-create the backing file -- so pre-create disk.img at the + # configured size before either ts starts. + shared_storage_dir = os.path.join(urtest.RunDirectory, 'shared-storage') + os.makedirs(shared_storage_dir, exist_ok=True) + self._shared_storage_path = os.path.join(shared_storage_dir, 'disk.img') + with open(self._shared_storage_path, 'ab') as f: + f.truncate(self.SHARED_DISK_SIZE_BYTES) + + # POSIX shm names: macOS PSHMNAMLEN limit is 31 chars including '/'. + # Keep the prefix short and unique per test run so concurrent autest + # runs do not collide. + self._shm_prefix = f'/cshm-{os.getpid() % 100000}-' + + def _configure_ts(self, name): + ts = urtest.MakeATSProcess(name) + # Master configures cache storage via storage.yaml. An absolute span path + # keeps the span independent of MakeATSProcess's per-instance STORAGEDIR so + # ts1 and ts2 share the same on-disk cache, which yields identical stripe + # geometry (hence identical shm identity). + ts.Disk.storage_yaml.AddLines( + [ + 'cache:', + ' spans:', + ' - name: disk.0', + f' path: {self._shared_storage_path}', + f' size: {self.SHARED_DISK_SIZE_BYTES}', + ' volumes:', + ' - id: 1', + ' scheme: http', + ' size: 100%', + ]) + ts.Disk.records_config.update( + { + 'proxy.config.cache.shm.enabled': 1, + 'proxy.config.cache.shm.name_prefix': self._shm_prefix, + 'proxy.config.cache.shm.use_hugepages': 0, + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'cache_shm', + # The per-stripe 'created/attached stripe' lines are Dbg() calls; + # route debug output to diags.log (default is stderr) so the + # ContainsExpression assertions below can match them. + 'proxy.config.diags.output.diag': 'L', + 'proxy.config.http.wait_for_cache': 1, + }) + ts.Disk.plugin_config.AddLine('xdebug.so --enable=x-cache,via') + ts.Disk.remap_config.AddLine(f'map / http://127.0.0.1:{self.server.Variables.http_port}/') + return ts + + def _add_diags_log_assertions(self): + # These assertions match the *stable core* of each cache-shm log line and + # deliberately stop before the trailing parenthetical qualifier. The shm + # code appends optional context to several of these messages as it evolves + # -- "attaching N stripes" became "attaching up to N stripes", + # "(fast restart)" became "(fast restart, recovery skipped)" on the stripe + # path and "(fast restart, partial -- storage changed)" on the control + # path. Anchoring on the invariant prefix (not the closing paren) keeps the + # test from breaking every time such a qualifier is added. Likewise, the + # excludes name only log strings that actually exist in the source: an + # exclude on a non-existent string can never fire and gives false comfort. + + # ts1 (cold start): creates fresh shm, marks it clean on shutdown, and must + # NOT report any "drop" reason since there is nothing to drop. + self.ts1.Disk.diags_log.Content += Testers.ContainsExpression( + r'cache shm: creating fresh control segment', 'ts1 should create a fresh shm control segment on first start') + self.ts1.Disk.diags_log.Content += Testers.ContainsExpression( + r'cache shm: created stripe \S+ \(\d+ bytes\) for key=', 'ts1 should create at least one shm-backed stripe segment') + self.ts1.Disk.diags_log.Content += Testers.ContainsExpression( + r'cache shm: marking clean shutdown', 'ts1 should mark the shm clean before exit') + self.ts1.Disk.diags_log.Content += Testers.ExcludesExpression( + r'cache shm: (schema|ABI) mismatch', 'ts1 should not detect any shm mismatch on cold start') + self.ts1.Disk.diags_log.Content += Testers.ExcludesExpression( + r'cache shm: previous run did not shutdown cleanly', 'ts1 should not see a dirty shm on cold start') + self.ts1.Disk.diags_log.Content += Testers.ExcludesExpression( + r'cache shm: stripe \S+ size mismatch', 'ts1 should not see a stripe size mismatch on cold start') + + # ts2 (warm start): attaches the existing control segment, fast-attaches the + # per-stripe segment, reuses the cached directory, and must NOT fall back to + # the disk-rebuild path. + self.ts2.Disk.diags_log.Content += Testers.ContainsExpression( + r'cache shm: attaching up to \d+ stripes \(fast restart', 'ts2 should attach the existing shm (fast restart)') + self.ts2.Disk.diags_log.Content += Testers.ContainsExpression( + r'cache shm: attached stripe \S+ \(\d+ bytes\) for key=', + 'ts2 should attach at least one shm-backed stripe segment') + self.ts2.Disk.diags_log.Content += Testers.ContainsExpression( + r"attaching cached directory from shm for '.+' \(fast restart", + 'ts2 should reuse the per-stripe directory from shm') + self.ts2.Disk.diags_log.Content += Testers.ExcludesExpression( + r'cache shm: creating fresh control segment', 'ts2 should not create a fresh control segment') + self.ts2.Disk.diags_log.Content += Testers.ExcludesExpression( + r'cache shm: (schema|ABI) mismatch', 'ts2 should not detect any shm mismatch on warm start') + self.ts2.Disk.diags_log.Content += Testers.ExcludesExpression( + r'cache shm: previous run did not shutdown cleanly', 'ts2 should see the shm marked clean') + self.ts2.Disk.diags_log.Content += Testers.ExcludesExpression( + r'shm directory invalid for', 'ts2 should not fall back from shm to disk read') + self.ts2.Disk.diags_log.Content += Testers.ExcludesExpression( + r'cache shm: stripe \S+ size mismatch', 'ts2 should fast-attach without a stripe size-mismatch recreate') + + def _start_ts1(self): + # Cold start ts1 against the verifier-server origin and replay the + # "fill" transaction: a cache miss that ATS fetches and stores. + tr = urtest.AddTestRun('Start ts1, then cache contents (fill)') + tr.AddVerifierClientProcess( + 'shm-fill-client', + self.REPLAY_FILE, + http_ports=[self.ts1.Variables.port], + keys='fill', + other_args='--thread-limit 1') + tr.Processes.Default.StartBefore(self.server) + tr.Processes.Default.StartBefore(self.ts1) + tr.StillRunningAfter = self.server + tr.StillRunningAfter = self.ts1 + + def _clean_shutdown_ts1(self): + # Drain + SIGTERM ts1. SIGTERM goes through AutoStopCont which invokes + # TS_LIFECYCLE_SHUTDOWN_HOOK -> sync_cache_dir_on_shutdown -> + # CacheShm::mark_clean_shutdown. + tr = urtest.AddTestRun('Drain and clean-shutdown ts1') + tr.Processes.Default.Env = self.ts1.Env + tr.Processes.Default.Command = ( + f'traffic_ctl server drain && sleep 1 && ' + f'{sys.executable} ./{self.TS_PID_SCRIPT} shm_ts1 --signal TERM && sleep 3') + tr.Processes.Default.ReturnCode = 0 + tr.StillRunningAfter = self.server + + def _start_ts2(self): + # ts2 attaches the CacheDir from the shm created by ts1. Replay the + # "hit" transaction: ATS must serve it from cache (X-Cache: hit-fresh) + # without contacting the origin -- the replay's 502 sentinel response + # would otherwise surface as a proxy-response mismatch. + tr = urtest.AddTestRun('Start ts2; verify shm fast-attach and cache HIT') + tr.AddVerifierClientProcess( + 'shm-hit-client', self.REPLAY_FILE, http_ports=[self.ts2.Variables.port], keys='hit', other_args='--thread-limit 1') + tr.Processes.Default.StartBefore(self.ts2) + tr.StillRunningAfter = self.server + tr.StillRunningAfter = self.ts2 + + def _clean_shutdown_ts2(self): + # Stop ts2 before clearing the shm: `cache shm clear` refuses to unlink a + # segment a live traffic_server still owns, so the owner must be gone + # (its retained owner_pid then reads as dead) before cleanup runs. + tr = urtest.AddTestRun('Drain and clean-shutdown ts2') + tr.Processes.Default.Env = self.ts2.Env + tr.Processes.Default.Command = ( + f'traffic_ctl server drain && sleep 1 && ' + f'{sys.executable} ./{self.TS_PID_SCRIPT} shm_ts2 --signal TERM && sleep 3') + tr.Processes.Default.ReturnCode = 0 + tr.StillRunningAfter = self.server + + def _cleanup_shm(self): + # A clean shutdown deliberately keeps the control + live stripe segments + # for the next fast restart, so they outlive the test. Unlink them by + # prefix to avoid leaking POSIX shm across repeated local runs (macOS has + # no /dev/shm to clear out of band). + tr = urtest.AddTestRun('Unlink the test shm segments') + tr.Processes.Default.Env = self.ts2.Env + tr.Processes.Default.Command = f'traffic_ctl cache shm clear --prefix {self._shm_prefix}' + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.stderr = Testers.ExcludesExpression( + 'Invalid argument', 'clear must skip tombstoned slots, not fail on them') + + def run(self): + self._start_ts1() + self._clean_shutdown_ts1() + self._start_ts2() + self._clean_shutdown_ts2() + self._cleanup_shm() + + CacheShmFastRestartTest().run() + urtest.execute() diff --git a/tests/uranium_tests/cache/test_cache_shm_purge_on_disable.py b/tests/uranium_tests/cache/test_cache_shm_purge_on_disable.py new file mode 100644 index 00000000000..7cfb81bdef1 --- /dev/null +++ b/tests/uranium_tests/cache/test_cache_shm_purge_on_disable.py @@ -0,0 +1,253 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_cache_shm_purge_on_disable(urtest: UraniumTest) -> None: + ''' + Purge-stale-on-start: when shm is disabled but a prior run left segments behind, + proxy.config.cache.shm.purge_stale_on_start=1 removes them at startup. + + This guards two hazards of running with the feature disabled after it had been + enabled (see records.yaml docs): (a) the leftover segments keep consuming tmpfs + the disabled instance never reads, and (b) a later re-enabled run would otherwise + fast-attach a directory that went stale while ATS ran disabled (writing only to + disk). + + Three scenarios, each on its own shm prefix + on-disk storage so they do not + interact: + + - PURGE (positive): a seed instance runs shm-enabled and clean-shuts-down, + leaving a clean control + stripe segment. A second instance runs disabled + with purge_stale_on_start=1 and must remove them. Confirmed three ways: the + seed's "clean" segment exists before (traffic_ctl cache shm status, exit 0), + the disabled instance logs the purge Note, and the segment is gone after + (status exits 2, "not found"). + + - KEEP (negative): same seed, but the disabled instance has + purge_stale_on_start=0. It must NOT log the purge and the segment must remain. + + - NOOP (no leftover): a disabled instance with purge_stale_on_start=1 against a + never-used prefix must do nothing quietly -- no purge Note, no "cannot open" + warning. + + The segments are inspected with traffic_ctl (POSIX shm is not path-addressable on + macOS, so /dev/shm cannot be listed directly). + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import os + import re + import sys + + urtest.Summary = ''' + shm.purge_stale_on_start removes leftover shm segments at startup when shm is + disabled, only when set, and only when a control segment exists. + ''' + urtest.ContinueOnFail = True + + class CacheShmPurgeOnDisableTest: + + TS_PID_SCRIPT = 'ts_process_handler.py' + DISK_SIZE_BYTES = 256 * 1024 * 1024 # 256 MiB; matches the other shm Uranium tests. + + # CtrlCommand sets this exit code when a shm control segment is absent/invalid + # (src/traffic_ctl/TrafficCtlStatus.h). + CTRL_EX_ERROR = 2 + + PURGE_NOTE = r"cache shm: purged stale segments while disabled \(removed [1-9]" + + def __init__(self): + urtest.Setup.Copy(os.path.join(urtest.TestDirectory, '..', 'logging', self.TS_PID_SCRIPT)) + + pid = os.getpid() % 100000 + # Each control name is "control"; keep well under macOS PSHMNAMLEN (31). + self._prefix_purge = f'/cshmp-{pid}-' # positive: must be purged + self._prefix_keep = f'/cshmk-{pid}-' # negative: must remain + self._prefix_noop = f'/cshmz-{pid}-' # no leftover: nothing to do + + # Seed (shm enabled) instances that create the leftover segments. + self.seed_purge = self._make_ts('cshm_seed_p', self._prefix_purge, 'disk_p.img', enabled=True, purge=False) + self.seed_keep = self._make_ts('cshm_seed_k', self._prefix_keep, 'disk_k.img', enabled=True, purge=False) + + # Disabled instances under test. + self.run_purge = self._make_ts('cshm_run_p', self._prefix_purge, 'disk_p.img', enabled=False, purge=True) + self.run_keep = self._make_ts('cshm_run_k', self._prefix_keep, 'disk_k.img', enabled=False, purge=False) + self.run_noop = self._make_ts('cshm_run_z', self._prefix_noop, 'disk_z.img', enabled=False, purge=True) + + self._add_diags_assertions() + + def _make_ts(self, name, prefix, disk_name, enabled, purge): + disk_path = self._ensure_disk(disk_name) + ts = urtest.MakeATSProcess(name) + ts.Disk.storage_yaml.AddLines( + [ + 'cache:', + ' spans:', + ' - name: disk.0', + f' path: {disk_path}', + f' size: {self.DISK_SIZE_BYTES}', + ' volumes:', + ' - id: 1', + ' scheme: http', + ' size: 100%', + ]) + ts.Disk.remap_config.AddLine('map / http://127.0.0.1:8080/') # never exercised; keeps remap.config non-empty + ts.Disk.records_config.update( + { + 'proxy.config.cache.shm.enabled': 1 if enabled else 0, + 'proxy.config.cache.shm.name_prefix': prefix, + 'proxy.config.cache.shm.use_hugepages': 0, + 'proxy.config.cache.shm.purge_stale_on_start': 1 if purge else 0, + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'cache_shm', + 'proxy.config.diags.output.diag': 'L', + 'proxy.config.http.wait_for_cache': 1, + }) + return ts + + def _ensure_disk(self, disk_name): + storage_dir = os.path.join(urtest.RunDirectory, 'storage') + os.makedirs(storage_dir, exist_ok=True) + path = os.path.join(storage_dir, disk_name) + if not os.path.exists(path): + with open(path, 'ab') as f: + f.truncate(self.DISK_SIZE_BYTES) + return path + + def _add_diags_assertions(self): + # Seeds create a fresh control segment and mark it clean on the way out -- + # that is the "fast-attachable but now stale" state the purge must clean up. + for seed in (self.seed_purge, self.seed_keep): + seed.Disk.diags_log.Content += Testers.ContainsExpression( + r'cache shm: creating fresh control segment', 'seed should create a fresh shm control segment') + seed.Disk.diags_log.Content += Testers.ContainsExpression( + r'cache shm: marking clean shutdown', 'seed should mark the shm clean before exit') + + # Positive: the disabled+purge instance logs the purge of at least one segment. + self.run_purge.Disk.diags_log.Content += Testers.ContainsExpression( + self.PURGE_NOTE, 'disabled instance with purge_stale_on_start=1 should purge the leftover segments') + + # Negative: purge_stale_on_start=0 must never purge. + self.run_keep.Disk.diags_log.Content += Testers.ExcludesExpression( + r'cache shm: purged stale segments', 'purge_stale_on_start=0 must not purge') + + # No-op: nothing exists for this prefix, so neither a purge nor an error. + self.run_noop.Disk.diags_log.Content += Testers.ExcludesExpression( + r'cache shm: purged stale segments', 'no leftover means nothing is purged') + self.run_noop.Disk.diags_log.Content += Testers.ExcludesExpression( + r'cache shm: cannot open control segment', 'a missing control segment is a quiet no-op, not a warning') + + def _shm_status(self, description, ts, prefix, expect_present): + """Run `traffic_ctl cache shm status` and assert the control segment is (not) there.""" + control_name = prefix + 'control' + tr = urtest.AddTestRun(description) + tr.Processes.Default.Env = ts.Env + tr.Processes.Default.Command = f'traffic_ctl cache shm status --prefix {prefix}' + if expect_present: + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.stdout = Testers.ContainsExpression( + r'Control segment:\s+' + re.escape(control_name), 'control segment should be present') + else: + tr.Processes.Default.ReturnCode = self.CTRL_EX_ERROR + tr.Processes.Default.Streams.stderr = Testers.ContainsExpression( + r"control segment '" + re.escape(control_name) + r"' not found", 'control segment should be gone') + return tr + + def _start_seed(self, description, seed, prefix): + # Starting the seed (shm enabled) creates the control + stripe segments; the + # status probe also confirms they exist while the seed is the live owner. + tr = self._shm_status(description, seed, prefix, expect_present=True) + tr.Processes.Default.StartBefore(seed) + tr.StillRunningAfter = seed + return tr + + def _clean_shutdown(self, description, seed, name): + tr = urtest.AddTestRun(description) + tr.Processes.Default.Env = seed.Env + tr.Processes.Default.Command = ( + f'traffic_ctl server drain && sleep 1 && ' + f'{sys.executable} ./{self.TS_PID_SCRIPT} {name} --signal TERM && sleep 3') + tr.Processes.Default.ReturnCode = 0 + + def _start_disabled(self, description, ts, prefix, expect_present): + # Start the disabled instance under test; its purge (or no-op) runs during + # cache init, so by the time it is ready the status below reflects the result. + tr = self._shm_status(description, ts, prefix, expect_present=expect_present) + tr.Processes.Default.StartBefore(ts) + tr.StillRunningAfter = ts + return tr + + def _cleanup(self): + tr = urtest.AddTestRun('Unlink any remaining test shm segments') + tr.Processes.Default.Env = self.run_keep.Env + tr.Processes.Default.Command = ( + f'traffic_ctl cache shm clear --prefix {self._prefix_purge} ; ' + f'traffic_ctl cache shm clear --prefix {self._prefix_keep} ; ' + f'traffic_ctl cache shm clear --prefix {self._prefix_noop}') + tr.Processes.Default.ReturnCode = 0 + + def run(self): + # PURGE (positive) + self._start_seed('PURGE: start shm-enabled seed; control segment is created', self.seed_purge, self._prefix_purge) + self._clean_shutdown('PURGE: clean-shutdown seed (leaves a clean segment)', self.seed_purge, 'cshm_seed_p') + # Probe with a Env whose bin/ autest has already populated (seed_purge was + # started above); run_purge has not started yet, so its bin/ does not exist. + self._shm_status( + 'PURGE: precondition -- clean leftover segment is present', + self.seed_purge, + self._prefix_purge, + expect_present=True).Processes.Default.Streams.stdout += Testers.ContainsExpression( + r'clean_shutdown:\s+1 \(clean\)', 'leftover segment should be marked clean (the stale-but-attachable case)') + self._start_disabled( + 'PURGE: start disabled+purge=1; leftover segments are removed', + self.run_purge, + self._prefix_purge, + expect_present=False) + + # KEEP (negative) + self._start_seed('KEEP: start shm-enabled seed; control segment is created', self.seed_keep, self._prefix_keep) + self._clean_shutdown('KEEP: clean-shutdown seed (leaves a clean segment)', self.seed_keep, 'cshm_seed_k') + self._start_disabled( + 'KEEP: start disabled+purge=0; leftover segments remain', self.run_keep, self._prefix_keep, expect_present=True) + + # NOOP (no leftover) + tr = urtest.AddTestRun('NOOP: start disabled+purge=1 against an unused prefix; nothing to do') + tr.Processes.Default.Env = self.run_noop.Env + tr.Processes.Default.Command = f'traffic_ctl cache shm status --prefix {self._prefix_noop}' + tr.Processes.Default.ReturnCode = self.CTRL_EX_ERROR # never existed + tr.Processes.Default.StartBefore(self.run_noop) + tr.StillRunningAfter = self.run_noop + + self._cleanup() + + CacheShmPurgeOnDisableTest().run() + urtest.execute() diff --git a/tests/uranium_tests/cache/test_cache_shm_schema_mismatch.py b/tests/uranium_tests/cache/test_cache_shm_schema_mismatch.py new file mode 100644 index 00000000000..30d96bf8f8d --- /dev/null +++ b/tests/uranium_tests/cache/test_cache_shm_schema_mismatch.py @@ -0,0 +1,226 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_cache_shm_schema_mismatch(urtest: UraniumTest) -> None: + ''' + Verify the shm schema-version trust gate: a control segment whose schema_version + does not match the running build is dropped, never fast-attached. ts1 cold-starts, + caches an object, and clean-shuts-down (marking the segment clean). The segment + file under /dev/shm is then tampered -- schema_version is overwritten with a + bogus value -- before ts2 starts against the same shm prefix. ts2 must detect the + mismatch, drop the segment, recreate it fresh, and rebuild the directory from disk. + + Linux-only: it pokes raw bytes in the /dev/shm segment file, which exists only on + Linux (macOS POSIX shm segments are not path-addressable). + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import os + import platform + import sys + import uuid + + urtest.Summary = ''' + A control segment with a mismatched schema_version is dropped and rebuilt from + disk, never fast-attached. + ''' + urtest.ContinueOnFail = True + + # The byte-poke drives the gate by editing /dev/shm directly, which is a Linux + # facility; macOS POSIX shm is not exposed as a file. There is no Condition for + # the platform, so gate with a lambda (ports.py branches on platform the same way). + urtest.SkipUnless(Condition(lambda: platform.system() == 'Linux', "shm byte-poke gates need Linux /dev/shm")) + + class CacheShmSchemaMismatchTest: + """ + The schema-version gate. The control header records the build's + CACHE_SHM_SCHEMA_VERSION; on attach, a segment whose recorded version differs + is dropped ("schema mismatch ( vs ), dropping") rather than trusted + -- the on-disk struct layout it describes may no longer match this build. The + ABI-hash gate (abi_hash @16) works identically; this test exercises the + schema field (@8) as the representative case. + + Sequence: ts1 creates a clean segment, then schema_version is poked to a bogus + value, then ts2 starts and must: + - log the schema mismatch and drop, + - recreate a fresh control segment, + - NOT fast-attach, + - and still serve a request (200). + """ + + TS_PID_SCRIPT = 'ts_process_handler.py' + POKE_SCRIPT = 'shm_poke.py' + + SHARED_DISK_SIZE_BYTES = 256 * 1024 * 1024 # 256 MiB + + # CacheShmControl layout (CacheShmLayout.h): magic[8] @0, schema_version @8. + SCHEMA_VERSION_OFFSET = 8 + # Little-endian uint32 = 9; the build's CACHE_SHM_SCHEMA_VERSION is small, so + # any value it never uses works. 9 is comfortably out of range. + BOGUS_SCHEMA_LE_HEX = '09000000' + + def __init__(self): + self._setup_shared_state() + self.ts1 = self._configure_ts('shmx_ts1') + self.ts2 = self._configure_ts('shmx_ts2') + self._add_diags_log_assertions() + self._url_path = f'/cache/40/{uuid.uuid4()}' + + def _setup_shared_state(self): + urtest.Setup.Copy(os.path.join(urtest.TestDirectory, '..', 'logging', self.TS_PID_SCRIPT)) + urtest.Setup.Copy(os.path.join(urtest.TestDirectory, self.POKE_SCRIPT)) + + shared_storage_dir = os.path.join(urtest.RunDirectory, 'shared-storage') + os.makedirs(shared_storage_dir, exist_ok=True) + self._shared_storage_path = os.path.join(shared_storage_dir, 'disk.img') + with open(self._shared_storage_path, 'ab') as f: + f.truncate(self.SHARED_DISK_SIZE_BYTES) + + # macOS PSHMNAMLEN is 31 chars incl. '/'; 'x' = schema-mismatch variant. + # (This test is Linux-only, but keep the prefix short for consistency.) + self._shm_prefix = f'/cshmx-{os.getpid() % 100000}-' + # The control segment is name_prefix + "control"; on Linux it is a file + # under /dev/shm by the same name (sans the leading '/'). + self._control_file = '/dev/shm/' + self._shm_prefix.lstrip('/') + 'control' + + def _configure_ts(self, name): + ts = urtest.MakeATSProcess(name) + ts.Disk.storage_yaml.AddLines( + [ + 'cache:', + ' spans:', + ' - name: disk.0', + f' path: {self._shared_storage_path}', + f' size: {self.SHARED_DISK_SIZE_BYTES}', + ' volumes:', + ' - id: 1', + ' scheme: http', + ' size: 100%', + ]) + ts.Disk.records_config.update( + { + 'proxy.config.cache.shm.enabled': 1, + 'proxy.config.cache.shm.name_prefix': self._shm_prefix, + 'proxy.config.cache.shm.use_hugepages': 0, + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'cache_shm', + 'proxy.config.diags.output.diag': 'L', + 'proxy.config.http.wait_for_cache': 1, + }) + ts.Disk.plugin_config.AddLine('xdebug.so --enable=x-cache,via') + ts.Disk.remap_config.AddLine('map / http://127.0.0.1/ @plugin=generator.so') + return ts + + def _add_diags_log_assertions(self): + # ts1 cold start, clean shutdown -- a valid, clean segment to tamper with. + self.ts1.Disk.diags_log.Content += Testers.ContainsExpression( + r'cache shm: creating fresh control segment', 'ts1 should create a fresh shm control segment on first start') + self.ts1.Disk.diags_log.Content += Testers.ContainsExpression( + r'cache shm: marking clean shutdown', 'ts1 should mark the shm clean before exit') + + # ts2 start against the poked segment: detect, drop, recreate, rebuild. + self.ts2.Disk.diags_log.Content += Testers.ContainsExpression( + r'cache shm: schema mismatch \(\d+ vs \d+\), dropping', 'ts2 must detect the schema mismatch and drop the segment') + self.ts2.Disk.diags_log.Content += Testers.ContainsExpression( + r'cache shm: creating fresh control segment', 'ts2 must recreate the control segment after the drop') + self.ts2.Disk.diags_log.Content += Testers.ExcludesExpression( + r'\(fast restart, recovery skipped\)', 'ts2 must rebuild from disk, never fast-attach the mismatched segment') + self.ts2.Disk.diags_log.Content += Testers.ExcludesExpression( + r'cache shm: previous run did not shutdown cleanly', + 'the drop must be due to the schema mismatch, not an unclean shutdown') + + def _populate_cache(self): + tr = urtest.AddTestRun('Cold-start ts1 and cache an object') + tr.Processes.Default.StartBefore(self.ts1) + tr.MakeCurlCommand( + f'-s -o /dev/null -w "%{{http_code}}\\n" ' + f'-H "x-debug: x-cache,via" ' + f'http://127.0.0.1:{self.ts1.Variables.port}{self._url_path}') + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.stdout = Testers.ContainsExpression('200', 'ts1 first GET should return 200') + tr.StillRunningAfter = self.ts1 + + def _clean_shutdown_ts1(self): + tr = urtest.AddTestRun('Drain and clean-shutdown ts1') + tr.Processes.Default.Env = self.ts1.Env + tr.Processes.Default.Command = ( + f'traffic_ctl server drain && sleep 1 && ' + f'{sys.executable} ./{self.TS_PID_SCRIPT} shmx_ts1 --signal TERM && sleep 3') + tr.Processes.Default.ReturnCode = 0 + + def _poke_schema_version(self): + # ts1 is dead; the segment is just a file now. Overwrite schema_version. + tr = urtest.AddTestRun('Tamper schema_version in the shm control segment') + tr.Processes.Default.Command = ( + f'{sys.executable} ./{self.POKE_SCRIPT} {self._control_file} ' + f'{self.SCHEMA_VERSION_OFFSET} {self.BOGUS_SCHEMA_LE_HEX}') + tr.Processes.Default.ReturnCode = 0 + + def _verify_mismatch_drop(self): + tr = urtest.AddTestRun('Start ts2; verify the schema mismatch is dropped and rebuilt from disk') + tr.Processes.Default.StartBefore(self.ts2) + tr.MakeCurlCommand( + f'-s -o /dev/null -w "%{{http_code}}\\n" ' + f'-H "x-debug: x-cache,via" ' + f'http://127.0.0.1:{self.ts2.Variables.port}{self._url_path}') + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.stdout = Testers.ContainsExpression( + '200', 'ts2 should serve correctly after dropping the mismatched segment') + tr.StillRunningAfter = self.ts2 + + def _clean_shutdown_ts2(self): + tr = urtest.AddTestRun('Drain and clean-shutdown ts2') + tr.Processes.Default.Env = self.ts2.Env + tr.Processes.Default.Command = ( + f'traffic_ctl server drain && sleep 1 && ' + f'{sys.executable} ./{self.TS_PID_SCRIPT} shmx_ts2 --signal TERM && sleep 3') + tr.Processes.Default.ReturnCode = 0 + + def _cleanup_shm(self): + tr = urtest.AddTestRun('Unlink the test shm segments') + tr.Processes.Default.Env = self.ts2.Env + tr.Processes.Default.Command = f'traffic_ctl cache shm clear --prefix {self._shm_prefix}' + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.stderr = Testers.ExcludesExpression( + 'Invalid argument', 'clear must skip tombstoned slots, not fail on them') + + def run(self): + self._populate_cache() + self._clean_shutdown_ts1() + self._poke_schema_version() + self._verify_mismatch_drop() + self._clean_shutdown_ts2() + self._cleanup_shm() + + CacheShmSchemaMismatchTest().run() + urtest.execute() diff --git a/tests/uranium_tests/cache/test_cache_shm_storage_mismatch.py b/tests/uranium_tests/cache/test_cache_shm_storage_mismatch.py new file mode 100644 index 00000000000..5765496f5e6 --- /dev/null +++ b/tests/uranium_tests/cache/test_cache_shm_storage_mismatch.py @@ -0,0 +1,229 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_cache_shm_storage_mismatch(urtest: UraniumTest) -> None: + ''' + Verify that a changed storage layout never fast-attaches a stale directory. + A storage.yaml change no longer drops the whole shm control segment; instead + each stripe is matched to its prior segment by its own identity. ts1 caches an + object against one storage file and clean-shuts-down (marking the shm clean); + ts2 starts against a *different* storage file but the *same* shm name prefix. + ts2 finds ts1's control segment, keeps it (partial attach), but because its + stripe identity no longer matches any recorded entry it creates a fresh stripe + segment and reclaims ts1's orphaned one -- it must never fast-attach a segment + that describes a different on-disk layout. + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import os + import sys + import uuid + + urtest.Summary = ''' + A changed storage layout never fast-attaches a stale directory: the control + segment is kept (partial attach), the relocated stripe creates a fresh segment, + and the orphaned prior segment is reclaimed. + ''' + urtest.ContinueOnFail = True + + class CacheShmStorageMismatchTest: + """ + The storage signature is a fingerprint of every span's path and geometry, + stored in the shm control header. It is no longer a hard gate: a storage + change keeps the control segment and lets each stripe attach by its own + identity (its hash_text, which includes the disk path). This test points + ts1 and ts2 at different storage files (a repath) while sharing one shm + prefix, and asserts ts2: + - keeps the existing control segment (does NOT recreate it), + - enters partial-attach mode because the storage signature changed, + - never fast-attaches any stripe segment (its identity differs, so the + stale directory built for storage A is never reused), + - creates a fresh stripe segment for its own (storage B) layout, + - reclaims ts1's now-orphaned stripe segment, + - and still serves a request (200). + Because the storage change does not gate the clean-shutdown check, ts2 must + NOT report the prior run as unclean: the only reason for the recreate is the + storage change. + """ + + TS_PID_SCRIPT = 'ts_process_handler.py' + + SHARED_DISK_SIZE_BYTES = 256 * 1024 * 1024 # 256 MiB + + def __init__(self): + self._setup_shared_state() + # ts1 and ts2 share the shm prefix but use different storage files. + self.ts1 = self._configure_ts('shms_ts1', self._storage_path_a) + self.ts2 = self._configure_ts('shms_ts2', self._storage_path_b) + self._add_diags_log_assertions() + self._url_path = f'/cache/40/{uuid.uuid4()}' + + def _setup_shared_state(self): + urtest.Setup.Copy(os.path.join(urtest.TestDirectory, '..', 'logging', self.TS_PID_SCRIPT)) + + shared_storage_dir = os.path.join(urtest.RunDirectory, 'shared-storage') + os.makedirs(shared_storage_dir, exist_ok=True) + # Two distinct storage files -> distinct span paths -> distinct + # storage signatures, which is exactly the "repath" case under test. + self._storage_path_a = os.path.join(shared_storage_dir, 'disk_a.img') + self._storage_path_b = os.path.join(shared_storage_dir, 'disk_b.img') + for path in (self._storage_path_a, self._storage_path_b): + with open(path, 'ab') as f: + f.truncate(self.SHARED_DISK_SIZE_BYTES) + + # macOS PSHMNAMLEN is 31 chars incl. '/'; 's' = storage-mismatch variant. + self._shm_prefix = f'/cshms-{os.getpid() % 100000}-' + + def _configure_ts(self, name, storage_path): + ts = urtest.MakeATSProcess(name) + ts.Disk.storage_yaml.AddLines( + [ + 'cache:', + ' spans:', + ' - name: disk.0', + f' path: {storage_path}', + f' size: {self.SHARED_DISK_SIZE_BYTES}', + ' volumes:', + ' - id: 1', + ' scheme: http', + ' size: 100%', + ]) + ts.Disk.records_config.update( + { + 'proxy.config.cache.shm.enabled': 1, + 'proxy.config.cache.shm.name_prefix': self._shm_prefix, + 'proxy.config.cache.shm.use_hugepages': 0, + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'cache_shm', + 'proxy.config.diags.output.diag': 'L', + 'proxy.config.http.wait_for_cache': 1, + }) + ts.Disk.plugin_config.AddLine('xdebug.so --enable=x-cache,via') + ts.Disk.remap_config.AddLine('map / http://127.0.0.1/ @plugin=generator.so') + return ts + + def _add_diags_log_assertions(self): + # ts1 cold start against storage A, clean shutdown. + self.ts1.Disk.diags_log.Content += Testers.ContainsExpression( + r'cache shm: creating fresh control segment', 'ts1 should create a fresh shm control segment on first start') + self.ts1.Disk.diags_log.Content += Testers.ContainsExpression( + r'created stripe \S+ \(\d+ bytes\) for key=', 'ts1 should create at least one shm-backed stripe segment') + self.ts1.Disk.diags_log.Content += Testers.ContainsExpression( + r'cache shm: marking clean shutdown', 'ts1 should mark the shm clean before exit') + + # ts2 start against storage B: the storage signature differs, so the + # control segment is kept (partial attach) but the relocated stripe + # creates a fresh segment rather than fast-attaching the stale one. + self.ts2.Disk.diags_log.Content += Testers.ContainsExpression( + r'attaching up to \d+ stripes \(fast restart, partial -- storage changed\)', + 'ts2 must enter partial-attach mode after the storage change') + self.ts2.Disk.diags_log.Content += Testers.ContainsExpression( + r'created stripe \S+ \(\d+ bytes\) for key=', 'ts2 must create a fresh stripe segment for its own layout') + self.ts2.Disk.diags_log.Content += Testers.ContainsExpression( + r'cache shm: reclaiming orphaned stripe segment', "ts2 must reclaim ts1's orphaned stripe segment") + self.ts2.Disk.diags_log.Content += Testers.ContainsExpression( + r'reclaimed \d+ orphaned stripe segment\(s\) after attach', 'ts2 must report the reclaim summary') + self.ts2.Disk.diags_log.Content += Testers.ExcludesExpression( + r'attached stripe \S+ \(\d+ bytes\) for key=', + 'ts2 must never fast-attach a stripe segment built for a different layout') + self.ts2.Disk.diags_log.Content += Testers.ExcludesExpression( + r'cache shm: creating fresh control segment', 'ts2 must keep the control segment across the storage change') + self.ts2.Disk.diags_log.Content += Testers.ExcludesExpression( + r'cache shm: (schema|ABI) mismatch', 'the recreate must be due to the storage change, not schema/ABI') + self.ts2.Disk.diags_log.Content += Testers.ExcludesExpression( + r'cache shm: previous run did not shutdown cleanly', + 'the recreate must be due to the storage change, not an unclean shutdown') + + def _populate_cache(self): + tr = urtest.AddTestRun('Populate cache via ts1 (storage A)') + tr.Processes.Default.StartBefore(self.ts1) + tr.MakeCurlCommand( + f'-s -o /dev/null -w "%{{http_code}}\\n" ' + f'-H "x-debug: x-cache,via" ' + f'http://127.0.0.1:{self.ts1.Variables.port}{self._url_path}') + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.stdout = Testers.ContainsExpression('200', 'ts1 first GET should return 200') + tr.StillRunningAfter = self.ts1 + + def _clean_shutdown_ts1(self): + tr = urtest.AddTestRun('Drain and clean-shutdown ts1') + tr.Processes.Default.Env = self.ts1.Env + tr.Processes.Default.Command = ( + f'traffic_ctl server drain && sleep 1 && ' + f'{sys.executable} ./{self.TS_PID_SCRIPT} shms_ts1 --signal TERM && sleep 3') + tr.Processes.Default.ReturnCode = 0 + + def _verify_partial_attach_and_reclaim(self): + tr = urtest.AddTestRun('Start ts2 (storage B); verify partial attach: fresh stripe + orphan reclaim') + tr.Processes.Default.StartBefore(self.ts2) + tr.MakeCurlCommand( + f'-s -o /dev/null -w "%{{http_code}}\\n" ' + f'-H "x-debug: x-cache,via" ' + f'http://127.0.0.1:{self.ts2.Variables.port}{self._url_path}') + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.stdout = Testers.ContainsExpression( + '200', 'ts2 should serve correctly after the partial attach') + tr.StillRunningAfter = self.ts2 + + def _clean_shutdown_ts2(self): + # Stop ts2 before clearing the shm: `cache shm clear` refuses to unlink a + # segment a live traffic_server still owns, so the owner must be gone + # (its retained owner_pid then reads as dead) before cleanup runs. + tr = urtest.AddTestRun('Drain and clean-shutdown ts2') + tr.Processes.Default.Env = self.ts2.Env + tr.Processes.Default.Command = ( + f'traffic_ctl server drain && sleep 1 && ' + f'{sys.executable} ./{self.TS_PID_SCRIPT} shms_ts2 --signal TERM && sleep 3') + tr.Processes.Default.ReturnCode = 0 + + def _cleanup_shm(self): + # A clean shutdown deliberately keeps the control + live stripe segments + # for the next fast restart, so they outlive the test. Unlink them by + # prefix to avoid leaking POSIX shm across repeated local runs (macOS has + # no /dev/shm to clear out of band). + tr = urtest.AddTestRun('Unlink the test shm segments') + tr.Processes.Default.Env = self.ts2.Env + tr.Processes.Default.Command = f'traffic_ctl cache shm clear --prefix {self._shm_prefix}' + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.stderr = Testers.ExcludesExpression( + 'Invalid argument', 'clear must skip tombstoned slots, not fail on them') + + def run(self): + self._populate_cache() + self._clean_shutdown_ts1() + self._verify_partial_attach_and_reclaim() + self._clean_shutdown_ts2() + self._cleanup_shm() + + CacheShmStorageMismatchTest().run() + urtest.execute() diff --git a/tests/uranium_tests/cache/test_cache_shm_unclean_shutdown.py b/tests/uranium_tests/cache/test_cache_shm_unclean_shutdown.py new file mode 100644 index 00000000000..db877be3379 --- /dev/null +++ b/tests/uranium_tests/cache/test_cache_shm_unclean_shutdown.py @@ -0,0 +1,205 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_cache_shm_unclean_shutdown(urtest: UraniumTest) -> None: + ''' + Verify the shm fast-restart path refuses to trust a directory left by a crash. + A clean shutdown marks the control segment clean; a crash (SIGKILL) does not, + so clean_shutdown stays 0. ts1 cold-starts, caches an object, and is *killed* + (no drain, no SIGTERM) so the shutdown hook never runs. ts2 starts against the + same on-disk cache and shm prefix: it must find the dirty segment, drop the + whole thing, rebuild the directory from disk, and never take the fast-attach + "recovery skipped" path. + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import os + import sys + import uuid + + urtest.Summary = ''' + An unclean shutdown (SIGKILL) leaves the shm control segment dirty, so the next + start drops it and rebuilds the directory from disk instead of fast-attaching. + ''' + urtest.ContinueOnFail = True + + class CacheShmUncleanShutdownTest: + """ + The crash-safety gate. clean_shutdown is set to 1 only by the shutdown hook + (CacheShm::mark_clean_shutdown); a SIGKILL bypasses it, leaving the segment + with clean_shutdown == 0. On the next start the control segment is found but + rejected -- a crash may have left dir entries pointing at content that never + reached disk, so no stripe can safely skip recovery. ts2 must: + - log "previous run did not shutdown cleanly, dropping", + - recreate a fresh control segment, + - NOT take the stripe fast-attach "recovery skipped" path, + - and still serve a request (200). + + This gate is cross-platform: clean_shutdown lives in the control segment, so + it does not depend on the Linux-only flock path. + """ + + TS_PID_SCRIPT = 'ts_process_handler.py' + + SHARED_DISK_SIZE_BYTES = 256 * 1024 * 1024 # 256 MiB + + def __init__(self): + self._setup_shared_state() + # ts1 and ts2 share the same on-disk cache file and shm prefix so ts2 + # would fast-attach ts1's directory -- were it not left dirty by the kill. + self.ts1 = self._configure_ts('shmu_ts1') + # ts1 is SIGKILLed mid-test, so it exits on signal 9 (returncode -9, or + # 137 where the runner reports 128+signal). Declare that expected exit so + # the managed-process check does not flag the deliberate kill. ts1 still + # starts normally, so leave Ready at its default (port-open) condition. + self.ts1.ReturnCode = Any(-9, 137) + self.ts2 = self._configure_ts('shmu_ts2') + self._add_diags_log_assertions() + self._url_path = f'/cache/40/{uuid.uuid4()}' + + def _setup_shared_state(self): + urtest.Setup.Copy(os.path.join(urtest.TestDirectory, '..', 'logging', self.TS_PID_SCRIPT)) + + shared_storage_dir = os.path.join(urtest.RunDirectory, 'shared-storage') + os.makedirs(shared_storage_dir, exist_ok=True) + self._shared_storage_path = os.path.join(shared_storage_dir, 'disk.img') + with open(self._shared_storage_path, 'ab') as f: + f.truncate(self.SHARED_DISK_SIZE_BYTES) + + # macOS PSHMNAMLEN is 31 chars incl. '/'; 'u' = unclean-shutdown variant. + self._shm_prefix = f'/cshmu-{os.getpid() % 100000}-' + + def _configure_ts(self, name): + ts = urtest.MakeATSProcess(name) + ts.Disk.storage_yaml.AddLines( + [ + 'cache:', + ' spans:', + ' - name: disk.0', + f' path: {self._shared_storage_path}', + f' size: {self.SHARED_DISK_SIZE_BYTES}', + ' volumes:', + ' - id: 1', + ' scheme: http', + ' size: 100%', + ]) + ts.Disk.records_config.update( + { + 'proxy.config.cache.shm.enabled': 1, + 'proxy.config.cache.shm.name_prefix': self._shm_prefix, + 'proxy.config.cache.shm.use_hugepages': 0, + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'cache_shm', + 'proxy.config.diags.output.diag': 'L', + 'proxy.config.http.wait_for_cache': 1, + }) + ts.Disk.plugin_config.AddLine('xdebug.so --enable=x-cache,via') + ts.Disk.remap_config.AddLine('map / http://127.0.0.1/ @plugin=generator.so') + return ts + + def _add_diags_log_assertions(self): + # ts1 cold start: creates a fresh segment but is killed before it can mark + # the shutdown clean. + self.ts1.Disk.diags_log.Content += Testers.ContainsExpression( + r'cache shm: creating fresh control segment', 'ts1 should create a fresh shm control segment on first start') + self.ts1.Disk.diags_log.Content += Testers.ExcludesExpression( + r'cache shm: marking clean shutdown', 'ts1 is SIGKILLed, so it must never mark the shm clean') + + # ts2 start: finds the dirty segment, drops it, recreates, and rebuilds + # from disk -- it must NOT fast-attach. + self.ts2.Disk.diags_log.Content += Testers.ContainsExpression( + r'cache shm: previous run did not shutdown cleanly, dropping', + 'ts2 must reject the dirty segment left by the crash') + self.ts2.Disk.diags_log.Content += Testers.ContainsExpression( + r'cache shm: creating fresh control segment', 'ts2 must recreate the control segment after dropping the dirty one') + self.ts2.Disk.diags_log.Content += Testers.ExcludesExpression( + r'\(fast restart, recovery skipped\)', 'ts2 must rebuild from disk, never take the fast-attach path') + self.ts2.Disk.diags_log.Content += Testers.ExcludesExpression( + r'cache shm: attaching up to \d+ stripes \(fast restart', 'ts2 must not attach the dirty control segment') + + def _populate_cache(self): + tr = urtest.AddTestRun('Cold-start ts1 and cache an object') + tr.Processes.Default.StartBefore(self.ts1) + tr.MakeCurlCommand( + f'-s -o /dev/null -w "%{{http_code}}\\n" ' + f'-H "x-debug: x-cache,via" ' + f'http://127.0.0.1:{self.ts1.Variables.port}{self._url_path}') + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.stdout = Testers.ContainsExpression('200', 'ts1 first GET should return 200') + tr.StillRunningAfter = self.ts1 + + def _kill_ts1(self): + # SIGKILL -- no drain, no SIGTERM -- so the shutdown hook never runs and + # the control segment is left with clean_shutdown == 0. + tr = urtest.AddTestRun('SIGKILL ts1 (unclean shutdown)') + tr.Processes.Default.Env = self.ts1.Env + tr.Processes.Default.Command = (f'{sys.executable} ./{self.TS_PID_SCRIPT} shmu_ts1 --signal KILL && sleep 3') + tr.Processes.Default.ReturnCode = 0 + + def _verify_dirty_drop(self): + tr = urtest.AddTestRun('Start ts2; verify the dirty segment is dropped and rebuilt from disk') + tr.Processes.Default.StartBefore(self.ts2) + tr.MakeCurlCommand( + f'-s -o /dev/null -w "%{{http_code}}\\n" ' + f'-H "x-debug: x-cache,via" ' + f'http://127.0.0.1:{self.ts2.Variables.port}{self._url_path}') + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.stdout = Testers.ContainsExpression( + '200', 'ts2 should serve correctly after dropping the dirty segment') + tr.StillRunningAfter = self.ts2 + + def _clean_shutdown_ts2(self): + tr = urtest.AddTestRun('Drain and clean-shutdown ts2') + tr.Processes.Default.Env = self.ts2.Env + tr.Processes.Default.Command = ( + f'traffic_ctl server drain && sleep 1 && ' + f'{sys.executable} ./{self.TS_PID_SCRIPT} shmu_ts2 --signal TERM && sleep 3') + tr.Processes.Default.ReturnCode = 0 + + def _cleanup_shm(self): + tr = urtest.AddTestRun('Unlink the test shm segments') + tr.Processes.Default.Env = self.ts2.Env + tr.Processes.Default.Command = f'traffic_ctl cache shm clear --prefix {self._shm_prefix}' + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.stderr = Testers.ExcludesExpression( + 'Invalid argument', 'clear must skip tombstoned slots, not fail on them') + + def run(self): + self._populate_cache() + self._kill_ts1() + self._verify_dirty_drop() + self._clean_shutdown_ts2() + self._cleanup_shm() + + CacheShmUncleanShutdownTest().run() + urtest.execute() diff --git a/tests/uranium_tests/cache/test_cache_write_lock_contention.py b/tests/uranium_tests/cache/test_cache_write_lock_contention.py new file mode 100644 index 00000000000..eacc382499c --- /dev/null +++ b/tests/uranium_tests/cache/test_cache_write_lock_contention.py @@ -0,0 +1,193 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_cache_write_lock_contention(urtest: UraniumTest) -> None: + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + ''' + Test cache write lock contention behavior with parallel requests for actions 5 and 6. + + This test issues multiple parallel curl requests to trigger actual cache write lock + contention. It is skipped by default because the behavior is timing-sensitive and + not suitable for CI environments. + + To run this test manually: + export RUN_CACHE_CONTENTION_TEST=1 + cd build/tests + ./autest.sh --sandbox /tmp/sbcursor --clean=none -f cache-write-lock-contention + ''' + + import os + + urtest.Summary = ''' + Test cache write lock contention with parallel requests (actions 5 and 6). + + This test is SKIPPED by default. Set RUN_CACHE_CONTENTION_TEST=1 to run it. + ''' + + urtest.ContinueOnFail = True + + urtest.SkipUnless( + Condition( + lambda: os.environ.get('RUN_CACHE_CONTENTION_TEST', '').lower() in ('1', 'true', 'yes'), + "Set RUN_CACHE_CONTENTION_TEST=1 to run this timing-sensitive test", True)) + + def make_parallel_curl(url, count, stagger_delay=None): + """ + Generate a parallel curl command string. + + Args: + url: The URL to request + count: Number of parallel requests + stagger_delay: If set, add this delay between requests (e.g., "0.1") + If None, all requests fire simultaneously + """ + parts = ['('] + for i in range(1, count + 1): + if i > 1 and stagger_delay: + parts.append(f'sleep {stagger_delay} && ') + parts.append( + f'{{curl}} -s -o /dev/null -w "req{i}: %{{{{http_code}}}}\\n" "{url}" ' + f'-H "Host: example.com" -H "X-Request: req{i}" & ') + parts.append('wait)') + return ''.join(parts) + + class ContentionTest: + """ + Base class for cache write lock contention tests. + + Tests parallel requests to trigger cache write lock contention and verify + that the configured fail_action behaves correctly. + """ + + def __init__(self, name, action, description, stale_scenario=False): + self.name = name + self.action = action + self.description = description + self.stale_scenario = stale_scenario + self._setup() + + def _setup(self): + origin_delay = 5 if self.stale_scenario else 3 + self.server = urtest.MakeOriginServer(f"server_{self.name}", delay=origin_delay) + + max_age = 1 if self.stale_scenario else 300 + self.server.addResponse( + "sessionlog.json", { + "headers": f"GET /test-{self.name} HTTP/1.1\r\nHost: example.com\r\n\r\n", + "timestamp": "1469733493.993", + "body": "" + }, { + "headers": + f"HTTP/1.1 200 OK\r\n" + f"Content-Length: 100\r\n" + f"Cache-Control: max-age={max_age}\r\n" + f"X-Origin: {self.name}\r\n" + f"Connection: close\r\n\r\n", + "timestamp": "1469733493.993", + "body": "X" * 100 + }) + + self.ts = urtest.MakeATSProcess(f"ts_{self.name}", enable_cache=True) + + read_retries = 2 if self.stale_scenario else 10 + retry_time = 100 if self.stale_scenario else 500 + + self.ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'http|cache|http_cache|http_trans', + 'proxy.config.http.cache.open_write_fail_action': self.action, + 'proxy.config.http.cache.max_open_write_retries': 1, + 'proxy.config.http.cache.max_open_write_retry_timeout': 0, + 'proxy.config.http.cache.max_open_read_retries': read_retries, + 'proxy.config.http.cache.open_read_retry_time': retry_time, + 'proxy.config.cache.enable_read_while_writer': 1, + 'proxy.config.http.cache.max_stale_age': 300, + }) + self.ts.Disk.remap_config.AddLine(f'map http://example.com/ http://127.0.0.1:{self.server.Variables.Port}/') + + def run(self): + url_path = f"/test-{self.name}" + url = f"http://127.0.0.1:{self.ts.Variables.port}{url_path}" + + if self.stale_scenario: + tr = urtest.AddTestRun(f"{self.description} - Prime cache") + tr.Processes.Default.StartBefore(self.server) + tr.Processes.Default.StartBefore(self.ts) + tr.MakeCurlCommand(f'-s -D - "{url}" -H "Host: example.com"', ts=self.ts) + tr.Processes.Default.ReturnCode = 0 + tr.StillRunningAfter = self.ts + tr.StillRunningAfter = self.server + + tr = urtest.AddTestRun(f"{self.description} - Wait for stale") + tr.Processes.Default.Command = "sleep 3" + tr.Processes.Default.ReturnCode = 0 + tr.StillRunningAfter = self.ts + tr.StillRunningAfter = self.server + + tr = urtest.AddTestRun(self.description) + if not self.stale_scenario: + tr.Processes.Default.StartBefore(self.server) + tr.Processes.Default.StartBefore(self.ts) + + count = 8 if self.stale_scenario else 5 + stagger = None if self.stale_scenario else "0.1" + tr.MakeCurlCommandMulti(make_parallel_curl(url, count, stagger), ts=self.ts) + tr.Processes.Default.ReturnCode = 0 + tr.StillRunningAfter = self.ts + tr.StillRunningAfter = self.server + + tr = urtest.AddTestRun(f"{self.description} - Verify cache") + tr.MakeCurlCommand(f'-s -D - "{url}" -H "Host: example.com"', ts=self.ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.stdout = Testers.ContainsExpression("200 OK", "Cache serves 200") + tr.StillRunningAfter = self.ts + tr.StillRunningAfter = self.server + + self.ts.Disk.traffic_out.Content = Testers.ExcludesExpression("FATAL|ink_release_assert|ink_abort", "No crashes") + + if self.stale_scenario and self.action == 6: + self.ts.Disk.traffic_out.Content += Testers.ContainsExpression( + "serving stale \\(action 6\\)|object stale, serving stale", "Stale fallback triggered") + else: + self.ts.Disk.traffic_out.Content += Testers.ContainsExpression( + "cache open read failure.*retrying|read while write", "Cache contention occurred") + + # Action 5: READ_RETRY - retries cache reads, goes to origin if exhausted + ContentionTest("a5_fresh", 5, "Action 5 - fresh contention").run() + ContentionTest("a5_stale", 5, "Action 5 - stale revalidation", stale_scenario=True).run() + + # Action 6: READ_RETRY_STALE_ON_REVALIDATE - like action 5, but serves stale if available + ContentionTest("a6_fresh", 6, "Action 6 - fresh contention").run() + ContentionTest("a6_stale", 6, "Action 6 - stale fallback", stale_scenario=True).run() + urtest.execute() diff --git a/tests/uranium_tests/cache/test_disjoint_wait_for_cache.py b/tests/uranium_tests/cache/test_disjoint_wait_for_cache.py new file mode 100644 index 00000000000..8eade32e3ca --- /dev/null +++ b/tests/uranium_tests/cache/test_disjoint_wait_for_cache.py @@ -0,0 +1,125 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_disjoint_wait_for_cache(urtest: UraniumTest) -> None: + ''' + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import uuid + + urtest.Summary = ''' + Same as cache-generation-disjoint, but uses proxy.config.http.wait_for_cache which should delay + the server from accepting connection till the cache is loaded + ''' + + urtest.ContinueOnFail = True + # Define default ATS + ts = urtest.MakeATSProcess("ts") + + # Setup some config file for this server. Note that setting wait_for_cache to 3 + # will intentionally override the value set in MakeATSProcess. + ts.Disk.records_config.update( + { + 'proxy.config.body_factory.enable_customizations': 3, # enable domain specific body factory + 'proxy.config.http.cache.generation': -1, # Start with cache turned off + 'proxy.config.http.wait_for_cache': 3, + }) + ts.Disk.plugin_config.AddLine('xdebug.so --enable=x-cache,x-cache-key,via,x-cache-generation') + ts.Disk.remap_config.AddLines( + [ + 'map /default/ http://127.0.0.1/ @plugin=generator.so', + # line 2 + 'map /generation1/ http://127.0.0.1/' + ' @plugin=conf_remap.so @pparam=proxy.config.http.cache.generation=1' + + ' @plugin=generator.so', + # line 3 + 'map /generation2/ http://127.0.0.1/' + ' @plugin=conf_remap.so @pparam=proxy.config.http.cache.generation=2' + + ' @plugin=generator.so', + ]) + + objectid = uuid.uuid4() + # first test is a miss for default + tr = urtest.AddTestRun() + tr.MakeCurlCommand( + '"http://127.0.0.1:{0}/default/cache/10/{1}" -H "x-debug: x-cache,x-cache-key,via,x-cache-generation" --verbose'.format( + ts.Variables.port, objectid), + ts=ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.StartBefore(urtest.Processes.ts) + tr.Processes.Default.Streams.All = "gold/miss_default-1.gold" + + # Same URL in generation 1 is a MISS. + tr = urtest.AddTestRun() + tr.MakeCurlCommand( + '"http://127.0.0.1:{0}/generation1/cache/10/{1}" -H "x-debug: x-cache,x-cache-key,via,x-cache-generation" --verbose -o /dev/null' + .format(ts.Variables.port, objectid), + ts=ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.All = "gold/miss_gen1.gold" + + # Same URL in generation 2 is still a MISS. + tr = urtest.AddTestRun() + tr.MakeCurlCommand( + '"http://127.0.0.1:{0}/generation2/cache/10/{1}" -H "x-debug: x-cache,x-cache-key,via,x-cache-generation" --verbose -o /dev/null' + .format(ts.Variables.port, objectid), + ts=ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.All = "gold/miss_gen2.gold" + + # Second touch is a HIT for default. + tr = urtest.AddTestRun() + tr.MakeCurlCommand( + '"http://127.0.0.1:{0}/default/cache/10/{1}" -H "x-debug: x-cache,x-cache-key,via,x-cache-generation" --verbose -o /dev/null' + .format(ts.Variables.port, objectid), + ts=ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.All = "gold/hit_default-1.gold" + + # Second touch is a HIT for generation1. + tr = urtest.AddTestRun() + tr.MakeCurlCommand( + '"http://127.0.0.1:{0}/generation1/cache/10/{1}" -H "x-debug: x-cache,x-cache-key,via,x-cache-generation" --verbose -o /dev/null' + .format(ts.Variables.port, objectid), + ts=ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.All = "gold/hit_gen1.gold" + + # Second touch is a HIT for generation2. + tr = urtest.AddTestRun() + tr.MakeCurlCommand( + '"http://127.0.0.1:{0}/generation2/cache/10/{1}" -H "x-debug: x-cache,x-cache-key,via,x-cache-generation" --verbose -o /dev/null' + .format(ts.Variables.port, objectid), + ts=ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.All = "gold/hit_gen2.gold" + urtest.execute() diff --git a/tests/uranium_tests/cache/test_host_down_range_recursion.py b/tests/uranium_tests/cache/test_host_down_range_recursion.py new file mode 100644 index 00000000000..72095dcce5d --- /dev/null +++ b/tests/uranium_tests/cache/test_host_down_range_recursion.py @@ -0,0 +1,101 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_host_down_range_recursion(urtest: UraniumTest) -> None: + ''' + Verify that a Range request to a DOWN host backed by a cache HIT does not + trigger unbounded recursion. + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + from jsonrpc import Request + + urtest.Summary = ''' + Verify that a Range request the cache cannot satisfy against a host marked + DOWN does not trigger unbounded recursion in HttpTransact. + ''' + + class HostDownRangeRecursionTest: + prime_replay = "replay/host_down_range_recursion_prime.replay.yaml" + range_replay = "replay/host_down_range_recursion_range.replay.yaml" + + def __init__(self): + # Same server process serves both replay files. Only the prime phase + # actually reaches the server; the range phase is expected to be + # short-circuited by the DOWN host check. + self._server = urtest.MakeVerifierServerProcess("server", self.prime_replay) + self._configure_ts() + + def _configure_ts(self): + self._ts = urtest.MakeATSProcess("ts", enable_cache=True) + + self._ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'http|host_statuses', + 'proxy.config.http.cache.range.write': 1, + 'proxy.config.http.insert_response_via_str': 3, + }) + + self._ts.Disk.remap_config.AddLine( + f'map http://backend.example.com/ http://127.0.0.1:{self._server.Variables.http_port}/') + + def run(self): + # Phase 1: prime the cache with the full object. + prime = urtest.AddTestRun("Prime cache with full object") + prime.AddVerifierClientProcess("prime-client", self.prime_replay, http_ports=[self._ts.Variables.port]) + prime.Processes.Default.StartBefore(self._server) + prime.Processes.Default.StartBefore(self._ts) + prime.StillRunningAfter = self._server + prime.StillRunningAfter = self._ts + + # Phase 2: mark the origin host DOWN via JSON-RPC. + mark_down = urtest.AddTestRun("Mark host DOWN") + mark_down.AddJsonRPCClientRequest( + self._ts, Request.admin_host_set_status(operation='down', host=['127.0.0.1'], reason='manual', time='0')) + mark_down.StillRunningAfter = self._server + mark_down.StillRunningAfter = self._ts + + # Phase 3: send an out-of-order multi-range request. The cache cannot + # satisfy the range, so ATS must fall back to the origin; with the host + # marked DOWN it should return 502 Bad Gateway. This is a regression + # test for a stack-overflow crash where this scenario instead drove an + # unbounded recursion between the cache-fallback and DNS-lookup paths. + range_run = urtest.AddTestRun("Out-of-order Range against DOWN host") + range_run.AddVerifierClientProcess("range-client", self.range_replay, http_ports=[self._ts.Variables.port]) + range_run.Processes.Default.TimeOut = 10 + range_run.StillRunningAfter = self._server + range_run.StillRunningAfter = self._ts + + HostDownRangeRecursionTest().run() + urtest.execute() diff --git a/tests/uranium_tests/cache/test_negative_caching.py b/tests/uranium_tests/cache/test_negative_caching.py new file mode 100644 index 00000000000..77b49859d81 --- /dev/null +++ b/tests/uranium_tests/cache/test_negative_caching.py @@ -0,0 +1,146 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_negative_caching(urtest: UraniumTest) -> None: + ''' + Test negative caching. + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + urtest.Summary = ''' + Test negative caching. + ''' + + # + # Verify correct proxy.config.http.negative_caching_lifetime behavior. + # These tests require multiple test runs with shared ATS processes, so use class-based approach. + # + ts = urtest.MakeATSProcess("ts-lifetime") + ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'http', + 'proxy.config.http.insert_age_in_response': 0, + 'proxy.config.http.negative_caching_enabled': 1, + 'proxy.config.http.negative_caching_lifetime': 2 + }) + # This should all behave the same as the default enabled case above. + tr = urtest.AddTestRun("Add a 404 response to the cache") + replay_file = "replay/negative-caching-default.replay.yaml" + server = tr.AddVerifierServerProcess("server-lifetime-no-cc", replay_file) + # Use the same port across the two servers so that the remap config will work + # across both. + server_port = server.Variables.http_port + tr.AddVerifierClientProcess("client-lifetime-no-cc", replay_file, http_ports=[ts.Variables.port]) + ts.Disk.remap_config.AddLine('map / http://127.0.0.1:{0}'.format(server_port)) + tr.Processes.Default.StartBefore(ts) + tr.StillRunningAfter = ts + + # Wait enough time that the item should be aged out of the cache. + tr = urtest.AddTestRun("Wait for cached object to be stale.") + tr.Processes.Default.Command = "sleep 4" + tr.StillRunningAfter = ts + + # Verify the item is retrieved from the server instead of the cache. + replay_file = "replay/negative-caching-timeout.replay.yaml" + tr = urtest.AddTestRun("Make sure object is stale") + tr.AddVerifierServerProcess("server-timeout", replay_file, http_ports=[server_port]) + tr.AddVerifierClientProcess("client-timeout", replay_file, http_ports=[ts.Variables.port]) + tr.StillRunningAfter = ts + + # + # Verify that the server's Cache-Control overrides the + # proxy.config.http.negative_caching_lifetime. + # + ts = urtest.MakeATSProcess("ts-lifetime-2") + ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'http', + 'proxy.config.http.insert_age_in_response': 0, + 'proxy.config.http.negative_caching_enabled': 1, + 'proxy.config.http.negative_caching_lifetime': 2 + }) + tr = urtest.AddTestRun("Add a 404 response with explicit max-age=300 to the cache") + replay_file = "replay/negative-caching-300-second-timeout.replay.yaml" + server = tr.AddVerifierServerProcess("server-lifetime-cc", replay_file) + # Use the same port across the two servers so that the remap config will work + # across both. + server_port = server.Variables.http_port + tr.AddVerifierClientProcess("client-lifetime-cc", replay_file, http_ports=[ts.Variables.port]) + ts.Disk.remap_config.AddLine('map / http://127.0.0.1:{0}'.format(server_port)) + tr.Processes.Default.StartBefore(ts) + tr.StillRunningAfter = ts + + # Wait enough time that the item should be aged out of the cache if + # proxy.config.http.negative_caching_lifetime is incorrectly used. + tr = urtest.AddTestRun("Wait for cached object to be stale if lifetime is incorrectly used.") + tr.Processes.Default.Command = "sleep 4" + tr.StillRunningAfter = ts + + # Verify the item is retrieved from the cache instead of going to the origin. + replay_file = "replay/negative-caching-no-timeout.replay.yaml" + tr = urtest.AddTestRun("Make sure object is fresh") + tr.AddVerifierServerProcess("server-no-timeout", replay_file, http_ports=[server_port]) + tr.AddVerifierClientProcess("client-no-timeout", replay_file, http_ports=[ts.Variables.port]) + tr.StillRunningAfter = ts + + # + # Verify that negative_caching_lifetime is respected even when cache.config + # has ttl-in-cache configured. + # + replay_file = "replay/negative-caching-ttl-in-cache.replay.yaml" + tr = urtest.AddTestRun("Verify negative_caching_lifetime and ttl-in-cache interaction.") + dns = tr.MakeDNServer("dns", default="127.0.0.1") + server = tr.AddVerifierServerProcess("server-ttl-in-cache", replay_file) + server_port = server.Variables.http_port + ts = tr.MakeATSProcess("ts-ttl-in-cache") + ts.Disk.records_config.update( + { + 'proxy.config.dns.nameservers': f"127.0.0.1:{dns.Variables.Port}", + 'proxy.config.dns.resolv_conf': 'NULL', + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'http', + 'proxy.config.http.insert_age_in_response': 0, + 'proxy.config.http.negative_caching_enabled': 1, + 'proxy.config.http.negative_caching_lifetime': 2 + }) + ts.Disk.remap_config.AddLine(f'map / http://backend.example.com:{server_port}') + # Configure cache.config with a long ttl-in-cache that should NOT override + # negative_caching_lifetime for negative responses. + ts.Disk.cache_config.AddLine('dest_domain=backend.example.com ttl-in-cache=30d') + p = tr.AddVerifierClientProcess("client-ttl-in-cache", replay_file, http_ports=[ts.Variables.port]) + p.StartBefore(dns) + p.StartBefore(server) + p.StartBefore(ts) + urtest.execute() diff --git a/tests/uranium_tests/cache/test_proxy_serve_stale_dns_fail.py b/tests/uranium_tests/cache/test_proxy_serve_stale_dns_fail.py new file mode 100644 index 00000000000..1245857e9a3 --- /dev/null +++ b/tests/uranium_tests/cache/test_proxy_serve_stale_dns_fail.py @@ -0,0 +1,102 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_proxy_serve_stale_dns_fail(urtest: UraniumTest) -> None: + ''' + Test proxy serving stale content when DNS lookup fails + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + urtest.ContinueOnFail = True + # Set up hierarchical caching processes + ts_child = urtest.MakeATSProcess("ts_child") + ts_parent = urtest.MakeATSProcess("ts_parent", enable_uds=False) + nameserver = urtest.MakeDNServer("dns") + server_name = "http://unknown.domain.com/" + + urtest.testName = "STALE" + + # Config child proxy to route to parent proxy + ts_child.Disk.records_config.update( + { + 'proxy.config.http.push_method_enabled': 1, + 'proxy.config.url_remap.pristine_host_hdr': 1, + 'proxy.config.http.cache.max_stale_age': 10, + 'proxy.config.http.parent_proxy.self_detect': 0, + 'proxy.config.dns.nameservers': f"127.0.0.1:{nameserver.Variables.Port}", + }) + ts_child.Disk.parent_config.AddLine( + f'dest_domain=. parent=localhost:{ts_parent.Variables.port} round_robin=consistent_hash go_direct=false') + ts_child.Disk.remap_config.AddLine(f'map http://localhost:{ts_child.Variables.port} {server_name}') + + # Configure parent proxy + ts_parent.Disk.records_config.update( + { + 'proxy.config.http.push_method_enabled': 1, + 'proxy.config.url_remap.pristine_host_hdr': 1, + 'proxy.config.http.cache.max_stale_age': 10, + 'proxy.config.dns.nameservers': f"127.0.0.1:{nameserver.Variables.Port}", + }) + ts_parent.Disk.remap_config.AddLine(f'map http://localhost:{ts_parent.Variables.port} {server_name}') + ts_parent.Disk.remap_config.AddLine(f'map {server_name} {server_name}') + + # Configure nameserver + nameserver.addRecords(records={"localhost": ["127.0.0.1"]}) + + # Object to push to proxies + stale_5 = "HTTP/1.1 200 OK\nServer: ATS/10.0.0\nAccept-Ranges: bytes\nContent-Length: 6\nCache-Control: public, max-age=5\n\nCACHED" + stale_10 = "HTTP/1.1 200 OK\nServer: ATS/10.0.0\nAccept-Ranges: bytes\nContent-Length: 6\nCache-Control: public, max-age=10\n\nCACHED" + + # Testing scenarios + child_curl_request = ( + # Test child serving stale with failed DNS OS lookup + f'{{curl}} -X PUSH -d "{stale_5}" "http://localhost:{ts_child.Variables.port}";' + f'{{curl}} -X PUSH -d "{stale_10}" "http://localhost:{ts_parent.Variables.port}";' + f'sleep 7; {{curl}} -s -v http://localhost:{ts_child.Variables.port};' + f'sleep 17; {{curl}} -s -v http://localhost:{ts_child.Variables.port};' + # Test parent serving stale with failed DNS OS lookup + f'{{curl_base}} -X PUSH -d "{stale_5}" "http://localhost:{ts_parent.Variables.port}";' + f'sleep 7; {{curl_base}} -s -v http://localhost:{ts_parent.Variables.port};' + f'sleep 17; {{curl_base}} -s -v http://localhost:{ts_parent.Variables.port};') + + # Test case for when parent server is down but child proxy can serve cache object + tr = urtest.AddTestRun() + tr.MakeCurlCommandMulti(child_curl_request, ts=ts_child) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.StartBefore(ts_child) + tr.Processes.Default.StartBefore(ts_parent) + tr.Processes.Default.StartBefore(nameserver) + tr.Processes.Default.Streams.stderr = "gold/serve_stale_dns_fail.gold" + tr.StillRunningAfter = ts_child + tr.StillRunningAfter = ts_parent + urtest.execute() diff --git a/tests/uranium_tests/cache/test_storage_metrics.py b/tests/uranium_tests/cache/test_storage_metrics.py new file mode 100644 index 00000000000..69bab85541a --- /dev/null +++ b/tests/uranium_tests/cache/test_storage_metrics.py @@ -0,0 +1,148 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_storage_metrics(urtest: UraniumTest) -> None: + ''' + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import os + + urtest.Summary = 'Test loading storage metrics' + urtest.ContinueOnFail = True + + # CAVEAT: Below test cases doesn't have multiple span cases that requires RAW devices. + test_cases = [ + { + "case": 0, + "description": "default config", + "storage": ''' + cache: + spans: + - name: disk.0 + path: storage + size: 256M + ''' + }, + { + "case": 1, + "description": "four equally divided volumes", + "storage": + ''' + cache: + spans: + - name: disk.0 + path: storage + size: 1G + volumes: + - id: 1 + size: 25% + - id: 2 + size: 25% + - id: 3 + size: 25% + - id: 4 + size: 25% + ''' + }, + { + "case": 2, + "description": "exclusive span", + "storage": + ''' + cache: + spans: + - name: disk.0 + path: storage + size: 256M + volumes: + - id: 1 + spans: + - use: disk.0 + size: 100% + ''', + "hosting": ''' + hostname=* volume=1 + ''' + }, + { + "case": 3, + "description": "two equally divided volumes without size option", + "storage": + ''' + cache: + spans: + - name: disk.0 + path: storage + size: 512M + volumes: + - id: 1 + - id: 2 + ''' + }, + ] + + class StorageMetricsTest: + """ + Test loading storage.yaml + + 1. Spawn TS process with configs in test_cases + 2. Get 'proxy.process.cache.*' metrics + 3. Check with 'gold/storage_N_stdout.gold' file + """ + + def run(self): + for config in test_cases: + i = config["case"] + ts = urtest.MakeATSProcess(f"ts_{i}") + ts.Disk.storage_yaml.AddLine(config["storage"]) + + if "hosting" in config: + ts.Disk.hosting_config.AddLine(config["hosting"]) + + ts.Disk.records_config.update({ + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'cache', + }) + + tr = urtest.AddTestRun() + + tr.Processes.Default.StartBefore(ts) + tr.Processes.Default.Env = ts.Env + tr.Processes.Default.Command = 'traffic_ctl --debug metric match proxy.process.cache.' + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.stdout = f"gold/storage_metrics_{i}_stdout.gold" + tr.StillRunningAfter = ts + + StorageMetricsTest().run() + urtest.execute() diff --git a/tests/gold_tests/chunked_encoding/CMakeLists.txt b/tests/uranium_tests/chunked_encoding/CMakeLists.txt similarity index 100% rename from tests/gold_tests/chunked_encoding/CMakeLists.txt rename to tests/uranium_tests/chunked_encoding/CMakeLists.txt diff --git a/tests/gold_tests/chunked_encoding/chunk_extension_client.py b/tests/uranium_tests/chunked_encoding/chunk_extension_client.py similarity index 100% rename from tests/gold_tests/chunked_encoding/chunk_extension_client.py rename to tests/uranium_tests/chunked_encoding/chunk_extension_client.py diff --git a/tests/gold_tests/chunked_encoding/chunk_trailer_client.py b/tests/uranium_tests/chunked_encoding/chunk_trailer_client.py similarity index 100% rename from tests/gold_tests/chunked_encoding/chunk_trailer_client.py rename to tests/uranium_tests/chunked_encoding/chunk_trailer_client.py diff --git a/tests/gold_tests/chunked_encoding/chunked_encoding_h2_server.py b/tests/uranium_tests/chunked_encoding/chunked_encoding_h2_server.py similarity index 100% rename from tests/gold_tests/chunked_encoding/chunked_encoding_h2_server.py rename to tests/uranium_tests/chunked_encoding/chunked_encoding_h2_server.py diff --git a/tests/gold_tests/chunked_encoding/gold/chunked_GET_200.gold b/tests/uranium_tests/chunked_encoding/gold/chunked_GET_200.gold similarity index 100% rename from tests/gold_tests/chunked_encoding/gold/chunked_GET_200.gold rename to tests/uranium_tests/chunked_encoding/gold/chunked_GET_200.gold diff --git a/tests/gold_tests/chunked_encoding/gold/chunked_GET_200_uds.gold b/tests/uranium_tests/chunked_encoding/gold/chunked_GET_200_uds.gold similarity index 100% rename from tests/gold_tests/chunked_encoding/gold/chunked_GET_200_uds.gold rename to tests/uranium_tests/chunked_encoding/gold/chunked_GET_200_uds.gold diff --git a/tests/gold_tests/chunked_encoding/gold/chunked_POST_200.gold b/tests/uranium_tests/chunked_encoding/gold/chunked_POST_200.gold similarity index 100% rename from tests/gold_tests/chunked_encoding/gold/chunked_POST_200.gold rename to tests/uranium_tests/chunked_encoding/gold/chunked_POST_200.gold diff --git a/tests/gold_tests/chunked_encoding/gold/h2_chunked_POST_200.gold b/tests/uranium_tests/chunked_encoding/gold/h2_chunked_POST_200.gold similarity index 100% rename from tests/gold_tests/chunked_encoding/gold/h2_chunked_POST_200.gold rename to tests/uranium_tests/chunked_encoding/gold/h2_chunked_POST_200.gold diff --git a/tests/gold_tests/chunked_encoding/gold/verifier_client_chunked.gold b/tests/uranium_tests/chunked_encoding/gold/verifier_client_chunked.gold similarity index 100% rename from tests/gold_tests/chunked_encoding/gold/verifier_client_chunked.gold rename to tests/uranium_tests/chunked_encoding/gold/verifier_client_chunked.gold diff --git a/tests/gold_tests/chunked_encoding/replays/chunk_extension_quoted_string.replay.yaml b/tests/uranium_tests/chunked_encoding/replays/chunk_extension_quoted_string.replay.yaml similarity index 100% rename from tests/gold_tests/chunked_encoding/replays/chunk_extension_quoted_string.replay.yaml rename to tests/uranium_tests/chunked_encoding/replays/chunk_extension_quoted_string.replay.yaml diff --git a/tests/gold_tests/chunked_encoding/replays/chunk_trailer_bare_lf.replay.yaml b/tests/uranium_tests/chunked_encoding/replays/chunk_trailer_bare_lf.replay.yaml similarity index 100% rename from tests/gold_tests/chunked_encoding/replays/chunk_trailer_bare_lf.replay.yaml rename to tests/uranium_tests/chunked_encoding/replays/chunk_trailer_bare_lf.replay.yaml diff --git a/tests/gold_tests/chunked_encoding/replays/chunked.replay.yaml b/tests/uranium_tests/chunked_encoding/replays/chunked.replay.yaml similarity index 100% rename from tests/gold_tests/chunked_encoding/replays/chunked.replay.yaml rename to tests/uranium_tests/chunked_encoding/replays/chunked.replay.yaml diff --git a/tests/gold_tests/chunked_encoding/replays/chunked_h1_flow_control.test.yaml b/tests/uranium_tests/chunked_encoding/replays/chunked_h1_flow_control.test.yaml similarity index 99% rename from tests/gold_tests/chunked_encoding/replays/chunked_h1_flow_control.test.yaml rename to tests/uranium_tests/chunked_encoding/replays/chunked_h1_flow_control.test.yaml index 8257559ac85..3d35ded225a 100644 --- a/tests/gold_tests/chunked_encoding/replays/chunked_h1_flow_control.test.yaml +++ b/tests/uranium_tests/chunked_encoding/replays/chunked_h1_flow_control.test.yaml @@ -29,7 +29,7 @@ meta: # "disable ... read_vio" throttle (HttpTunnel.cc) and a re-enable when the # consumer catches up. The response must still complete with the full body. -autest: +urtest: description: 'HTTP/1.0 origin -> HTTP/1.1 chunked client do-chunk flow-control throttle' dns: diff --git a/tests/gold_tests/chunked_encoding/replays/chunked_h2_flow_control.test.yaml b/tests/uranium_tests/chunked_encoding/replays/chunked_h2_flow_control.test.yaml similarity index 99% rename from tests/gold_tests/chunked_encoding/replays/chunked_h2_flow_control.test.yaml rename to tests/uranium_tests/chunked_encoding/replays/chunked_h2_flow_control.test.yaml index f771a2bb1e7..a2f7b6d7552 100644 --- a/tests/gold_tests/chunked_encoding/replays/chunked_h2_flow_control.test.yaml +++ b/tests/uranium_tests/chunked_encoding/replays/chunked_h2_flow_control.test.yaml @@ -31,7 +31,7 @@ meta: # the transaction would stall because VC_EVENT_READ_COMPLETE / EOS / # PRECOMPLETE would be dropped. -autest: +urtest: description: 'HTTP/1.1 chunked origin -> HTTP/2 client dechunk flow-control throttle' dns: diff --git a/tests/gold_tests/chunked_encoding/replays/chunked_in_http_1_0.replay.yaml b/tests/uranium_tests/chunked_encoding/replays/chunked_in_http_1_0.replay.yaml similarity index 100% rename from tests/gold_tests/chunked_encoding/replays/chunked_in_http_1_0.replay.yaml rename to tests/uranium_tests/chunked_encoding/replays/chunked_in_http_1_0.replay.yaml diff --git a/tests/gold_tests/chunked_encoding/replays/chunked_not_last.test.yaml b/tests/uranium_tests/chunked_encoding/replays/chunked_not_last.test.yaml similarity index 99% rename from tests/gold_tests/chunked_encoding/replays/chunked_not_last.test.yaml rename to tests/uranium_tests/chunked_encoding/replays/chunked_not_last.test.yaml index c051753f987..23e5cdabb54 100644 --- a/tests/gold_tests/chunked_encoding/replays/chunked_not_last.test.yaml +++ b/tests/uranium_tests/chunked_encoding/replays/chunked_not_last.test.yaml @@ -21,7 +21,7 @@ meta: version: "1.0" -autest: +urtest: description: 'Verify chunked must be the last Transfer-Encoding value' server: diff --git a/tests/gold_tests/chunked_encoding/replays/chunked_passthru_flow_control.test.yaml b/tests/uranium_tests/chunked_encoding/replays/chunked_passthru_flow_control.test.yaml similarity index 99% rename from tests/gold_tests/chunked_encoding/replays/chunked_passthru_flow_control.test.yaml rename to tests/uranium_tests/chunked_encoding/replays/chunked_passthru_flow_control.test.yaml index e20d1ea3c37..78f3b3e7d94 100644 --- a/tests/gold_tests/chunked_encoding/replays/chunked_passthru_flow_control.test.yaml +++ b/tests/uranium_tests/chunked_encoding/replays/chunked_passthru_flow_control.test.yaml @@ -38,7 +38,7 @@ meta: # stays drained and the high_water check reflects only the consumer's backlog. # The response must complete with the full body and no inactivity-timeout stall. -autest: +urtest: description: 'HTTP/1.1 chunked origin -> HTTP/1.1 chunked client passthru flow-control throttle (drop_chunked_trailers=0)' dns: diff --git a/tests/gold_tests/chunked_encoding/replays/chunked_trailer_dropped.replay.yaml b/tests/uranium_tests/chunked_encoding/replays/chunked_trailer_dropped.replay.yaml similarity index 100% rename from tests/gold_tests/chunked_encoding/replays/chunked_trailer_dropped.replay.yaml rename to tests/uranium_tests/chunked_encoding/replays/chunked_trailer_dropped.replay.yaml diff --git a/tests/gold_tests/chunked_encoding/replays/chunked_trailer_proxied.replay.yaml b/tests/uranium_tests/chunked_encoding/replays/chunked_trailer_proxied.replay.yaml similarity index 100% rename from tests/gold_tests/chunked_encoding/replays/chunked_trailer_proxied.replay.yaml rename to tests/uranium_tests/chunked_encoding/replays/chunked_trailer_proxied.replay.yaml diff --git a/tests/gold_tests/chunked_encoding/replays/large_chunked.test.yaml b/tests/uranium_tests/chunked_encoding/replays/large_chunked.test.yaml similarity index 98% rename from tests/gold_tests/chunked_encoding/replays/large_chunked.test.yaml rename to tests/uranium_tests/chunked_encoding/replays/large_chunked.test.yaml index d02f4acb02f..9d9f4f10e73 100644 --- a/tests/gold_tests/chunked_encoding/replays/large_chunked.test.yaml +++ b/tests/uranium_tests/chunked_encoding/replays/large_chunked.test.yaml @@ -17,8 +17,8 @@ meta: version: "1.0" -# Configuration section for autest integration -autest: +# Configuration for the Uranium test runner +urtest: description: 'Large chunked response from origin server' requires: curl_features: [http2] diff --git a/tests/gold_tests/chunked_encoding/replays/malformed_chunked_header.replay.yaml b/tests/uranium_tests/chunked_encoding/replays/malformed_chunked_header.replay.yaml similarity index 100% rename from tests/gold_tests/chunked_encoding/replays/malformed_chunked_header.replay.yaml rename to tests/uranium_tests/chunked_encoding/replays/malformed_chunked_header.replay.yaml diff --git a/tests/gold_tests/chunked_encoding/server4.sh b/tests/uranium_tests/chunked_encoding/server4.sh similarity index 100% rename from tests/gold_tests/chunked_encoding/server4.sh rename to tests/uranium_tests/chunked_encoding/server4.sh diff --git a/tests/gold_tests/chunked_encoding/smuggle-client.c b/tests/uranium_tests/chunked_encoding/smuggle-client.c similarity index 100% rename from tests/gold_tests/chunked_encoding/smuggle-client.c rename to tests/uranium_tests/chunked_encoding/smuggle-client.c diff --git a/tests/uranium_tests/chunked_encoding/test_bad_chunked_encoding.py b/tests/uranium_tests/chunked_encoding/test_bad_chunked_encoding.py new file mode 100644 index 00000000000..3bb39e3adf6 --- /dev/null +++ b/tests/uranium_tests/chunked_encoding/test_bad_chunked_encoding.py @@ -0,0 +1,247 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_bad_chunked_encoding(urtest: UraniumTest) -> None: + ''' + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import os + + urtest.Summary = ''' + Test unsupported values for chunked_encoding + ''' + + urtest.ContinueOnFail = True + + # Define default ATS + ts = urtest.MakeATSProcess("ts1") + server = urtest.MakeOriginServer("server") + + testName = "" + request_header = { + "headers": "POST /case1 HTTP/1.1\r\nHost: www.example.com\r\nuuid:1\r\n\r\n", + "timestamp": "1469733493.993", + "body": "stuff" + } + response_header = { + "headers": "HTTP/1.1 200 OK\r\nServer: uServer\r\nConnection: close\r\nTransfer-Encoding: chunked\r\n\r\n", + "timestamp": "1469733493.993", + "body": "more stuff" + } + + server.addResponse("sessionlog.json", request_header, response_header) + + ts.Disk.records_config.update({'proxy.config.diags.debug.enabled': 0, 'proxy.config.diags.debug.tags': 'http'}) + + ts.Disk.remap_config.AddLine('map / http://127.0.0.1:{0}'.format(server.Variables.Port)) + + # HTTP1.1 POST: www.example.com/case1 with gzip transfer-encoding + tr = urtest.AddTestRun() + tr.TimeOut = 5 + tr.MakeCurlCommand( + '-H "host: example.com" -H "transfer-encoding: gzip" -d "stuff" http://127.0.0.1:{0}/case1 --verbose'.format( + ts.Variables.port), + ts=ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.StartBefore(server) + tr.Processes.Default.StartBefore(ts) + tr.Processes.Default.Streams.All = Testers.ContainsExpression("501 Field not implemented", "Should fail") + tr.Processes.Default.Streams.All = Testers.ExcludesExpression("200 OK", "Should not succeed") + tr.StillRunningAfter = server + tr.StillRunningAfter = ts + + # HTTP1.1 POST: www.example.com/case1 with gzip and chunked transfer-encoding + tr = urtest.AddTestRun() + tr.TimeOut = 5 + tr.MakeCurlCommand( + '-H "host: example.com" -H "transfer-encoding: gzip" -H "transfer-encoding: chunked" -d "stuff" http://127.0.0.1:{0}/case1 --verbose' + .format(ts.Variables.port), + ts=ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.All = Testers.ContainsExpression("501 Field not implemented", "Should fail") + tr.Processes.Default.Streams.All = Testers.ExcludesExpression("200 OK", "Should not succeed") + tr.StillRunningAfter = server + tr.StillRunningAfter = ts + + class HTTP10Test: + chunkedReplayFile = "replays/chunked_in_http_1_0.replay.yaml" + + def __init__(self): + self.setupOriginServer() + self.setupTS() + + def setupOriginServer(self): + self.server = urtest.MakeVerifierServerProcess("verifier-server1", self.chunkedReplayFile) + + def setupTS(self): + self.ts = urtest.MakeATSProcess("ts2", enable_tls=True, enable_cache=False) + self.ts.addDefaultSSLFiles() + self.ts.Disk.records_config.update( + { + "proxy.config.diags.debug.enabled": 1, + "proxy.config.diags.debug.tags": "http", + "proxy.config.ssl.server.cert.path": f'{self.ts.Variables.SSLDir}', + "proxy.config.ssl.server.private_key.path": f'{self.ts.Variables.SSLDir}', + "proxy.config.ssl.client.verify.server.policy": 'PERMISSIVE', + }) + self.ts.Disk.ssl_multicert_yaml.AddLines( + """ + ssl_multicert: + - dest_ip: "*" + ssl_cert_name: server.pem + ssl_key_name: server.key + """.split("\n")) + self.ts.Disk.remap_config.AddLine(f"map / http://127.0.0.1:{self.server.Variables.http_port}/",) + + def runChunkedTraffic(self): + tr = urtest.AddTestRun() + tr.AddVerifierClientProcess( + "client1", self.chunkedReplayFile, http_ports=[self.ts.Variables.port], https_ports=[self.ts.Variables.ssl_port]) + tr.Processes.Default.StartBefore(self.server) + tr.Processes.Default.StartBefore(self.ts) + tr.StillRunningAfter = self.server + tr.StillRunningAfter = self.ts + + def run(self): + self.runChunkedTraffic() + + HTTP10Test().run() + + class MalformedChunkHeaderTest: + chunkedReplayFile = "replays/malformed_chunked_header.replay.yaml" + + def __init__(self): + self.setupOriginServer() + self.setupTS() + + def setupOriginServer(self): + self.server = urtest.MakeVerifierServerProcess("verifier-server2", self.chunkedReplayFile) + + # The server's responses will fail the first three transactions + # because ATS will close the connection due to the malformed + # chunk headers. + self.server.Streams.stdout += Testers.ContainsExpression( + "Unexpected chunked content for key 1: too small", "Verify that writing the first response failed.") + self.server.Streams.stdout += Testers.ExcludesExpression( + "chunked body of 3 bytes for key 2 with chunk stream", "Verify that writing the second response failed.") + self.server.Streams.stdout += Testers.ContainsExpression( + "Unexpected chunked content for key 3: too small", "Verify that writing the third response failed.") + self.server.Streams.stdout += Testers.ContainsExpression( + "Unexpected chunked content for key 8: too small", "Verify that writing the sixth response failed.") + + # ATS should close the connection before any body gets through. "abcwxyz" + # is the body sent by the client for each of these chunked cases. + self.server.Streams.stdout += Testers.ExcludesExpression("abcwxyz", "Verify that the body never got through.") + + def setupTS(self): + self.ts = urtest.MakeATSProcess("ts3", enable_tls=True, enable_cache=False) + self.ts.addDefaultSSLFiles() + self.ts.Disk.records_config.update( + { + "proxy.config.diags.debug.enabled": 1, + "proxy.config.diags.debug.tags": "http", + "proxy.config.ssl.server.cert.path": f'{self.ts.Variables.SSLDir}', + "proxy.config.ssl.server.private_key.path": f'{self.ts.Variables.SSLDir}', + "proxy.config.ssl.client.verify.server.policy": 'PERMISSIVE', + }) + self.ts.Disk.ssl_multicert_yaml.AddLines( + """ + ssl_multicert: + - dest_ip: "*" + ssl_cert_name: server.pem + ssl_key_name: server.key + """.split("\n")) + self.ts.Disk.remap_config.AddLine(f"map / http://127.0.0.1:{self.server.Variables.http_port}/",) + self.ts.Disk.traffic_out.Content += Testers.ContainsExpression( + "user agent post chunk decoding error", "Verify that ATS detected a problem parsing a chunk.") + + def runChunkedTraffic(self): + tr = urtest.AddTestRun() + tr.AddVerifierClientProcess( + "client2", self.chunkedReplayFile, http_ports=[self.ts.Variables.port], https_ports=[self.ts.Variables.ssl_port]) + tr.Processes.Default.StartBefore(self.server) + tr.Processes.Default.StartBefore(self.ts) + tr.StillRunningAfter = self.server + tr.StillRunningAfter = self.ts + + # The aborted connections will result in errors and a non-zero return + # code from the verifier client. + tr.Processes.Default.ReturnCode = 1 + tr.Processes.Default.Streams.stdout += Testers.ContainsExpression( + r"(Unexpected chunked content for key 101: too small|Failed HTTP/1 transaction with key: 101)", + "Verify that ATS closed the forth transaction.") + tr.Processes.Default.Streams.stdout += Testers.ContainsExpression( + r"(Unexpected chunked content for key 102: too small|Failed HTTP/1 transaction with key: 102)", + "Verify that ATS closed the fifth transaction.") + tr.Processes.Default.Streams.stdout += Testers.ContainsExpression( + r"(Unexpected chunked content for key 103: too small|Failed HTTP/1 transaction with key: 103)", + "Verify that ATS closed the sixth transaction.") + + tr.Processes.Default.Streams.stdout += Testers.ContainsExpression( + r"Received an HTTP/1 400 response for key 1 with headers", + "Verify that ATS returns a response for uuid:1 transaction.") + + tr.Processes.Default.Streams.stdout += Testers.ContainsExpression( + r"Received an HTTP/1 400 response for key 2 with headers", + "Verify that ATS returns a response for uuid:2 transaction.") + + tr.Processes.Default.Streams.stdout += Testers.ContainsExpression( + r"Received an HTTP/1 400 response for key 3 with headers", + "Verify that ATS returns a response for uuid:3 transaction.") + + tr.Processes.Default.Streams.stdout += Testers.ContainsExpression( + r"Received an HTTP/1 400 response for key 4 with headers", + "Verify that ATS returns a response for uuid:4 transaction.") + + tr.Processes.Default.Streams.stdout += Testers.ContainsExpression( + r"Received an HTTP/1 400 response for key 5 with headers", + "Verify that ATS returns a response for uuid:5 transaction.") + + tr.Processes.Default.Streams.stdout += Testers.ContainsExpression( + r"Received an HTTP/1 400 response for key 6 with headers", + "Verify that ATS returns a response for uuid:6 transaction.") + + tr.Processes.Default.Streams.stdout += Testers.ContainsExpression( + r"Received an HTTP/1 400 response for key 7 with headers", + "Verify that ATS returns a response for uuid:7 transaction.") + + # ATS should close the connection before any body gets through. "def" + # is the body sent by the server for each of these chunked cases. + tr.Processes.Default.Streams.stdout += Testers.ExcludesExpression("def", "Verify that the body never got through.") + + def run(self): + self.runChunkedTraffic() + + MalformedChunkHeaderTest().run() + urtest.execute() diff --git a/tests/uranium_tests/chunked_encoding/test_chunk_extension_quoted_string.py b/tests/uranium_tests/chunked_encoding/test_chunk_extension_quoted_string.py new file mode 100644 index 00000000000..a03fb6dd505 --- /dev/null +++ b/tests/uranium_tests/chunked_encoding/test_chunk_extension_quoted_string.py @@ -0,0 +1,104 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_chunk_extension_quoted_string(urtest: UraniumTest) -> None: + ''' + Verify that a chunk extension whose quoted-string value contains CR or LF is + rejected. Per RFC 9110 Section 5.6.4 a quoted-string cannot contain a bare CR or + LF, so such a chunk size line is malformed. ATS must reject the request rather + than interpreting the embedded octets as a second, smuggled request: the client + gets a single error response and the embedded GET never reaches the origin. + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + urtest.Summary = ''' + A chunk extension quoted-string value containing CR/LF is rejected with 400. + ''' + + class ChunkExtensionQuotedStringTest: + + def __init__(self): + self._setup_origin() + self._setup_ts() + + def _setup_origin(self): + # A proxy-verifier origin parses chunked requests correctly and serves the + # legitimate POST with 200 (keep-alive). If the proxy forwards the embedded + # GET /second, the origin answers it with SECOND-ENDPOINT, so a smuggled + # request shows up as a second response on the client. + self._server = urtest.MakeVerifierServerProcess("verifier-server", "replays/chunk_extension_quoted_string.replay.yaml") + + def _setup_ts(self): + self._ts = urtest.MakeATSProcess("ts", enable_cache=False) + self._ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 0, + 'proxy.config.diags.debug.tags': 'http', + 'proxy.config.http.strict_chunk_parsing': 1, + }) + self._ts.Disk.remap_config.AddLine(f'map / http://127.0.0.1:{self._server.Variables.http_port}') + + def _check(self, tr): + tr.Processes.Default.ReturnCode = 0 + # The malformed chunk size line is rejected, so the client gets exactly one + # error response and the embedded GET /second is never forwarded. A parser + # that ignores the quoted-string would forward that GET as a second request + # and the origin's SECOND-ENDPOINT body would reach the client (two + # responses). Keying on the smuggled body is deterministic; the exact error + # status is not (ATS may relay the origin's response to the forwarded POST + # headers, or generate its own 400). + tr.Processes.Default.Streams.stdout += Testers.ContainsExpression( + "responses=1", "the client must get exactly one response") + tr.Processes.Default.Streams.stdout += Testers.ExcludesExpression( + "SECOND-ENDPOINT", "the embedded GET must not be smuggled to the origin") + tr.StillRunningAfter = self._server + tr.StillRunningAfter = self._ts + + def run(self): + # Case 1: the whole request arrives in one read. + tr = urtest.AddTestRun("Quoted extension value containing CRLF") + tr.Setup.Copy("chunk_extension_client.py") + tr.Processes.Default.StartBefore(self._server) + tr.Processes.Default.StartBefore(self._ts) + tr.Processes.Default.Command = f'python3 chunk_extension_client.py 127.0.0.1 {self._ts.Variables.port}' + self._check(tr) + + # Case 2: the same request split across two writes at a read boundary + # inside the extension. The proxy must resume parsing and still reject it. + tr = urtest.AddTestRun("Quoted extension split across reads") + tr.Setup.Copy("chunk_extension_client.py") + tr.Processes.Default.Command = f'python3 chunk_extension_client.py 127.0.0.1 {self._ts.Variables.port} --split' + self._check(tr) + + ChunkExtensionQuotedStringTest().run() + urtest.execute() diff --git a/tests/uranium_tests/chunked_encoding/test_chunk_trailer_bare_lf.py b/tests/uranium_tests/chunked_encoding/test_chunk_trailer_bare_lf.py new file mode 100644 index 00000000000..bc89fe4d744 --- /dev/null +++ b/tests/uranium_tests/chunked_encoding/test_chunk_trailer_bare_lf.py @@ -0,0 +1,106 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_chunk_trailer_bare_lf(urtest: UraniumTest) -> None: + ''' + Verify that a chunked request whose trailer section is terminated by a bare LF + instead of CRLF is rejected. Per RFC 9112 Section 7.1 the trailer section ends + with an empty line, "CRLF"; a bare LF blank line is not a valid terminator. ATS + must reject the request rather than ending the body one byte early and + interpreting the trailing octets as a second, smuggled request: the client gets a + single response and the embedded GET never reaches the origin. + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + urtest.Summary = ''' + A chunked trailer terminated by a bare LF instead of CRLF is rejected. + ''' + + class ChunkTrailerBareLfTest: + + def __init__(self): + self._setup_origin() + self._setup_ts() + + def _setup_origin(self): + # A proxy-verifier origin parses chunked requests correctly and serves the + # legitimate POST with 200 (keep-alive). If the proxy forwards the embedded + # GET /smuggled, the origin answers it with SECOND-ENDPOINT, so a smuggled + # request shows up as a second response on the client. + self._server = urtest.MakeVerifierServerProcess("verifier-server", "replays/chunk_trailer_bare_lf.replay.yaml") + + def _setup_ts(self): + self._ts = urtest.MakeATSProcess("ts", enable_cache=False) + self._ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 0, + 'proxy.config.diags.debug.tags': 'http', + 'proxy.config.http.strict_chunk_parsing': 1, + }) + self._ts.Disk.remap_config.AddLine(f'map / http://127.0.0.1:{self._server.Variables.http_port}') + + def _check(self, tr): + tr.Processes.Default.ReturnCode = 0 + # The bare-LF trailer terminator is rejected, so the client gets exactly one + # response and the embedded GET /smuggled is never forwarded. A parser that + # accepts the bare LF would forward that GET as a second request and the + # origin's SECOND-ENDPOINT body would reach the client (two responses). + # Keying on the smuggled body is deterministic; the exact error status is + # not (ATS may relay the origin's response to the forwarded POST headers, or + # generate its own 400). + tr.Processes.Default.Streams.stdout += Testers.ContainsExpression( + "responses=1", "the client must get exactly one response") + tr.Processes.Default.Streams.stdout += Testers.ExcludesExpression( + "SECOND-ENDPOINT", "the embedded GET must not be smuggled to the origin") + tr.StillRunningAfter = self._server + tr.StillRunningAfter = self._ts + + def run(self): + # Case 1: the whole request arrives in one read. + tr = urtest.AddTestRun("Chunked trailer terminated by a bare LF") + tr.Setup.Copy("chunk_trailer_client.py") + tr.Processes.Default.StartBefore(self._server) + tr.Processes.Default.StartBefore(self._ts) + tr.Processes.Default.Command = f'python3 chunk_trailer_client.py 127.0.0.1 {self._ts.Variables.port}' + self._check(tr) + + # Case 2: the same request split across two writes at a read boundary right + # after the final "0\r\n". The proxy must resume parsing the trailer and + # still reject the bare-LF terminator. + tr = urtest.AddTestRun("Chunked trailer bare LF split across reads") + tr.Setup.Copy("chunk_trailer_client.py") + tr.Processes.Default.Command = f'python3 chunk_trailer_client.py 127.0.0.1 {self._ts.Variables.port} --split' + self._check(tr) + + ChunkTrailerBareLfTest().run() + urtest.execute() diff --git a/tests/uranium_tests/chunked_encoding/test_chunked_encoding.py b/tests/uranium_tests/chunked_encoding/test_chunked_encoding.py new file mode 100644 index 00000000000..0d7e99cf807 --- /dev/null +++ b/tests/uranium_tests/chunked_encoding/test_chunked_encoding.py @@ -0,0 +1,283 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_chunked_encoding(urtest: UraniumTest) -> None: + ''' + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import os + + urtest.Summary = ''' + Test chunked encoding processing + ''' + + urtest.SkipUnless(Condition.HasCurlFeature('http2')) + urtest.ContinueOnFail = True + + urtest.GetTcpPort("upstream_port") + + # Define default ATS + ts = urtest.MakeATSProcess("ts", enable_tls=True) + server = urtest.MakeOriginServer("server") + server2 = urtest.MakeOriginServer("server2", ssl=True) + server3 = urtest.MakeOriginServer("server3") + + server4 = urtest.Processes.Process( + "server4", "bash -c '" + urtest.TestDirectory + "/server4.sh {} outserver4'".format(urtest.Variables.upstream_port)) + + testName = "" + request_header = {"headers": "GET / HTTP/1.1\r\nHost: www.example.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + response_header = { + "headers": "HTTP/1.1 200 OK\r\nServer: uServer\r\nConnection: close\r\nTransfer-Encoding: chunked\r\n\r\n", + "timestamp": "1469733493.993", + "body": "" + } + + request_header2 = { + "headers": + "POST / HTTP/1.1\r\nHost: www.anotherexample.com\r\nContent-Type: application/x-www-form-urlencoded\r\nContent-Length: 11\r\n\r\n", + "timestamp": "1415926535.898", + "body": "knock knock" + } + response_header2 = { + "headers": "HTTP/1.1 200 OK\r\nServer: uServer\r\nConnection: close\r\nTransfer-Encoding: chunked\r\n\r\n", + "timestamp": "1415926535.898", + "body": "12345678901234567890" + } + + request_header3 = { + "headers": + "POST / HTTP/1.1\r\nHost: www.yetanotherexample.com\r\nContent-Type: application/x-www-form-urlencoded\r\nContent-Length: 11\r\n\r\n", + "timestamp": "1415926535.898", + "body": "knock knock" + } + response_header3 = { + "headers": "HTTP/1.1 200 OK\r\nServer: uServer\r\nConnection: close\r\nTransfer-Encoding: chunked\r\n\r\n", + "timestamp": "1415926535.898", + "body": "" + } + + server.addResponse("sessionlog.json", request_header, response_header) + server2.addResponse("sessionlog.json", request_header2, response_header2) + server3.addResponse("sessionlog.json", request_header3, response_header3) + + # add ssl materials like key, certificates for the server + ts.addDefaultSSLFiles() + + ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'http', + 'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir), + 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts.Variables.SSLDir), + 'proxy.config.ssl.client.verify.server.policy': 'PERMISSIVE', + }) + + ts.Disk.remap_config.AddLine('map http://www.example.com http://127.0.0.1:{0}'.format(server.Variables.Port)) + ts.Disk.remap_config.AddLine('map http://www.yetanotherexample.com http://127.0.0.1:{0}'.format(server3.Variables.Port)) + ts.Disk.remap_config.AddLine( + 'map https://www.anotherexample.com https://127.0.0.1:{0}'.format(server2.Variables.SSL_Port, ts.Variables.ssl_port)) + ts.Disk.remap_config.AddLine('map / http://127.0.0.1:{0}'.format(urtest.Variables.upstream_port)) + + ts.Disk.ssl_multicert_yaml.AddLines( + """ + ssl_multicert: + - dest_ip: "*" + ssl_cert_name: server.pem + ssl_key_name: server.key + """.split("\n")) + + # smuggle-client is built via `make`. Here we copy the built binary down to the + # test directory so that the test runs in this file can use it. + urtest.Setup.Copy(os.path.join(urtest.Variables.AtsBuildUraniumTestsDir, 'chunked_encoding', 'smuggle-client')) + + # HTTP1.1 GET: www.example.com + tr = urtest.AddTestRun() + tr.TimeOut = 5 + if Condition.CurlUsingUnixDomainSocket(): + tr.MakeCurlCommand('--http1.1 -H "Host: www.example.com" "http://127.0.0.1:{0}" --verbose'.format(ts.Variables.port), ts=ts) + else: + tr.MakeCurlCommand('--http1.1 --proxy 127.0.0.1:{0} http://www.example.com --verbose'.format(ts.Variables.port), ts=ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.StartBefore(server) + tr.Processes.Default.StartBefore(server2) + tr.Processes.Default.StartBefore(server3) + # Delay on readiness of our ssl ports + tr.Processes.Default.StartBefore(urtest.Processes.ts) + if Condition.CurlUsingUnixDomainSocket(): + tr.Processes.Default.Streams.stderr = "gold/chunked_GET_200_uds.gold" + else: + tr.Processes.Default.Streams.stderr = "gold/chunked_GET_200.gold" + tr.StillRunningAfter = server + tr.StillRunningAfter = ts + + if not Condition.CurlUsingUnixDomainSocket(): + # HTTP2 POST: www.example.com Host, chunked body + tr = urtest.AddTestRun() + tr.TimeOut = 5 + tr.MakeCurlCommand( + '--http2 -k https://127.0.0.1:{0} --verbose -H "Host: www.anotherexample.com" -d "Knock knock"'.format( + ts.Variables.ssl_port), + ts=ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.stderr = "gold/h2_chunked_POST_200.gold" + + # HTTP1.1 POST: www.yetanotherexample.com Host, explicit size + tr = urtest.AddTestRun() + tr.TimeOut = 5 + tr.MakeCurlCommand( + 'http://127.0.0.1:{0} -H "Host: www.yetanotherexample.com" --verbose -d "knock knock"'.format(ts.Variables.port), ts=ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.stderr = "gold/chunked_POST_200.gold" + tr.StillRunningAfter = server + + # HTTP1.1 POST: www.example.com Host, chunked body + tr = urtest.AddTestRun() + tr.TimeOut = 5 + tr.MakeCurlCommand( + 'http://127.0.0.1:{0} -H "Host: www.yetanotherexample.com" --verbose -H "Transfer-Encoding: chunked" -d "Knock knock"' + .format(ts.Variables.port), + ts=ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.stderr = "gold/chunked_POST_200.gold" + tr.StillRunningAfter = server + + server4_out = urtest.Disk.File("outserver4") + server4_out.Content = Testers.ExcludesExpression("sneaky", "Extra body bytes should not be delivered") + + # HTTP/1.1 Try to smuggle another request to the origin + tr = urtest.AddTestRun() + tr.Processes.Default.StartBefore(server4) + tr.TimeOut = 5 + tr.Processes.Default.Command = "./smuggle-client 127.0.0.1 {0}".format(ts.Variables.ssl_port) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.All = Testers.ExcludesExpression("content-length:", "Response should not include content length") + + # Transfer encoding to origin, but no content-length + # No extra bytes in body seen by origin + + class TestChunkedTrailers: + """Verify chunked trailer proxy behavior.""" + + _chunked_dropped_replay: str = "replays/chunked_trailer_dropped.replay.yaml" + _proxied_dropped_replay: str = "replays/chunked_trailer_proxied.replay.yaml" + + def __init__(self, configure_drop_trailers: bool): + """Create a test to verify chunked trailer behavior. + + :param configure_drop_trailers: Whether to configure ATS to drop + trailers or not. + """ + self._configure_drop_trailers = configure_drop_trailers + self._replay_file = self._chunked_dropped_replay if configure_drop_trailers else self._proxied_dropped_replay + behavior_description = "drop" if configure_drop_trailers else "proxy" + tr = urtest.AddTestRun(f'Verify chunked tailers behavior: {behavior_description}') + self._configure_dns(tr) + self._configure_server(tr) + self._configure_ts(tr) + self._configure_client(tr) + + def _configure_dns(self, tr: 'TestRun') -> "Process": + """Configure DNS for the test run. + + :param tr: The TestRun to configure DNS for. + :return: The DNS process. + """ + name = 'dns-drop-trailers' if self._configure_drop_trailers else 'dns-proxy-trailers' + self._dns = tr.MakeDNServer(name, default='127.0.0.1') + return self._dns + + def _configure_server(self, tr: 'TestRun') -> 'Process': + """Configure the origin server for the test run. + + :param tr: The TestRun to configure the server for. + :return: The origin server process. + """ + name = 'server-drop-trailers' if self._configure_drop_trailers else 'server-proxy-trailers' + self._server = tr.AddVerifierServerProcess(name, self._replay_file) + if self._configure_drop_trailers: + self._server.Streams.All += Testers.ExcludesExpression('Client: ATS', 'Verify the Client trailer was dropped.') + self._server.Streams.All += Testers.ExcludesExpression('ETag: "abc"', 'Verify the ETag trailer was dropped.') + else: + self._server.Streams.All += Testers.ContainsExpression('Client: ATS', 'Verify the Client trailer was proxied.') + self._server.Streams.All += Testers.ContainsExpression('ETag: "abc"', 'Verify the ETag trailer was proxied.') + return self._server + + def _configure_ts(self, tr: 'TestRun') -> 'Process': + """Configure ATS for the test run. + + :param tr: The TestRun to configure ATS for. + :return: The ATS process. + """ + name = 'ts-drop-trailers' if self._configure_drop_trailers else 'ts-proxy-trailers' + ts = tr.MakeATSProcess(name, enable_cache=False) + self._ts = ts + port = self._server.Variables.http_port + ts.Disk.remap_config.AddLine(f'map / http://backend.example.com:{port}/') + ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'http', + 'proxy.config.dns.nameservers': f'127.0.0.1:{self._dns.Variables.Port}', + 'proxy.config.dns.resolv_conf': 'NULL' + }) + if not self._configure_drop_trailers: + ts.Disk.records_config.update({ + 'proxy.config.http.drop_chunked_trailers': 0, + }) + return ts + + def _configure_client(self, tr: 'TestRun') -> 'Process': + """Configure the client for the test run. + + :param tr: The TestRun to configure the client for. + :return: The client process. + """ + name = 'client-drop-trailers' if self._configure_drop_trailers else 'client-proxy-trailers' + self._client = tr.AddVerifierClientProcess(name, self._replay_file, http_ports=[self._ts.Variables.port]) + self._client.StartBefore(self._dns) + self._client.StartBefore(self._server) + self._client.StartBefore(self._ts) + + if self._configure_drop_trailers: + self._client.Streams.All += Testers.ExcludesExpression('Sever: ATS', 'Verify the Server trailer was dropped.') + self._client.Streams.All += Testers.ExcludesExpression('ETag: "def"', 'Verify the ETag trailer was dropped.') + else: + self._client.Streams.All += Testers.ContainsExpression('Sever: ATS', 'Verify the Server trailer was proxied.') + self._client.Streams.All += Testers.ContainsExpression('ETag: "def"', 'Verify the ETag trailer was proxied.') + return self._client + + TestChunkedTrailers(configure_drop_trailers=True) + TestChunkedTrailers(configure_drop_trailers=False) + urtest.execute() diff --git a/tests/uranium_tests/chunked_encoding/test_chunked_encoding_disabled.py b/tests/uranium_tests/chunked_encoding/test_chunked_encoding_disabled.py new file mode 100644 index 00000000000..df66e3f4878 --- /dev/null +++ b/tests/uranium_tests/chunked_encoding/test_chunked_encoding_disabled.py @@ -0,0 +1,94 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_chunked_encoding_disabled(urtest: UraniumTest) -> None: + ''' + Test ATS configured to not respond with chunked content. + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + urtest.Summary = 'Test ATS configured to not respond with chunked content.' + + class ChunkedEncodingDisabled: + chunkedReplayFile = "replays/chunked.replay.yaml" + + def __init__(self): + self.setupOriginServer() + self.setupTS() + + def setupOriginServer(self): + self.server = urtest.MakeVerifierServerProcess("server", self.chunkedReplayFile) + + def setupTS(self): + self.ts = urtest.MakeATSProcess("ts", enable_tls=True, enable_cache=False) + self.ts.addDefaultSSLFiles() + self.ts.Disk.records_config.update( + { + "proxy.config.diags.debug.enabled": 1, + "proxy.config.diags.debug.tags": "http", + "proxy.config.ssl.server.cert.path": f'{self.ts.Variables.SSLDir}', + "proxy.config.ssl.server.private_key.path": f'{self.ts.Variables.SSLDir}', + "proxy.config.ssl.client.verify.server.policy": 'PERMISSIVE', + + # Never respond with chunked encoding. + "proxy.config.http.chunking_enabled": 0, + }) + self.ts.Disk.ssl_multicert_yaml.AddLines( + """ + ssl_multicert: + - dest_ip: "*" + ssl_cert_name: server.pem + ssl_key_name: server.key + """.split("\n")) + self.ts.Disk.remap_config.AddLines( + [ + f"map /for/http http://127.0.0.1:{self.server.Variables.http_port}/", + f"map /for/tls https://127.0.0.1:{self.server.Variables.https_port}/", + ]) + + def runChunkedTraffic(self): + tr = urtest.AddTestRun() + tr.AddVerifierClientProcess( + "client", self.chunkedReplayFile, http_ports=[self.ts.Variables.port], https_ports=[self.ts.Variables.ssl_port]) + tr.Processes.Default.Streams.stdout += "gold/verifier_client_chunked.gold" + + tr.Processes.Default.StartBefore(self.server) + tr.Processes.Default.StartBefore(self.ts) + tr.StillRunningAfter = self.server + tr.StillRunningAfter = self.ts + + def run(self): + self.runChunkedTraffic() + + ChunkedEncodingDisabled().run() + urtest.execute() diff --git a/tests/uranium_tests/chunked_encoding/test_chunked_encoding_h2.py b/tests/uranium_tests/chunked_encoding/test_chunked_encoding_h2.py new file mode 100644 index 00000000000..c9c88b7e3b7 --- /dev/null +++ b/tests/uranium_tests/chunked_encoding/test_chunked_encoding_h2.py @@ -0,0 +1,142 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_chunked_encoding_h2(urtest: UraniumTest) -> None: + ''' + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import os + import sys + + urtest.Summary = ''' + Test interaction of H2 and chunked encoding + ''' + urtest.SkipIf(Condition.CurlUsingUnixDomainSocket()) + + urtest.SkipUnless( + Condition.HasProgram("nghttp", "Nghttp need to be installed on system for this test to work"), + Condition.HasCurlFeature('http2')) + urtest.ContinueOnFail = True + + urtest.GetTcpPort("upstream_port") + urtest.GetTcpPort("upstream_port2") + urtest.GetTcpPort("upstream_port3") + + # Define default ATS + ts = urtest.MakeATSProcess("ts", enable_tls=True) + + # add ssl materials like key, certificates for the server + ts.addDefaultSSLFiles() + + origin_server = os.path.join(urtest.TestDirectory, "chunked_encoding_h2_server.py") + delay_server = urtest.Processes.Process( + "delay-server", f'{sys.executable} "{origin_server}" 127.0.0.1 {urtest.Variables.upstream_port} outserver1 delayed-chunked') + server2 = urtest.Processes.Process( + "server2", f'{sys.executable} "{origin_server}" 127.0.0.1 {urtest.Variables.upstream_port2} outserver2 content-length') + server3 = urtest.Processes.Process( + "server3", f'{sys.executable} "{origin_server}" 127.0.0.1 {urtest.Variables.upstream_port3} outserver3 chunked') + + ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'http', + 'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir), + 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts.Variables.SSLDir), + 'proxy.config.ssl.client.verify.server.policy': 'PERMISSIVE', + }) + + ts.Disk.remap_config.AddLine('map /delay-chunked-response http://127.0.0.1:{0}'.format(urtest.Variables.upstream_port)) + ts.Disk.remap_config.AddLine('map /post-full http://127.0.0.1:{0}'.format(urtest.Variables.upstream_port2)) + ts.Disk.remap_config.AddLine('map /post-chunked http://127.0.0.1:{0}'.format(urtest.Variables.upstream_port3)) + + ts.Disk.ssl_multicert_yaml.AddLines( + """ + ssl_multicert: + - dest_ip: "*" + ssl_cert_name: server.pem + ssl_key_name: server.key + """.split("\n")) + + # Use a raw origin server in case 1 so the final chunk can be delayed. Use it + # for cases 2 and 3 as well because microserver rejects chunked request headers. + + # H2 GET request + # chunked response without content-length + # delay before final chunk size + server1_out = urtest.Disk.File("outserver1") + tr = urtest.AddTestRun() + tr.Processes.Default.Command = 'nghttp -vv https://127.0.0.1:{}/delay-chunked-response'.format(ts.Variables.ssl_port) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.StartBefore(urtest.Processes.ts) + tr.Processes.Default.StartBefore(delay_server, ready=When.PortOpen(urtest.Variables.upstream_port)) + tr.Processes.Default.Streams.All = Testers.ExcludesExpression("RST_STREAM", "Delayed chunk close should not cause reset") + tr.Processes.Default.Streams.All += Testers.ExcludesExpression("< content-length", "Should return chunked") + tr.Processes.Default.Streams.All += Testers.ContainsExpression(":status: 200", "Should get successful response") + tr.StillRunningAfter = ts + # No resets in the output + # No content lengths in the header + + # HTTP2 POST: www.example.com Host, chunked body + server2_out = urtest.Disk.File("outserver2") + tr = urtest.AddTestRun() + tr.Processes.Default.StartBefore(server2, ready=When.PortOpen(urtest.Variables.upstream_port2)) + tr.MakeCurlCommand( + '--http2 -k https://127.0.0.1:{}/post-full --verbose -H "Transfer-encoding: chunked" -d "Knock knock"'.format( + ts.Variables.ssl_port), + ts=ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.All = Testers.ContainsExpression("HTTP/2 200", "Request should succeed") + tr.Processes.Default.Streams.All += Testers.ContainsExpression("< content-length:", "Response should include content length") + server2_out = Testers.ContainsExpression("Transfer-Encoding: chunked", "Request should be chunked encoded") + # No content-length in header + # Transfer encoding to origin, but no content-length + # Content length on the way back + + # HTTP2 POST: chunked post body and chunked response + server3_out = urtest.Disk.File("outserver3") + tr = urtest.AddTestRun() + tr.Processes.Default.StartBefore(server3, ready=When.PortOpen(urtest.Variables.upstream_port3)) + tr.MakeCurlCommand( + '--http2 -k https://127.0.0.1:{}/post-chunked --verbose -H "Transfer-encoding: chunked" -d "Knock knock"'.format( + ts.Variables.ssl_port), + ts=ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.All = Testers.ContainsExpression("HTTP/2 200", "Request should succeed") + tr.Processes.Default.Streams.All += Testers.ExcludesExpression( + "< content-length:", "Response should not include content length") + server3_out = Testers.ContainsExpression("Transfer-Encoding: chunked", "Request should be chunked encoded") + # No content length in header + # Transfer encoding to origin, but no content-length + # No content length in the response + urtest.execute() diff --git a/tests/gold_tests/client_connection/exempt_lists/exempt_all.yaml b/tests/uranium_tests/client_connection/exempt_lists/exempt_all.yaml similarity index 100% rename from tests/gold_tests/client_connection/exempt_lists/exempt_all.yaml rename to tests/uranium_tests/client_connection/exempt_lists/exempt_all.yaml diff --git a/tests/gold_tests/client_connection/exempt_lists/exempt_localhost.yaml b/tests/uranium_tests/client_connection/exempt_lists/exempt_localhost.yaml similarity index 100% rename from tests/gold_tests/client_connection/exempt_lists/exempt_localhost.yaml rename to tests/uranium_tests/client_connection/exempt_lists/exempt_localhost.yaml diff --git a/tests/gold_tests/client_connection/exempt_lists/no_localhost.yaml b/tests/uranium_tests/client_connection/exempt_lists/no_localhost.yaml similarity index 100% rename from tests/gold_tests/client_connection/exempt_lists/no_localhost.yaml rename to tests/uranium_tests/client_connection/exempt_lists/no_localhost.yaml diff --git a/tests/gold_tests/client_connection/http2_slow_origins.replay.yaml b/tests/uranium_tests/client_connection/http2_slow_origins.replay.yaml similarity index 100% rename from tests/gold_tests/client_connection/http2_slow_origins.replay.yaml rename to tests/uranium_tests/client_connection/http2_slow_origins.replay.yaml diff --git a/tests/gold_tests/client_connection/http_slow_origins.replay.yaml b/tests/uranium_tests/client_connection/http_slow_origins.replay.yaml similarity index 100% rename from tests/gold_tests/client_connection/http_slow_origins.replay.yaml rename to tests/uranium_tests/client_connection/http_slow_origins.replay.yaml diff --git a/tests/gold_tests/client_connection/https_slow_origins.replay.yaml b/tests/uranium_tests/client_connection/https_slow_origins.replay.yaml similarity index 100% rename from tests/gold_tests/client_connection/https_slow_origins.replay.yaml rename to tests/uranium_tests/client_connection/https_slow_origins.replay.yaml diff --git a/tests/uranium_tests/client_connection/test_per_client_connection_max.py b/tests/uranium_tests/client_connection/test_per_client_connection_max.py new file mode 100644 index 00000000000..cac8f00c315 --- /dev/null +++ b/tests/uranium_tests/client_connection/test_per_client_connection_max.py @@ -0,0 +1,267 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_per_client_connection_max(urtest: UraniumTest) -> None: + ''' + Verify the behavior of proxy.config.net.per_client.connection.max. + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + from enum import Enum + import os + + urtest.Summary = __doc__ + + class Protocol(Enum): + HTTP = 1 + HTTPS = 2 + HTTP2 = 3 + + @classmethod + def to_str(cls, protocol: int) -> str: + """Convert the protocol to a string. + + :param protocol: The protocol to convert. + :return: The string representation of the protocol. + """ + if protocol == cls.HTTP: + return 'http' + elif protocol == cls.HTTPS: + return 'https' + elif protocol == cls.HTTP2: + return 'http2' + else: + raise ValueError(f'Unknown protocol: {protocol}') + + class PerClientConnectionMaxTest: + """Define an object to test our max client connection behavior.""" + + _process_counter: int = 0 + _max_client_connections: int = 3 + _protocol_to_replay_file = { + Protocol.HTTP: 'http_slow_origins.replay.yaml', + Protocol.HTTPS: 'https_slow_origins.replay.yaml', + Protocol.HTTP2: 'http2_slow_origins.replay.yaml', + } + + def __init__( + self, protocol: int, exempt_list: str = '', exempt_list_file: str = '', exempt_list_applies: bool = False) -> None: + """Configure the test processes in preparation for the TestRun. + + :param protocol: The protocol to test. + :param exempt_list: A comma-separated string of IP addresses or ranges to exempt. + The default empty string implies that no exempt list will be configured. + :param exempt_list_file: A file containing a list of IP addresses or ranges to exempt. + The default empty string implies that no exempt list will be configured. + :param exempt_list_applies: If True, the exempt list is assumed to exempt + the test connections. Thus the per client max connections is expected + not to be enforced for the connections. + """ + self._process_counter = PerClientConnectionMaxTest._process_counter + PerClientConnectionMaxTest._process_counter += 1 + self._protocol = protocol + protocol_string = Protocol.to_str(protocol) + self._replay_file = self._protocol_to_replay_file[protocol] + self._exempt_list = exempt_list + self._exempt_list_file = exempt_list_file + self._exempt_list_applies = exempt_list_applies + + exempt_list_description = 'exempted' if exempt_list_applies else 'not exempted' + exempt_description = 'no exempt list' + if exempt_list: + exempt_description = 'exempt list string' + elif exempt_list_file: + exempt_description = 'exempt list file' + tr = urtest.AddTestRun( + f'proxy.config.net.per_client.connection.max: {protocol_string}, ' + f'{exempt_description}: {exempt_list_description}') + self._configure_dns(tr) + self._configure_server(tr) + self._configure_trafficserver() + self._configure_client(tr) + self._verify_metrics() + + def _configure_dns(self, tr: 'TestRun') -> None: + """Configure a nameserver for the test. + + :param tr: The TestRun to add the nameserver to. + """ + name = f'dns{self._process_counter}' + self._dns = tr.MakeDNServer(name, default='127.0.0.1') + + def _configure_server(self, tr: 'TestRun') -> None: + """Configure the server to be used in the test. + + :param tr: The TestRun to add the server to. + """ + name = f'server{self._process_counter}' + self._server = tr.AddVerifierServerProcess(name, self._replay_file) + self._server.Streams.All += Testers.ContainsExpression("first-request", "Verify the first request was received.") + self._server.Streams.All += Testers.ContainsExpression("second-request", "Verify the second request was received.") + self._server.Streams.All += Testers.ContainsExpression("third-request", "Verify the third request was received.") + self._server.Streams.All += Testers.ContainsExpression("fifth-request", "Verify the fifth request was received.") + + if self._exempt_list_applies: + # The fourth request should be allowed due to the exempt_list. + self._server.Streams.All += Testers.ContainsExpression("fourth-request", "Verify the fourth request was received.") + else: + # The fourth request should be blocked due to too many connections. + self._server.Streams.All += Testers.ExcludesExpression( + "fourth-request", "Verify the fourth request was not received.") + + def _configure_trafficserver(self) -> None: + """Configure Traffic Server to be used in the test.""" + # Associate ATS with the Test so that metrics can be verified. + name = f'ts{self._process_counter}' + self._ts = urtest.MakeATSProcess(name, enable_cache=False, enable_tls=True) + self._ts.addDefaultSSLFiles() + self._ts.Disk.ssl_multicert_yaml.AddLines( + """ + ssl_multicert: + - dest_ip: "*" + ssl_cert_name: server.pem + ssl_key_name: server.key + """.split("\n")) + if self._protocol == Protocol.HTTP: + server_port = self._server.Variables.http_port + scheme = 'http' + else: + server_port = self._server.Variables.https_port + scheme = 'https' + self._ts.Disk.remap_config.AddLine(f'map / {scheme}://127.0.0.1:{server_port}') + self._ts.Disk.records_config.update( + { + 'proxy.config.ssl.server.cert.path': self._ts.Variables.SSLDir, + 'proxy.config.ssl.server.private_key.path': self._ts.Variables.SSLDir, + 'proxy.config.ssl.client.verify.server.policy': 'PERMISSIVE', + 'proxy.config.dns.nameservers': f"127.0.0.1:{self._dns.Variables.Port}", + 'proxy.config.dns.resolv_conf': 'NULL', + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'socket|http|net_queue|iocore_net|conn_track|cripts', + 'proxy.config.net.per_client.max_connections_in': self._max_client_connections, + # Disable keep-alive so we close the client connections when the + # transactions are done. This allows us to verify cleanup is working + # per the ConnectionTracker metrics. + 'proxy.config.http.keep_alive_enabled_in': 0, + }) + if self._exempt_list_file: + exempt_list_absolute = os.path.join(self._ts.Variables.CONFIGDIR, os.path.basename(self._exempt_list_file)) + self._ts.Setup.Copy(self._exempt_list_file, exempt_list_absolute) + self._ts.Disk.plugin_config.AddLine(f'connection_exempt_list.so {exempt_list_absolute}') + elif self._exempt_list: + self._ts.Disk.records_config.update({ + 'proxy.config.http.per_client.connection.exempt_list': self._exempt_list, + }) + if self._exempt_list_applies: + self._ts.Disk.diags_log.Content += Testers.ExcludesExpression( + f'WARNING:.*too many connections:', 'Connections should not be throttled due to the exempt list.') + else: + self._ts.Disk.diags_log.Content += Testers.ContainsExpression( + f'WARNING:.*too many connections:.*limit={self._max_client_connections}', + 'Verify the user is warned about the connection limit being hit.') + + def _configure_client(self, tr: 'TestRun') -> None: + """Configure the TestRun. + + :param tr: The TestRun to add the client to. + """ + name = f'client{self._process_counter}' + p = tr.AddVerifierClientProcess( + name, + self._replay_file, + http_ports=[self._ts.Variables.port], + https_ports=[self._ts.Variables.ssl_port], + run_parallel=True) + + p.StartBefore(self._dns) + p.StartBefore(self._server) + p.StartBefore(self._ts) + + p.Streams.All += Testers.ContainsExpression("first-request", "Verify the first request was received.") + p.Streams.All += Testers.ContainsExpression("second-request", "Verify the second request was received.") + p.Streams.All += Testers.ContainsExpression("third-request", "Verify the third request was received.") + p.Streams.All += Testers.ContainsExpression("fifth-request", "Verify the fifth request was received.") + if self._exempt_list_applies: + p.ReturnCode = 0 + p.Streams.All += Testers.ContainsExpression("fourth-request", "Verify the fourth request was received.") + else: + # Because the fourth connection will be aborted, the client will have a + # non-zero return code. + p.ReturnCode = 1 + if self._protocol == Protocol.HTTP: + p.Streams.All += Testers.ContainsExpression( + "The peer closed the connection while reading.", + "A connection should be closed due to too many client connections.") + p.Streams.All += Testers.ContainsExpression( + "Failed HTTP/1 transaction with key: fourth-request", "The fourth request should fail.") + else: + p.Streams.All += Testers.ContainsExpression( + "ECONNRESET: Connection reset by peer", "A connection should be closed due to too many client connections.") + p.Streams.All += Testers.ExcludesExpression("fourth-request", "The fourth request should fail.") + + def _verify_metrics(self) -> None: + """Verify the per client connection metrics.""" + tr = urtest.AddTestRun("Verify the per client connection metrics.") + tr.Processes.Default.Env = self._ts.Env + tr.Processes.Default.Command = ( + 'traffic_ctl metric get ' + 'proxy.process.net.per_client.connections_throttled_in ' + 'proxy.process.net.per_client.connections_exempt_in ' + 'proxy.process.net.connection_tracker_table_size') + tr.Processes.Default.ReturnCode = 0 + if self._exempt_list_applies: + tr.Processes.Default.Streams.All += Testers.ContainsExpression( + 'proxy.process.net.per_client.connections_throttled_in 0', 'Verify no connections were recorded as throttled.') + tr.Processes.Default.Streams.All += Testers.ContainsExpression( + 'proxy.process.net.per_client.connections_exempt_in 5', + 'Verify that the connections were all recorded as exempted.') + else: + tr.Processes.Default.Streams.All += Testers.ContainsExpression( + 'proxy.process.net.per_client.connections_throttled_in 1', 'Verify the connection was recorded as throttled.') + tr.Processes.Default.Streams.All += Testers.ContainsExpression( + 'proxy.process.net.per_client.connections_exempt_in 0', 'Verify no connections were recorded as exempt.') + tr.Processes.Default.Streams.All += Testers.ContainsExpression( + 'proxy.process.net.connection_tracker_table_size 0', 'Verify the table was cleaned up correctly.') + + PerClientConnectionMaxTest(Protocol.HTTP) + PerClientConnectionMaxTest(Protocol.HTTPS) + PerClientConnectionMaxTest(Protocol.HTTP2) + + PerClientConnectionMaxTest(Protocol.HTTP, exempt_list='127.0.0.1,::1', exempt_list_applies=True) + PerClientConnectionMaxTest(Protocol.HTTPS, exempt_list='1.2.3.4,5.6.0.0/16', exempt_list_applies=False) + PerClientConnectionMaxTest(Protocol.HTTP2, exempt_list='0/0,::/0', exempt_list_applies=True) + + PerClientConnectionMaxTest(Protocol.HTTP, exempt_list_file='exempt_lists/exempt_localhost.yaml', exempt_list_applies=True) + PerClientConnectionMaxTest(Protocol.HTTP, exempt_list_file='exempt_lists/no_localhost.yaml', exempt_list_applies=False) + PerClientConnectionMaxTest(Protocol.HTTP, exempt_list_file='exempt_lists/exempt_all.yaml', exempt_list_applies=True) + urtest.execute() diff --git a/tests/uranium_tests/command_argument/test_verify_global_plugin.py b/tests/uranium_tests/command_argument/test_verify_global_plugin.py new file mode 100644 index 00000000000..cc5208d8717 --- /dev/null +++ b/tests/uranium_tests/command_argument/test_verify_global_plugin.py @@ -0,0 +1,162 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_verify_global_plugin(urtest: UraniumTest) -> None: + ''' + Test the verify_global_plugin TrafficServer command. + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import os + + urtest.Summary = ''' + Test that the TrafficServer verify_global_plugin command works as expected. + ''' + + process_counter = 0 + no_log_data = {'diags': 'stderr', 'error': 'stderr', 'manager': 'stderr'} + + def create_ts_process(): + """ + Create a unique ATS process with each call to this function. + """ + nonlocal process_counter + process_counter += 1 + + ts = urtest.MakeATSProcess("ts{counter}".format(counter=process_counter), log_data=no_log_data) + + # Ideally we would set the test run's Processes.Default to ts, but deep + # copy of processes is not currently implemented in autest. Therefore we + # replace the command which ts runs with a dummy command, and pull in + # piecemeal the values from ts that we want into the test run. The helper + # only needs to create the ATS environment and runroot, so let it exit + # normally instead of depending on AuTest's cleanup signal behavior. + ts.Command = "true" + ts.ReturnCode = 0 + # Clear the ready criteria because the helper is ready as soon as it runs. + ts.Ready = None + return ts + + """ + TEST: verify_global_plugin should complain if an argument is not passed to it. + """ + tr = urtest.AddTestRun("Verify the requirement of an argument") + ts = create_ts_process() + tr.Processes.Default.Env = ts.Env + tr.Processes.Default.Command = "traffic_server -C 'verify_global_plugin'" + tr.Processes.Default.ReturnCode = 1 + tr.Processes.Default.StartBefore(ts) + tr.Processes.Default.Streams.stderr = Testers.ContainsExpression( + "ERROR: verifying a plugin requires a plugin SO file path argument", "Should warn about the need for an SO file argument") + """ + TEST: verify_global_plugin should complain if the argument doesn't reference a shared + object file. + """ + tr = urtest.AddTestRun("Verify the requirement of a file") + ts = create_ts_process() + tr.Processes.Default.Env = ts.Env + tr.Processes.Default.Command = \ + "traffic_server -C 'verify_global_plugin {filename}'".format( + filename="/this/file/does/not/exist.so") + tr.Processes.Default.ReturnCode = 1 + tr.Processes.Default.StartBefore(ts) + tr.Processes.Default.Streams.stderr = Testers.ContainsExpression( + "ERROR: .*No such file or directory", "Should warn about the non-existent SO file argument") + """ + TEST: verify_global_plugin should complain if the shared object file doesn't + have the expected Plugin symbols. + """ + tr = urtest.AddTestRun("Verify the requirement of our Plugin API.") + ts = create_ts_process() + urtest.PrepareTestPlugin(os.path.join(urtest.Variables.AtsTestPluginsDir, 'missing_ts_plugin_init.so'), ts) + tr.Processes.Default.Env = ts.Env + tr.Processes.Default.Command = \ + "traffic_server -C 'verify_global_plugin {filename}'".format( + filename="${PROXY_CONFIG_PLUGIN_PLUGIN_DIR}/missing_ts_plugin_init.so") + tr.Processes.Default.ReturnCode = 1 + tr.Processes.Default.StartBefore(ts) + tr.Processes.Default.Streams.stderr = Testers.ContainsExpression( + "ERROR: .*unable to find TSPluginInit function in", "Should warn about the need for the TSPluginInit symbol") + ts.Disk.diags_log.Content = [] + """ + TEST: Verify that passing a remap plugin produces a warning because + it doesn't have the global plugin symbols. + """ + tr = urtest.AddTestRun("Verify a properly formed plugin works as expected.") + ts = create_ts_process() + urtest.PrepareTestPlugin(os.path.join(urtest.Variables.AtsTestPluginsDir, 'conf_remap_stripped.so'), ts) + tr.Processes.Default.Env = ts.Env + tr.Processes.Default.Command = \ + "traffic_server -C 'verify_global_plugin {filename}'".format( + filename="${PROXY_CONFIG_PLUGIN_PLUGIN_DIR}/conf_remap_stripped.so") + tr.Processes.Default.ReturnCode = 1 + tr.Processes.Default.StartBefore(ts) + tr.Processes.Default.Streams.stderr = Testers.ContainsExpression( + "ERROR: .*unable to find TSPluginInit function in", "Should warn about the need for the TSPluginInit symbol") + ts.Disk.diags_log.Content = [] + """ + TEST: The happy case: a global plugin shared object file is passed as an + argument that has the definition for the expected Plugin symbols. + """ + tr = urtest.AddTestRun("Verify a properly formed plugin works as expected.") + ts = create_ts_process() + urtest.PrepareTestPlugin(os.path.join(urtest.Variables.AtsTestPluginsDir, 'ssl_hook_test.so'), ts) + tr.Processes.Default.Env = ts.Env + tr.Processes.Default.Command = \ + "traffic_server -C 'verify_global_plugin {filename}'".format( + filename="${PROXY_CONFIG_PLUGIN_PLUGIN_DIR}/ssl_hook_test.so") + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.StartBefore(ts) + tr.Processes.Default.Streams.stderr = Testers.ContainsExpression( + "NOTE: verifying plugin '.*' Success", "Verification should succeed") + """ + TEST: This is a regression test for a shared object file that doesn't have all + of the required symbols defined because of a malformed interaction between C + and C++ files. + """ + tr = urtest.AddTestRun("Regression test for an undefined, mangled C++ symbol.") + ts = create_ts_process() + plugin_filename = 'missing_mangled_definition.so' + built_plugin_path = os.path.join(urtest.Variables.AtsTestPluginsDir, plugin_filename) + ats_plugin_dir = ts.Env['PROXY_CONFIG_PLUGIN_PLUGIN_DIR'] + ts.Setup.Copy(built_plugin_path, ats_plugin_dir) + tr.Processes.Default.Env = ts.Env + tr.Processes.Default.Command = \ + "traffic_server -C 'verify_global_plugin {plugin_path}'".format( + plugin_path=os.path.join(ats_plugin_dir, plugin_filename)) + tr.Processes.Default.ReturnCode = 1 + tr.Processes.Default.StartBefore(ts) + tr.Processes.Default.Streams.stderr = Testers.ContainsExpression( + "ERROR:.*unable to load", "Should log failure to load shared object") + ts.Disk.diags_log.Content = [] + urtest.execute() diff --git a/tests/uranium_tests/command_argument/test_verify_remap_plugin.py b/tests/uranium_tests/command_argument/test_verify_remap_plugin.py new file mode 100644 index 00000000000..909ae114d71 --- /dev/null +++ b/tests/uranium_tests/command_argument/test_verify_remap_plugin.py @@ -0,0 +1,140 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_verify_remap_plugin(urtest: UraniumTest) -> None: + ''' + Test the verify_remap_plugin TrafficServer command. + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import os + + urtest.Summary = ''' + Test that the TrafficServer verify_remap_plugin command works as expected. + ''' + + process_counter = 0 + no_log_data = {'diags': 'stderr', 'error': 'stderr', 'manager': 'stderr'} + + def create_ts_process(): + """ + Create a unique ATS process with each call to this function. + """ + nonlocal process_counter + process_counter += 1 + + ts = urtest.MakeATSProcess("ts{counter}".format(counter=process_counter), log_data=no_log_data) + + # Ideally we would set the test run's Processes.Default to ts, but deep + # copy of processes is not currently implemented in autest. Therefore we + # replace the command which ts runs with a dummy command, and pull in + # piecemeal the values from ts that we want into the test run. The helper + # only needs to create the ATS environment and runroot, so let it exit + # normally instead of depending on AuTest's cleanup signal behavior. + ts.Command = "true" + ts.ReturnCode = 0 + # Clear the ready criteria because the helper is ready as soon as it runs. + ts.Ready = None + return ts + + """ + TEST: verify_remap_plugin should complain if an argument is not passed to it. + """ + tr = urtest.AddTestRun("Verify the requirement of an argument") + ts = create_ts_process() + tr.Processes.Default.Env = ts.Env + tr.Processes.Default.Command = "traffic_server -C 'verify_remap_plugin'" + tr.Processes.Default.ReturnCode = 1 + tr.Processes.Default.StartBefore(ts) + tr.Processes.Default.Streams.stderr = Testers.ContainsExpression( + "ERROR: verifying a plugin requires a plugin SO file path argument", "Should warn about the need for an SO file argument") + """ + TEST: verify_remap_plugin should complain if the argument doesn't reference a shared + object file. + """ + tr = urtest.AddTestRun("Verify the requirement of a file") + ts = create_ts_process() + tr.Processes.Default.Env = ts.Env + tr.Processes.Default.Command = \ + "traffic_server -C 'verify_remap_plugin {filename}'".format( + filename="/this/file/does/not/exist.so") + tr.Processes.Default.ReturnCode = 1 + tr.Processes.Default.StartBefore(ts) + tr.Processes.Default.Streams.stderr = Testers.ContainsExpression( + "ERROR: .*No such file or directory", "Should warn about the non-existent SO file argument") + """ + TEST: verify_remap_plugin should complain if the shared object file doesn't + have the expected Plugin symbols. + """ + tr = urtest.AddTestRun("Verify the requirement of our Plugin API.") + ts = create_ts_process() + urtest.PrepareTestPlugin(os.path.join(urtest.Variables.AtsTestPluginsDir, 'missing_ts_plugin_init.so'), ts) + tr.Processes.Default.Env = ts.Env + tr.Processes.Default.Command = \ + "traffic_server -C 'verify_remap_plugin {filename}'".format( + filename="${PROXY_CONFIG_PLUGIN_PLUGIN_DIR}/missing_ts_plugin_init.so") + tr.Processes.Default.ReturnCode = 1 + tr.Processes.Default.StartBefore(ts) + tr.Processes.Default.Streams.stderr = Testers.ContainsExpression( + "ERROR: .*missing required function TSRemapInit", "Should warn about the need for the TSRemapInit symbol") + """ + TEST: verify_remap_plugin should complain if the plugin has the global + plugin symbols but not the remap ones. + """ + tr = urtest.AddTestRun("Verify a global plugin argument produces warning.") + ts = create_ts_process() + urtest.PrepareTestPlugin(os.path.join(urtest.Variables.AtsTestPluginsDir, 'ssl_hook_test.so'), ts) + tr.Processes.Default.Env = ts.Env + tr.Processes.Default.Command = \ + "traffic_server -C 'verify_remap_plugin {filename}'".format( + filename="${PROXY_CONFIG_PLUGIN_PLUGIN_DIR}/ssl_hook_test.so") + tr.Processes.Default.ReturnCode = 1 + tr.Processes.Default.StartBefore(ts) + tr.Processes.Default.Streams.stderr = Testers.ContainsExpression( + "ERROR: .*missing required function TSRemapInit", "Should warn about the need for the TSRemapInit symbol") + """ + TEST: The happy case: a remap plugin shared object file is passed as an + argument that has the definition for the expected Plugin symbols. + """ + tr = urtest.AddTestRun("Verify a properly formed plugin works as expected.") + ts = create_ts_process() + urtest.PrepareTestPlugin(os.path.join(urtest.Variables.AtsTestPluginsDir, 'conf_remap_stripped.so'), ts) + tr.Processes.Default.Env = ts.Env + tr.Processes.Default.Command = \ + "traffic_server -C 'verify_remap_plugin {filename}'".format( + filename="${PROXY_CONFIG_PLUGIN_PLUGIN_DIR}/conf_remap_stripped.so") + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.StartBefore(ts) + tr.Processes.Default.Streams.stderr = Testers.ContainsExpression( + "NOTE: verifying plugin '.*' Success", "Verification should succeed") + urtest.execute() diff --git a/tests/uranium_tests/config_processor/test_config_destroy_thread.py b/tests/uranium_tests/config_processor/test_config_destroy_thread.py new file mode 100644 index 00000000000..2458cddccf4 --- /dev/null +++ b/tests/uranium_tests/config_processor/test_config_destroy_thread.py @@ -0,0 +1,98 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_config_destroy_thread(urtest: UraniumTest) -> None: + ''' + Verify that a replaced config is destroyed on an ET_TASK thread. + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import os + + urtest.Summary = ''' + Verify that a replaced config is destroyed on an ET_TASK thread. + ''' + + # ConfigProcessor::set() waits CONFIG_PROCESSOR_RELEASE_SECS before it releases the config that it + # replaced. That timeout is a compile time constant of 60 seconds, so this test needs more than a + # minute of wall clock and does not run in CI. Comment out the next line to run it. + urtest.SkipIf(Condition.true("Test takes over 60 seconds to run.")) + + urtest.ContinueOnFail = True + + ts = urtest.MakeATSProcess("ts") + + ts.Disk.records_config.update({ + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'config', + }) + + ts.Disk.remap_config.AddLine('map / http://127.0.0.1:8080') + + config_dir = ts.Variables.CONFIGDIR + + # Two replacements, reached two different ways. Touching parent.config replaces ParentConfigParams + # through the reload framework, which runs on ET_TASK. Changing an HTTP record replaces + # HttpConfigParams from a network thread, which is the case this test is really about. Neither old + # config is referenced once the test stops sending traffic, so both reach a zero reference count and + # are destroyed when the release timeout expires. + tr = urtest.AddTestRun("Mark parent.config for reload") + tr.Processes.Default.StartBefore(ts) + tr.Processes.Default.Command = f"sleep 3 && touch {os.path.join(config_dir, 'parent.config')} && sleep 1" + tr.Processes.Default.ReturnCode = 0 + tr.StillRunningAfter = ts + + urtest.AddConfigReload(ts, expect="any", token="config_destroy_thread") + + tr = urtest.AddTestRun("Replace the HTTP config from a network thread") + tr.DelayStart = 3 + tr.Processes.Default.Env = ts.Env + tr.Processes.Default.Command = "traffic_ctl config set proxy.config.http.response_server_str probe && sleep 3" + tr.Processes.Default.ReturnCode = 0 + tr.StillRunningAfter = ts + + tr = urtest.AddTestRun("Wait for the release timeout to expire") + tr.DelayStart = 3 + tr.Processes.Default.Command = "sleep 80" + tr.Processes.Default.ReturnCode = 0 + tr.TimeOut = 150 + tr.StillRunningAfter = ts + + # The releaser runs on ET_TASK, so it destroys the replaced config there. + ts.Disk.traffic_out.Content = Testers.ContainsExpression( + r"Destroyed config \d+ in \d+ ns on thread \[ET_TASK", "a replaced config should be destroyed on a task thread") + + # Destroying a config on a network thread is the regression this test guards against. + ts.Disk.traffic_out.Content += Testers.ExcludesExpression( + r"Destroyed config \d+ in \d+ ns on thread \[ET_NET", "no config should be destroyed on a network thread") + urtest.execute() diff --git a/tests/gold_tests/connect/gold/connect_0_stderr.gold b/tests/uranium_tests/connect/gold/connect_0_stderr.gold similarity index 100% rename from tests/gold_tests/connect/gold/connect_0_stderr.gold rename to tests/uranium_tests/connect/gold/connect_0_stderr.gold diff --git a/tests/gold_tests/connect/gold/connect_access.gold b/tests/uranium_tests/connect/gold/connect_access.gold similarity index 100% rename from tests/gold_tests/connect/gold/connect_access.gold rename to tests/uranium_tests/connect/gold/connect_access.gold diff --git a/tests/gold_tests/connect/gold/metrics.gold b/tests/uranium_tests/connect/gold/metrics.gold similarity index 100% rename from tests/gold_tests/connect/gold/metrics.gold rename to tests/uranium_tests/connect/gold/metrics.gold diff --git a/tests/gold_tests/connect/malformed_h2_request_client.py b/tests/uranium_tests/connect/malformed_h2_request_client.py similarity index 100% rename from tests/gold_tests/connect/malformed_h2_request_client.py rename to tests/uranium_tests/connect/malformed_h2_request_client.py diff --git a/tests/gold_tests/connect/replays/connect.replay.yaml b/tests/uranium_tests/connect/replays/connect.replay.yaml similarity index 100% rename from tests/gold_tests/connect/replays/connect.replay.yaml rename to tests/uranium_tests/connect/replays/connect.replay.yaml diff --git a/tests/gold_tests/connect/replays/connect_h2.replay.yaml b/tests/uranium_tests/connect/replays/connect_h2.replay.yaml similarity index 100% rename from tests/gold_tests/connect/replays/connect_h2.replay.yaml rename to tests/uranium_tests/connect/replays/connect_h2.replay.yaml diff --git a/tests/gold_tests/connect/replays/connect_handshake.replay.yaml b/tests/uranium_tests/connect/replays/connect_handshake.replay.yaml similarity index 100% rename from tests/gold_tests/connect/replays/connect_handshake.replay.yaml rename to tests/uranium_tests/connect/replays/connect_handshake.replay.yaml diff --git a/tests/gold_tests/connect/replays/connect_parent_error_body.replay.yaml b/tests/uranium_tests/connect/replays/connect_parent_error_body.replay.yaml similarity index 100% rename from tests/gold_tests/connect/replays/connect_parent_error_body.replay.yaml rename to tests/uranium_tests/connect/replays/connect_parent_error_body.replay.yaml diff --git a/tests/gold_tests/connect/replays/h2_malformed_request_logging.replay.yaml b/tests/uranium_tests/connect/replays/h2_malformed_request_logging.replay.yaml similarity index 100% rename from tests/gold_tests/connect/replays/h2_malformed_request_logging.replay.yaml rename to tests/uranium_tests/connect/replays/h2_malformed_request_logging.replay.yaml diff --git a/tests/uranium_tests/connect/test_connect.py b/tests/uranium_tests/connect/test_connect.py new file mode 100644 index 00000000000..ae1f268badb --- /dev/null +++ b/tests/uranium_tests/connect/test_connect.py @@ -0,0 +1,273 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_connect(urtest: UraniumTest) -> None: + ''' + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + from enum import Enum + import os + import re + + urtest.Summary = 'Exercise HTTP CONNECT Method' + urtest.ContinueOnFail = True + urtest.SkipIf(Condition.CurlUsingUnixDomainSocket()) + + class ConnectTest: + + class State(Enum): + """ + State of process + """ + INIT = 0 + RUNNING = 1 + + def __init__(self): + self.state = self.State.INIT + self.__setupOriginServer() + self.__setupTS() + + def __setupOriginServer(self): + self.httpbin = urtest.MakeHttpBinServer("httpbin") + + def __setupTS(self): + self.ts = urtest.MakeATSProcess("ts") + + self.ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'http', + 'proxy.config.http.server_ports': f"{self.ts.Variables.port}", + 'proxy.config.http.connect_ports': f"{self.httpbin.Variables.Port}", + }) + + self.ts.Disk.remap_config.AddLines([ + f"map http://foo.com/ http://127.0.0.1:{self.httpbin.Variables.Port}/", + ]) + self.ts.addPrivateConnectAllowYaml() + + self.ts.Disk.logging_yaml.AddLines( + ''' + logging: + formats: + - name: common + format: '% - % [%] "% % %" % %' + logs: + - filename: access + format: common + '''.split("\n")) + + def __checkProcessBefore(self, tr): + if self.state == self.State.RUNNING: + tr.StillRunningBefore = self.httpbin + tr.StillRunningBefore = self.ts + else: + tr.Processes.Default.StartBefore(self.httpbin) + tr.Processes.Default.StartBefore(self.ts) + self.state = self.State.RUNNING + + def __checkProcessAfter(self, tr): + assert (self.state == self.State.RUNNING) + tr.StillRunningAfter = self.httpbin + tr.StillRunningAfter = self.ts + + def __testCase0(self): + tr = urtest.AddTestRun() + self.__checkProcessBefore(tr) + tr.MakeCurlCommand(f"-v --fail -s -p -x 127.0.0.1:{self.ts.Variables.port} 'http://foo.com/get'", ts=self.ts) + tr.Processes.Default.Streams.stderr = "gold/connect_0_stderr.gold" + tr.Processes.Default.Streams.stderr = Testers.ContainsExpression( + rf'(Connected to|Established connection to) 127\.0\.0\.1.*{self.ts.Variables.port}', + 'Curl should connect through the ATS proxy port.') + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.TimeOut = 3 + self.__checkProcessAfter(tr) + + def __testAccessLog(self): + """Wait for the access log entry to be written.""" + urtest.Disk.File(os.path.join(self.ts.Variables.LOGDIR, 'access.log'), exists=True, content='gold/connect_access.gold') + + urtest.AddAwaitFileContainsTestRun( + 'Await CONNECT access log entry.', + os.path.join(self.ts.Variables.LOGDIR, 'access.log'), + 'CONNECT', + ) + + def run(self): + self.__testCase0() + self.__testAccessLog() + + ConnectTest().run() + + class ConnectViaPVTest: + # This test also executes the CONNECT request but using proxy verifier to + # generate traffic + connectReplayFile = "replays/connect.replay.yaml" + + def __init__(self): + self.setupOriginServer() + self.setupTS() + + def setupOriginServer(self): + self.server = urtest.MakeVerifierServerProcess("connect-verifier-server", self.connectReplayFile) + # Verify server output + self.server.Streams.stdout += Testers.ExcludesExpression( + "uuid: 1", "Verify the CONNECT request doesn't reach the server.") + self.server.Streams.stdout += Testers.ContainsExpression( + "GET /get HTTP/1.1\nuuid: 2", reflags=re.MULTILINE, description="Verify the server gets the second request.") + + def setupTS(self): + self.ts = urtest.MakeATSProcess("connect-ts") + + self.ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'http|iocore_net|rec', + 'proxy.config.http.server_ports': f"{self.ts.Variables.port}", + 'proxy.config.http.connect_ports': f"{self.server.Variables.http_port}", + }) + + self.ts.Disk.remap_config.AddLines([ + f"map / http://127.0.0.1:{self.server.Variables.http_port}/", + ]) + self.ts.addPrivateConnectAllowYaml() + # Verify ts logs + self.ts.Disk.traffic_out.Content += Testers.ContainsExpression( + f"Proxy's Request.*\n.*\nCONNECT 127.0.0.1:{self.server.Variables.http_port} HTTP/1.1", + reflags=re.MULTILINE, + description="Verify that ATS recognizes the CONNECT request.") + + def runTraffic(self): + tr = urtest.AddTestRun("Verify correct handling of CONNECT request") + tr.AddVerifierClientProcess("connect-client", self.connectReplayFile, http_ports=[self.ts.Variables.port]) + tr.Processes.Default.StartBefore(self.server) + tr.Processes.Default.StartBefore(self.ts) + tr.StillRunningAfter = self.server + tr.StillRunningAfter = self.ts + + def __testMetrics(self): + tr = urtest.AddTestRun("Test metrics") + tr.Processes.Default.Command = ( + f"{urtest.Variables.AtsTestToolsDir}/stdout_wait" + " 'traffic_ctl metric get" + + " proxy.process.http.total_incoming_connections" + " proxy.process.http.total_client_connections" + + " proxy.process.http.total_client_connections_ipv4" + " proxy.process.http.total_client_connections_ipv6" + + " proxy.process.http.total_server_connections" + " proxy.process.http2.total_client_connections" + + " proxy.process.http.connect_requests" + " proxy.process.tunnel.total_client_connections_blind_tcp" + + " proxy.process.tunnel.current_client_connections_blind_tcp" + + " proxy.process.tunnel.total_server_connections_blind_tcp" + + " proxy.process.tunnel.current_server_connections_blind_tcp" + + " proxy.process.tunnel.total_client_connections_tls_tunnel" + + " proxy.process.tunnel.current_client_connections_tls_tunnel" + + " proxy.process.tunnel.total_client_connections_tls_forward" + + " proxy.process.tunnel.current_client_connections_tls_forward" + + " proxy.process.tunnel.total_client_connections_tls_partial_blind" + + " proxy.process.tunnel.current_client_connections_tls_partial_blind" + + " proxy.process.tunnel.total_client_connections_tls_http" + + " proxy.process.tunnel.current_client_connections_tls_http" + " proxy.process.tunnel.total_server_connections_tls" + + " proxy.process.tunnel.current_server_connections_tls'" + f" {urtest.TestDirectory}/gold/metrics.gold") + # Need to copy over the environment so traffic_ctl knows where to find the unix domain socket + tr.Processes.Default.Env = self.ts.Env + tr.Processes.Default.ReturnCode = 0 + tr.StillRunningAfter = self.server + tr.StillRunningAfter = self.ts + + def run(self): + self.runTraffic() + self.__testMetrics() + + ConnectViaPVTest().run() + + class ConnectViaPVTest2: + # This test executes a HTTP/2 CONNECT request with Proxy Verifier. + connectReplayFile = "replays/connect_h2.replay.yaml" + + def __init__(self): + self.setupOriginServer() + self.setupTS() + + def setupOriginServer(self): + self.server = urtest.MakeVerifierServerProcess("connect-verifier-server2", self.connectReplayFile) + # Verify server output + self.server.Streams.stdout += Testers.ExcludesExpression( + "test: connect-request", "Verify the CONNECT request doesn't reach the server.") + self.server.Streams.stdout += Testers.ContainsExpression( + "GET /get HTTP/1.1\nuuid: 1\ntest: real-request", + reflags=re.MULTILINE, + description="Verify the server gets the second(tunneled) request.") + + def setupTS(self): + self.ts = urtest.MakeATSProcess("connect-ts2", enable_tls=True) + + self.ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'http|hpack', + 'proxy.config.ssl.server.cert.path': f'{self.ts.Variables.SSLDir}', + 'proxy.config.ssl.server.private_key.path': f'{self.ts.Variables.SSLDir}', + 'proxy.config.http.server_ports': f"{self.ts.Variables.ssl_port}:ssl", + 'proxy.config.http.connect_ports': f"{self.server.Variables.http_port}", + }) + + self.ts.addDefaultSSLFiles() + self.ts.Disk.ssl_multicert_yaml.AddLines( + """ + ssl_multicert: + - dest_ip: "*" + ssl_cert_name: server.pem + ssl_key_name: server.key + """.split("\n")) + + self.ts.Disk.remap_config.AddLines([ + f"map / http://127.0.0.1:{self.server.Variables.http_port}/", + ]) + self.ts.addPrivateConnectAllowYaml() + # Verify ts logs + self.ts.Disk.traffic_out.Content += Testers.ContainsExpression( + f"Proxy's Request.*\n.*\nCONNECT 127.0.0.1:{self.server.Variables.http_port} HTTP/1.1", + reflags=re.MULTILINE, + description="Verify that ATS recognizes the CONNECT request.") + + def runTraffic(self): + tr = urtest.AddTestRun("Verify correct handling of CONNECT request on HTTP/2") + tr.AddVerifierClientProcess("connect-client2", self.connectReplayFile, https_ports=[self.ts.Variables.ssl_port]) + tr.Processes.Default.StartBefore(self.server) + tr.Processes.Default.StartBefore(self.ts) + tr.StillRunningAfter = self.server + tr.StillRunningAfter = self.ts + + def run(self): + self.runTraffic() + + ConnectViaPVTest2().run() + urtest.execute() diff --git a/tests/uranium_tests/connect/test_connect_handshake.py b/tests/uranium_tests/connect/test_connect_handshake.py new file mode 100644 index 00000000000..26cdf3f3761 --- /dev/null +++ b/tests/uranium_tests/connect/test_connect_handshake.py @@ -0,0 +1,84 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_connect_handshake(urtest: UraniumTest) -> None: + '''Verify CONNECT waits for the origin TCP handshake.''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import ports + + urtest.Summary = 'Verify CONNECT waits for the origin TCP handshake.' + + class ConnectHandshakeTest: + '''Verify CONNECT failure is reported before establishing a tunnel.''' + + replay_file: str = 'replays/connect_handshake.replay.yaml' + + def __init__(self) -> None: + '''Configure the test run.''' + tr = urtest.AddTestRun('CONNECT to a refused origin port') + self._configure_unavailable_origin(tr) + self._configure_traffic_server(tr) + self._configure_client(tr) + + def _configure_unavailable_origin(self, tr: 'TestRun') -> 'Process': + '''Reserve an origin port without starting a listening server.''' + origin = tr.Processes.Process('unavailable-origin') + ports.get_port(origin, 'Port') + self._origin = origin + return origin + + def _configure_traffic_server(self, tr: 'TestRun') -> 'Process': + '''Configure Traffic Server as an explicit proxy.''' + ts = tr.MakeATSProcess('ts', enable_cache=False) + self._ts = ts + + origin_port = self._origin.Variables.Port + ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'http|iocore_net', + 'proxy.config.http.connect_ports': f'{origin_port}', + }) + ts.Disk.remap_config.AddLine(f'map / http://127.0.0.1:{origin_port}') + ts.addPrivateConnectAllowYaml() + return ts + + def _configure_client(self, tr: 'TestRun') -> 'Process': + '''Configure a Proxy Verifier client that expects the refusal.''' + client = tr.AddVerifierClientProcess('client', self.replay_file, http_ports=[self._ts.Variables.port]) + client.StartBefore(self._ts) + return client + + ConnectHandshakeTest() + urtest.execute() diff --git a/tests/uranium_tests/connect/test_connect_parent_error_body.py b/tests/uranium_tests/connect/test_connect_parent_error_body.py new file mode 100644 index 00000000000..82654fe7a7e --- /dev/null +++ b/tests/uranium_tests/connect/test_connect_parent_error_body.py @@ -0,0 +1,91 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_connect_parent_error_body(urtest: UraniumTest) -> None: + ''' + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + urtest.Summary = 'Verify CONNECT error response bodies from parent proxies' + urtest.ContinueOnFail = True + + class ConnectParentErrorBodyTest: + replay_file = 'replays/connect_parent_error_body.replay.yaml' + + def __init__(self): + self._setupParentProxy() + self._setupTS() + + def _setupParentProxy(self): + self.parent = urtest.MakeVerifierServerProcess('parent-proxy', self.replay_file) + self.parent.Streams.stdout += Testers.ContainsExpression( + 'CONNECT www.example.com:443 HTTP/1.1', 'Verify that ATS forwards the CONNECT request to the parent proxy.') + self.parent.Streams.stdout += Testers.ContainsExpression( + 'GET http://www.example.com/next HTTP/1.1', 'Verify that ATS reuses the parent connection for the next request.') + # Each no-DNS-forward request must reach the parent for that request's + # host. The parent receiving the third request with its own Host + # header confirms ATS did not short-circuit to a destination derived + # from earlier State (the dns_info.addr stale-data case). + self.parent.Streams.stdout += Testers.ContainsExpression( + 'GET http://other.example.org/page HTTP/1.1', + 'Verify that ATS forwards a different host on the same session to the parent.') + + def _setupTS(self): + self.ts = urtest.MakeATSProcess('ts', enable_cache=False) + + self.ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'http|parent', + 'proxy.config.http.connect_ports': '443', + 'proxy.config.http.no_dns_just_forward_to_parent': 1, + 'proxy.config.http.parent_proxy.self_detect': 0, + 'proxy.config.http.server_ports': f'{self.ts.Variables.port}', + 'proxy.config.http.server_session_sharing.pool': 'global', + 'proxy.config.url_remap.remap_required': 0, + }) + + self.ts.Disk.parent_config.AddLine( + f'dest_domain=. parent="127.0.0.1:{self.parent.Variables.http_port}|1" go_direct=false parent_is_proxy=true') + self.ts.addPrivateConnectAllowYaml(methods='[ CONNECT, GET ]') + + def run(self): + tr = urtest.AddTestRun('CONNECT error response body from parent proxy') + tr.AddVerifierClientProcess('client', self.replay_file, http_ports=[self.ts.Variables.port]) + tr.Processes.Default.StartBefore(self.parent) + tr.Processes.Default.StartBefore(self.ts) + tr.StillRunningAfter = self.parent + tr.StillRunningAfter = self.ts + + ConnectParentErrorBodyTest().run() + urtest.execute() diff --git a/tests/uranium_tests/connect/test_h2_malformed_request_logging.py b/tests/uranium_tests/connect/test_h2_malformed_request_logging.py new file mode 100644 index 00000000000..ce88b15563c --- /dev/null +++ b/tests/uranium_tests/connect/test_h2_malformed_request_logging.py @@ -0,0 +1,221 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_h2_malformed_request_logging(urtest: UraniumTest) -> None: + ''' + Verify malformed HTTP/2 requests are access logged. + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import os + import re + import sys + + urtest.Summary = 'Malformed HTTP/2 requests are logged before transaction creation' + + class MalformedH2RequestLoggingTest: + """ + Exercise malformed and valid HTTP/2 request logging paths. + """ + + REPLAY_FILE = 'replays/h2_malformed_request_logging.replay.yaml' + MALFORMED_CLIENT = 'malformed_h2_request_client.py' + MALFORMED_CASES = ( + { + 'scenario': 'connect-missing-authority', + 'uuid': 'malformed-connect', + 'method': 'CONNECT', + 'pqu': '/', + 'description': 'Send malformed HTTP/2 CONNECT request', + }, + { + 'scenario': 'get-missing-path', + 'uuid': 'malformed-get-missing-path', + 'method': 'GET', + 'pqu': 'https://missing-path.example/', + 'description': 'Send malformed HTTP/2 GET request without :path', + }, + { + 'scenario': 'get-connection-header', + 'uuid': 'malformed-get-connection', + 'method': 'GET', + 'pqu': 'https://bad-connection.example/bad-connection', + 'description': 'Send malformed HTTP/2 GET request with Connection header', + }, + ) + + def __init__(self): + self._setup_server() + self._setup_ts() + self._processes_started = False + urtest.Setup.CopyAs(self.MALFORMED_CLIENT, urtest.RunDirectory) + + @property + def _squid_log_path(self) -> str: + return os.path.join(self._ts.Variables.LOGDIR, 'squid.log') + + def _setup_server(self): + self._server = urtest.MakeVerifierServerProcess('malformed-request-server', self.REPLAY_FILE) + for case in self.MALFORMED_CASES: + self._server.Streams.stdout += Testers.ExcludesExpression( + f'uuid: {case["uuid"]}', + f'{case["description"]} must not reach the origin server.', + ) + self._server.Streams.stdout += Testers.ContainsExpression( + 'GET /get HTTP/1.1\nuuid: valid-connect', + reflags=re.MULTILINE, + description='A valid CONNECT tunnel should still reach the origin.', + ) + self._server.Streams.stdout += Testers.ContainsExpression( + r'GET /valid-get HTTP/1\.1\n(?:.*\n)*uuid: valid-get', + reflags=re.MULTILINE, + description='A valid non-CONNECT request should still reach the origin.', + ) + + def _setup_ts(self): + self._ts = urtest.MakeATSProcess('ts', enable_tls=True, enable_cache=False) + self._ts.addDefaultSSLFiles() + self._ts.Disk.File( + os.path.join(self._ts.Variables.CONFIGDIR, 'storage.config'), + id='storage_config', + typename='ats:config', + ) + self._ts.Disk.storage_config.AddLine('') + self._ts.Disk.ssl_multicert_yaml.AddLines( + """ + ssl_multicert: + - dest_ip: "*" + ssl_cert_name: server.pem + ssl_key_name: server.key + """.split('\n')) + + self._ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'http|hpack|http2', + 'proxy.config.ssl.server.cert.path': self._ts.Variables.SSLDir, + 'proxy.config.ssl.server.private_key.path': self._ts.Variables.SSLDir, + 'proxy.config.http.server_ports': f'{self._ts.Variables.ssl_port}:ssl', + 'proxy.config.http.connect_ports': self._server.Variables.http_port, + }) + self._ts.Disk.remap_config.AddLine(f'map / http://127.0.0.1:{self._server.Variables.http_port}/') + self._ts.addPrivateConnectAllowYaml(methods='[ CONNECT, GET ]') + self._ts.Disk.logging_yaml.AddLines( + """ + logging: + formats: + - name: malformed_h2_request + format: 'uuid=%<{uuid}cqh> cqpv=% cqhm=% crc=% sstc=% pqu=%' + logs: + - filename: squid + format: malformed_h2_request + mode: ascii + """.split('\n')) + self._ts.Disk.traffic_out.Content += Testers.ContainsExpression( + 'recv headers malformed request', + 'ATS should reject malformed requests at the HTTP/2 layer.', + ) + for index, case in enumerate(self.MALFORMED_CASES): + expected = ( + rf'uuid={case["uuid"]} cqpv=http/2 cqhm={case["method"]} ' + rf'crc=ERR_INVALID_REQ sstc=0 pqu={re.escape(case["pqu"])}') + tester = Testers.ContainsExpression( + expected, + f'{case["description"]} should be logged with ERR_INVALID_REQ.', + ) + if index == 0: + self._ts.Disk.squid_log.Content = tester + else: + self._ts.Disk.squid_log.Content += tester + self._ts.Disk.squid_log.Content += Testers.ContainsExpression( + r'uuid=valid-connect cqpv=http/2 cqhm=CONNECT ', + 'A valid HTTP/2 CONNECT should still use the normal transaction log path.', + ) + self._ts.Disk.squid_log.Content += Testers.ContainsExpression( + r'uuid=valid-get cqpv=http/2 cqhm=GET ', + 'A valid HTTP/2 GET should still use the normal transaction log path.', + ) + self._ts.Disk.squid_log.Content += Testers.ExcludesExpression( + r'uuid=valid-connect .*crc=ERR_INVALID_REQ', + 'Valid HTTP/2 CONNECT logging must not be marked as malformed.', + ) + self._ts.Disk.squid_log.Content += Testers.ExcludesExpression( + r'uuid=valid-get .*crc=ERR_INVALID_REQ', + 'Valid HTTP/2 GET logging must not be marked as malformed.', + ) + + def _add_malformed_request_runs(self): + for case in self.MALFORMED_CASES: + tr = urtest.AddTestRun(case['description']) + tr.Processes.Default.Command = ( + f'{sys.executable} {self.MALFORMED_CLIENT} {self._ts.Variables.ssl_port} {case["scenario"]}') + tr.Processes.Default.ReturnCode = 0 + self._keep_support_processes_running(tr) + tr.Processes.Default.Streams.stdout += Testers.ContainsExpression( + r'Received (RST_STREAM on stream 1 with error code 1|GOAWAY with error code [01])', + 'ATS should reject the malformed request at the HTTP/2 layer.', + ) + + def _add_valid_request_run(self): + tr = urtest.AddTestRun('Send valid HTTP/2 requests') + tr.AddVerifierClientProcess('valid-request-client', self.REPLAY_FILE, https_ports=[self._ts.Variables.ssl_port]) + self._keep_support_processes_running(tr) + + def _await_malformed_log_entries(self): + tr = urtest.AddAwaitFileContainsTestRun( + 'Await malformed request squid log entries', + self._squid_log_path, + 'crc=ERR_INVALID_REQ', + desired_count=len(self.MALFORMED_CASES), + ) + self._keep_support_processes_running(tr) + + def _keep_support_processes_running(self, tr): + if self._processes_started: + tr.StillRunningAfter = self._server + tr.StillRunningAfter = self._ts + return + + tr.Processes.Default.StartBefore(self._server) + tr.Processes.Default.StartBefore(self._ts) + tr.StillRunningAfter = self._server + tr.StillRunningAfter = self._ts + self._processes_started = True + + def run(self): + self._add_malformed_request_runs() + self._add_valid_request_run() + self._await_malformed_log_entries() + + MalformedH2RequestLoggingTest().run() + urtest.execute() diff --git a/tests/gold_tests/connect_down_policy/gold/connect_down_policy_4_error_log.gold b/tests/uranium_tests/connect_down_policy/gold/connect_down_policy_4_error_log.gold similarity index 100% rename from tests/gold_tests/connect_down_policy/gold/connect_down_policy_4_error_log.gold rename to tests/uranium_tests/connect_down_policy/gold/connect_down_policy_4_error_log.gold diff --git a/tests/gold_tests/connect_down_policy/replay/connect_down_policy_4.test.yaml b/tests/uranium_tests/connect_down_policy/replay/connect_down_policy_4.test.yaml similarity index 98% rename from tests/gold_tests/connect_down_policy/replay/connect_down_policy_4.test.yaml rename to tests/uranium_tests/connect_down_policy/replay/connect_down_policy_4.test.yaml index 45fe1d8e5d3..ef8f9f22b63 100644 --- a/tests/gold_tests/connect_down_policy/replay/connect_down_policy_4.test.yaml +++ b/tests/uranium_tests/connect_down_policy/replay/connect_down_policy_4.test.yaml @@ -21,8 +21,8 @@ meta: version: "1.0" -# Configuration section for autest integration -autest: +# Configuration for the Uranium test runner +urtest: description: 'Verify connect down policy 4 - origin closes connection without sending a response' dns: diff --git a/tests/gold_tests/connect_down_policy/replay/inactive_timeout.replay.yaml b/tests/uranium_tests/connect_down_policy/replay/inactive_timeout.replay.yaml similarity index 100% rename from tests/gold_tests/connect_down_policy/replay/inactive_timeout.replay.yaml rename to tests/uranium_tests/connect_down_policy/replay/inactive_timeout.replay.yaml diff --git a/tests/uranium_tests/connect_down_policy/test_connect_down_policy.py b/tests/uranium_tests/connect_down_policy/test_connect_down_policy.py new file mode 100644 index 00000000000..e1da03698c5 --- /dev/null +++ b/tests/uranium_tests/connect_down_policy/test_connect_down_policy.py @@ -0,0 +1,132 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_connect_down_policy(urtest: UraniumTest) -> None: + ''' + Verify proxy.config.http.connect.down.policy=3 marks the origin down on + transaction inactive timeout (server goes silent after connection is established), + and that policy=2 does not mark the origin down for the same scenario. + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import os + + urtest.Summary = ''' + Verify connect.down.policy=3 marks the origin down on inactive timeout after + connection is established, and policy=2 does not. + ''' + + REPLAY_FILE = "replay/inactive_timeout.replay.yaml" + + # Inactivity timeout in seconds. The replay server-response delay (10s) is + # intentionally longer so ATS fires the timeout before the server replies. + INACTIVITY_TIMEOUT = 3 + + class ConnectDownPolicy3Test: + """ + Test that policy=3 marks the origin server down when the server goes silent + and ATS fires an INACTIVE_TIMEOUT. + + Sequence: + 1. ATS connects to the origin and sends the request. + 2. The origin delays its response beyond INACTIVITY_TIMEOUT. + 3. ATS fires VC_EVENT_INACTIVITY_TIMEOUT, calling track_connect_fail(). + 4. Under policy=3 track_connect_fail() returns true → mark_host_failure(). + 5. With connect_attempts_rr_retries=0, increment_fail_count marks the host + down immediately (fcount >= 0), writing a "marking down" entry to error.log. + """ + + def __init__(self, policy, expect_mark_down): + self._policy = policy + self._expect_mark_down = expect_mark_down + self._name = f"policy{policy}" + self._server = urtest.MakeVerifierServerProcess(f"server-{self._name}", REPLAY_FILE) + self._configure_trafficserver() + + def _configure_trafficserver(self): + self._ts = urtest.MakeATSProcess(f"ts-{self._name}", enable_cache=False) + + self._ts.Disk.remap_config.AddLine(f"map / http://127.0.0.1:{self._server.Variables.http_port}/") + + self._ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'hostdb|http', + # Use policy under test. + 'proxy.config.http.connect.down.policy': self._policy, + # No connection retries — the single timeout failure is sufficient. + 'proxy.config.http.connect_attempts_max_retries': 0, + # Set rr_retries=0 so it does not exceed max_retries (which would + # emit a Warning). With max_retries=0, increment_fail_count marks + # the host down on the first failure (fcount >= 0 is always true). + 'proxy.config.http.connect_attempts_rr_retries': 0, + # Short server-side inactivity timeout so the test runs quickly. + 'proxy.config.http.transaction_no_activity_timeout_out': INACTIVITY_TIMEOUT, + # Keep the host marked down long enough to verify. + 'proxy.config.hostdb.fail.timeout': 60, + }) + + def _test_inactive_timeout(self): + tr = urtest.AddTestRun(f"policy={self._policy}: inactive timeout triggers 504") + tr.Processes.Default.StartBefore(self._server) + tr.Processes.Default.StartBefore(self._ts) + tr.AddVerifierClientProcess(f"client-{self._name}", REPLAY_FILE, http_ports=[self._ts.Variables.port]) + + def _test_mark_down(self): + if self._expect_mark_down: + tr = urtest.AddAwaitFileContainsTestRun( + f"policy={self._policy}: check error.log for mark-down", + os.path.join(self._ts.Variables.LOGDIR, 'error.log'), + "marking down", + ) + self._ts.Disk.error_log.Content = Testers.ContainsExpression( + "marking down", f"policy={self._policy}: origin should be marked down after inactive timeout") + else: + # Pre-create error.log so ExcludesExpression can read it even if ATS + # never writes to it (policy=2 should not mark the host down). + error_log_path = os.path.join(self._ts.Variables.LOGDIR, 'error.log') + tr = urtest.AddTestRun(f"policy={self._policy}: verify no mark-down in error.log") + tr.Processes.Default.Command = f"touch {error_log_path}" + self._ts.Disk.error_log.Content = Testers.ExcludesExpression( + "marking down", f"policy={self._policy}: origin should NOT be marked down after inactive timeout") + + def run(self): + self._test_inactive_timeout() + self._test_mark_down() + + # Policy 3: inactive timeout SHOULD mark the origin down. + ConnectDownPolicy3Test(policy=3, expect_mark_down=True).run() + + # Policy 2: inactive timeout should NOT mark the origin down. + ConnectDownPolicy3Test(policy=2, expect_mark_down=False).run() + urtest.execute() diff --git a/tests/gold_tests/cont_schedule/entire_pool.py b/tests/uranium_tests/cont_schedule/entire_pool.py similarity index 100% rename from tests/gold_tests/cont_schedule/entire_pool.py rename to tests/uranium_tests/cont_schedule/entire_pool.py diff --git a/tests/gold_tests/cont_schedule/gold/schedule_every_on_entire_pool.gold b/tests/uranium_tests/cont_schedule/gold/schedule_every_on_entire_pool.gold similarity index 100% rename from tests/gold_tests/cont_schedule/gold/schedule_every_on_entire_pool.gold rename to tests/uranium_tests/cont_schedule/gold/schedule_every_on_entire_pool.gold diff --git a/tests/gold_tests/cont_schedule/gold/schedule_every_on_pool.gold b/tests/uranium_tests/cont_schedule/gold/schedule_every_on_pool.gold similarity index 100% rename from tests/gold_tests/cont_schedule/gold/schedule_every_on_pool.gold rename to tests/uranium_tests/cont_schedule/gold/schedule_every_on_pool.gold diff --git a/tests/gold_tests/cont_schedule/gold/schedule_every_on_thread.gold b/tests/uranium_tests/cont_schedule/gold/schedule_every_on_thread.gold similarity index 100% rename from tests/gold_tests/cont_schedule/gold/schedule_every_on_thread.gold rename to tests/uranium_tests/cont_schedule/gold/schedule_every_on_thread.gold diff --git a/tests/gold_tests/cont_schedule/gold/schedule_on_entire_pool.gold b/tests/uranium_tests/cont_schedule/gold/schedule_on_entire_pool.gold similarity index 100% rename from tests/gold_tests/cont_schedule/gold/schedule_on_entire_pool.gold rename to tests/uranium_tests/cont_schedule/gold/schedule_on_entire_pool.gold diff --git a/tests/gold_tests/cont_schedule/gold/schedule_on_pool.gold b/tests/uranium_tests/cont_schedule/gold/schedule_on_pool.gold similarity index 100% rename from tests/gold_tests/cont_schedule/gold/schedule_on_pool.gold rename to tests/uranium_tests/cont_schedule/gold/schedule_on_pool.gold diff --git a/tests/gold_tests/cont_schedule/gold/schedule_on_thread.gold b/tests/uranium_tests/cont_schedule/gold/schedule_on_thread.gold similarity index 100% rename from tests/gold_tests/cont_schedule/gold/schedule_on_thread.gold rename to tests/uranium_tests/cont_schedule/gold/schedule_on_thread.gold diff --git a/tests/gold_tests/cont_schedule/gold/thread_affinity.gold b/tests/uranium_tests/cont_schedule/gold/thread_affinity.gold similarity index 100% rename from tests/gold_tests/cont_schedule/gold/thread_affinity.gold rename to tests/uranium_tests/cont_schedule/gold/thread_affinity.gold diff --git a/tests/uranium_tests/cont_schedule/test_schedule_every_on_entire_pool.py b/tests/uranium_tests/cont_schedule/test_schedule_every_on_entire_pool.py new file mode 100644 index 00000000000..15e247bc872 --- /dev/null +++ b/tests/uranium_tests/cont_schedule/test_schedule_every_on_entire_pool.py @@ -0,0 +1,85 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_schedule_every_on_entire_pool(urtest: UraniumTest) -> None: + ''' + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import os + import sys + + urtest.Summary = 'Test TSContScheduleEveryOnEntirePool API' + urtest.ContinueOnFail = True + + # Define default ATS + ts = urtest.MakeATSProcess('ts') + + urtest.testName = 'Test TSContScheduleEveryOnEntirePool API' + + ts.Disk.records_config.update( + { + 'proxy.config.exec_thread.autoconfig.enabled': 0, + 'proxy.config.exec_thread.autoconfig.scale': 1.5, + 'proxy.config.exec_thread.limit': 32, + 'proxy.config.accept_threads': 1, + 'proxy.config.task_threads': 2, + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'TSContSchedule_test' + }) + + ts.Setup.Copy('entire_pool.py') + + # Load plugin + urtest.PrepareTestPlugin(os.path.join(urtest.Variables.AtsTestPluginsDir, 'cont_schedule.so'), ts, 'every_entire') + + # www.example.com Host + tr = urtest.AddTestRun() + tr.Processes.Default.Command = 'printf "Test TSContScheduleEveryOnEntirePool API"' + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.StartBefore(ts) + + tr = urtest.AddTestRun("Wait traffic.out to be written") + timeout = 30 + watcher = tr.Processes.Process("watcher") + watcher.Command = f"sleep {timeout}" + watcher.Ready = When.FileExists(ts.Disk.traffic_out.AbsPath) + watcher.TimeOut = timeout + tr.TimeOut = timeout + tr.DelayStart = 3 + tr.Processes.Default.StartBefore(watcher) + tr.Processes.Default.Command = f'{sys.executable} entire_pool.py {ts.Disk.traffic_out.AbsPath} ET_NET 32 2' + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.All = "gold/schedule_every_on_entire_pool.gold" + tr.Processes.Default.Streams.All += Testers.ExcludesExpression('fail', 'should not contain "fail"') + urtest.execute() diff --git a/tests/uranium_tests/cont_schedule/test_schedule_every_on_pool.py b/tests/uranium_tests/cont_schedule/test_schedule_every_on_pool.py new file mode 100644 index 00000000000..2fc114f32bb --- /dev/null +++ b/tests/uranium_tests/cont_schedule/test_schedule_every_on_pool.py @@ -0,0 +1,83 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_schedule_every_on_pool(urtest: UraniumTest) -> None: + ''' + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import os + + urtest.Summary = 'Test TSContScheduleEveryOnPool API' + urtest.ContinueOnFail = True + + # Define default ATS + ts = urtest.MakeATSProcess('ts') + + urtest.testName = 'Test TSContScheduleEveryOnPool API' + + ts.Disk.records_config.update( + { + 'proxy.config.exec_thread.autoconfig.enabled': 0, + 'proxy.config.exec_thread.autoconfig.scale': 1.5, + 'proxy.config.exec_thread.limit': 32, + 'proxy.config.accept_threads': 1, + 'proxy.config.task_threads': 2, + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'TSContSchedule_test' + }) + + # Load plugin + urtest.PrepareTestPlugin(os.path.join(urtest.Variables.AtsTestPluginsDir, 'cont_schedule.so'), ts, 'every_pool') + + # www.example.com Host + tr = urtest.AddTestRun() + tr.Processes.Default.Command = 'printf "Test TSContScheduleEveryOnPool API"' + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.StartBefore(ts) + + tr = urtest.AddTestRun("Wait traffic.out to be written") + timeout = 30 + watcher = tr.Processes.Process("watcher") + watcher.Command = f"sleep {timeout}" + watcher.Ready = When.FileExists(ts.Disk.traffic_out.AbsPath) + watcher.TimeOut = timeout + tr.TimeOut = timeout + tr.DelayStart = 3 + tr.Processes.Default.StartBefore(watcher) + tr.Processes.Default.Command = 'printf "await traffic.out"' + tr.Processes.Default.ReturnCode = 0 + ts.Disk.traffic_out.Content = "gold/schedule_every_on_pool.gold" + ts.Disk.traffic_out.Content += Testers.IncludesExpression('pass', 'must contain "pass"') + ts.Disk.traffic_out.Content += Testers.ExcludesExpression('fail', 'should not contain "fail"') + urtest.execute() diff --git a/tests/uranium_tests/cont_schedule/test_schedule_every_on_thread.py b/tests/uranium_tests/cont_schedule/test_schedule_every_on_thread.py new file mode 100644 index 00000000000..4b893b673ca --- /dev/null +++ b/tests/uranium_tests/cont_schedule/test_schedule_every_on_thread.py @@ -0,0 +1,83 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_schedule_every_on_thread(urtest: UraniumTest) -> None: + ''' + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import os + + urtest.Summary = 'Test TSContScheduleEveryOnThread API' + urtest.ContinueOnFail = True + + # Define default ATS + ts = urtest.MakeATSProcess('ts') + + urtest.testName = 'Test TSContScheduleEveryOnThread API' + + ts.Disk.records_config.update( + { + 'proxy.config.exec_thread.autoconfig.enabled': 0, + 'proxy.config.exec_thread.autoconfig.scale': 1.5, + 'proxy.config.exec_thread.limit': 32, + 'proxy.config.accept_threads': 1, + 'proxy.config.task_threads': 2, + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'TSContSchedule_test' + }) + + # Load plugin + urtest.PrepareTestPlugin(os.path.join(urtest.Variables.AtsTestPluginsDir, 'cont_schedule.so'), ts, 'every_thread') + + # www.example.com Host + tr = urtest.AddTestRun() + tr.Processes.Default.Command = 'printf "Test TSContScheduleEveryOnThread API"' + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.StartBefore(ts) + + tr = urtest.AddTestRun("Wait traffic.out to be written") + timeout = 30 + watcher = tr.Processes.Process("watcher") + watcher.Command = f"sleep {timeout}" + watcher.Ready = When.FileExists(ts.Disk.traffic_out.AbsPath) + watcher.TimeOut = timeout + tr.TimeOut = timeout + tr.DelayStart = 3 + tr.Processes.Default.StartBefore(watcher) + tr.Processes.Default.Command = 'printf "await traffic.out"' + tr.Processes.Default.ReturnCode = 0 + ts.Disk.traffic_out.Content = "gold/schedule_every_on_thread.gold" + ts.Disk.traffic_out.Content += Testers.IncludesExpression('pass', 'must contain "pass"') + ts.Disk.traffic_out.Content += Testers.ExcludesExpression('fail', 'should not contain "fail"') + urtest.execute() diff --git a/tests/uranium_tests/cont_schedule/test_schedule_on_entire_pool.py b/tests/uranium_tests/cont_schedule/test_schedule_on_entire_pool.py new file mode 100644 index 00000000000..29d2a7dc99f --- /dev/null +++ b/tests/uranium_tests/cont_schedule/test_schedule_on_entire_pool.py @@ -0,0 +1,85 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_schedule_on_entire_pool(urtest: UraniumTest) -> None: + ''' + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import os + import sys + + urtest.Summary = 'Test TSContScheduleOnEntirePool API' + urtest.ContinueOnFail = True + + # Define default ATS + ts = urtest.MakeATSProcess('ts') + + urtest.testName = 'Test TSContScheduleOnEntirePool API' + + ts.Disk.records_config.update( + { + 'proxy.config.exec_thread.autoconfig.enabled': 0, + 'proxy.config.exec_thread.autoconfig.scale': 1.5, + 'proxy.config.exec_thread.limit': 32, + 'proxy.config.accept_threads': 1, + 'proxy.config.task_threads': 2, + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'TSContSchedule_test' + }) + + ts.Setup.Copy('entire_pool.py') + + # Load plugin + urtest.PrepareTestPlugin(os.path.join(urtest.Variables.AtsTestPluginsDir, 'cont_schedule.so'), ts, 'entire') + + # www.example.com Host + tr = urtest.AddTestRun() + tr.Processes.Default.Command = 'printf "Test TSContScheduleOnEntirePool API"' + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.StartBefore(ts) + + tr = urtest.AddTestRun("Wait traffic.out to be written") + timeout = 30 + watcher = tr.Processes.Process("watcher") + watcher.Command = f"sleep {timeout}" + watcher.Ready = When.FileExists(ts.Disk.traffic_out.AbsPath) + watcher.TimeOut = timeout + tr.TimeOut = timeout + tr.DelayStart = 2 + tr.Processes.Default.StartBefore(watcher) + tr.Processes.Default.Command = f'{sys.executable} entire_pool.py {ts.Disk.traffic_out.AbsPath} ET_NET 32 1' + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.All = "gold/schedule_on_entire_pool.gold" + tr.Processes.Default.Streams.All += Testers.ExcludesExpression('fail', 'should not contain "fail"') + urtest.execute() diff --git a/tests/uranium_tests/cont_schedule/test_schedule_on_pool.py b/tests/uranium_tests/cont_schedule/test_schedule_on_pool.py new file mode 100644 index 00000000000..31d7a8ebb5e --- /dev/null +++ b/tests/uranium_tests/cont_schedule/test_schedule_on_pool.py @@ -0,0 +1,70 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_schedule_on_pool(urtest: UraniumTest) -> None: + ''' + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import os + + urtest.Summary = 'Test TSContScheduleOnPool API' + urtest.ContinueOnFail = True + + # Define default ATS + ts = urtest.MakeATSProcess('ts') + + urtest.testName = 'Test TSContScheduleOnPool API' + + ts.Disk.records_config.update( + { + 'proxy.config.exec_thread.autoconfig.enabled': 0, + 'proxy.config.exec_thread.autoconfig.scale': 1.5, + 'proxy.config.exec_thread.limit': 32, + 'proxy.config.accept_threads': 1, + 'proxy.config.task_threads': 2, + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'TSContSchedule_test' + }) + + # Load plugin + urtest.PrepareTestPlugin(os.path.join(urtest.Variables.AtsTestPluginsDir, 'cont_schedule.so'), ts, 'pool') + + # www.example.com Host + tr = urtest.AddTestRun() + tr.Processes.Default.Command = 'printf "Test TSContScheduleOnPool API"' + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.StartBefore(ts) + ts.Disk.traffic_out.Content = "gold/schedule_on_pool.gold" + ts.Disk.traffic_out.Content += Testers.ExcludesExpression('fail', 'should not contain "fail"') + urtest.execute() diff --git a/tests/uranium_tests/cont_schedule/test_schedule_on_thread.py b/tests/uranium_tests/cont_schedule/test_schedule_on_thread.py new file mode 100644 index 00000000000..587d5234fbd --- /dev/null +++ b/tests/uranium_tests/cont_schedule/test_schedule_on_thread.py @@ -0,0 +1,70 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_schedule_on_thread(urtest: UraniumTest) -> None: + ''' + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import os + + urtest.Summary = 'Test TSContScheduleOnThread API' + urtest.ContinueOnFail = True + + # Define default ATS + ts = urtest.MakeATSProcess('ts') + + urtest.testName = 'Test TSContScheduleOnThread API' + + ts.Disk.records_config.update( + { + 'proxy.config.exec_thread.autoconfig.enabled': 0, + 'proxy.config.exec_thread.autoconfig.scale': 1.5, + 'proxy.config.exec_thread.limit': 32, + 'proxy.config.accept_threads': 1, + 'proxy.config.task_threads': 2, + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'TSContSchedule_test' + }) + + # Load plugin + urtest.PrepareTestPlugin(os.path.join(urtest.Variables.AtsTestPluginsDir, 'cont_schedule.so'), ts, 'thread') + + # www.example.com Host + tr = urtest.AddTestRun() + tr.Processes.Default.Command = 'printf "Test TSContScheduleOnThread API"' + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.StartBefore(ts) + ts.Disk.traffic_out.Content = "gold/schedule_on_thread.gold" + ts.Disk.traffic_out.Content += Testers.ExcludesExpression('fail', 'should not contain "fail"') + urtest.execute() diff --git a/tests/uranium_tests/cont_schedule/test_thread_affinity.py b/tests/uranium_tests/cont_schedule/test_thread_affinity.py new file mode 100644 index 00000000000..f794ecea322 --- /dev/null +++ b/tests/uranium_tests/cont_schedule/test_thread_affinity.py @@ -0,0 +1,70 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_thread_affinity(urtest: UraniumTest) -> None: + ''' + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import os + + urtest.Summary = 'Test TSContThreadAffinity APIs' + urtest.ContinueOnFail = True + + # Define default ATS + ts = urtest.MakeATSProcess('ts') + + urtest.testName = 'Test TSContThreadAffinity APIs' + + ts.Disk.records_config.update( + { + 'proxy.config.exec_thread.autoconfig.enabled': 0, + 'proxy.config.exec_thread.autoconfig.scale': 1.5, + 'proxy.config.exec_thread.limit': 32, + 'proxy.config.accept_threads': 1, + 'proxy.config.task_threads': 2, + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'TSContSchedule_test' + }) + + # Load plugin + urtest.PrepareTestPlugin(os.path.join(urtest.Variables.AtsTestPluginsDir, 'cont_schedule.so'), ts, 'affinity') + + # www.example.com Host + tr = urtest.AddTestRun() + tr.Processes.Default.Command = 'printf "Test TSContThreadAffinity API"' + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.StartBefore(ts) + ts.Disk.traffic_out.Content = "gold/thread_affinity.gold" + ts.Disk.traffic_out.Content += Testers.ExcludesExpression('fail', 'should not contain "fail"') + urtest.execute() diff --git a/tests/gold_tests/continuations/plugins/CMakeLists.txt b/tests/uranium_tests/continuations/plugins/CMakeLists.txt similarity index 93% rename from tests/gold_tests/continuations/plugins/CMakeLists.txt rename to tests/uranium_tests/continuations/plugins/CMakeLists.txt index 452b6c8e1a6..a3302d730c9 100644 --- a/tests/gold_tests/continuations/plugins/CMakeLists.txt +++ b/tests/uranium_tests/continuations/plugins/CMakeLists.txt @@ -15,4 +15,4 @@ # ####################### -add_autest_plugin(session_id_verify session_id_verify.cc) +add_urtest_plugin(session_id_verify session_id_verify.cc) diff --git a/tests/gold_tests/continuations/plugins/session_id_verify.cc b/tests/uranium_tests/continuations/plugins/session_id_verify.cc similarity index 100% rename from tests/gold_tests/continuations/plugins/session_id_verify.cc rename to tests/uranium_tests/continuations/plugins/session_id_verify.cc diff --git a/tests/uranium_tests/continuations/test_double.py b/tests/uranium_tests/continuations/test_double.py new file mode 100644 index 00000000000..38171891824 --- /dev/null +++ b/tests/uranium_tests/continuations/test_double.py @@ -0,0 +1,154 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_double(urtest: UraniumTest) -> None: + ''' + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import os + import subprocess + + urtest.Summary = ''' + Test transactions and sessions for http1, making sure the two continuations catch the same number of hooks. + ''' + + urtest.ContinueOnFail = True + # Define default ATS. Disable the cache to simplify the test. + ts = urtest.MakeATSProcess("ts", enable_cache=False) + server = urtest.MakeOriginServer("server") + + urtest.testName = "" + request_header = {"headers": "GET / HTTP/1.1\r\nHost: double_h2.test\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + # expected response from the origin server + response_header = { + "headers": "HTTP/1.1 200 OK\r\nConnection: close\r\nContent-Length:0\r\n\r\n", + "timestamp": "1469733493.993", + "body": "" + } + + # add response to the server dictionary + server.addResponse("sessionfile.log", request_header, response_header) + + # add port and remap rule + ts.Disk.remap_config.AddLine('map / http://127.0.0.1:{0}'.format(server.Variables.Port)) + + ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'continuations_verify.*', + }) + + # add plugin to assist with test metrics + urtest.PrepareTestPlugin(os.path.join(urtest.Variables.AtsTestPluginsDir, 'continuations_verify.so'), ts) + + comparator_command = ''' + if test "`traffic_ctl metric get continuations_verify.{0}.close.1 | cut -d ' ' -f 2`" -eq "`traffic_ctl metric get continuations_verify.{0}.close.2 | cut -d ' ' -f 2`" ; then\ + echo yes;\ + else \ + echo no; \ + fi; \ + traffic_ctl metric match continuations_verify + ''' + + cmd = '-vs http://127.0.0.1:{0}/'.format(ts.Variables.port) + numberOfRequests = 55 + + tr = urtest.AddTestRun() + + # Create a bunch of curl commands to be executed in parallel. Default.Process is set in SpawnCurlCommands. + # On Fedora 28/29, it seems that curl will occasionally timeout after a couple seconds and return exitcode 2 + # Examining the packet capture shows that Traffic Server dutifully sends the response + ps = tr.SpawnCurlCommands(cmdstr=cmd, count=numberOfRequests, retcode=Any(0, 2), ts=ts) + tr.Processes.Default.Env = ts.Env + tr.Processes.Default.ReturnCode = Any(0, 2) + + # Execution order is: ts/server, ps(curl cmds), Default Process. + tr.Processes.Default.StartBefore(server, ready=When.PortOpen(server.Variables.Port)) + tr.Processes.Default.StartBefore(urtest.Processes.ts) + ts.StartAfter(*ps) + server.StartAfter(*ps) + tr.StillRunningAfter = ts + + # Signal that all the curl processes have completed and poll for done metric + tr = urtest.AddTestRun("Curl Done") + tr.Processes.Default.Command = "traffic_ctl plugin msg done done" + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Env = ts.Env + tr.StillRunningAfter = ts + + def make_done_stat_ready(tsenv): + + def done_stat_ready(process, hasRunFor, **kw): + retval = subprocess.run( + "traffic_ctl metric get continuations_verify.test.done", + shell=True, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + env=tsenv) + return retval.returncode == 0 and b'1' in retval.stdout + + return done_stat_ready + + watcher = urtest.Processes.Process("watcher", "sleep 20") + + tr = urtest.AddTestRun("Wait for stats") + tr.Processes.Default.StartBefore(watcher, ready=make_done_stat_ready(ts.Env)) + tr.Processes.Default.Command = "traffic_ctl metric get continuations_verify.test.done" + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Env = ts.Env + + # number of sessions/transactions opened and closed are equal + tr = urtest.AddTestRun("Check Ssn") + tr.Processes.Default.Command = comparator_command.format('ssn') + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Env = ts.Env + tr.Processes.Default.Streams.stdout = Testers.ContainsExpression("yes", 'should verify contents') + # Session and Txn's should be non-zero + tr.Processes.Default.Streams.stdout += Testers.ExcludesExpression("continations_verify.ssn.close.1 0", 'should be nonzero') + tr.Processes.Default.Streams.stdout += Testers.ExcludesExpression("continations_verify.ssn.close.2 0", 'should be nonzero') + tr.Processes.Default.Streams.stdout += Testers.ExcludesExpression("continations_verify.txn.close.1 0", 'should be nonzero') + tr.Processes.Default.Streams.stdout += Testers.ExcludesExpression("continations_verify.txn.close.2 0", 'should be nonzero') + tr.Processes.Default.Streams.stdout += Testers.ContainsExpression( + "continuations_verify.txn.close.1 {}".format(numberOfRequests), 'should be the number of transactions we made') + tr.Processes.Default.Streams.stdout += Testers.ContainsExpression( + "continuations_verify.txn.close.2 {}".format(numberOfRequests), 'should be the number of transactions we made') + tr.StillRunningAfter = ts + + tr = urtest.AddTestRun("Check Txn") + tr.Processes.Default.Command = comparator_command.format('txn') + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Env = ts.Env + tr.Processes.Default.Streams.stdout = Testers.ContainsExpression("yes", 'should verify contents') + tr.StillRunningAfter = ts + urtest.execute() diff --git a/tests/uranium_tests/continuations/test_double_h2.py b/tests/uranium_tests/continuations/test_double_h2.py new file mode 100644 index 00000000000..6855d031df2 --- /dev/null +++ b/tests/uranium_tests/continuations/test_double_h2.py @@ -0,0 +1,174 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_double_h2(urtest: UraniumTest) -> None: + ''' + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import os + import subprocess + + urtest.Summary = ''' + Test transactions and sessions for http2, making sure the two continuations catch the same number of hooks. + ''' + urtest.SkipUnless(Condition.HasCurlFeature('http2')) + urtest.SkipIf(Condition.CurlUsingUnixDomainSocket()) + urtest.ContinueOnFail = True + # Define default ATS. Disable the cache to simplify the test. + ts = urtest.MakeATSProcess("ts", enable_tls=True, enable_cache=False) + server = urtest.MakeOriginServer("server") + server2 = urtest.MakeOriginServer("server2") + + urtest.testName = "" + request_header = {"headers": "GET / HTTP/1.1\r\nHost: double_h2.test\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + # expected response from the origin server + response_header = { + "headers": "HTTP/1.1 200 OK\r\nConnection: close\r\nContent-Length:0\r\n\r\n", + "timestamp": "1469733493.993", + "body": "" + } + + # add response to the server dictionary + server.addResponse("sessionfile.log", request_header, response_header) + + # add ssl materials like key, certificates for the server + ts.addDefaultSSLFiles() + + # add port and remap rule + ts.Disk.remap_config.AddLine('map / http://127.0.0.1:{0}'.format(server.Variables.Port)) + + ts.Disk.ssl_multicert_yaml.AddLines( + """ + ssl_multicert: + - dest_ip: "*" + ssl_cert_name: server.pem + ssl_key_name: server.key + """.split("\n")) + + ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'continuations_verify', + 'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir), + 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts.Variables.SSLDir), + 'proxy.config.cache.enable_read_while_writer': 0, + 'proxy.config.http2.max_concurrent_streams_in': 65535 + }) + + # add plugin to assist with test metrics + urtest.PrepareTestPlugin(os.path.join(urtest.Variables.AtsTestPluginsDir, 'continuations_verify.so'), ts) + + comparator_command = ''' + if test "`traffic_ctl metric get continuations_verify.{0}.close.1 | cut -d ' ' -f 2`" -eq "`traffic_ctl metric get continuations_verify.{0}.close.2 | cut -d ' ' -f 2`" ; then\ + echo yes;\ + else \ + echo no; \ + fi; \ + traffic_ctl metric match continuations_verify + ''' + + # curl with http2 + cmd = '--http2 -k -vs https://127.0.0.1:{0}/'.format(ts.Variables.ssl_port) + numberOfRequests = 25 + + tr = urtest.AddTestRun() + + # Create a bunch of curl commands to be executed in parallel. Default.Process is set in SpawnCurlCommands. + # On Fedora 28/29, it seems that curl will occasionally timeout after a couple seconds and return exitcode 2 + # Examining the packet capture shows that Traffic Server dutifully sends the response + ps = tr.SpawnCurlCommands(cmdstr=cmd, count=numberOfRequests, retcode=Any(0, 2), ts=ts) + tr.Processes.Default.Env = ts.Env + tr.Processes.Default.ReturnCode = Any(0, 2) + + # Execution order is: ts/server, ps(curl cmds), Default Process. + tr.Processes.Default.StartBefore(server, ready=When.PortOpen(server.Variables.Port)) + # Adds a delay once the ts port is ready. This is because we cannot test the ts state. + tr.Processes.Default.StartBefore(urtest.Processes.ts) + ts.StartAfter(*ps) + server.StartAfter(*ps) + tr.StillRunningAfter = ts + + # Signal that all the curl processes have completed + tr = urtest.AddTestRun("Curl Done") + tr.Processes.Default.Command = "traffic_ctl plugin msg done done" + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Env = ts.Env + tr.StillRunningAfter = ts + + # Parking this as a ready tester on a meaningless process + # To stall the test runs that check for the stats until the + # stats have propagated and are ready to read. + + def make_done_stat_ready(tsenv): + + def done_stat_ready(process, hasRunFor, **kw): + retval = subprocess.run( + "traffic_ctl metric get continuations_verify.test.done", + shell=True, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + env=tsenv) + return retval.returncode == 0 and b'1' in retval.stdout + + return done_stat_ready + + # number of sessions/transactions opened and closed are equal + tr = urtest.AddTestRun("Check Ssn") + server2.StartupTimeout = 60 + # Again, here the important thing is the ready function not the server2 process + tr.Processes.Default.StartBefore(server2, ready=make_done_stat_ready(ts.Env)) + tr.Processes.Default.Command = comparator_command.format('ssn') + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Env = ts.Env + tr.Processes.Default.Streams.stdout = Testers.ContainsExpression("yes", 'should verify contents') + # Session and Txn's should be non-zero + tr.Processes.Default.Streams.stdout += Testers.ExcludesExpression("continations_verify.ssn.close.1 0", 'should be nonzero') + tr.Processes.Default.Streams.stdout += Testers.ExcludesExpression("continations_verify.ssn.close.2 0", 'should be nonzero') + tr.Processes.Default.Streams.stdout += Testers.ExcludesExpression("continations_verify.txn.close.1 0", 'should be nonzero') + tr.Processes.Default.Streams.stdout += Testers.ExcludesExpression("continations_verify.txn.close.2 0", 'should be nonzero') + tr.Processes.Default.Streams.stdout += Testers.ContainsExpression( + "continuations_verify.txn.close.1 {}".format(numberOfRequests), 'should be the number of transactions we made') + tr.Processes.Default.Streams.stdout += Testers.ContainsExpression( + "continuations_verify.txn.close.2 {}".format(numberOfRequests), 'should be the number of transactions we made') + tr.StillRunningAfter = ts + tr.StillRunningAfter = server2 + + tr = urtest.AddTestRun("Check Txn") + tr.Processes.Default.Command = comparator_command.format('txn') + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Env = ts.Env + tr.Processes.Default.Streams.stdout = Testers.ContainsExpression("yes", 'should verify contents') + tr.StillRunningAfter = ts + tr.StillRunningAfter = server2 + urtest.execute() diff --git a/tests/uranium_tests/continuations/test_openclose.py b/tests/uranium_tests/continuations/test_openclose.py new file mode 100644 index 00000000000..56587bbf86f --- /dev/null +++ b/tests/uranium_tests/continuations/test_openclose.py @@ -0,0 +1,161 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_openclose(urtest: UraniumTest) -> None: + ''' + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import os + import subprocess + + urtest.Summary = ''' + Test transactions and sessions, making sure they open and close in the proper order. + ''' + + # Define default ATS. Disable the cache to simplify the test. + ts = urtest.MakeATSProcess("ts", enable_cache=False) + + server = urtest.MakeOriginServer("server") + server2 = urtest.MakeOriginServer("server2") + + urtest.testName = "" + request_header = {"headers": "GET / HTTP/1.1\r\nHost: oc.test\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + # expected response from the origin server + response_header = { + "headers": "HTTP/1.1 200 OK\r\nConnection: close\r\nContent-Length:0\r\n\r\n", + "timestamp": "1469733493.993", + "body": "" + } + + urtest.PrepareTestPlugin(os.path.join(urtest.Variables.AtsTestPluginsDir, 'ssntxnorder_verify.so'), ts) + + # add response to the server dictionary + server.addResponse("sessionfile.log", request_header, response_header) + ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 0, + 'proxy.config.diags.debug.tags': 'ssntxnorder_verify.*', + 'proxy.config.cache.enable_read_while_writer': 0 + }) + + ts.Disk.remap_config.AddLine('map http://oc.test:{0} http://127.0.0.1:{1}'.format(ts.Variables.port, server.Variables.Port)) + + # Add connection close to ensure that the client connection closes promptly after completing the transaction + cmd = '-H "Connection: close" -vs -H "host:oc.test" http://127.0.0.1:{0}'.format(ts.Variables.port) + numberOfRequests = 100 + + tr = urtest.AddTestRun() + # Create a bunch of curl commands to be executed in parallel. Default.Process is set in SpawnCurlCommands. + # On Fedora 28/29, it seems that curl will occasionally timeout after a couple seconds and return exitcode 2 + # Examining the packet capture shows that Traffic Server dutifully sends the response + ps = tr.SpawnCurlCommands(cmdstr=cmd, count=numberOfRequests, retcode=Any(0, 2), ts=ts) + tr.Processes.Default.Env = ts.Env + tr.Processes.Default.ReturnCode = Any(0, 2) + + # Execution order is: ts/server, ps(curl cmds), Default Process. + tr.Processes.Default.StartBefore(server, ready=When.PortOpen(server.Variables.Port)) + tr.Processes.Default.StartBefore(urtest.Processes.ts) + ts.StartAfter(*ps) + server.StartAfter(*ps) + tr.StillRunningAfter = ts + + # Signal that all the curl processes have completed + tr = urtest.AddTestRun("Curl Done") + tr.DelayStart = 2 # Delaying a couple seconds to make sure the global continuation's lock contention resolves. + tr.Processes.Default.Command = "traffic_ctl plugin msg done done" + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Env = ts.Env + tr.StillRunningAfter = ts + + # Parking this as a ready tester on a meaningless process + # To stall the test runs that check for the stats until the + # stats have propagated and are ready to read. + + def make_done_stat_ready(tsenv): + + def done_stat_ready(process, hasRunFor, **kw): + retval = subprocess.run( + "traffic_ctl metric get ssntxnorder_verify.test.done", + shell=True, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + env=tsenv) + return retval.returncode == 0 and b'1' in retval.stdout + + return done_stat_ready + + # number of sessions/transactions opened and closed are equal + tr = urtest.AddTestRun("Check Ssn order errors") + server2.StartupTimeout = 60 + # Again, here the important thing is the ready function not the server2 process + tr.Processes.Default.StartBefore(server2, ready=make_done_stat_ready(ts.Env)) + tr.Processes.Default.Command = 'traffic_ctl metric get ssntxnorder_verify.err' + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Env = ts.Env + tr.Processes.Default.Streams.All = Testers.ContainsExpression( + 'ssntxnorder_verify.err 0', 'incorrect statistic return, or possible error.') + tr.StillRunningAfter = ts + tr.StillRunningAfter = server + + comparator_command = ''' + if test "`traffic_ctl metric get ssntxnorder_verify.{0}.start | cut -d ' ' -f 2`" -eq "`traffic_ctl metric get ssntxnorder_verify.{0}.close | cut -d ' ' -f 2`" ; then\ + echo yes;\ + else \ + echo no; \ + fi; \ + traffic_ctl metric match ssntxnorder_verify + ''' + + # number of sessions/transactions opened and closed are equal + tr = urtest.AddTestRun("Check for ssn open/close") + tr.Processes.Default.Command = comparator_command.format('ssn') + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Env = ts.Env + tr.Processes.Default.Streams.stdout = Testers.ContainsExpression("yes", 'should verify contents') + tr.Processes.Default.Streams.stdout += Testers.ExcludesExpression("ssntxnorder_verify.ssn.start 0", 'should be nonzero') + tr.StillRunningAfter = ts + tr.StillRunningAfter = server + + tr = urtest.AddTestRun("Check for txn/open/close") + tr.Processes.Default.Command = comparator_command.format('txn') + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Env = ts.Env + tr.Processes.Default.Streams.stdout = Testers.ContainsExpression("yes", 'should verify contents') + tr.Processes.Default.Streams.stdout += Testers.ExcludesExpression("ssntxnorder_verify.txn.start 0", 'should be nonzero') + # and we receive the same number of transactions as we asked it to make + tr.Processes.Default.Streams.stdout += Testers.ContainsExpression( + "ssntxnorder_verify.txn.start {}".format(numberOfRequests), 'should be the number of transactions we made') + tr.StillRunningAfter = ts + tr.StillRunningAfter = server + urtest.execute() diff --git a/tests/uranium_tests/continuations/test_openclose_h2.py b/tests/uranium_tests/continuations/test_openclose_h2.py new file mode 100644 index 00000000000..ed232774436 --- /dev/null +++ b/tests/uranium_tests/continuations/test_openclose_h2.py @@ -0,0 +1,178 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_openclose_h2(urtest: UraniumTest) -> None: + ''' + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import os + import subprocess + + urtest.Summary = ''' + Test transactions and sessions over http2, making sure they open and close in the proper order. + ''' + + urtest.SkipUnless(Condition.HasCurlFeature('http2')) + urtest.SkipIf(Condition.CurlUsingUnixDomainSocket()) + + # Define default ATS. Disable the cache to simplify the test. + ts = urtest.MakeATSProcess("ts", enable_tls=True, enable_cache=False) + + server = urtest.MakeOriginServer("server") + server2 = urtest.MakeOriginServer("server2") + + urtest.testName = "" + request_header = {"headers": "GET / HTTP/1.1\r\nHost: oc.test\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + # expected response from the origin server + response_header = { + "headers": "HTTP/1.1 200 OK\r\nConnection: close\r\nContent-Length:0\r\n\r\n", + "timestamp": "1469733493.993", + "body": "" + } + + # add ssl materials like key, certificates for the server + ts.addDefaultSSLFiles() + + urtest.PrepareTestPlugin(os.path.join(urtest.Variables.AtsTestPluginsDir, 'ssntxnorder_verify.so'), ts) + + # add response to the server dictionary + server.addResponse("sessionfile.log", request_header, response_header) + ts.Disk.records_config.update( + { + 'proxy.config.http2.zombie_debug_timeout_in': 10, + 'proxy.config.diags.debug.enabled': 0, + 'proxy.config.diags.debug.tags': 'ssntxnorder_verify', + 'proxy.config.cache.enable_read_while_writer': 0, + 'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir), + 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts.Variables.SSLDir), + }) + + ts.Disk.remap_config.AddLine( + 'map https://oc.test:{0} http://127.0.0.1:{1}'.format(ts.Variables.ssl_port, server.Variables.Port)) + ts.Disk.ssl_multicert_yaml.AddLines( + """ + ssl_multicert: + - dest_ip: "*" + ssl_cert_name: server.pem + ssl_key_name: server.key + """.split("\n")) + + cmd = '-k --resolve oc.test:{0}:127.0.0.1 --http2 https://oc.test:{0}'.format(ts.Variables.ssl_port) + numberOfRequests = 100 + + tr = urtest.AddTestRun() + # Create a bunch of curl commands to be executed in parallel. Default.Process is set in SpawnCurlCommands. + # On Fedora 28/29, it seems that curl will occasionally timeout after a couple seconds and return exitcode 2 + # Examining the packet capture shows that Traffic Server dutifully sends the response + ps = tr.SpawnCurlCommands(cmdstr=cmd, count=numberOfRequests, retcode=Any(0, 2), ts=ts) + tr.Processes.Default.Env = ts.Env + tr.Processes.Default.ReturnCode = Any(0, 2) + + # Execution order is: ts/server, ps(curl cmds), Default Process. + tr.Processes.Default.StartBefore(server, ready=When.PortOpen(server.Variables.Port)) + tr.Processes.Default.StartBefore(urtest.Processes.ts) + # Don't know why we need both the start before and the start after + ts.StartAfter(*ps) + server.StartAfter(*ps) + tr.StillRunningAfter = ts + + # Signal that all the curl processes have completed + tr = urtest.AddTestRun("Curl Done") + tr.DelayStart = 2 # Delaying a couple seconds to make sure the global continuation's lock contention resolves. + tr.Processes.Default.Command = "traffic_ctl plugin msg done done" + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Env = ts.Env + tr.StillRunningAfter = ts + + # Parking this as a ready tester on a meaningless process + # To stall the test runs that check for the stats until the + # stats have propagated and are ready to read. + + def make_done_stat_ready(tsenv): + + def done_stat_ready(process, hasRunFor, **kw): + retval = subprocess.run( + "traffic_ctl metric get ssntxnorder_verify.test.done", + shell=True, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + env=tsenv) + return retval.returncode == 0 and b'1' in retval.stdout + + return done_stat_ready + + # number of sessions/transactions opened and closed are equal + tr = urtest.AddTestRun("Check Ssn order errors") + server2.StartupTimeout = 60 + # Again, here the important thing is the ready function not the server2 process + tr.Processes.Default.StartBefore(server2, ready=make_done_stat_ready(ts.Env)) + tr.Processes.Default.Command = 'traffic_ctl metric get ssntxnorder_verify.err' + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Env = ts.Env + tr.Processes.Default.Streams.All = Testers.ContainsExpression( + 'ssntxnorder_verify.err 0', 'incorrect statistic return, or possible error.') + tr.StillRunningAfter = ts + tr.StillRunningAfter = server + + comparator_command = ''' + if test "`traffic_ctl metric get ssntxnorder_verify.{0}.start | cut -d ' ' -f 2`" -eq "`traffic_ctl metric get ssntxnorder_verify.{0}.close | cut -d ' ' -f 2`" ; then\ + echo yes;\ + else \ + echo no; \ + fi; \ + traffic_ctl metric match ssntxnorder_verify + ''' + + # number of sessions/transactions opened and closed are equal + tr = urtest.AddTestRun("Check for ssn open/close") + tr.Processes.Default.Command = comparator_command.format('ssn') + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Env = ts.Env + tr.Processes.Default.Streams.stdout = Testers.ContainsExpression("yes", 'should verify contents') + tr.Processes.Default.Streams.stdout += Testers.ExcludesExpression("ssntxnorder_verify.ssn.start 0", 'should be nonzero') + tr.StillRunningAfter = ts + tr.StillRunningAfter = server + + tr = urtest.AddTestRun("Check for txn/open/close") + tr.Processes.Default.Command = comparator_command.format('txn') + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Env = ts.Env + tr.Processes.Default.Streams.stdout = Testers.ContainsExpression("yes", 'should verify contents') + tr.Processes.Default.Streams.stdout += Testers.ExcludesExpression("ssntxnorder_verify.txn.start 0", 'should be nonzero') + # and we receive the same number of transactions as we asked it to make + tr.Processes.Default.Streams.stdout += Testers.ContainsExpression( + "ssntxnorder_verify.txn.start {}".format(numberOfRequests), 'should be the number of transactions we made') + tr.StillRunningAfter = ts + tr.StillRunningAfter = server + urtest.execute() diff --git a/tests/uranium_tests/continuations/test_session_id.py b/tests/uranium_tests/continuations/test_session_id.py new file mode 100644 index 00000000000..8c36c8458dd --- /dev/null +++ b/tests/uranium_tests/continuations/test_session_id.py @@ -0,0 +1,135 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_session_id(urtest: UraniumTest) -> None: + ''' + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import os + + urtest.Summary = ''' + Verify session ID properties. + ''' + + urtest.SkipUnless(Condition.HasCurlFeature('http2')) + + # Configure the server. + server = urtest.MakeOriginServer("server") + request_header = {"headers": "GET / HTTP/1.1\r\nHost: example.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + response_header = { + "headers": "HTTP/1.1 200 OK\r\nConnection: close\r\nContent-Length:0\r\n\r\n", + "timestamp": "1469733493.993", + "body": "" + } + server.addResponse("sessionfile.log", request_header, response_header) + + # Configure ATS. Disable the cache to simplify the test. + ts = urtest.MakeATSProcess("ts", enable_tls=True, enable_cache=False) + + ts.addDefaultSSLFiles() + + urtest.PrepareTestPlugin( + os.path.join(urtest.Variables.AtsBuildUraniumTestsDir, 'continuations', 'plugins', '.libs', 'session_id_verify.so'), ts) + + ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'session_id_verify', + 'proxy.config.cache.enable_read_while_writer': 0, + 'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir), + 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts.Variables.SSLDir), + }) + + ts.Disk.remap_config.AddLine('map / http://127.0.0.1:{0}'.format(server.Variables.Port)) + + ts.Disk.ssl_multicert_yaml.AddLines( + """ + ssl_multicert: + - dest_ip: "*" + ssl_cert_name: server.pem + ssl_key_name: server.key + """.split("\n")) + + # + # Run some HTTP/1 traffic. + # + tr = urtest.AddTestRun("Perform HTTP/1 transactions") + cmd = '-v -H "host:example.com" http://127.0.0.1:{0}'.format(ts.Variables.port) + numberOfRequests = 100 + # Create a bunch of curl commands to be executed in parallel. Default.Process + # is set in SpawnCurlCommands. On Fedora 28/29, it seems that curl will + # occasionally timeout after a couple seconds and return exitcode 2 + # Examining the packet capture shows that Traffic Server dutifully sends the response + ps = tr.SpawnCurlCommands(cmdstr=cmd, count=numberOfRequests, retcode=Any(0, 2), ts=ts) + tr.Processes.Default.Env = ts.Env + tr.Processes.Default.ReturnCode = Any(0, 2) + tr.Processes.Default.StartBefore(server, ready=When.PortOpen(server.Variables.Port)) + tr.Processes.Default.StartBefore(urtest.Processes.ts) + ts.StartAfter(*ps) + server.StartAfter(*ps) + + tr.StillRunningAfter = ts + tr.StillRunningAfter = server + + # + # Run some HTTP/2 traffic. + # + if not Condition.CurlUsingUnixDomainSocket(): + tr = urtest.AddTestRun("Perform HTTP/2 transactions") + cmd = '-v -k --http2 -H "host:example.com" https://127.0.0.1:{0}'.format(ts.Variables.ssl_port) + ps = tr.SpawnCurlCommands(cmdstr=cmd, count=numberOfRequests, retcode=Any(0, 2), ts=ts) + tr.Processes.Default.Env = ts.Env + tr.Processes.Default.ReturnCode = Any(0, 2) + + # + # Verify that the session ids are unique. + # + + # AuTest already searches for errors in diags.log and fails if it encounters + # them. The test plugin prints an error to this log if it sees duplicate ids. + # The following is to verify that we encountered the expected ids. + + def verify_session_count(output): + nonlocal numberOfRequests + if Condition.CurlUsingUnixDomainSocket(): + nReq = numberOfRequests + else: + nReq = numberOfRequests * 2 + session_ids = [line[0:line.find("\n")] for line in str(output).split("session id: ")[1:]] + if len(session_ids) != nReq: + return "Found {} session_id's, expected {}".format(len(session_ids), nReq) + return "" + + ts.Disk.traffic_out.Content += Testers.FileContentCallback(verify_session_count, 'verify_session_count') + urtest.execute() diff --git a/tests/gold_tests/cripts/cripts_bundle.test.yaml b/tests/uranium_tests/cripts/cripts_bundle.test.yaml similarity index 99% rename from tests/gold_tests/cripts/cripts_bundle.test.yaml rename to tests/uranium_tests/cripts/cripts_bundle.test.yaml index 72c3dfef3af..841e1630291 100644 --- a/tests/gold_tests/cripts/cripts_bundle.test.yaml +++ b/tests/uranium_tests/cripts/cripts_bundle.test.yaml @@ -17,7 +17,7 @@ meta: version: "1.0" -# Configuration for autest integration. This replay file is the harness for +# Configuration for the Uranium test runner. This replay file is the harness for # a bundle of cripts tests — bundle_headers.cript exercising cripts::Bundle::Headers # and direct_headers.cript exercising the equivalent direct Cripts API, with room # to add more cripts and more transactions under the same driver. Multiple parallel @@ -25,7 +25,7 @@ meta: # exposure surface; under a TSAN build this is the regression target for # any future bug that puts shared mutable state back into a Cripts plugin # instance. -autest: +urtest: description: 'cripts: bundle of feature tests (bundle_headers.cript: every Bundle::Headers dynamic substitution across all four hooks; direct_headers.cript: the equivalent direct Cripts API path)' requires: ats_features: [TS_HAS_CRIPTS] diff --git a/tests/gold_tests/cripts/files/basic.cript b/tests/uranium_tests/cripts/files/basic.cript similarity index 100% rename from tests/gold_tests/cripts/files/basic.cript rename to tests/uranium_tests/cripts/files/basic.cript diff --git a/tests/gold_tests/cripts/files/bundle_headers.cript b/tests/uranium_tests/cripts/files/bundle_headers.cript similarity index 100% rename from tests/gold_tests/cripts/files/bundle_headers.cript rename to tests/uranium_tests/cripts/files/bundle_headers.cript diff --git a/tests/gold_tests/cripts/files/direct_headers.cript b/tests/uranium_tests/cripts/files/direct_headers.cript similarity index 100% rename from tests/gold_tests/cripts/files/direct_headers.cript rename to tests/uranium_tests/cripts/files/direct_headers.cript diff --git a/tests/gold_tests/cripts/files/query_copy.cript b/tests/uranium_tests/cripts/files/query_copy.cript similarity index 100% rename from tests/gold_tests/cripts/files/query_copy.cript rename to tests/uranium_tests/cripts/files/query_copy.cript diff --git a/tests/gold_tests/cripts/files/server_bundle_headers.cript b/tests/uranium_tests/cripts/files/server_bundle_headers.cript similarity index 100% rename from tests/gold_tests/cripts/files/server_bundle_headers.cript rename to tests/uranium_tests/cripts/files/server_bundle_headers.cript diff --git a/tests/gold_tests/cripts/gold/basic_cript.gold b/tests/uranium_tests/cripts/gold/basic_cript.gold similarity index 100% rename from tests/gold_tests/cripts/gold/basic_cript.gold rename to tests/uranium_tests/cripts/gold/basic_cript.gold diff --git a/tests/gold_tests/cripts/gold/certs_cript.gold b/tests/uranium_tests/cripts/gold/certs_cript.gold similarity index 100% rename from tests/gold_tests/cripts/gold/certs_cript.gold rename to tests/uranium_tests/cripts/gold/certs_cript.gold diff --git a/tests/uranium_tests/cripts/test_cripts.py b/tests/uranium_tests/cripts/test_cripts.py new file mode 100644 index 00000000000..45d32d24b2c --- /dev/null +++ b/tests/uranium_tests/cripts/test_cripts.py @@ -0,0 +1,165 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_cripts(urtest: UraniumTest) -> None: + ''' + Test basic cripts functionality + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import os + + # Needed if we want to use sed -i '' on macOS, but autest doesn't like that ... + # import platform + + urtest.testName = "cripts: basic functions" + urtest.Summary = ''' + Simple cripts test that sets a response header back to the client + ''' + urtest.ContinueOnFail = True + + class CriptsBasicTest: + + def __init__(self): + self.setUpOriginServer() + self.setUpTS() + + def setUpOriginServer(self): + self.server = urtest.MakeOriginServer("server") + + request_header = { + "headers": "GET / HTTP/1.1\r\nHost: www.example.com\r\n\r\n", + "timestamp": "1469733493.993", + "body": "" + } + response_header = { + "headers": "HTTP/1.1 200 OK\r\nresponseHeader: unchanged\r\nContent-Length: 0\r\nConnection: close\r\n\r\n", + "timestamp": "1469733493.993", + "body": "" + } + self.server.addResponse("sessionfile.log", request_header, response_header) + + query_request_header = { + "headers": "GET /path/to/resource?foo=1&bar=2&baz=3 HTTP/1.1\r\nHost: does.not.matter\r\n\r\n", + "timestamp": "1469733493.993", + "body": "" + } + query_response_header = { + "headers": "HTTP/1.1 200 OK\r\nContent-Length: 0\r\nConnection: close\r\n\r\n", + "timestamp": "1469733493.993", + "body": "" + } + self.server.addResponse("sessionfile.log", query_request_header, query_response_header) + + def setUpTS(self): + self.ts = urtest.MakeATSProcess("ts_in", enable_tls=True, enable_cache=False, enable_cripts=True) + + self.ts.addDefaultSSLFiles() + self.ts.Disk.ssl_multicert_yaml.AddLines( + """ + ssl_multicert: + - dest_ip: "*" + ssl_cert_name: server.pem + ssl_key_name: server.key + """.split("\n")) + + self.ts.Setup.Copy('files/basic.cript', self.ts.Variables.CONFIGDIR) + self.ts.Setup.Copy('files/query_copy.cript', self.ts.Variables.CONFIGDIR) + + self.ts.Disk.records_config.update( + { + 'proxy.config.plugin.dynamic_reload_mode': 1, + "proxy.config.ssl.server.cert.path": f"{self.ts.Variables.SSLDir}", + "proxy.config.ssl.server.private_key.path": f"{self.ts.Variables.SSLDir}", + }) + + self.ts.Disk.remap_config.AddLine( + f'map http://www.example.com http://127.0.0.1:{self.server.Variables.Port} @plugin=basic.cript') + self.ts.Disk.remap_config.AddLine( + f'map https://www.example.com:{self.ts.Variables.ssl_port} http://127.0.0.1:{self.server.Variables.Port} @plugin=basic.cript' + ) + self.ts.Disk.remap_config.AddLine( + f'map http://query.example.com http://127.0.0.1:{self.server.Variables.Port} @plugin=query_copy.cript') + + def runHeaderTest(self): + tr = urtest.AddTestRun('Exercise traffic through cripts.') + tr.MakeCurlCommand(f'-v -H "Host: www.example.com" http://127.0.0.1:{self.ts.Variables.port}', ts=self.ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.StartBefore(self.server, ready=When.PortOpen(self.server.Variables.Port)) + tr.Processes.Default.StartBefore(self.ts) + tr.Processes.Default.Streams.stderr = "gold/basic_cript.gold" + tr.StillRunningAfter = self.server + + def runCertsTest(self): + tr = urtest.AddTestRun('Exercise Cripts certificate introspection.') + tr.MakeCurlCommand( + f'-v --http1.1 -k -H "Host: www.example.com:{self.ts.Variables.ssl_port}" https://127.0.0.1:{self.ts.Variables.ssl_port}', + ts=self.ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.stderr = "gold/certs_cript.gold" + tr.StillRunningAfter = self.server + + def runQueryCopyTest(self): + tr = urtest.AddTestRun('Exercise Query/Path copy ctor / copy-assign in cripts.') + tr.MakeCurlCommand( + f'-v -H "Host: query.example.com" "http://127.0.0.1:{self.ts.Variables.port}/path/to/resource?foo=1&bar=2&baz=3"', + ts=self.ts) + tr.Processes.Default.ReturnCode = 0 + # Live URL is untouched: original query is preserved. + tr.Processes.Default.Streams.stderr = Testers.ContainsExpression( + r"X-Original-Query: foo=1&bar=2&baz=3", "Original query must round-trip unchanged") + # Mutating the copy (Erase "foo") must not have leaked into the original. + tr.Processes.Default.Streams.stderr += Testers.ContainsExpression( + r"X-Copy-Query: bar=2&baz=3", "Erase on the copy must drop foo without affecting the original") + # Copy-assignment path produces a snapshot equal to the original. + tr.Processes.Default.Streams.stderr += Testers.ContainsExpression( + r"X-Assigned-Query: foo=1&bar=2&baz=3", "Copy-assigned query must equal the original") + # Live URL path is untouched: original path is preserved. + tr.Processes.Default.Streams.stderr += Testers.ContainsExpression( + r"X-Original-Path: path/to/resource", "Original path must round-trip unchanged") + # Mutating the copy (segment[0] = "edited") must not have leaked into the original. + tr.Processes.Default.Streams.stderr += Testers.ContainsExpression( + r"X-Copy-Path: edited/to/resource", "Segment edit on the copy must not affect the original") + # Copy-assignment path produces a snapshot equal to the original. + tr.Processes.Default.Streams.stderr += Testers.ContainsExpression( + r"X-Assigned-Path: path/to/resource", "Copy-assigned path must equal the original") + tr.StillRunningAfter = self.server + + def run(self): + self.runHeaderTest() + self.runQueryCopyTest() + if not Condition.CurlUsingUnixDomainSocket(): + self.runCertsTest() + + CriptsBasicTest().run() + urtest.execute() diff --git a/tests/gold_tests/dns/gold/connect_attempts_rr_max_retries_error_log.gold b/tests/uranium_tests/dns/gold/connect_attempts_rr_max_retries_error_log.gold similarity index 100% rename from tests/gold_tests/dns/gold/connect_attempts_rr_max_retries_error_log.gold rename to tests/uranium_tests/dns/gold/connect_attempts_rr_max_retries_error_log.gold diff --git a/tests/gold_tests/dns/gold/connect_attempts_rr_no_error_log.gold b/tests/uranium_tests/dns/gold/connect_attempts_rr_no_error_log.gold similarity index 100% rename from tests/gold_tests/dns/gold/connect_attempts_rr_no_error_log.gold rename to tests/uranium_tests/dns/gold/connect_attempts_rr_no_error_log.gold diff --git a/tests/gold_tests/dns/gold/connect_attempts_rr_retries_error_log.gold b/tests/uranium_tests/dns/gold/connect_attempts_rr_retries_error_log.gold similarity index 100% rename from tests/gold_tests/dns/gold/connect_attempts_rr_retries_error_log.gold rename to tests/uranium_tests/dns/gold/connect_attempts_rr_retries_error_log.gold diff --git a/tests/gold_tests/dns/gold/connect_attempts_single_max_retries_error_log.gold b/tests/uranium_tests/dns/gold/connect_attempts_single_max_retries_error_log.gold similarity index 100% rename from tests/gold_tests/dns/gold/connect_attempts_single_max_retries_error_log.gold rename to tests/uranium_tests/dns/gold/connect_attempts_single_max_retries_error_log.gold diff --git a/tests/gold_tests/dns/gold/test_case_0_stderr.gold b/tests/uranium_tests/dns/gold/test_case_0_stderr.gold similarity index 100% rename from tests/gold_tests/dns/gold/test_case_0_stderr.gold rename to tests/uranium_tests/dns/gold/test_case_0_stderr.gold diff --git a/tests/gold_tests/dns/gold/test_case_1_stderr.gold b/tests/uranium_tests/dns/gold/test_case_1_stderr.gold similarity index 100% rename from tests/gold_tests/dns/gold/test_case_1_stderr.gold rename to tests/uranium_tests/dns/gold/test_case_1_stderr.gold diff --git a/tests/gold_tests/dns/hosts_file b/tests/uranium_tests/dns/hosts_file similarity index 100% rename from tests/gold_tests/dns/hosts_file rename to tests/uranium_tests/dns/hosts_file diff --git a/tests/gold_tests/dns/replay/connect_attempts_rr_max_retries.test.yaml b/tests/uranium_tests/dns/replay/connect_attempts_rr_max_retries.test.yaml similarity index 98% rename from tests/gold_tests/dns/replay/connect_attempts_rr_max_retries.test.yaml rename to tests/uranium_tests/dns/replay/connect_attempts_rr_max_retries.test.yaml index 834c60fb912..a4a248bdbb5 100644 --- a/tests/gold_tests/dns/replay/connect_attempts_rr_max_retries.test.yaml +++ b/tests/uranium_tests/dns/replay/connect_attempts_rr_max_retries.test.yaml @@ -17,8 +17,8 @@ meta: version: "1.0" -# Configuration section for autest integration -autest: +# Configuration for the Uranium test runner +urtest: description: 'Verify connect attempts behavior - round robin' dns: diff --git a/tests/gold_tests/dns/replay/connect_attempts_rr_no_retry.test.yaml b/tests/uranium_tests/dns/replay/connect_attempts_rr_no_retry.test.yaml similarity index 98% rename from tests/gold_tests/dns/replay/connect_attempts_rr_no_retry.test.yaml rename to tests/uranium_tests/dns/replay/connect_attempts_rr_no_retry.test.yaml index 258653fbb8c..45843520c5a 100644 --- a/tests/gold_tests/dns/replay/connect_attempts_rr_no_retry.test.yaml +++ b/tests/uranium_tests/dns/replay/connect_attempts_rr_no_retry.test.yaml @@ -17,8 +17,8 @@ meta: version: "1.0" -# Configuration section for autest integration -autest: +# Configuration for the Uranium test runner +urtest: description: 'Verify connect attempts behavior - no retry' dns: diff --git a/tests/gold_tests/dns/replay/connect_attempts_rr_retries.test.yaml b/tests/uranium_tests/dns/replay/connect_attempts_rr_retries.test.yaml similarity index 98% rename from tests/gold_tests/dns/replay/connect_attempts_rr_retries.test.yaml rename to tests/uranium_tests/dns/replay/connect_attempts_rr_retries.test.yaml index 971e623ff33..83c8d09411d 100644 --- a/tests/gold_tests/dns/replay/connect_attempts_rr_retries.test.yaml +++ b/tests/uranium_tests/dns/replay/connect_attempts_rr_retries.test.yaml @@ -17,8 +17,8 @@ meta: version: "1.0" -# Configuration section for autest integration -autest: +# Configuration for the Uranium test runner +urtest: description: 'Verify connect attempts behavior - round robin' dns: diff --git a/tests/gold_tests/dns/replay/connect_attempts_single_max_retries.test.yaml b/tests/uranium_tests/dns/replay/connect_attempts_single_max_retries.test.yaml similarity index 98% rename from tests/gold_tests/dns/replay/connect_attempts_single_max_retries.test.yaml rename to tests/uranium_tests/dns/replay/connect_attempts_single_max_retries.test.yaml index 85285c551a1..46ec9a0378d 100644 --- a/tests/gold_tests/dns/replay/connect_attempts_single_max_retries.test.yaml +++ b/tests/uranium_tests/dns/replay/connect_attempts_single_max_retries.test.yaml @@ -17,8 +17,8 @@ meta: version: "1.0" -# Configuration section for autest integration -autest: +# Configuration for the Uranium test runner +urtest: description: 'Verify connect attempts behavior - no round-robin (single host)' dns: diff --git a/tests/gold_tests/dns/replay/multiple_host_requests.replay.yaml b/tests/uranium_tests/dns/replay/multiple_host_requests.replay.yaml similarity index 100% rename from tests/gold_tests/dns/replay/multiple_host_requests.replay.yaml rename to tests/uranium_tests/dns/replay/multiple_host_requests.replay.yaml diff --git a/tests/gold_tests/dns/replay/reverse_lookup.replay.yaml b/tests/uranium_tests/dns/replay/reverse_lookup.replay.yaml similarity index 100% rename from tests/gold_tests/dns/replay/reverse_lookup.replay.yaml rename to tests/uranium_tests/dns/replay/reverse_lookup.replay.yaml diff --git a/tests/gold_tests/dns/replay/server_down.replay.yaml b/tests/uranium_tests/dns/replay/server_down.replay.yaml similarity index 100% rename from tests/gold_tests/dns/replay/server_down.replay.yaml rename to tests/uranium_tests/dns/replay/server_down.replay.yaml diff --git a/tests/gold_tests/dns/replay/server_error.replay.yaml b/tests/uranium_tests/dns/replay/server_error.replay.yaml similarity index 100% rename from tests/gold_tests/dns/replay/server_error.replay.yaml rename to tests/uranium_tests/dns/replay/server_error.replay.yaml diff --git a/tests/gold_tests/dns/replay/single_transaction.replay.yaml b/tests/uranium_tests/dns/replay/single_transaction.replay.yaml similarity index 100% rename from tests/gold_tests/dns/replay/single_transaction.replay.yaml rename to tests/uranium_tests/dns/replay/single_transaction.replay.yaml diff --git a/tests/uranium_tests/dns/test_dns_down_nameserver.py b/tests/uranium_tests/dns/test_dns_down_nameserver.py new file mode 100644 index 00000000000..739c5001880 --- /dev/null +++ b/tests/uranium_tests/dns/test_dns_down_nameserver.py @@ -0,0 +1,206 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_dns_down_nameserver(urtest: UraniumTest) -> None: + ''' + Verify ATS handles down name servers correctly. + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + from ports import get_port + + # This value tracks DNS_PRIMARY_RETRY_PERIOD in P_DNSProcessor.h. + DNS_PRIMARY_RETRY_PERIOD = 5 + + urtest.Summary = ''' + Verify ATS handles down name servers correctly. + ''' + + class DownDNSNameserverTest: + """Encapsulate logic for testing down name server functionality. + + Note that this test verifies that ATS behaves correctly with respect to + down *name* servers. This is different than testing down origin servers, + which is an entirely different feature and implementation. + """ + + _replay_file = "replay/multiple_host_requests.replay.yaml" + + def __init__(self): + """Initialize the Test processes for the test runs.""" + self._dns_port = None + self._configure_origin_server() + self._configure_traffic_server() + self._servers_are_running = False + + self._client_counter = 0 + self._dns_counter = 0 + + def _configure_origin_server(self): + """Configure the origin server and the DNS port.""" + self._server = urtest.MakeVerifierServerProcess("server", self._replay_file) + + # Associate the DNS port with the long-running server. This way + # ATS can be configured to use it across the multiple test runs + # and DNS servers. + self._dns_port = get_port(self._server, "DNSPort") + + def _configure_traffic_server(self): + """Configure Traffic Server.""" + if not self._dns_port: + raise RuntimeError("The DNS port was not configured.") + + # Since we are testing DNS name resolution and whether these + # transactions reach the origin, disable the caching to ensure none of + # the responses come out of the cache without going to the origin. + self._ts = urtest.MakeATSProcess("ts", enable_cache=False) + + self._ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'hostdb|dns', + 'proxy.config.dns.nameservers': f'127.0.0.1:{self._dns_port}', + 'proxy.config.dns.resolv_conf': 'NULL' + }) + + # Cause a name resolution for each, unique path. + self._ts.Disk.remap_config.AddLines( + [ + f'map /first/host http://first.host.com:{self._server.Variables.http_port}/', + f'map /second/host http://second.host.com:{self._server.Variables.http_port}/', + f'map /third/host http://third.host.com:{self._server.Variables.http_port}/', + ]) + + def _run_transaction(self, start_dns: bool, keyname: str): + """Run a transaction with the name server reachable. + + :param start_dns: Whether the TestRun should configure a name server to + be running for ATS to talk to. + + :param keyname: The identifier for the transaction to run. + """ + tr = urtest.AddTestRun() + + tr.AddVerifierClientProcess( + f'client{self._client_counter}', + self._replay_file, + http_ports=[self._ts.Variables.port], + other_args=f'--keys {keyname}') + self._client_counter += 1 + + if start_dns: + dns = tr.MakeDNServer(f'dns{self._dns_counter}', default='127.0.0.1', port=self._dns_port) + self._dns_counter += 1 + tr.Processes.Default.StartBefore(dns) + + if not self._servers_are_running: + tr.Processes.Default.StartBefore(self._server) + tr.Processes.Default.StartBefore(self._ts) + self._servers_are_running = True + + # Verify that the client tried to send the transaction. + tr.Processes.Default.Streams.All += Testers.ContainsExpression( + f'uuid: {keyname}', f'The client should have sent a transaction with uuid {keyname}') + + # The client will report an error if ATS could not complete the + # transaction due to DNS resolution issues. + if not start_dns: + tr.Processes.Default.ReturnCode = 1 + + def _delay_for_dns_retry_period(self, start_dns: bool): + """Wait for the failed DNS retry period.""" + tr = urtest.AddTestRun() + + if start_dns: + dns = tr.MakeDNServer(f'dns{self._dns_counter}', default='127.0.0.1', port=self._dns_port) + self._dns_counter += 1 + tr.Processes.Default.StartBefore(dns) + + # Exceed the retry period. + tr.Processes.Default.Command = f'sleep {DNS_PRIMARY_RETRY_PERIOD + 1}' + + def _test_dns_reachable(self): + """This is the base case: DNS is reachable. + + This verifies that ATS can use the name server to resolve a name when + the name server is reachable. + """ + self._run_transaction(start_dns=True, keyname='first_host') + + def _test_dns_reachable_within_the_retry_period(self): + """Test nameserver connectivity resolution within the retry period. + + Have the name server be inaccessible, but bring it back online within + the retry period. + """ + # Trigger a name resolution with the name server down. + self._run_transaction(start_dns=False, keyname='second_host') + + # Wait long enough that ATS to retry the name server, with the server + # up. + self._delay_for_dns_retry_period(start_dns=True) + + # ATS should now use the working name server again to resolve the host + # name. + self._run_transaction(start_dns=True, keyname='second_host') + + def _test_dns_reachable_outside_the_retry_period(self): + """Test nameserver connectivity resolution outside the retry period. + + Have the name server be inaccessible, but bring it back online outside + the retry period. This verifies that we handle it coming back online + after the first attempt to re-connect to it. + """ + # Trigger a name resolution with the name server down. + self._run_transaction(start_dns=False, keyname='third_host') + + # Wait long enough that ATS to retry connectivity to the name server, + # with the server down. This will naturally fail. + self._delay_for_dns_retry_period(start_dns=False) + + # Wait long enough that ATS to retry connectivity a second time to name + # server, this time with the server up. + self._delay_for_dns_retry_period(start_dns=True) + + # ATS should now try the name server again, detect that it is up, and + # complete the transaction. + self._run_transaction(start_dns=True, keyname='third_host') + + def run(self): + """Exercise ATS down name server functionality.""" + self._test_dns_reachable() + self._test_dns_reachable_within_the_retry_period() + self._test_dns_reachable_outside_the_retry_period() + + DownDNSNameserverTest().run() + urtest.execute() diff --git a/tests/uranium_tests/dns/test_dns_host_down.py b/tests/uranium_tests/dns/test_dns_host_down.py new file mode 100644 index 00000000000..ee293f41016 --- /dev/null +++ b/tests/uranium_tests/dns/test_dns_host_down.py @@ -0,0 +1,114 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_dns_host_down(urtest: UraniumTest) -> None: + ''' + Verify ATS handles down origin servers with domain cached correctly. + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + from ports import get_port + import os + + urtest.Summary = ''' + Verify ATS handles down origin servers with cached domain correctly. + ''' + + class DownCachedOriginServerTest: + replay_file = "replay/server_down.replay.yaml" + + def __init__(self): + """Initialize the Test processes for the test runs.""" + self._server = urtest.MakeVerifierServerProcess("server", DownCachedOriginServerTest.replay_file) + self._configure_trafficserver() + + def _configure_trafficserver(self): + """Configure Traffic Server.""" + self._ts = urtest.MakeATSProcess("ts", enable_cache=False) + + self._ts.Disk.remap_config.AddLine(f"map / http://resolve.this.com:{self._server.Variables.http_port}/") + + self._ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'hostdb|dns|http|socket', + 'proxy.config.http.connect_attempts_max_retries': 0, + 'proxy.config.http.connect_attempts_rr_retries': 0, + 'proxy.config.hostdb.fail.timeout': 10, + 'proxy.config.dns.resolv_conf': 'NULL', + 'proxy.config.hostdb.ttl_mode': 1, + 'proxy.config.hostdb.timeout': 2, + 'proxy.config.hostdb.lookup_timeout': 2, + 'proxy.config.http.transaction_no_activity_timeout_in': 2, + 'proxy.config.http.connect_attempts_timeout': 2, + 'proxy.config.hostdb.host_file.interval': 1, + 'proxy.config.hostdb.host_file.path': os.path.join(urtest.TestDirectory, "hosts_file"), + }) + + def _test_host_mark_down(self): + tr = urtest.AddTestRun() + + tr.Processes.Default.StartBefore(self._server) + tr.Processes.Default.StartBefore(self._ts) + + tr.AddVerifierClientProcess("client-1", DownCachedOriginServerTest.replay_file, http_ports=[self._ts.Variables.port]) + + # Verify error log marking host down exists + def _test_error_log(self): + tr = urtest.AddAwaitFileContainsTestRun( + 'Await error.log mark-down entry.', + os.path.join(self._ts.Variables.LOGDIR, 'error.log'), + "/dns/mark/down' fail_count='1' marking down", + ) + self._ts.Disk.error_log.Content = Testers.ContainsExpression( + "/dns/mark/down' fail_count='1' marking down", "host should be marked down") + + # Verify down_server_no_requests metric is incremented: + # - once when the first request marks the origin DOWN (502) + # - once when the second request is rejected because HostDB has no live address (500) + def _test_down_server_no_requests_metric(self): + tr = urtest.AddTestRun('Check proxy.process.http.down_server.no_requests metric') + tr.Processes.Default.Command = 'traffic_ctl metric get proxy.process.http.down_server.no_requests' + tr.Processes.Default.Env = self._ts.Env + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.All = Testers.ContainsExpression( + 'proxy.process.http.down_server.no_requests 2', + 'down_server.no_requests should be 2 (mark-down on first txn, no-live-address on second)') + tr.StillRunningAfter = self._ts + + def run(self): + self._test_host_mark_down() + self._test_error_log() + self._test_down_server_no_requests_metric() + + DownCachedOriginServerTest().run() + urtest.execute() diff --git a/tests/uranium_tests/dns/test_dns_reverse_lookup.py b/tests/uranium_tests/dns/test_dns_reverse_lookup.py new file mode 100644 index 00000000000..6d5e08ea693 --- /dev/null +++ b/tests/uranium_tests/dns/test_dns_reverse_lookup.py @@ -0,0 +1,105 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_dns_reverse_lookup(urtest: UraniumTest) -> None: + ''' + Verify that ATS can perform a reverse lookup when necessary + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + from ports import get_port + import os + + urtest.Summary = ''' + Verify ATS can perform a reverse lookup when necessary + ''' + + # + # This test verifies a fix for a regression that incorrectly marked a hostdb record as failed doing a reverse DNS + # lookup from the hosts file. If a parent.config exists, then ATS will perform a reverse lookup on remaps that + # have an ip address in the map-to url. While this doesn't fail the initil request, subsequent requests that resolve + # to the same host record will see the failed lookup attempt and fail the request with a host not found. + # + # This test verfiies the correct behavior in case future code chages introduce a similar regression + # + + class DNSReverseLookupTest: + replay_file = "replay/reverse_lookup.replay.yaml" + + def __init__(self): + self._server = urtest.MakeVerifierServerProcess("server", DNSReverseLookupTest.replay_file) + self._configure_trafficserver() + + def _configure_trafficserver(self): + self._ts = urtest.MakeATSProcess("ts", enable_cache=False) + + # This first rule would trigger the bug + self._ts.Disk.remap_config.AddLine(f"map /test1 http://127.0.0.1:{self._server.Variables.http_port}/",) + # This first rule would fail in the presense of the bug, but this test verifies correct behavior + self._ts.Disk.remap_config.AddLine(f"map /test2 http://localhost:{self._server.Variables.http_port}/",) + self._ts.Disk.parent_config.AddLine( + f'dest_domain=. parent=parent_host:{self._ts.Variables.port} round_robin=consistent_hash go_direct=false') + self._ts.Disk.parent_config.AddLine( + # this doesn't need to match, just exist so ats will do the reverse lookup + f'dest_host=other_host scheme=http parent="parent_host:8080"') + + self._ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'hostdb|dns|http', + 'proxy.config.http.connect_attempts_max_retries': 0, + 'proxy.config.http.connect_attempts_rr_retries': 0, + 'proxy.config.hostdb.fail.timeout': 10, + 'proxy.config.dns.resolv_conf': 'NULL', + 'proxy.config.hostdb.ttl_mode': 1, + 'proxy.config.hostdb.timeout': 2, + 'proxy.config.hostdb.lookup_timeout': 2, + 'proxy.config.http.transaction_no_activity_timeout_in': 2, + 'proxy.config.http.connect_attempts_timeout': 2, + 'proxy.config.hostdb.host_file.interval': 1, + 'proxy.config.hostdb.host_file.path': os.path.join(urtest.TestDirectory, "hosts_file"), + }) + + def test_rev_dns(self): + tr = urtest.AddTestRun() + + tr.Processes.Default.StartBefore(self._server) + tr.Processes.Default.StartBefore(self._ts) + + tr.AddVerifierClientProcess("client", DNSReverseLookupTest.replay_file, http_ports=[self._ts.Variables.port]) + + def run(self): + self.test_rev_dns() + + DNSReverseLookupTest().run() + urtest.execute() diff --git a/tests/uranium_tests/dns/test_dns_ttl.py b/tests/uranium_tests/dns/test_dns_ttl.py new file mode 100644 index 00000000000..c1545860d88 --- /dev/null +++ b/tests/uranium_tests/dns/test_dns_ttl.py @@ -0,0 +1,163 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_dns_ttl(urtest: UraniumTest) -> None: + ''' + Test DNS TTL behavior. + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import ports + + urtest.Summary = 'Test DNS TTL behavior' + + class TtlDnsTest: + single_transaction_replay = "replay/single_transaction.replay.yaml" + server_error_replay = "replay/server_error.replay.yaml" + process_counter = 1 + + # The TTL to set for every resolved hostname. + dnsTTL = 1 + + # The DNS query timeout. + queryTimeout = 1 + + def __init__(self, configure_serve_stale=False, exceed_serve_stale=False): + """ + Args: + configure_serve_stale: (bool) Whether the ATS process should be configured to + serve stale DNS entries. + + exceed_serve_stale: (bool) Configure the serve_stale timeout to be low + enough that the timed out DNS response will not be used. + """ + self.configure_serve_stale = configure_serve_stale + self.exceed_serve_stale = exceed_serve_stale + + self.server_process_counter = TtlDnsTest.get_unique_process_counter() + TtlDnsTest.process_counter += 1 + + self.setupOriginServer() + self.setupTS() + + @classmethod + def get_unique_process_counter(cls): + this_counter = cls.process_counter + cls.process_counter += 1 + return this_counter + + def addDNSServerToTestRun(self, test_run): + dns = test_run.MakeDNServer("dns", port=self.dns_port) + dns.addRecords(records={'resolve.this.com': ['127.0.0.1']}) + return dns + + def setupOriginServer(self): + self.server = urtest.MakeVerifierServerProcess( + f"server-{self.server_process_counter}", TtlDnsTest.single_transaction_replay) + + def setupTS(self): + self.ts = urtest.MakeATSProcess(f"ts-{self.server_process_counter}", enable_cache=False) + self.dns_port = ports.get_port(self.ts, 'dns_port') + self.ts.Disk.records_config.update( + { + "proxy.config.diags.debug.enabled": 1, + "proxy.config.diags.debug.tags": "dns", + 'proxy.config.dns.nameservers': f'127.0.0.1:{self.dns_port}', + 'proxy.config.dns.resolv_conf': 'NULL', + + # Configure ATS to treat each resolved name to have a 1 second + # time to live. + "proxy.config.hostdb.ttl_mode": 1, + "proxy.config.hostdb.timeout": self.dnsTTL, + + # MicroDNS will be down for the second transaction. Have ATS give + # up trying to talk to it after one second. + "proxy.config.hostdb.lookup_timeout": self.queryTimeout, + }) + if self.configure_serve_stale: + if self.exceed_serve_stale: + stale_timeout = 1 + else: + stale_timeout = 300 + + self.ts.Disk.records_config.update({"proxy.config.hostdb.serve_stale_for": stale_timeout}) + self.ts.Disk.remap_config.AddLine(f"map / http://resolve.this.com:{self.server.Variables.http_port}/") + + def testRunWithDNS(self): + tr = urtest.AddTestRun() + + # Run the DNS server with this test run so it will not be running in + # the next one. + dns = self.addDNSServerToTestRun(tr) + process_number = TtlDnsTest.get_unique_process_counter() + tr.AddVerifierClientProcess( + f"client-{process_number}", TtlDnsTest.single_transaction_replay, http_ports=[self.ts.Variables.port]) + + tr.Processes.Default.StartBefore(dns) + tr.Processes.Default.StartBefore(self.server) + tr.Processes.Default.StartBefore(self.ts) + + tr.StillRunningAfter = dns + tr.StillRunningAfter = self.server + tr.StillRunningAfter = self.ts + + def testRunWithoutDNS(self): + tr = urtest.AddTestRun() + + # Delay running the second transaction for long enough to guarantee + # that both the TTL and the DNS query timeout (lookup_timeout) are + # exceeded. + tr.DelayStart = 3 + + # Will the stale resolved DNS response be used? + if self.configure_serve_stale and not self.exceed_serve_stale: + # Yes: expect a proxied transaction with a 200 OK response. + replay_file = TtlDnsTest.single_transaction_replay + else: + # No: expect a 5xx response because the server name could not be + # resolved. + replay_file = TtlDnsTest.server_error_replay + process_number = TtlDnsTest.get_unique_process_counter() + tr.AddVerifierClientProcess(f"client-{process_number}", replay_file, http_ports=[self.ts.Variables.port]) + + tr.StillRunningAfter = self.server + tr.StillRunningAfter = self.ts + + def run(self): + self.testRunWithDNS() + self.testRunWithoutDNS() + + TtlDnsTest().run() + TtlDnsTest(configure_serve_stale=True, exceed_serve_stale=False).run() + TtlDnsTest(configure_serve_stale=True, exceed_serve_stale=True).run() + urtest.execute() diff --git a/tests/uranium_tests/dns/test_splitdns.py b/tests/uranium_tests/dns/test_splitdns.py new file mode 100644 index 00000000000..981de9c8e0c --- /dev/null +++ b/tests/uranium_tests/dns/test_splitdns.py @@ -0,0 +1,96 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_splitdns(urtest: UraniumTest) -> None: + ''' + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + urtest.Summary = 'Test Split DNS' + + class SplitDNSTest: + + def __init__(self): + self.setupDNSServer() + self.setupOriginServer() + self.setupTS() + + def setupDNSServer(self): + self.dns = urtest.MakeDNServer("dns") + self.dns.addRecords(records={'foo.ts.a.o.': ['127.0.0.1']}) + + def setupOriginServer(self): + self.origin_server = urtest.MakeOriginServer("origin_server") + self.origin_server.addResponse( + "sessionlog.json", {"headers": "GET / HTTP/1.1\r\nHost: localhost\r\n\r\n"}, + {"headers": "HTTP/1.1 200 OK\r\nServer: microserver\r\nConnection: close\r\n\r\n"}) + + def setupTS(self): + self.ts = urtest.MakeATSProcess("ts", enable_cache=False) + self.ts.Disk.records_config.update( + { + "proxy.config.dns.splitDNS.enabled": 1, + "proxy.config.diags.debug.enabled": 1, + "proxy.config.diags.debug.tags": "dns|splitdns", + }) + self.ts.Disk.splitdns_config.AddLine(f"dest_domain=foo.ts.a.o named=127.0.0.1:{self.dns.Variables.Port}") + self.ts.Disk.remap_config.AddLine(f"map /foo/ http://foo.ts.a.o:{self.origin_server.Variables.Port}/") + self.ts.Disk.remap_config.AddLine(f"map /bar/ http://127.0.0.1:{self.origin_server.Variables.Port}/") + + def addTestCase0(self): + tr = urtest.AddTestRun() + tr.MakeCurlCommand(f"-v http://localhost:{self.ts.Variables.port}/foo/", ts=self.ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.stderr = "gold/test_case_0_stderr.gold" + tr.Processes.Default.StartBefore(self.dns) + tr.Processes.Default.StartBefore(self.origin_server) + tr.Processes.Default.StartBefore(self.ts) + tr.StillRunningAfter = self.dns + tr.StillRunningAfter = self.origin_server + tr.StillRunningAfter = self.ts + + def addTestCase1(self): + tr = urtest.AddTestRun() + tr.MakeCurlCommand(f"-v http://localhost:{self.ts.Variables.port}/bar/", ts=self.ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.stderr = "gold/test_case_1_stderr.gold" + tr.StillRunningAfter = self.dns + tr.StillRunningAfter = self.origin_server + tr.StillRunningAfter = self.ts + + def run(self): + self.addTestCase0() + self.addTestCase1() + + SplitDNSTest().run() + urtest.execute() diff --git a/tests/uranium_tests/dns/test_splitdns_reload.py b/tests/uranium_tests/dns/test_splitdns_reload.py new file mode 100644 index 00000000000..81b1ee33d80 --- /dev/null +++ b/tests/uranium_tests/dns/test_splitdns_reload.py @@ -0,0 +1,99 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_splitdns_reload(urtest: UraniumTest) -> None: + ''' + Test splitdns.config reload via ConfigRegistry. + + Verifies that: + 1. splitdns.config reload works after file touch + 2. The reload handler is invoked (diags log check) + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import os + + urtest.Summary = 'Test splitdns.config reload via ConfigRegistry.' + urtest.ContinueOnFail = True + + class SplitDNSReloadTest: + + def __init__(self): + self.setupDNSServer() + self.setupOriginServer() + self.setupTS() + + def setupDNSServer(self): + self.dns = urtest.MakeDNServer("dns") + self.dns.addRecords(records={'foo.ts.a.o.': ['127.0.0.1']}) + + def setupOriginServer(self): + self.origin_server = urtest.MakeOriginServer("origin_server") + self.origin_server.addResponse( + "sessionlog.json", {"headers": "GET / HTTP/1.1\r\nHost: localhost\r\n\r\n"}, + {"headers": "HTTP/1.1 200 OK\r\nServer: microserver\r\nConnection: close\r\n\r\n"}) + + def setupTS(self): + self.ts = urtest.MakeATSProcess("ts", enable_cache=False) + self.ts.Disk.records_config.update( + { + 'proxy.config.dns.splitDNS.enabled': 1, + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'splitdns|config', + }) + self.ts.Disk.splitdns_config.AddLine(f"dest_domain=foo.ts.a.o named=127.0.0.1:{self.dns.Variables.Port}") + self.ts.Disk.remap_config.AddLine(f"map /foo/ http://foo.ts.a.o:{self.origin_server.Variables.Port}/") + + def run(self): + config_dir = self.ts.Variables.CONFIGDIR + + # Test 1: Verify basic SplitDNS works (startup loads config) + tr = urtest.AddTestRun("Verify SplitDNS works at startup") + tr.MakeCurlCommand(f"-v http://localhost:{self.ts.Variables.port}/foo/", ts=self.ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.StartBefore(self.dns) + tr.Processes.Default.StartBefore(self.origin_server) + tr.Processes.Default.StartBefore(self.ts) + tr.StillRunningAfter = self.ts + + # Test 2: Touch splitdns.config -> reload -> handler fires + tr = urtest.AddTestRun("Touch splitdns.config") + tr.Processes.Default.Command = (f"touch {os.path.join(config_dir, 'splitdns.config')} && sleep 1") + tr.Processes.Default.ReturnCode = 0 + tr.StillRunningAfter = self.ts + + tr = urtest.AddConfigReload(self.ts, expect_tasks=["splitdns.config"], description="Reload after splitdns.config touch") + tr.StillRunningAfter = self.ts + + SplitDNSReloadTest().run() + urtest.execute() diff --git a/tests/gold_tests/early_hints/early_hints_server.py b/tests/uranium_tests/early_hints/early_hints_server.py similarity index 100% rename from tests/gold_tests/early_hints/early_hints_server.py rename to tests/uranium_tests/early_hints/early_hints_server.py diff --git a/tests/uranium_tests/early_hints/test_early_hints.py b/tests/uranium_tests/early_hints/test_early_hints.py new file mode 100644 index 00000000000..2e69e50bb84 --- /dev/null +++ b/tests/uranium_tests/early_hints/test_early_hints.py @@ -0,0 +1,193 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_early_hints(urtest: UraniumTest) -> None: + ''' + Verify correct handling of 103 Early Hints responses. + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + from enum import Enum, auto + import os + from ports import get_port + import re + import sys + + class Protocol(Enum): + HTTP = auto() + HTTPS = auto() + HTTP2 = auto() + HTTP3 = auto() + + @classmethod + def to_string(cls, protocol): + if protocol == cls.HTTP: + return 'HTTP' + elif protocol == cls.HTTPS: + return 'HTTPS' + elif protocol == cls.HTTP2: + return 'HTTP2' + elif protocol == cls.HTTP3: + return 'HTTP3' + else: + return None + + class TestEarlyHints: + '''Verify that ATS can properly handle a 103 response.''' + + _early_hints_server = 'early_hints_server.py' + + def __init__(self, protocol: Protocol): + '''Create a test run for the given protocol. + :param protocol: The protocol the client will use for the test. + ''' + self._protocol = protocol + self._protocol_str = Protocol.to_string(protocol) + tr = urtest.AddTestRun(f'Early hints with client protocol: {self._protocol_str}') + self._configure_dns(tr) + self._configure_server(tr) + ts = self._configure_ts(tr) + self._copy_scripts(tr, ts) + self._configure_client(tr) + + def _configure_dns(self, tr: 'TestRun'): + '''Configure the DNS for the test run. + :param tr: The TestRun for the DNS process. + ''' + dns = tr.MakeDNServer(f'dns_{self._protocol_str}', default='127.0.0.1') + self._dns = dns + return dns + + def _configure_server(self, tr: 'TestRun'): + '''Configure the origin server for the test run. + + :param tr: The TestRun for the origin server. + ''' + tr.Setup.Copy(self._early_hints_server) + server = tr.Processes.Process(f'server_{self._protocol_str}') + server_port = get_port(server, "http_port") + server.Command = \ + f'{sys.executable} {self._early_hints_server} 127.0.0.1 {server_port} ' + server.Ready = When.PortOpenv4(server_port) + + self._server = server + return server + + def _configure_ts(self, tr: 'TestRun'): + '''Configure the traffic server for the test run. + + :param tr: The TestRun for the traffic server. + ''' + ts = urtest.MakeATSProcess(f'ts_{self._protocol_str}', enable_tls=True, enable_quic=self._protocol == Protocol.HTTP3) + self._ts = ts + ts.Disk.remap_config.AddLine(f'map / http://backend.server.com:{self._server.Variables.http_port}') + ts.addDefaultSSLFiles() + ts.Disk.ssl_multicert_yaml.AddLines( + """ + ssl_multicert: + - dest_ip: "*" + ssl_cert_name: server.pem + ssl_key_name: server.key + """.split("\n")) + ts.Disk.records_config.update( + { + 'proxy.config.ssl.server.cert.path': ts.Variables.SSLDir, + 'proxy.config.ssl.server.private_key.path': ts.Variables.SSLDir, + 'proxy.config.dns.nameservers': f'127.0.0.1:{self._dns.Variables.Port}', + 'proxy.config.dns.resolv_conf': 'NULL', + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'http', + }) + return ts + + def _copy_scripts(self, tr: 'TestRun', ts: 'TestATSProcess'): + '''Copy the python server and helper files to the test run directory. + :param tr: The TestRun for the server. + :param ts: The TestATSProcess for the traffic server. This is needed + for the ATS tools directory it stores in Variables. + ''' + tr.Setup.Copy(self._early_hints_server) + tools_dir = ts.Variables.AtsTestToolsDir + http_utils = os.path.join(tools_dir, 'http_utils.py') + tr.Setup.CopyAs(http_utils, urtest.RunDirectory) + + def _configure_client(self, tr: 'TestRun'): + '''Configure a client to use the given protocol to ATS. + :param tr: The TestRun for the client. + ''' + client = tr.Processes.Default + if self._protocol == Protocol.HTTP: + protocol_arg = '--http1.1' + scheme = 'http' + ts_port = self._ts.Variables.port + elif self._protocol == Protocol.HTTPS: + protocol_arg = '-k --http1.1' + scheme = 'https' + ts_port = self._ts.Variables.ssl_port + elif self._protocol == Protocol.HTTP2: + protocol_arg = '-k --http2' + scheme = 'https' + ts_port = self._ts.Variables.ssl_port + elif self._protocol == Protocol.HTTP3: + protocol_arg = '-k --http3-only' + scheme = 'https' + ts_port = self._ts.Variables.ssl_port + tr.MakeCurlCommand( + f'-v {protocol_arg} ' + f'--resolve "server.com:{ts_port}:127.0.0.1" ' + f'-H "Host: server.com" ' + f'{scheme}://server.com:{ts_port}/{self._protocol_str}', + ts=self._ts) + + client.ReturnCode = 0 + self._ts.StartBefore(self._dns) + self._ts.StartBefore(self._server) + client.StartBefore(self._ts) + + # Note that the server is configured to send two 103 responses. + client.Streams.All += Testers.ContainsExpression( + 'HTTP/.* 103.*HTTP/.* 103', + 'Verify that two 103 Early Hints responses were received.', + reflags=re.MULTILINE | re.DOTALL) + client.Streams.All += Testers.ContainsExpression( + 'ink: ; rel=preload', 'Verify preload link header was received.') + client.Streams.All += Testers.ContainsExpression('HTTP/.* 200', 'Verify 200 OK response was received.') + client.Streams.All += Testers.ContainsExpression('10bytebody', 'Verify the body to the 200 OK was received.') + + TestEarlyHints(Protocol.HTTP) + if not Condition.CurlUsingUnixDomainSocket(): + TestEarlyHints(Protocol.HTTPS) + TestEarlyHints(Protocol.HTTP2) + if Condition.HasATSFeature('TS_USE_QUIC') and Condition.HasCurlFeature('http3') and Condition.HasCurlOption('--http3-only'): + TestEarlyHints(Protocol.HTTP3) + urtest.execute() diff --git a/tests/gold_tests/forward_proxy/forward_proxy.replay.yaml b/tests/uranium_tests/forward_proxy/forward_proxy.replay.yaml similarity index 100% rename from tests/gold_tests/forward_proxy/forward_proxy.replay.yaml rename to tests/uranium_tests/forward_proxy/forward_proxy.replay.yaml diff --git a/tests/uranium_tests/forward_proxy/test_forward_proxy.py b/tests/uranium_tests/forward_proxy/test_forward_proxy.py new file mode 100644 index 00000000000..6dba61d5ab0 --- /dev/null +++ b/tests/uranium_tests/forward_proxy/test_forward_proxy.py @@ -0,0 +1,134 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_forward_proxy(urtest: UraniumTest) -> None: + """ + """ + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + from typing import Union + + urtest.Summary = 'Verify ATS can function as a forward proxy' + urtest.ContinueOnFail = True + urtest.SkipIf(Condition.CurlUsingUnixDomainSocket()) + + class ForwardProxyTest: + _scheme_proto_mismatch_policy: Union[int, None] + _ts_counter: int = 0 + _server_counter: int = 0 + + def __init__(self, verify_scheme_matches_protocol: Union[int, None]): + """Construct a ForwardProxyTest object. + + :param verify_scheme_matches_protocol: The value with which to + configure Traffic Server's + proxy.config.ssl.client.scheme_proto_mismatch_policy. A value of None + means that no value will be explicitly set in the records.yaml. + :type verify_scheme_matches_protocol: int or None + """ + self._scheme_proto_mismatch_policy = verify_scheme_matches_protocol + self.setupOriginServer() + self.setupTS() + + def setupOriginServer(self): + """Configure the Proxy Verifier server.""" + proc_name = f"server{ForwardProxyTest._server_counter}" + self.server = urtest.MakeVerifierServerProcess(proc_name, "forward_proxy.replay.yaml") + ForwardProxyTest._server_counter += 1 + if self._scheme_proto_mismatch_policy in (2, None): + self.server.Streams.All = Testers.ExcludesExpression( + 'Received an HTTP/1 request with key 1', 'Verify that the server did not receive the request.') + else: + self.server.Streams.All = Testers.ContainsExpression( + 'Received an HTTP/1 request with key 1', 'Verify that the server received the request.') + + def setupTS(self): + """Configure the Traffic Server process.""" + proc_name = f"ts{ForwardProxyTest._ts_counter}" + self.ts = urtest.MakeATSProcess(proc_name, enable_tls=True, enable_cache=False) + ForwardProxyTest._ts_counter += 1 + self.ts.addDefaultSSLFiles() + self.ts.Disk.ssl_multicert_yaml.AddLines( + """ + ssl_multicert: + - dest_ip: "*" + ssl_cert_name: server.pem + ssl_key_name: server.key + """.split("\n")) + self.ts.Disk.remap_config.AddLine(f"map / http://127.0.0.1:{self.server.Variables.http_port}/") + + self.ts.Disk.records_config.update( + { + 'proxy.config.ssl.server.cert.path': self.ts.Variables.SSLDir, + 'proxy.config.ssl.server.private_key.path': self.ts.Variables.SSLDir, + 'proxy.config.ssl.client.verify.server.policy': 'PERMISSIVE', + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': "http", + }) + + if self._scheme_proto_mismatch_policy is not None: + self.ts.Disk.records_config.update( + { + 'proxy.config.ssl.client.scheme_proto_mismatch_policy': self._scheme_proto_mismatch_policy, + }) + + def addProxyHttpsToHttpCase(self): + """Test ATS as an HTTPS forward proxy behind an HTTP server.""" + tr = urtest.AddTestRun() + tr.Processes.Default.StartBefore(self.server) + tr.Processes.Default.StartBefore(self.ts) + tr.MakeCurlCommand( + f'--proxy-insecure -v -H "uuid: 1" ' + f'--proxy "https://127.0.0.1:{self.ts.Variables.ssl_port}/" ' + f'http://example.com/', + ts=self.ts) + tr.Processes.Default.ReturnCode = 0 + tr.StillRunningAfter = self.server + tr.StillRunningAfter = self.ts + + if self._scheme_proto_mismatch_policy in (2, None): + tr.Processes.Default.Streams.All = Testers.ContainsExpression( + '< HTTP/1.1 400 Invalid HTTP Request', 'Verify that the request was rejected.') + else: + tr.Processes.Default.Streams.All = Testers.ContainsExpression( + '< HTTP/1.1 200 OK', 'Verify that curl received a 200 OK response.') + + def run(self): + """Configure the TestRun instances for this set of tests.""" + self.addProxyHttpsToHttpCase() + + ForwardProxyTest(verify_scheme_matches_protocol=None).run() + ForwardProxyTest(verify_scheme_matches_protocol=0).run() + ForwardProxyTest(verify_scheme_matches_protocol=1).run() + ForwardProxyTest(verify_scheme_matches_protocol=2).run() + urtest.execute() diff --git a/tests/gold_tests/h2/clients/h2_extension_settings.py b/tests/uranium_tests/h2/clients/h2_extension_settings.py similarity index 100% rename from tests/gold_tests/h2/clients/h2_extension_settings.py rename to tests/uranium_tests/h2/clients/h2_extension_settings.py diff --git a/tests/gold_tests/h2/clients/h2_max_active_streams.py b/tests/uranium_tests/h2/clients/h2_max_active_streams.py similarity index 100% rename from tests/gold_tests/h2/clients/h2_max_active_streams.py rename to tests/uranium_tests/h2/clients/h2_max_active_streams.py diff --git a/tests/gold_tests/h2/clients/h2_max_settings_per_minute.py b/tests/uranium_tests/h2/clients/h2_max_settings_per_minute.py similarity index 100% rename from tests/gold_tests/h2/clients/h2_max_settings_per_minute.py rename to tests/uranium_tests/h2/clients/h2_max_settings_per_minute.py diff --git a/tests/gold_tests/h2/clients/h2_settings_ack_stall.py b/tests/uranium_tests/h2/clients/h2_settings_ack_stall.py similarity index 100% rename from tests/gold_tests/h2/clients/h2_settings_ack_stall.py rename to tests/uranium_tests/h2/clients/h2_settings_ack_stall.py diff --git a/tests/gold_tests/h2/clients/h2empty_data_frame.py b/tests/uranium_tests/h2/clients/h2empty_data_frame.py similarity index 100% rename from tests/gold_tests/h2/clients/h2empty_data_frame.py rename to tests/uranium_tests/h2/clients/h2empty_data_frame.py diff --git a/tests/gold_tests/h2/expect_100_continue.yaml b/tests/uranium_tests/h2/expect_100_continue.yaml similarity index 100% rename from tests/gold_tests/h2/expect_100_continue.yaml rename to tests/uranium_tests/h2/expect_100_continue.yaml diff --git a/tests/gold_tests/h2/gold/active_timeout.gold b/tests/uranium_tests/h2/gold/active_timeout.gold similarity index 100% rename from tests/gold_tests/h2/gold/active_timeout.gold rename to tests/uranium_tests/h2/gold/active_timeout.gold diff --git a/tests/gold_tests/h2/gold/bigfile.gold b/tests/uranium_tests/h2/gold/bigfile.gold similarity index 100% rename from tests/gold_tests/h2/gold/bigfile.gold rename to tests/uranium_tests/h2/gold/bigfile.gold diff --git a/tests/gold_tests/h2/gold/chunked.gold b/tests/uranium_tests/h2/gold/chunked.gold similarity index 100% rename from tests/gold_tests/h2/gold/chunked.gold rename to tests/uranium_tests/h2/gold/chunked.gold diff --git a/tests/gold_tests/h2/gold/h2spec_stdout.gold b/tests/uranium_tests/h2/gold/h2spec_stdout.gold similarity index 100% rename from tests/gold_tests/h2/gold/h2spec_stdout.gold rename to tests/uranium_tests/h2/gold/h2spec_stdout.gold diff --git a/tests/gold_tests/h2/gold/http-request-method-metrics.gold b/tests/uranium_tests/h2/gold/http-request-method-metrics.gold similarity index 100% rename from tests/gold_tests/h2/gold/http-request-method-metrics.gold rename to tests/uranium_tests/h2/gold/http-request-method-metrics.gold diff --git a/tests/gold_tests/h2/gold/http2_8_stdout.gold b/tests/uranium_tests/h2/gold/http2_8_stdout.gold similarity index 100% rename from tests/gold_tests/h2/gold/http2_8_stdout.gold rename to tests/uranium_tests/h2/gold/http2_8_stdout.gold diff --git a/tests/gold_tests/h2/gold/http2_9_stderr.gold b/tests/uranium_tests/h2/gold/http2_9_stderr.gold similarity index 100% rename from tests/gold_tests/h2/gold/http2_9_stderr.gold rename to tests/uranium_tests/h2/gold/http2_9_stderr.gold diff --git a/tests/gold_tests/h2/gold/http2_9_stdout.gold b/tests/uranium_tests/h2/gold/http2_9_stdout.gold similarity index 100% rename from tests/gold_tests/h2/gold/http2_9_stdout.gold rename to tests/uranium_tests/h2/gold/http2_9_stdout.gold diff --git a/tests/gold_tests/h2/gold/httpbin_0_stderr.gold b/tests/uranium_tests/h2/gold/httpbin_0_stderr.gold similarity index 100% rename from tests/gold_tests/h2/gold/httpbin_0_stderr.gold rename to tests/uranium_tests/h2/gold/httpbin_0_stderr.gold diff --git a/tests/gold_tests/h2/gold/httpbin_0_stdout.gold b/tests/uranium_tests/h2/gold/httpbin_0_stdout.gold similarity index 100% rename from tests/gold_tests/h2/gold/httpbin_0_stdout.gold rename to tests/uranium_tests/h2/gold/httpbin_0_stdout.gold diff --git a/tests/gold_tests/h2/gold/httpbin_1_stderr.gold b/tests/uranium_tests/h2/gold/httpbin_1_stderr.gold similarity index 100% rename from tests/gold_tests/h2/gold/httpbin_1_stderr.gold rename to tests/uranium_tests/h2/gold/httpbin_1_stderr.gold diff --git a/tests/gold_tests/h2/gold/httpbin_1_stdout.gold b/tests/uranium_tests/h2/gold/httpbin_1_stdout.gold similarity index 100% rename from tests/gold_tests/h2/gold/httpbin_1_stdout.gold rename to tests/uranium_tests/h2/gold/httpbin_1_stdout.gold diff --git a/tests/gold_tests/h2/gold/httpbin_2_stderr.gold b/tests/uranium_tests/h2/gold/httpbin_2_stderr.gold similarity index 100% rename from tests/gold_tests/h2/gold/httpbin_2_stderr.gold rename to tests/uranium_tests/h2/gold/httpbin_2_stderr.gold diff --git a/tests/gold_tests/h2/gold/httpbin_2_stdout.gold b/tests/uranium_tests/h2/gold/httpbin_2_stdout.gold similarity index 100% rename from tests/gold_tests/h2/gold/httpbin_2_stdout.gold rename to tests/uranium_tests/h2/gold/httpbin_2_stdout.gold diff --git a/tests/gold_tests/h2/gold/httpbin_3_stderr.gold b/tests/uranium_tests/h2/gold/httpbin_3_stderr.gold similarity index 100% rename from tests/gold_tests/h2/gold/httpbin_3_stderr.gold rename to tests/uranium_tests/h2/gold/httpbin_3_stderr.gold diff --git a/tests/gold_tests/h2/gold/httpbin_3_stdout.gold b/tests/uranium_tests/h2/gold/httpbin_3_stdout.gold similarity index 100% rename from tests/gold_tests/h2/gold/httpbin_3_stdout.gold rename to tests/uranium_tests/h2/gold/httpbin_3_stdout.gold diff --git a/tests/gold_tests/h2/gold/httpbin_access.gold b/tests/uranium_tests/h2/gold/httpbin_access.gold similarity index 100% rename from tests/gold_tests/h2/gold/httpbin_access.gold rename to tests/uranium_tests/h2/gold/httpbin_access.gold diff --git a/tests/gold_tests/h2/gold/log.gold b/tests/uranium_tests/h2/gold/log.gold similarity index 100% rename from tests/gold_tests/h2/gold/log.gold rename to tests/uranium_tests/h2/gold/log.gold diff --git a/tests/gold_tests/h2/gold/nghttp_0_stdout.gold b/tests/uranium_tests/h2/gold/nghttp_0_stdout.gold similarity index 100% rename from tests/gold_tests/h2/gold/nghttp_0_stdout.gold rename to tests/uranium_tests/h2/gold/nghttp_0_stdout.gold diff --git a/tests/gold_tests/h2/gold/nghttp_1_stdout.gold b/tests/uranium_tests/h2/gold/nghttp_1_stdout.gold similarity index 100% rename from tests/gold_tests/h2/gold/nghttp_1_stdout.gold rename to tests/uranium_tests/h2/gold/nghttp_1_stdout.gold diff --git a/tests/gold_tests/h2/gold/nghttp_ts_stderr.gold b/tests/uranium_tests/h2/gold/nghttp_ts_stderr.gold similarity index 100% rename from tests/gold_tests/h2/gold/nghttp_ts_stderr.gold rename to tests/uranium_tests/h2/gold/nghttp_ts_stderr.gold diff --git a/tests/gold_tests/h2/gold/post_chunked.gold b/tests/uranium_tests/h2/gold/post_chunked.gold similarity index 100% rename from tests/gold_tests/h2/gold/post_chunked.gold rename to tests/uranium_tests/h2/gold/post_chunked.gold diff --git a/tests/gold_tests/h2/gold/priority_0_stderr.gold b/tests/uranium_tests/h2/gold/priority_0_stderr.gold similarity index 100% rename from tests/gold_tests/h2/gold/priority_0_stderr.gold rename to tests/uranium_tests/h2/gold/priority_0_stderr.gold diff --git a/tests/gold_tests/h2/gold/priority_0_stdout.gold b/tests/uranium_tests/h2/gold/priority_0_stdout.gold similarity index 100% rename from tests/gold_tests/h2/gold/priority_0_stdout.gold rename to tests/uranium_tests/h2/gold/priority_0_stdout.gold diff --git a/tests/gold_tests/h2/gold/remap-200.gold b/tests/uranium_tests/h2/gold/remap-200.gold similarity index 100% rename from tests/gold_tests/h2/gold/remap-200.gold rename to tests/uranium_tests/h2/gold/remap-200.gold diff --git a/tests/gold_tests/h2/gold/replay.gold b/tests/uranium_tests/h2/gold/replay.gold similarity index 100% rename from tests/gold_tests/h2/gold/replay.gold rename to tests/uranium_tests/h2/gold/replay.gold diff --git a/tests/gold_tests/h2/gold/server_after_headers.gold b/tests/uranium_tests/h2/gold/server_after_headers.gold similarity index 100% rename from tests/gold_tests/h2/gold/server_after_headers.gold rename to tests/uranium_tests/h2/gold/server_after_headers.gold diff --git a/tests/gold_tests/h2/grpc/grpc_client.py b/tests/uranium_tests/h2/grpc/grpc_client.py similarity index 100% rename from tests/gold_tests/h2/grpc/grpc_client.py rename to tests/uranium_tests/h2/grpc/grpc_client.py diff --git a/tests/gold_tests/h2/grpc/grpc_server.py b/tests/uranium_tests/h2/grpc/grpc_server.py similarity index 100% rename from tests/gold_tests/h2/grpc/grpc_server.py rename to tests/uranium_tests/h2/grpc/grpc_server.py diff --git a/tests/gold_tests/h2/grpc/simple.proto b/tests/uranium_tests/h2/grpc/simple.proto similarity index 100% rename from tests/gold_tests/h2/grpc/simple.proto rename to tests/uranium_tests/h2/grpc/simple.proto diff --git a/tests/uranium_tests/h2/grpc/test_grpc.py b/tests/uranium_tests/h2/grpc/test_grpc.py new file mode 100644 index 00000000000..34811c236d4 --- /dev/null +++ b/tests/uranium_tests/h2/grpc/test_grpc.py @@ -0,0 +1,183 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_grpc(urtest: UraniumTest) -> None: + """Test basic gRPC traffic.""" + + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import os + from ports import get_port + import sys + + class TestGrpc(): + """Test basic gRPC traffic.""" + + num_client_connections = 50 + + def __init__(self, description: str): + """Configure a TestRun for gRPC traffic. + + :param description: The description for the test runs. + """ + self._description = description + + def _configure_dns(self, tr: 'TestRun') -> 'Process': + """Configure a locally running MicroDNS server. + + :param tr: The TestRun with which to associate the MicroDNS server. + :return: The MicroDNS server process. + """ + self._dns = tr.MakeDNServer("dns", default=['127.0.0.1']) + return self._dns + + def _configure_traffic_server(self, tr: 'TestRun', dns_port: int, server_port: int) -> 'Process': + """Configure the traffic server process. + + :param tr: The TestRun with which to associate the traffic server. + :param dns_port: The MicroDNS server port that traffic server should connect to. + :param server_port: The gRPC server port that traffic server should connect to. + :return: The traffic server process. + """ + self._ts = tr.MakeATSProcess("ts", enable_tls=True, enable_cache=False) + + self._ts.addDefaultSSLFiles() + self._ts.Disk.ssl_multicert_yaml.AddLines( + """ + ssl_multicert: + - dest_ip: "*" + ssl_cert_name: server.pem + ssl_key_name: server.key + """.split("\n")) + + self._ts.Disk.remap_config.AddLine(f"map / https://example.com:{server_port}/") + + self._ts.Disk.records_config.update( + { + "proxy.config.ssl.server.cert.path": self._ts.Variables.SSLDir, + "proxy.config.ssl.server.private_key.path": self._ts.Variables.SSLDir, + 'proxy.config.ssl.client.alpn_protocols': 'h2,http/1.1', + 'proxy.config.http.server_session_sharing.pool': 'thread', + 'proxy.config.ssl.client.verify.server.policy': 'PERMISSIVE', + 'proxy.config.dns.nameservers': f"127.0.0.1:{dns_port}", + 'proxy.config.dns.resolv_conf': "NULL", + + # Disable debug logging to avoid excessive log file size. I keep + # it here for convenience of use during manual debugging. + "proxy.config.diags.debug.enabled": 0, + "proxy.config.diags.debug.tags": "http", + + # The Python gRPC module uses many WINDO_UPDATE frames of small + # sizes, so we have to disable the min_avg_window_update to + # avoid ATS generating ERRORS logs and GOAWAY frames for them. + "proxy.config.http2.min_avg_window_update": 0, + }) + return self._ts + + def _configure_grpc_server(self, tr: 'TestRun') -> 'Process': + """Start the gRPC server. + + :param tr: The TestRun with which to associate the gRPC server. + :return: The gRPC server process. + """ + tr.Setup.Copy('grpc_server.py') + self._server = tr.Processes.Process('server') + + server_pem = os.path.join(urtest.Variables.AtsTestToolsDir, "ssl", "server.pem") + server_key = os.path.join(urtest.Variables.AtsTestToolsDir, "ssl", "server.key") + self._server.Setup.Copy(server_pem) + self._server.Setup.Copy(server_key) + + port = get_port(self._server, 'port') + # Each connection performs two requests, so multiply the number of + # connections by 2 to get the expected number of transactions. + command = ( + f'{sys.executable} {tr.RunDirectory}/grpc_server.py {port} ' + f'server.pem server.key {TestGrpc.num_client_connections * 2}') + self._server.Command = command + self._server.ReturnCode = 0 + return self._server + + def _configure_grpc_client(self, tr: 'TestRun', proxy_port: int) -> None: + """Start the gRPC client. + + :param tr: The TestRun with which to associate the gRPC client. + :param proxy_port: The proxy_port to which to connect. + """ + tr.Setup.Copy('grpc_client.py') + ts_cert = os.path.join(self._ts.Variables.SSLDir, 'server.pem') + # The cert is for example.com, so we must use that domain. + hostname = 'example.com' + command = ( + f'{sys.executable} {tr.RunDirectory}/grpc_client.py ' + f'{hostname} {proxy_port} {ts_cert} {TestGrpc.num_client_connections}') + tr.Processes.Default.Command = command + tr.Processes.Default.ReturnCode = 0 + tr.TimeOut = 10 + + def _compile_protobuf_files(self) -> None: + """Compile the protobuf files.""" + tr = urtest.AddTestRun(f'{self._description}: compile the protobuf files.') + tr.Setup.Copy('simple.proto') + command = ( + f'{sys.executable} -m grpc_tools.protoc -I{tr.RunDirectory} ' + f'--python_out={tr.RunDirectory} --grpc_python_out={tr.RunDirectory} simple.proto') + tr.Processes.Default.Command = command + pb2_file = os.path.join(tr.RunDirectory, 'simple_pb2.py') + tr.Disk.File(pb2_file, id='pb2', exists=True) + + pb2_grpc_file = os.path.join(tr.RunDirectory, 'simple_pb2_grpc.py') + tr.Disk.File(pb2_grpc_file, id='pb2_grpc', exists=True) + + def _run_test_traffic(self) -> None: + """Configure the TestRun for the client and servers.""" + tr = urtest.AddTestRun(f'{self._description}: run the gRPC traffic.') + + dns = self._configure_dns(tr) + server = self._configure_grpc_server(tr) + ts = self._configure_traffic_server(tr, dns.Variables.Port, server.Variables.port) + + tr.Processes.Default.StartBefore(dns) + tr.Processes.Default.StartBefore(server) + tr.Processes.Default.StartBefore(ts) + + self._configure_grpc_client(tr, ts.Variables.ssl_port) + + def run(self) -> None: + """Configure the various test runs for the gRPC test.""" + self._compile_protobuf_files() + self._run_test_traffic() + + test = TestGrpc("Test basic gRPC traffic") + test.run() + urtest.execute() diff --git a/tests/gold_tests/h2/h1_trailer_client.py b/tests/uranium_tests/h2/h1_trailer_client.py similarity index 100% rename from tests/gold_tests/h2/h1_trailer_client.py rename to tests/uranium_tests/h2/h1_trailer_client.py diff --git a/tests/gold_tests/h2/h2_origin_trailers_h1.replay.yaml b/tests/uranium_tests/h2/h2_origin_trailers_h1.replay.yaml similarity index 100% rename from tests/gold_tests/h2/h2_origin_trailers_h1.replay.yaml rename to tests/uranium_tests/h2/h2_origin_trailers_h1.replay.yaml diff --git a/tests/gold_tests/h2/h2active_timeout.py b/tests/uranium_tests/h2/h2active_timeout.py similarity index 100% rename from tests/gold_tests/h2/h2active_timeout.py rename to tests/uranium_tests/h2/h2active_timeout.py diff --git a/tests/gold_tests/h2/h2client.py b/tests/uranium_tests/h2/h2client.py similarity index 100% rename from tests/gold_tests/h2/h2client.py rename to tests/uranium_tests/h2/h2client.py diff --git a/tests/gold_tests/h2/h2get_with_body.yaml b/tests/uranium_tests/h2/h2get_with_body.yaml similarity index 100% rename from tests/gold_tests/h2/h2get_with_body.yaml rename to tests/uranium_tests/h2/h2get_with_body.yaml diff --git a/tests/gold_tests/h2/http2_close_connection.yaml b/tests/uranium_tests/h2/http2_close_connection.yaml similarity index 100% rename from tests/gold_tests/h2/http2_close_connection.yaml rename to tests/uranium_tests/h2/http2_close_connection.yaml diff --git a/tests/gold_tests/h2/http2_flow_control.replay.yaml b/tests/uranium_tests/h2/http2_flow_control.replay.yaml similarity index 100% rename from tests/gold_tests/h2/http2_flow_control.replay.yaml rename to tests/uranium_tests/h2/http2_flow_control.replay.yaml diff --git a/tests/gold_tests/h2/http2_flow_control_chunked.replay.yaml b/tests/uranium_tests/h2/http2_flow_control_chunked.replay.yaml similarity index 100% rename from tests/gold_tests/h2/http2_flow_control_chunked.replay.yaml rename to tests/uranium_tests/h2/http2_flow_control_chunked.replay.yaml diff --git a/tests/gold_tests/h2/replay/http2_client_reset_keeps_session_reading.test.yaml b/tests/uranium_tests/h2/replay/http2_client_reset_keeps_session_reading.test.yaml similarity index 99% rename from tests/gold_tests/h2/replay/http2_client_reset_keeps_session_reading.test.yaml rename to tests/uranium_tests/h2/replay/http2_client_reset_keeps_session_reading.test.yaml index 52e5196d51b..3c72dfaeaa8 100644 --- a/tests/gold_tests/h2/replay/http2_client_reset_keeps_session_reading.test.yaml +++ b/tests/uranium_tests/h2/replay/http2_client_reset_keeps_session_reading.test.yaml @@ -33,7 +33,7 @@ meta: version: '1.0' -autest: +urtest: description: 'Verify an HTTP/2 stream reset does not stop the session from reading' requires: openssl: '1.1.1' diff --git a/tests/gold_tests/h2/replay/http2_concurrent_streams.replay.yaml b/tests/uranium_tests/h2/replay/http2_concurrent_streams.replay.yaml similarity index 100% rename from tests/gold_tests/h2/replay/http2_concurrent_streams.replay.yaml rename to tests/uranium_tests/h2/replay/http2_concurrent_streams.replay.yaml diff --git a/tests/gold_tests/h2/replay/http2_max_active_streams_advisory.replay.yaml b/tests/uranium_tests/h2/replay/http2_max_active_streams_advisory.replay.yaml similarity index 100% rename from tests/gold_tests/h2/replay/http2_max_active_streams_advisory.replay.yaml rename to tests/uranium_tests/h2/replay/http2_max_active_streams_advisory.replay.yaml diff --git a/tests/gold_tests/h2/replay/http2_max_active_streams_enforce.replay.yaml b/tests/uranium_tests/h2/replay/http2_max_active_streams_enforce.replay.yaml similarity index 100% rename from tests/gold_tests/h2/replay/http2_max_active_streams_enforce.replay.yaml rename to tests/uranium_tests/h2/replay/http2_max_active_streams_enforce.replay.yaml diff --git a/tests/gold_tests/h2/replay/http2_settings_ack_stall.replay.yaml b/tests/uranium_tests/h2/replay/http2_settings_ack_stall.replay.yaml similarity index 100% rename from tests/gold_tests/h2/replay/http2_settings_ack_stall.replay.yaml rename to tests/uranium_tests/h2/replay/http2_settings_ack_stall.replay.yaml diff --git a/tests/gold_tests/h2/replay/http2_txn_start_read_gate.replay.yaml b/tests/uranium_tests/h2/replay/http2_txn_start_read_gate.replay.yaml similarity index 100% rename from tests/gold_tests/h2/replay/http2_txn_start_read_gate.replay.yaml rename to tests/uranium_tests/h2/replay/http2_txn_start_read_gate.replay.yaml diff --git a/tests/gold_tests/h2/replay_h2_origin_cert_reverify.yaml b/tests/uranium_tests/h2/replay_h2_origin_cert_reverify.yaml similarity index 100% rename from tests/gold_tests/h2/replay_h2_origin_cert_reverify.yaml rename to tests/uranium_tests/h2/replay_h2_origin_cert_reverify.yaml diff --git a/tests/gold_tests/h2/replay_h2origin/h1-client-h2-origin.yaml b/tests/uranium_tests/h2/replay_h2origin/h1-client-h2-origin.yaml similarity index 100% rename from tests/gold_tests/h2/replay_h2origin/h1-client-h2-origin.yaml rename to tests/uranium_tests/h2/replay_h2origin/h1-client-h2-origin.yaml diff --git a/tests/gold_tests/h2/replay_h2origin/h2-origin.yaml b/tests/uranium_tests/h2/replay_h2origin/h2-origin.yaml similarity index 100% rename from tests/gold_tests/h2/replay_h2origin/h2-origin.yaml rename to tests/uranium_tests/h2/replay_h2origin/h2-origin.yaml diff --git a/tests/gold_tests/h2/replay_rst_stream/http2_rst_stream_client_after_data.yaml b/tests/uranium_tests/h2/replay_rst_stream/http2_rst_stream_client_after_data.yaml similarity index 100% rename from tests/gold_tests/h2/replay_rst_stream/http2_rst_stream_client_after_data.yaml rename to tests/uranium_tests/h2/replay_rst_stream/http2_rst_stream_client_after_data.yaml diff --git a/tests/gold_tests/h2/replay_rst_stream/http2_rst_stream_client_after_headers.yaml b/tests/uranium_tests/h2/replay_rst_stream/http2_rst_stream_client_after_headers.yaml similarity index 100% rename from tests/gold_tests/h2/replay_rst_stream/http2_rst_stream_client_after_headers.yaml rename to tests/uranium_tests/h2/replay_rst_stream/http2_rst_stream_client_after_headers.yaml diff --git a/tests/gold_tests/h2/replay_rst_stream/http2_rst_stream_server_after_headers.yaml b/tests/uranium_tests/h2/replay_rst_stream/http2_rst_stream_server_after_headers.yaml similarity index 100% rename from tests/gold_tests/h2/replay_rst_stream/http2_rst_stream_server_after_headers.yaml rename to tests/uranium_tests/h2/replay_rst_stream/http2_rst_stream_server_after_headers.yaml diff --git a/tests/gold_tests/h2/rules/graceful_shutdown.conf b/tests/uranium_tests/h2/rules/graceful_shutdown.conf similarity index 100% rename from tests/gold_tests/h2/rules/graceful_shutdown.conf rename to tests/uranium_tests/h2/rules/graceful_shutdown.conf diff --git a/tests/gold_tests/h2/rules/huge_resp_hdrs.conf b/tests/uranium_tests/h2/rules/huge_resp_hdrs.conf similarity index 100% rename from tests/gold_tests/h2/rules/huge_resp_hdrs.conf rename to tests/uranium_tests/h2/rules/huge_resp_hdrs.conf diff --git a/tests/uranium_tests/h2/test_h2_origin_cert_reverify.py b/tests/uranium_tests/h2/test_h2_origin_cert_reverify.py new file mode 100644 index 00000000000..d739165d6e7 --- /dev/null +++ b/tests/uranium_tests/h2/test_h2_origin_cert_reverify.py @@ -0,0 +1,99 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_h2_origin_cert_reverify(urtest: UraniumTest) -> None: + ''' + Verify HTTP/2 origin session reuse re-checks the origin certificate name. + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + urtest.Summary = ''' + Verify HTTP/2 origin session reuse re-checks the origin certificate name. + ''' + + ts = urtest.MakeATSProcess("ts", enable_tls=True) + server = urtest.MakeVerifierServerProcess( + "h2-origin", "replay_h2_origin_cert_reverify.yaml", ssl_cert="../tls/ssl/signed-foo.pem", ca_cert="../tls/ssl/signer.pem") + + ts.addDefaultSSLFiles() + ts.addSSLfile("../tls/ssl/signer.pem") + + ts.Disk.records_config.update( + { + 'proxy.config.ssl.server.cert.path': ts.Variables.SSLDir, + 'proxy.config.ssl.server.private_key.path': ts.Variables.SSLDir, + 'proxy.config.ssl.client.CA.cert.path': ts.Variables.SSLDir, + 'proxy.config.ssl.client.CA.cert.filename': 'signer.pem', + 'proxy.config.ssl.client.alpn_protocols': 'h2,http/1.1', + 'proxy.config.ssl.client.verify.server.policy': 'ENFORCED', + 'proxy.config.ssl.client.verify.server.properties': 'ALL', + 'proxy.config.url_remap.pristine_host_hdr': 1, + 'proxy.config.http.server_session_sharing.pool': 'thread', + 'proxy.config.http.server_session_sharing.match': 'ip', + 'proxy.config.exec_thread.autoconfig.enabled': 0, + 'proxy.config.exec_thread.limit': 1, + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'http|ssl_verify', + }) + + ts.Disk.ssl_multicert_yaml.AddLines( + """ + ssl_multicert: + - dest_ip: "*" + ssl_cert_name: server.pem + ssl_key_name: server.key + """.split("\n")) + + ts.Disk.remap_config.AddLine(f"map / https://127.0.0.1:{server.Variables.https_port}/") + + tr = urtest.AddTestRun("Prime an H2 origin connection for foo.com") + tr.Processes.Default.StartBefore(server) + tr.Processes.Default.StartBefore(ts) + tr.MakeCurlCommand(f"-v -H 'Host: foo.com' -H 'uuid: foo' http://127.0.0.1:{ts.Variables.port}/foo", ts=ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.stdout = Testers.ContainsExpression("foo-response", "foo.com should receive the origin response") + tr.StillRunningAfter = server + tr.StillRunningAfter = ts + + tr = urtest.AddTestRun("Reject reuse for bar.com") + tr.MakeCurlCommand(f"-v -H 'Host: bar.com' http://127.0.0.1:{ts.Variables.port}/bar", ts=ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.stdout = Testers.ContainsExpression( + "Could Not Connect", "bar.com should fail certificate name verification") + tr.StillRunningAfter = server + tr.StillRunningAfter = ts + + ts.Disk.diags_log.Content = Testers.ContainsExpression( + r"WARNING: Origin hostname \(bar.com\) not in certificate. Action=Terminate", + "The pooled H2 origin session should be rejected for bar.com.") + urtest.execute() diff --git a/tests/uranium_tests/h2/test_h2_origin_trailers_h1.py b/tests/uranium_tests/h2/test_h2_origin_trailers_h1.py new file mode 100644 index 00000000000..8019d7d6ca6 --- /dev/null +++ b/tests/uranium_tests/h2/test_h2_origin_trailers_h1.py @@ -0,0 +1,113 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_h2_origin_trailers_h1(urtest: UraniumTest) -> None: + ''' + Verify HTTP/2 origin trailers are not forwarded to HTTP/1 clients. + ''' + + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import sys + + urtest.Summary = __doc__ + + urtest.SkipUnless(Condition.HasProxyVerifierVersion('2.8.0')) + + class TestOriginTrailers: + """Verify HTTP/2 origin trailers are handled safely by client protocol.""" + + _replay_file = 'h2_origin_trailers_h1.replay.yaml' + + def __init__(self): + self._h1_server = self._configure_server('h2-origin-h1') + self._h1_ts = self._configure_ats('ts-h1', self._h1_server) + self._configure_h1_client() + + self._h2_server = self._configure_server('h2-origin-h2') + self._h2_ts = self._configure_ats('ts-h2', self._h2_server) + self._configure_h2_client() + + def _configure_server(self, name): + return urtest.MakeVerifierServerProcess(name, self._replay_file) + + def _configure_ats(self, name, server): + ts = urtest.MakeATSProcess(name, enable_tls=True, enable_cache=False) + ts.addDefaultSSLFiles() + + ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'http|http2', + 'proxy.config.exec_thread.autoconfig.enabled': 0, + 'proxy.config.exec_thread.limit': 1, + 'proxy.config.http.server_session_sharing.pool': 'thread', + 'proxy.config.http.server_session_sharing.match': 'ip,sni,cert', + 'proxy.config.ssl.client.alpn_protocols': 'h2,http/1.1', + 'proxy.config.ssl.client.verify.server.policy': 'PERMISSIVE', + 'proxy.config.ssl.server.cert.path': ts.Variables.SSLDir, + 'proxy.config.ssl.server.private_key.path': ts.Variables.SSLDir, + }) + + ts.Disk.ssl_multicert_yaml.AddLines( + """ + ssl_multicert: + - dest_ip: "*" + ssl_cert_name: server.pem + ssl_key_name: server.key + """.split("\n")) + + ts.Disk.remap_config.AddLine(f'map / https://127.0.0.1:{server.Variables.https_port}') + return ts + + def _configure_h1_client(self): + urtest.Setup.CopyAs('h1_trailer_client.py', urtest.RunDirectory) + + tr = urtest.AddTestRun('HTTP/2 origin trailers are dropped for HTTP/1 clients') + tr.Processes.Default.StartBefore(self._h1_server) + tr.Processes.Default.StartBefore(self._h1_ts) + tr.Processes.Default.Command = f'{sys.executable} h1_trailer_client.py 127.0.0.1 {self._h1_ts.Variables.port}' + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.All += Testers.ContainsExpression( + 'No H2 origin trailers were forwarded to the HTTP/1 client.', 'The HTTP/1 response must end at its terminal chunk.') + + def _configure_h2_client(self): + tr = urtest.AddTestRun('HTTP/2 origin trailers are forwarded to HTTP/2 clients') + client = tr.AddVerifierClientProcess('h2-client', self._replay_file, https_ports=[self._h2_ts.Variables.ssl_port]) + client.StartBefore(self._h2_server) + client.StartBefore(self._h2_ts) + client.Streams.All += Testers.ContainsExpression( + 'x-ats-h2-trailer: smuggled', 'The HTTP/2 client must receive the origin trailer.') + + TestOriginTrailers() + urtest.execute() diff --git a/tests/uranium_tests/h2/test_h2disable.py b/tests/uranium_tests/h2/test_h2disable.py new file mode 100644 index 00000000000..22ed01ab028 --- /dev/null +++ b/tests/uranium_tests/h2/test_h2disable.py @@ -0,0 +1,117 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_h2disable(urtest: UraniumTest) -> None: + ''' + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + urtest.Summary = ''' + Test disabling H2 on a per domain basis + ''' + + urtest.SkipUnless(Condition.HasCurlFeature('http2')) + + # Define default ATS + ts = urtest.MakeATSProcess("ts", enable_tls=True) + server = urtest.MakeOriginServer("server") + + request_header = {"headers": "GET / HTTP/1.1\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + server.addResponse("sessionlog.json", request_header, response_header) + + # add ssl materials like key, certificates for the server + ts.addDefaultSSLFiles() + + ts.Disk.remap_config.AddLine('map / http://127.0.0.1:{0}'.format(server.Variables.Port)) + + ts.Disk.ssl_multicert_yaml.AddLines( + """ + ssl_multicert: + - dest_ip: "*" + ssl_cert_name: server.pem + ssl_key_name: server.key + """.split("\n")) + + # Case 1, global config policy=permissive properties=signature + # override for foo.com policy=enforced properties=all + ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 0, + 'proxy.config.diags.debug.tags': 'http|ssl', + 'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir), + 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts.Variables.SSLDir), + 'proxy.config.url_remap.pristine_host_hdr': 1, + 'proxy.config.accept_threads': 1 + }) + + ts.Disk.sni_yaml.AddLines([ + 'sni:', + '- fqdn: bar.com', + ' http2: off', + '- fqdn: "*.foo.com"', + ' http2: off', + ]) + + tr = urtest.AddTestRun("Negotiate-h2") + tr.MakeCurlCommand("-v -k --resolve 'foo.com:{0}:127.0.0.1' https://foo.com:{0}".format(ts.Variables.ssl_port), ts=ts) + tr.ReturnCode = 0 + tr.Processes.Default.StartBefore(server) + tr.Processes.Default.StartBefore(urtest.Processes.ts) + tr.StillRunningAfter = server + tr.StillRunningAfter = ts + tr.Processes.Default.TimeOut = 5 + tr.Processes.Default.Streams.All = Testers.ExcludesExpression("Could Not Connect", "Curl attempt should have succeeded") + tr.Processes.Default.Streams.All += Testers.ContainsExpression("[Uu]sing HTTP/?2", "Curl should negotiate HTTP2") + tr.TimeOut = 5 + + tr2 = urtest.AddTestRun("Do not negotiate h2") + tr2.MakeCurlCommand("-v -k --resolve 'bar.com:{0}:127.0.0.1' https://bar.com:{0}".format(ts.Variables.ssl_port), ts=ts) + tr2.ReturnCode = 0 + tr2.StillRunningAfter = server + tr2.Processes.Default.TimeOut = 5 + tr2.StillRunningAfter = ts + tr2.Processes.Default.Streams.All = Testers.ExcludesExpression("Could Not Connect", "Curl attempt should have succeeded") + tr2.Processes.Default.Streams.All += Testers.ExcludesExpression("[Uu]sing HTTP/?2", "Curl should not negotiate HTTP2") + tr2.TimeOut = 5 + + tr2 = urtest.AddTestRun("Do not negotiate h2") + tr2.MakeCurlCommand("-v -k --resolve 'bob.foo.com:{0}:127.0.0.1' https://bob.foo.com:{0}".format(ts.Variables.ssl_port), ts=ts) + tr2.ReturnCode = 0 + tr2.StillRunningAfter = server + tr2.Processes.Default.TimeOut = 5 + tr2.StillRunningAfter = ts + tr2.Processes.Default.Streams.All = Testers.ExcludesExpression("Could Not Connect", "Curl attempt should have succeeded") + tr2.Processes.Default.Streams.All += Testers.ExcludesExpression("[Uu]sing HTTP/?2", "Curl should not negotiate HTTP2") + tr2.TimeOut = 5 + urtest.execute() diff --git a/tests/uranium_tests/h2/test_h2disable_no_accept_threads.py b/tests/uranium_tests/h2/test_h2disable_no_accept_threads.py new file mode 100644 index 00000000000..d7660fc530f --- /dev/null +++ b/tests/uranium_tests/h2/test_h2disable_no_accept_threads.py @@ -0,0 +1,117 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_h2disable_no_accept_threads(urtest: UraniumTest) -> None: + ''' + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + urtest.Summary = ''' + Test disabling H2 on a per domain basis + ''' + + urtest.SkipUnless(Condition.HasCurlFeature('http2')) + + # Define default ATS + ts = urtest.MakeATSProcess("ts", enable_tls=True) + server = urtest.MakeOriginServer("server") + + request_header = {"headers": "GET / HTTP/1.1\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + server.addResponse("sessionlog.json", request_header, response_header) + + # add ssl materials like key, certificates for the server + ts.addDefaultSSLFiles() + + ts.Disk.remap_config.AddLine('map / http://127.0.0.1:{0}'.format(server.Variables.Port)) + + ts.Disk.ssl_multicert_yaml.AddLines( + """ + ssl_multicert: + - dest_ip: "*" + ssl_cert_name: server.pem + ssl_key_name: server.key + """.split("\n")) + + # Case 1, global config policy=permissive properties=signature + # override for foo.com policy=enforced properties=all + ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 0, + 'proxy.config.diags.debug.tags': 'http|ssl', + 'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir), + 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts.Variables.SSLDir), + 'proxy.config.url_remap.pristine_host_hdr': 1, + 'proxy.config.accept_threads': 0 + }) + + ts.Disk.sni_yaml.AddLines([ + 'sni:', + '- fqdn: bar.com', + ' http2: off', + '- fqdn: "*.foo.com"', + ' http2: off', + ]) + + tr = urtest.AddTestRun("Negotiate-h2") + tr.MakeCurlCommand("-v -k --resolve 'foo.com:{0}:127.0.0.1' https://foo.com:{0}".format(ts.Variables.ssl_port), ts=ts) + tr.ReturnCode = 0 + tr.Processes.Default.StartBefore(server) + tr.Processes.Default.StartBefore(urtest.Processes.ts) + tr.StillRunningAfter = server + tr.StillRunningAfter = ts + tr.Processes.Default.TimeOut = 5 + tr.Processes.Default.Streams.All = Testers.ExcludesExpression("Could Not Connect", "Curl attempt should have succeeded") + tr.Processes.Default.Streams.All += Testers.ContainsExpression("[Uu]sing HTTP/?2", "Curl should negotiate HTTP2") + tr.TimeOut = 5 + + tr2 = urtest.AddTestRun("Do not negotiate h2") + tr2.MakeCurlCommand("-v -k --resolve 'bar.com:{0}:127.0.0.1' https://bar.com:{0}".format(ts.Variables.ssl_port), ts=ts) + tr2.ReturnCode = 0 + tr2.StillRunningAfter = server + tr2.Processes.Default.TimeOut = 5 + tr2.StillRunningAfter = ts + tr2.Processes.Default.Streams.All = Testers.ExcludesExpression("Could Not Connect", "Curl attempt should have succeeded") + tr2.Processes.Default.Streams.All += Testers.ExcludesExpression("[Uu]sing HTTP/?2", "Curl should not negotiate HTTP2") + tr2.TimeOut = 5 + + tr2 = urtest.AddTestRun("Do not negotiate h2") + tr2.MakeCurlCommand("-v -k --resolve 'bob.foo.com:{0}:127.0.0.1' https://bob.foo.com:{0}".format(ts.Variables.ssl_port), ts=ts) + tr2.ReturnCode = 0 + tr2.StillRunningAfter = server + tr2.Processes.Default.TimeOut = 5 + tr2.StillRunningAfter = ts + tr2.Processes.Default.Streams.All = Testers.ExcludesExpression("Could Not Connect", "Curl attempt should have succeeded") + tr2.Processes.Default.Streams.All += Testers.ExcludesExpression("[Uu]sing HTTP/?2", "Curl should not negotiate HTTP2") + tr2.TimeOut = 5 + urtest.execute() diff --git a/tests/uranium_tests/h2/test_h2enable.py b/tests/uranium_tests/h2/test_h2enable.py new file mode 100644 index 00000000000..9321c8d7d0b --- /dev/null +++ b/tests/uranium_tests/h2/test_h2enable.py @@ -0,0 +1,118 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_h2enable(urtest: UraniumTest) -> None: + ''' + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + urtest.Summary = ''' + Test enabling H2 on a per domain basis + ''' + + urtest.SkipUnless(Condition.HasCurlFeature('http2')) + + # Define default ATS + ts = urtest.MakeATSProcess("ts", enable_tls=True) + server = urtest.MakeOriginServer("server") + + request_header = {"headers": "GET / HTTP/1.1\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + server.addResponse("sessionlog.json", request_header, response_header) + + # add ssl materials like key, certificates for the server + ts.addDefaultSSLFiles() + + ts.Disk.remap_config.AddLine('map / http://127.0.0.1:{0}'.format(server.Variables.Port)) + + ts.Disk.ssl_multicert_yaml.AddLines( + """ + ssl_multicert: + - dest_ip: "*" + ssl_cert_name: server.pem + ssl_key_name: server.key + """.split("\n")) + + # Set up port 4444 with HTTP1 only, no HTTP/2 + ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 0, + 'proxy.config.diags.debug.tags': 'http|ssl', + 'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir), + 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts.Variables.SSLDir), + 'proxy.config.url_remap.pristine_host_hdr': 1, + 'proxy.config.accept_threads': 1, + 'proxy.config.http.server_ports': '{0}:ssl:proto=http {1}'.format(ts.Variables.ssl_port, ts.Variables.port) + }) + + ts.Disk.sni_yaml.AddLines([ + 'sni:', + '- fqdn: bar.com', + ' http2: on', + '- fqdn: "*.foo.com"', + ' http2: on', + ]) + + tr = urtest.AddTestRun("Do-not-Negotiate-h2") + tr.MakeCurlCommand("-v -k --ipv4 --resolve 'foo.com:{0}:127.0.0.1' https://foo.com:{0}".format(ts.Variables.ssl_port), ts=ts) + tr.ReturnCode = 0 + tr.Processes.Default.StartBefore(server, ready=When.PortOpen(server.Variables.Port)) + tr.Processes.Default.StartBefore(urtest.Processes.ts) + tr.StillRunningAfter = server + tr.StillRunningAfter = ts + tr.Processes.Default.TimeOut = 10 + tr.Processes.Default.Streams.All = Testers.ExcludesExpression("Could Not Connect", "Curl attempt should have succeeded") + tr.Processes.Default.Streams.All += Testers.ExcludesExpression("[Uu]sing HTTP/?2", "Curl should negotiate HTTP2") + tr.TimeOut = 10 + + tr2 = urtest.AddTestRun("Do negotiate h2") + tr2.MakeCurlCommand("-v -k --ipv4 --resolve 'bar.com:{0}:127.0.0.1' https://bar.com:{0}".format(ts.Variables.ssl_port), ts=ts) + tr2.ReturnCode = 0 + tr2.StillRunningAfter = server + tr2.Processes.Default.TimeOut = 10 + tr2.StillRunningAfter = ts + tr2.Processes.Default.Streams.All = Testers.ExcludesExpression("Could Not Connect", "Curl attempt should have succeeded") + tr2.Processes.Default.Streams.All += Testers.ContainsExpression("[Uu]sing HTTP/?2", "Curl should not negotiate HTTP2") + tr2.TimeOut = 10 + + tr2 = urtest.AddTestRun("Do negotiate h2") + tr2.MakeCurlCommand( + "-v -k --ipv4 --resolve 'bob.foo.com:{0}:127.0.0.1' https://bob.foo.com:{0}".format(ts.Variables.ssl_port), ts=ts) + tr2.ReturnCode = 0 + tr2.StillRunningAfter = server + tr2.Processes.Default.TimeOut = 10 + tr2.StillRunningAfter = ts + tr2.Processes.Default.Streams.All = Testers.ExcludesExpression("Could Not Connect", "Curl attempt should have succeeded") + tr2.Processes.Default.Streams.All += Testers.ContainsExpression("[Uu]sing HTTP/?2", "Curl should not negotiate HTTP2") + tr2.TimeOut = 10 + urtest.execute() diff --git a/tests/uranium_tests/h2/test_h2enable_no_accept_threads.py b/tests/uranium_tests/h2/test_h2enable_no_accept_threads.py new file mode 100644 index 00000000000..f100420cfab --- /dev/null +++ b/tests/uranium_tests/h2/test_h2enable_no_accept_threads.py @@ -0,0 +1,117 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_h2enable_no_accept_threads(urtest: UraniumTest) -> None: + ''' + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + urtest.Summary = ''' + Test enabling H2 on a per domain basis + ''' + + urtest.SkipUnless(Condition.HasCurlFeature('http2')) + + # Define default ATS + ts = urtest.MakeATSProcess("ts", enable_tls=True) + server = urtest.MakeOriginServer("server") + + request_header = {"headers": "GET / HTTP/1.1\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + server.addResponse("sessionlog.json", request_header, response_header) + + # add ssl materials like key, certificates for the server + ts.addDefaultSSLFiles() + + ts.Disk.remap_config.AddLine('map / http://127.0.0.1:{0}'.format(server.Variables.Port)) + + ts.Disk.ssl_multicert_yaml.AddLines( + """ + ssl_multicert: + - dest_ip: "*" + ssl_cert_name: server.pem + ssl_key_name: server.key + """.split("\n")) + + # Set up port 4444 with HTTP1 only, no HTTP/2 + ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 0, + 'proxy.config.diags.debug.tags': 'http|ssl', + 'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir), + 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts.Variables.SSLDir), + 'proxy.config.url_remap.pristine_host_hdr': 1, + 'proxy.config.http.server_ports': '{0}:ssl:proto=http {1}'.format(ts.Variables.ssl_port, ts.Variables.port), + 'proxy.config.accept_threads': 0 + }) + + ts.Disk.sni_yaml.AddLines([ + 'sni:', + '- fqdn: bar.com', + ' http2: on', + '- fqdn: "*.foo.com"', + ' http2: on', + ]) + + tr = urtest.AddTestRun("Do-not-Negotiate-h2") + tr.MakeCurlCommand("-v -k --resolve 'foo.com:{0}:127.0.0.1' https://foo.com:{0}".format(ts.Variables.ssl_port), ts=ts) + tr.ReturnCode = 0 + tr.Processes.Default.StartBefore(server, ready=When.PortOpen(server.Variables.Port)) + tr.Processes.Default.StartBefore(urtest.Processes.ts) + tr.StillRunningAfter = server + tr.StillRunningAfter = ts + tr.Processes.Default.TimeOut = 5 + tr.Processes.Default.Streams.All = Testers.ExcludesExpression("Could Not Connect", "Curl attempt should have succeeded") + tr.Processes.Default.Streams.All += Testers.ExcludesExpression("[Uu]sing HTTP/?2", "Curl should negotiate HTTP2") + tr.TimeOut = 5 + + tr2 = urtest.AddTestRun("Do negotiate h2") + tr2.MakeCurlCommand("-v -k --resolve 'bar.com:{0}:127.0.0.1' https://bar.com:{0}".format(ts.Variables.ssl_port), ts=ts) + tr2.ReturnCode = 0 + tr2.StillRunningAfter = server + tr2.Processes.Default.TimeOut = 5 + tr2.StillRunningAfter = ts + tr2.Processes.Default.Streams.All = Testers.ExcludesExpression("Could Not Connect", "Curl attempt should have succeeded") + tr2.Processes.Default.Streams.All += Testers.ContainsExpression("[Uu]sing HTTP/?2", "Curl should not negotiate HTTP2") + tr2.TimeOut = 5 + + tr2 = urtest.AddTestRun("Do negotiate h2") + tr2.MakeCurlCommand("-v -k --resolve 'bob.foo.com:{0}:127.0.0.1' https://bob.foo.com:{0}".format(ts.Variables.ssl_port), ts=ts) + tr2.ReturnCode = 0 + tr2.StillRunningAfter = server + tr2.Processes.Default.TimeOut = 5 + tr2.StillRunningAfter = ts + tr2.Processes.Default.Streams.All = Testers.ExcludesExpression("Could Not Connect", "Curl attempt should have succeeded") + tr2.Processes.Default.Streams.All += Testers.ContainsExpression("[Uu]sing HTTP/?2", "Curl should not negotiate HTTP2") + tr2.TimeOut = 5 + urtest.execute() diff --git a/tests/uranium_tests/h2/test_h2get_with_body.py b/tests/uranium_tests/h2/test_h2get_with_body.py new file mode 100644 index 00000000000..614a1540483 --- /dev/null +++ b/tests/uranium_tests/h2/test_h2get_with_body.py @@ -0,0 +1,92 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_h2get_with_body(urtest: UraniumTest) -> None: + ''' + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import os + + urtest.Summary = ''' + Test http/2 GET method that has a body + ''' + + urtest.SkipUnless(Condition.HasProxyVerifierVersion('2.8.0')) + + pv_server = urtest.MakeVerifierServerProcess("pv_server", "h2get_with_body.yaml") + + ts = urtest.MakeATSProcess('ts', select_ports=True, enable_tls=True, enable_cache=True) + + ts.addDefaultSSLFiles() + ts.Disk.ssl_multicert_yaml.AddLines( + """ + ssl_multicert: + - dest_ip: "*" + ssl_cert_name: server.pem + ssl_key_name: server.key + """.split("\n")) + ts.Disk.records_config.update( + { + "proxy.config.http.server_ports": f"{ts.Variables.port} {ts.Variables.ssl_port}:ssl", + "proxy.config.http.background_fill_active_timeout": "0", + "proxy.config.http.background_fill_completed_threshold": "0.0", + "proxy.config.http.cache.required_headers": 0, # Force cache + "proxy.config.http.insert_response_via_str": 2, + 'proxy.config.http.server_session_sharing.pool': 'thread', + 'proxy.config.http.server_session_sharing.match': 'ip,sni,cert', + 'proxy.config.exec_thread.autoconfig.enabled': 0, + 'proxy.config.exec_thread.limit': 1, + 'proxy.config.ssl.server.cert.path': f"{ts.Variables.SSLDir}", + 'proxy.config.ssl.server.private_key.path': f"{ts.Variables.SSLDir}", + 'proxy.config.ssl.client.alpn_protocols': 'h2,http/1.1', + 'proxy.config.ssl.client.verify.server.policy': 'PERMISSIVE', + "proxy.config.diags.debug.enabled": 3, + "proxy.config.diags.debug.tags": "http", + }) + + ts.Disk.remap_config.AddLines([f'map / https://127.0.0.1:{pv_server.Variables.https_port}']) + + tr = urtest.AddTestRun() + tr.Processes.Default.StartBefore(pv_server) + tr.Processes.Default.StartBefore(ts) + tr.AddVerifierClientProcess( + "pv_client", "h2get_with_body.yaml", http_ports=[ts.Variables.port], https_ports=[ts.Variables.ssl_port]) + tr.Processes.Default.ReturnCode = 0 + + tr.Processes.Default.Streams.All += Testers.ContainsExpression( + 'Equals Success: Key: "1", Content Data: "body", Value: "server_test"', 'Response check') + + pv_server.Streams.All += Testers.ContainsExpression( + 'Equals Success: Key: "1", Content Data: "body", Value: "client_test"', 'Request check') + urtest.execute() diff --git a/tests/uranium_tests/h2/test_h2origin.py b/tests/uranium_tests/h2/test_h2origin.py new file mode 100644 index 00000000000..27c15c7de8a --- /dev/null +++ b/tests/uranium_tests/h2/test_h2origin.py @@ -0,0 +1,147 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_h2origin(urtest: UraniumTest) -> None: + ''' + Test communication to origin with H2 + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + urtest.Summary = ''' + Test communication to origin with H2 + ''' + + urtest.ContinueOnFail = True + + # + # Communicate to origin with HTTP/2 + # + ts = urtest.MakeATSProcess("ts", enable_tls="true") + + # add ssl materials like key, certificates for the server + ts.addDefaultSSLFiles() + replay_file = "replay_h2origin/" + server = urtest.MakeVerifierServerProcess("h2-origin", replay_file) + server_expect = urtest.MakeVerifierServerProcess("server-expect", "expect_100_continue.yaml") + ts.Disk.records_config.update( + { + 'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir), + 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts.Variables.SSLDir), + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'http', + 'proxy.config.exec_thread.autoconfig.enabled': 0, + # Allow for more parallelism + 'proxy.config.exec_thread.limit': 4, + 'proxy.config.ssl.client.alpn_protocols': 'h2,http/1.1', + # Sticking with thread pool because global pool does not work with h2 + 'proxy.config.http.server_session_sharing.pool': 'thread', + 'proxy.config.http.server_session_sharing.match': 'ip,sni,cert', + 'proxy.config.ssl.client.verify.server.policy': 'PERMISSIVE', + }) + + ts.Disk.remap_config.AddLines( + [ + f'map /expect http://127.0.0.1:{server_expect.Variables.http_port}', + f'map / https://127.0.0.1:{server.Variables.https_port}' + ]) + ts.Disk.ssl_multicert_yaml.AddLines( + """ + ssl_multicert: + - dest_ip: "*" + ssl_cert_name: server.pem + ssl_key_name: server.key + """.split("\n")) + + ts.Disk.logging_yaml.AddLines( + ''' + logging: + formats: + - name: testformat + format: '% % % % %<{uuid}cqh> % % % % % %' + logs: + - mode: ascii + format: testformat + filename: squid + '''.split("\n")) + + tr = urtest.AddTestRun("Test traffic to origin using HTTP/2") + tr.Processes.Default.StartBefore(server) + tr.Processes.Default.StartBefore(ts) + tr.AddVerifierClientProcess("client", replay_file, http_ports=[ts.Variables.port], https_ports=[ts.Variables.ssl_port]) + tr.StillRunningAfter = ts + tr.TimeOut = 60 + + # A regression test for #9857. + tr = urtest.AddTestRun("Test an empty body POST request with an Expect: 100-continue header") + tr.AddVerifierClientProcess( + "client-expect", "expect_100_continue.yaml", http_ports=[ts.Variables.port], https_ports=[ts.Variables.ssl_port]) + tr.Processes.Default.StartBefore(server_expect) + tr.Processes.Default.ReturnCode = 0 + + tr = urtest.AddTestRun("Wait for the squid.log to be written") + timeout = 30 + watcher = tr.Processes.Process("watcher") + watcher.Command = f"sleep {timeout}" + watcher.Ready = When.FileContains(ts.Disk.squid_log.Name, r'16 http/2 http/2') + watcher.TimeOut = timeout + tr.StillRunningAfter = ts + tr.StillRunningAfter = server + tr.TimeOut = timeout + tr.Processes.Default.StartBefore(watcher) + tr.Processes.Default.Command = 'echo await_squid_log' + tr.Processes.Default.ReturnCode = 0 + + # UUIDs 1-4 should be http/1.1 clients and H2 origin + # UUIDs 5-11 and 15-16 should be http/2 clients and H2 origins + ts.Disk.squid_log.Content = Testers.ContainsExpression(" [1-4] http/1.1 http/2", "cases 1-4 request http/1.1") + ts.Disk.squid_log.Content += Testers.ExcludesExpression(" [1-4] http/2 http/2", "cases 1-4 request http/1.1") + ts.Disk.squid_log.Content += Testers.ContainsExpression(" 1[1-4] http/1.1 http/2", "cases 12-14 request http/1.1") + ts.Disk.squid_log.Content += Testers.ExcludesExpression(" 1[2-4] http/2 http/2", "cases 12-14 request http/1.1") + ts.Disk.squid_log.Content += Testers.ContainsExpression(" [5-9] http/2 http/2", "cases 5-11 request http/2") + ts.Disk.squid_log.Content += Testers.ExcludesExpression(" [5-9] http/1.1 http/2", "cases 5-11 request http/2") + ts.Disk.squid_log.Content += Testers.ContainsExpression(" 1[0-1] http/2 http/2", "cases 5-11 request http/2") + ts.Disk.squid_log.Content += Testers.ExcludesExpression(" 1[0-1] http/1.1 http/2", "cases 5-11 request http/2") + ts.Disk.squid_log.Content += Testers.ContainsExpression(" 1[5-6] http/2 http/2", "cases 15-16 request http/2") + ts.Disk.squid_log.Content += Testers.ExcludesExpression(" 1[5-6] http/1.1 http/2", "cases 15-16 request http/2") + + tr = urtest.AddTestRun("Test HTTP method Metrics") + tr.Processes.Default.Command = ( + f"{urtest.Variables.AtsTestToolsDir}/stdout_wait" + " 'traffic_ctl metric get" + " proxy.process.http.get_requests" + + " proxy.process.http.post_requests" + " proxy.process.http.put_requests'" + + f" {urtest.TestDirectory}/gold/http-request-method-metrics.gold") + # Need to copy over the environment so traffic_ctl knows where to find the unix + # domain socket + tr.Processes.Default.Env = ts.Env + tr.Processes.Default.ReturnCode = 0 + tr.StillRunningAfter = ts + urtest.execute() diff --git a/tests/uranium_tests/h2/test_h2origin_single_thread.py b/tests/uranium_tests/h2/test_h2origin_single_thread.py new file mode 100644 index 00000000000..2b277ae149e --- /dev/null +++ b/tests/uranium_tests/h2/test_h2origin_single_thread.py @@ -0,0 +1,113 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_h2origin_single_thread(urtest: UraniumTest) -> None: + ''' + Test communication to origin with H2 + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + urtest.Summary = ''' + Test communication to origin with H2 + ''' + + urtest.ContinueOnFail = True + + # + # Communicate to origin with HTTP/2 + # + ts = urtest.MakeATSProcess("ts", enable_tls="true") + + # add ssl materials like key, certificates for the server + ts.addDefaultSSLFiles() + replay_file = "replay_h2origin" + server = urtest.MakeVerifierServerProcess("h2-origin", replay_file) + ts.Disk.records_config.update( + { + 'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir), + 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts.Variables.SSLDir), + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'http', + 'proxy.config.exec_thread.autoconfig.enabled': 0, + # Limiting ourselves to 1 thread to exercise origin reuse + 'proxy.config.exec_thread.limit': 1, + 'proxy.config.ssl.client.alpn_protocols': 'h2,http/1.1', + # Sticking with hybrid pool because global pool does not work with h2 + 'proxy.config.http.server_session_sharing.pool': 'hybrid', + 'proxy.config.http.server_session_sharing.match': 'hostonly', + 'proxy.config.ssl.client.verify.server.policy': 'PERMISSIVE', + }) + + ts.Disk.remap_config.AddLine('map / https://127.0.0.1:{0}'.format(server.Variables.https_port)) + ts.Disk.ssl_multicert_yaml.AddLines( + """ + ssl_multicert: + - dest_ip: "*" + ssl_cert_name: server.pem + ssl_key_name: server.key + """.split("\n")) + + ts.Disk.logging_yaml.AddLines( + ''' + logging: + formats: + - name: testformat + format: '% % % % %<{uuid}cqh> % % % % % %' + logs: + - mode: ascii + format: testformat + filename: squid + '''.split("\n")) + + tr = urtest.AddTestRun("Test traffic to origin using HTTP/2") + tr.Processes.Default.StartBefore(server) + tr.Processes.Default.StartBefore(ts) + tr.AddVerifierClientProcess("client", replay_file, http_ports=[ts.Variables.port], https_ports=[ts.Variables.ssl_port]) + tr.StillRunningAfter = ts + + # Just a check to flush out the traffic log until we have a clean shutdown for traffic_server + tr = urtest.AddTestRun("Wait for the access log to write out") + tr.DelayStart = 10 + tr.StillRunningAfter = ts + tr.StillRunningAfter = server + tr.Processes.Default.Command = 'ls' + tr.Processes.Default.ReturnCode = 0 + + # UUIDs 1-4 should be http/1.1 clients and H2 origin + # UUIDs 5-9 should be http/2 clients and H2 origins + ts.Disk.squid_log.Content = Testers.ContainsExpression(" [1-4] http/1.1 http/2", "cases 1-4 request http/1.1") + ts.Disk.squid_log.Content += Testers.ExcludesExpression(" [1-4] http/2 http/2", "cases 1-4 request http/1.1") + ts.Disk.squid_log.Content += Testers.ContainsExpression(" [5-9] http/2 http/2", "cases 5-9 request http/2") + ts.Disk.squid_log.Content += Testers.ExcludesExpression(" [5-9] http/1.1 http/2", "cases 5-9 request http/2") + ts.Disk.squid_log.Content += Testers.ContainsExpression(" http/2 1 1 1 [2-9]", "At least one case of origin reuse") + urtest.execute() diff --git a/tests/uranium_tests/h2/test_h2spec.py b/tests/uranium_tests/h2/test_h2spec.py new file mode 100644 index 00000000000..ac8f96c983e --- /dev/null +++ b/tests/uranium_tests/h2/test_h2spec.py @@ -0,0 +1,95 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_h2spec(urtest: UraniumTest) -> None: + ''' + Test HTTP/2 with h2spec + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + urtest.Summary = ''' + Test HTTP/2 with httpspec + ''' + + urtest.SkipUnless(Condition.HasProgram("h2spec", "h2spec need to be installed on system for this test to work"),) + urtest.ContinueOnFail = True + + # ---- + # Setup httpbin Origin Server + # ---- + httpbin = urtest.MakeHttpBinServer("httpbin") + + # ---- + # Setup ATS. Disable the cache to simplify the test. + # ---- + ts = urtest.MakeATSProcess("ts", enable_tls=True, enable_cache=False) + + # add ssl materials like key, certificates for the server + ts.addDefaultSSLFiles() + + ts.Disk.remap_config.AddLine('map / http://127.0.0.1:{0}'.format(httpbin.Variables.Port)) + ts.Disk.ssl_multicert_yaml.AddLines( + """ + ssl_multicert: + - dest_ip: "*" + ssl_cert_name: server.pem + ssl_key_name: server.key + """.split("\n")) + ts.Disk.records_config.update( + { + 'proxy.config.http.insert_request_via_str': 1, + 'proxy.config.http.insert_response_via_str': 1, + 'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir), + 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts.Variables.SSLDir), + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'http', + }) + + # ---- + # Test Cases + # ---- + + # In case you need to disable some of the tests, you can specify sections like http2/6.4. + h2spec_targets = "generic http2/3 http2/4 http2/5 http2/6 http2/7 http2/8 hpack" + + test_run = urtest.AddTestRun() + test_run.Processes.Default.Command = 'h2spec {0} -t -k --timeout 10 -p {1}'.format(h2spec_targets, ts.Variables.ssl_port) + test_run.Processes.Default.ReturnCode = 0 + test_run.Processes.Default.StartBefore(httpbin) + test_run.Processes.Default.StartBefore(urtest.Processes.ts) + test_run.Processes.Default.Streams.stdout = "gold/h2spec_stdout.gold" + test_run.StillRunningAfter = httpbin + + # Over riding the built in ERROR check since we expect some error cases + ts.Disk.diags_log.Content = Testers.ContainsExpression("ERROR: HTTP/2", "h2spec tests should have error log") + urtest.execute() diff --git a/tests/uranium_tests/h2/test_http2.py b/tests/uranium_tests/h2/test_http2.py new file mode 100644 index 00000000000..ad1c1c86a83 --- /dev/null +++ b/tests/uranium_tests/h2/test_http2.py @@ -0,0 +1,308 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_http2(urtest: UraniumTest) -> None: + ''' + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import os + import sys + + urtest.Summary = ''' + Test a basic remap of a http/2 connection + ''' + + urtest.SkipUnless(Condition.HasCurlFeature('http2')) + urtest.ContinueOnFail = True + + # ---- + # Setup Origin Server + # ---- + server = urtest.MakeOriginServer("server") + + # For Test Case 1 & 5 - / + server.addResponse( + "sessionlog.json", { + "headers": "GET / HTTP/1.1\r\nHost: www.example.com\r\n\r\n", + "timestamp": "1469733493.993", + "body": "" + }, { + "headers": "HTTP/1.1 200 OK\r\nServer: microserver\r\nConnection: close\r\n\r\n", + "timestamp": "1469733493.993", + "body": "" + }) + + # For Test Case 2 - /bigfile + # Add info for the large H2 download test + server.addResponse( + "sessionlog.json", { + "headers": "GET /bigfile HTTP/1.1\r\nHost: www.example.com\r\n\r\n", + "timestamp": "1469733493.993", + "body": "" + }, { + "headers": + "HTTP/1.1 200 OK\r\nServer: microserver\r\nConnection: close\r\nCache-Control: max-age=3600\r\nContent-Length: 191414\r\n\r\n", + "timestamp": "1469733493.993", + "body": "" + }) + + # For Test Case 3 - /test2 + server.addResponse( + "sessionlog.json", { + "headers": "GET /test2 HTTP/1.1\r\nHost: www.example.com\r\n\r\n", + "timestamp": "1469733493.993", + "body": "" + }, { + "headers": "HTTP/1.1 200 OK\r\nServer: microserver\r\nTransfer-Encoding: chunked\r\nConnection: close\r\n\r\n", + "timestamp": "1469733493.993", + "body": "" + }) + + # For Test Case 6 - /postchunked + post_body = "12345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890" + server.addResponse( + "sessionlog.json", { + "headers": "POST /postchunked HTTP/1.1\r\nHost: www.example.com\r\n\r\n", + "timestamp": "1469733493.993", + "body": post_body + }, { + "headers": "HTTP/1.1 200 OK\r\nServer: microserver\r\nConnection: close\r\nContent-Length: 10\r\n\r\n", + "timestamp": "1469733493.993", + "body": "0123456789" + }) + + # For Test Case 7 - /bigpostchunked + # Make a post body that will be split across at least two frames + big_post_body = "0123456789" * 131070 + server.addResponse( + "sessionlog.json", { + "headers": "POST /bigpostchunked HTTP/1.1\r\nHost: www.example.com\r\n\r\n", + "timestamp": "1469733493.993", + "body": big_post_body + }, { + "headers": "HTTP/1.1 200 OK\r\nServer: microserver\r\nConnection: close\r\nContent-Length: 10\r\n\r\n", + "timestamp": "1469733493.993", + "body": "0123456789" + }) + + big_post_body_file = open(os.path.join(urtest.RunDirectory, "big_post_body"), "w") + big_post_body_file.write(big_post_body) + big_post_body_file.close() + + # For Test Case 8 - /huge_resp_hdrs + server.addResponse( + "sessionlog.json", { + "headers": "GET /huge_resp_hdrs HTTP/1.1\r\nHost: www.example.com\r\n\r\n", + "timestamp": "1469733493.993", + "body": "" + }, { + "headers": "HTTP/1.1 200 OK\r\nServer: microserver\r\nConnection: close\r\nContent-Length: 6\r\n\r\n", + "timestamp": "1469733493.993", + "body": "200 OK" + }) + + # For Test Case 9 - /status/204 + server.addResponse( + "sessionlog.json", { + "headers": "GET /status/204 HTTP/1.1\r\nHost: www.example.com\r\n\r\n", + "timestamp": "1469733493.993", + "body": "" + }, { + "headers": "HTTP/1.1 204 No Content\r\nServer: microserver\r\nConnection: close\r\n\r\n", + "timestamp": "1469733493.993", + "body": "" + }) + + # ---- + # Setup ATS + # ---- + ts = urtest.MakeATSProcess("ts", enable_tls=True, enable_cache=False) + + # add ssl materials like key, certificates for the server + ts.addDefaultSSLFiles() + + ts.Setup.CopyAs('rules/huge_resp_hdrs.conf', urtest.RunDirectory) + ts.Disk.remap_config.AddLine( + 'map /huge_resp_hdrs http://127.0.0.1:{0}/huge_resp_hdrs @plugin=header_rewrite.so @pparam={1}/huge_resp_hdrs.conf '.format( + server.Variables.Port, urtest.RunDirectory)) + + ts.Disk.remap_config.AddLine('map / http://127.0.0.1:{0}'.format(server.Variables.Port)) + + ts.Disk.ssl_multicert_yaml.AddLines( + """ + ssl_multicert: + - dest_ip: "*" + ssl_cert_name: server.pem + ssl_key_name: server.key + """.split("\n")) + ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'http', + 'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir), + 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts.Variables.SSLDir), + 'proxy.config.http2.active_timeout_in': 3, + 'proxy.config.http2.max_concurrent_streams_in': 65535, + }) + + ts.Setup.CopyAs('h2client.py', urtest.RunDirectory) + ts.Setup.CopyAs('h2active_timeout.py', urtest.RunDirectory) + ts.Setup.CopyAs('clients/h2_extension_settings.py', urtest.RunDirectory) + + settings_limit_ts = urtest.MakeATSProcess("ts_settings_limit", enable_tls=True, enable_cache=False) + settings_limit_ts.addDefaultSSLFiles() + settings_limit_ts.Setup.CopyAs('clients/h2_max_settings_per_minute.py', urtest.RunDirectory) + settings_limit_ts.Disk.records_config.update( + { + 'proxy.config.ssl.server.cert.path': f'{settings_limit_ts.Variables.SSLDir}', + 'proxy.config.ssl.server.private_key.path': f'{settings_limit_ts.Variables.SSLDir}', + 'proxy.config.http2.max_settings_per_frame': -1, + 'proxy.config.http2.max_settings_per_minute': 1, + 'proxy.config.http2.max_settings_frames_per_minute': 100, + }) + settings_limit_ts.Disk.ssl_multicert_yaml.AddLines( + """ + ssl_multicert: + - dest_ip: "*" + ssl_cert_name: server.pem + ssl_key_name: server.key + """.split("\n")) + settings_limit_ts.Disk.diags_log.Content = Testers.ContainsExpression( + "ERROR: HTTP/2 connection error.*recv settings too frequent setting changes", + "ATS should log the SETTINGS limit connection error.") + + # ---- + # Test Cases + # ---- + + # Test Case 1: basic H2 interaction + tr = urtest.AddTestRun("basic H2 interaction") + tr.Processes.Default.Command = f'{sys.executable} h2client.py {ts.Variables.ssl_port} / --verify_default_body' + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.StartBefore(server) + tr.Processes.Default.StartBefore(urtest.Processes.ts) + tr.Processes.Default.Streams.stdout = "gold/remap-200.gold" + tr.StillRunningAfter = server + + # Test Case 2: Make sure all the big file gets back. Regression test for issue 1646 + tr = urtest.AddTestRun("big file download") + tr.Processes.Default.Command = f'{sys.executable} h2client.py {ts.Variables.ssl_port} /bigfile --repeat 2 --verify_default_body' + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.stdout = "gold/bigfile.gold" + tr.StillRunningAfter = server + + # Test Case 3: Chunked content + tr = urtest.AddTestRun("chunked content") + tr.Processes.Default.Command = f'{sys.executable} h2client.py {ts.Variables.ssl_port} /test2 --print_body' + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.stdout = "gold/chunked.gold" + tr.StillRunningAfter = server + + # NOTE: Skipping this test run because traffic-replay doesn't currently support H2 + # Test Case 4: Multiple request + # client_path = os.path.join(Test.Variables.AtsTestToolsDir, 'traffic-replay/') + # tr = Test.AddTestRun("multiple request") + # tr.Processes.Default.Command = \ + # (f"{sys.executable} {client_path} -type h2 -log_dir {server.Variables.DataDir} " + # f"-port {ts.Variables.port} -host '127.0.0.1' -s_port {ts.Variables.ssl_port} -v -colorize False") + # tr.Processes.Default.ReturnCode = 0 + # tr.Processes.Default.Streams.stdout = "gold/replay.gold" + # tr.StillRunningAfter = server + + # Test Case 5: h2_active_timeout + tr = urtest.AddTestRun("h2_active_timeout") + tr.Processes.Default.Command = f'{sys.executable} h2active_timeout.py {ts.Variables.ssl_port} / 4' + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.All = "gold/active_timeout.gold" + tr.StillRunningAfter = server + + # Test Case 6: Post with chunked body + # While HTTP/2 does not support Transfer-encoding we pass that into curl to encourage it to not set the content length + # on the post body + tr = urtest.AddTestRun("post with chunked body") + tr.MakeCurlCommand( + '-s -k -H "Transfer-Encoding: chunked" -d "{0}" https://127.0.0.1:{1}/postchunked'.format(post_body, ts.Variables.ssl_port), + ts=ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.All = "gold/post_chunked.gold" + tr.StillRunningAfter = server + + # Test Case 7: Post with big chunked body + # While HTTP/2 does not support Transfer-encoding we pass that into curl to encourage it to not set the content length + # on the post body + tr = urtest.AddTestRun("post with big chunked body") + tr.MakeCurlCommand( + '-s -k -H "Transfer-Encoding: chunked" -d @big_post_body https://127.0.0.1:{0}/bigpostchunked'.format( + ts.Variables.ssl_port), + ts=ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.All = "gold/post_chunked.gold" + tr.StillRunningAfter = server + + # Test Case 8: Huge response header + tr = urtest.AddTestRun("huge response header") + # Different versions of curl have "bytes data" at various places in the output. + # Normalize them by simply filtering out those lines since they are not + # important to this test. + tr.MakeCurlCommand(f'-vs -k --http2 https://127.0.0.1:{ts.Variables.ssl_port}/huge_resp_hdrs |& grep -v "bytes data"', ts=ts) + tr.Processes.Default.ReturnCode = 0 + # Different versions of curl will have different cases for HTTP/2 field names. + tr.Processes.Default.Streams.stdout = Testers.GoldFile("gold/http2_8_stdout.gold", case_insensitive=True) + tr.StillRunningAfter = server + + # Test Case 9: Header Only Response - e.g. 204 + tr = urtest.AddTestRun("header only response") + tr.MakeCurlCommand('-vs -k --http2 https://127.0.0.1:{0}/status/204'.format(ts.Variables.ssl_port), ts=ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.stdout = "gold/http2_9_stdout.gold" + # Different versions of curl will have different cases for HTTP/2 field names. + tr.Processes.Default.Streams.stderr = Testers.GoldFile("gold/http2_9_stderr.gold", case_insensitive=True) + tr.StillRunningAfter = server + + # Test Case 10: max_settings_per_minute with max_settings_per_frame disabled + tr = urtest.AddTestRun("max_settings_per_minute with max_settings_per_frame disabled") + tr.Processes.Default.Command = f'{sys.executable} h2_max_settings_per_minute.py {settings_limit_ts.Variables.ssl_port}' + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.StartBefore(settings_limit_ts) + tr.Processes.Default.Streams.stdout += Testers.ContainsExpression( + "Received GOAWAY with error code 11", "Received ENHANCE_YOUR_CALM GOAWAY.") + + # Test Case 11: Extension settings fit within the default SETTINGS limits. + tr = urtest.AddTestRun("HTTP/2 extension settings") + tr.Processes.Default.Command = f'{sys.executable} h2_extension_settings.py {ts.Variables.ssl_port}' + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.stdout = Testers.ContainsExpression( + "Received 200 response", "The request following the extension settings should succeed.") + tr.StillRunningAfter = server + urtest.execute() diff --git a/tests/uranium_tests/h2/test_http2_close_connection.py b/tests/uranium_tests/h2/test_http2_close_connection.py new file mode 100644 index 00000000000..9299b2a3795 --- /dev/null +++ b/tests/uranium_tests/h2/test_http2_close_connection.py @@ -0,0 +1,91 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_http2_close_connection(urtest: UraniumTest) -> None: + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import os + + urtest.Summary = ''' + Check whether the HTTP/2 grace shutdown works as intended. + ''' + + urtest.SkipUnless(Condition.HasProxyVerifierVersion('2.8.0')) + + pv_server = urtest.MakeVerifierServerProcess("pv_server", "http2_close_connection.yaml") + + ts = urtest.MakeATSProcess('ts', select_ports=True, enable_tls=True) + + ts.addDefaultSSLFiles() + ts.Disk.ssl_multicert_yaml.AddLines( + """ + ssl_multicert: + - dest_ip: "*" + ssl_cert_name: server.pem + ssl_key_name: server.key + """.split("\n")) + ts.Disk.records_config.update( + { + "proxy.config.http.server_ports": f"{ts.Variables.port} {ts.Variables.ssl_port}:ssl", + 'proxy.config.ssl.server.cert.path': f'{ts.Variables.SSLDir}', + 'proxy.config.ssl.server.private_key.path': f'{ts.Variables.SSLDir}', + 'proxy.config.ssl.client.verify.server.policy': 'PERMISSIVE', + 'proxy.config.diags.debug.enabled': 3, + 'proxy.config.diags.debug.tags': 'http', + 'proxy.config.exec_thread.autoconfig.enabled': 0, + 'proxy.config.exec_thread.limit': 4, + 'proxy.config.ssl.client.alpn_protocols': 'h2,http/1.1', + 'proxy.config.http.server_session_sharing.pool': 'thread', + 'proxy.config.http.server_session_sharing.match': 'ip,sni,cert', + }) + + ts.Disk.remap_config.AddLines([f'map / https://127.0.0.1:{pv_server.Variables.https_port}']) + + urtest.PrepareTestPlugin(os.path.join(urtest.Variables.AtsTestPluginsDir, 'http2_close_connection.so'), ts) + + tr = urtest.AddTestRun() + tr.Processes.Default.StartBefore(pv_server) + tr.Processes.Default.StartBefore(ts) + tr.AddVerifierClientProcess( + "pv_client", "http2_close_connection.yaml", http_ports=[ts.Variables.port], https_ports=[ts.Variables.ssl_port]) + tr.Processes.Default.ReturnCode = 0 + + tr.Processes.Default.Streams.All += Testers.ContainsExpression( + 'Equals Success: Key: "1", Content Data: "body", Value: "server_test_1"', 'Response check') + tr.Processes.Default.Streams.All += Testers.ContainsExpression( + 'Received GOAWAY frame with last stream id 2147483647, error code 0', 'initial GOAWAY frame with last stream id set to max') + tr.Processes.Default.Streams.All += Testers.ContainsExpression( + 'Received GOAWAY frame with last stream id 1, error code 0', 'updated GOAWAY frame with last stream id set to 1') + + pv_server.Streams.All += Testers.ExcludesExpression('server_test_2', 'Only one response should be sent') + urtest.execute() diff --git a/tests/uranium_tests/h2/test_http2_concurrent_streams.py b/tests/uranium_tests/h2/test_http2_concurrent_streams.py new file mode 100644 index 00000000000..382c01b3bfe --- /dev/null +++ b/tests/uranium_tests/h2/test_http2_concurrent_streams.py @@ -0,0 +1,83 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_http2_concurrent_streams(urtest: UraniumTest) -> None: + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import sys + + urtest.Sumary = ''' + Verify Concurrent Streams Handling + ''' + + class Http2ConcurrentStreamsTest: + replayFile = "replay/http2_concurrent_streams.replay.yaml" + + def __init__(self): + self.__setupOriginServer() + self.__setupTS() + + def __setupOriginServer(self): + self._server = urtest.MakeVerifierServerProcess("verifier-server", self.replayFile) + + def __setupTS(self): + self._ts = urtest.MakeATSProcess(f"ts", enable_tls=True, enable_cache=True) + self._ts.addDefaultSSLFiles() + self._ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'http2', + 'proxy.config.ssl.server.cert.path': f"{self._ts.Variables.SSLDir}", + 'proxy.config.ssl.server.private_key.path': f"{self._ts.Variables.SSLDir}", + 'proxy.config.http.insert_response_via_str': 2, + }) + self._ts.Disk.remap_config.AddLine(f"map / http://127.0.0.1:{self._server.Variables.http_port}") + self._ts.Disk.ssl_multicert_yaml.AddLines( + """ + ssl_multicert: + - dest_ip: "*" + ssl_cert_name: server.pem + ssl_key_name: server.key + """.split("\n")) + + def run(self): + tr = urtest.AddTestRun() + tr.AddVerifierClientProcess( + "verifier-client", self.replayFile, http_ports=[self._ts.Variables.port], https_ports=[self._ts.Variables.ssl_port]) + tr.Processes.Default.StartBefore(self._ts) + tr.Processes.Default.StartBefore(self._server) + tr.StillRunningAfter = self._ts + tr.StillRunningAfter = self._server + + Http2ConcurrentStreamsTest().run() + urtest.execute() diff --git a/tests/uranium_tests/h2/test_http2_crlf_header_validation.py b/tests/uranium_tests/h2/test_http2_crlf_header_validation.py new file mode 100644 index 00000000000..6c3e245ce3f --- /dev/null +++ b/tests/uranium_tests/h2/test_http2_crlf_header_validation.py @@ -0,0 +1,118 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_http2_crlf_header_validation(urtest: UraniumTest) -> None: + ''' + Verify HTTP/2 requests with NUL, CR, or LF in header values are rejected. + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import sys + + urtest.Summary = 'HTTP/2 requests with NUL, CR, or LF in header values are rejected' + urtest.ContinueOnFail = True + + MALFORMED_CLIENT = 'malformed_h2_request_client.py' + + CONTROL_CHARACTER_CASES = ( + { + 'scenario': 'crlf-in-header-value', + 'description': 'HTTP/2 request with CRLF in header value', + }, + { + 'scenario': 'cr-in-header-value', + 'description': 'HTTP/2 request with bare CR in header value', + }, + { + 'scenario': 'lf-in-header-value', + 'description': 'HTTP/2 request with bare LF in header value', + }, + { + 'scenario': 'nul-in-header-value', + 'description': 'HTTP/2 request with NUL in header value', + }, + ) + + server = urtest.MakeOriginServer('server') + server.Streams.All = Testers.ExcludesExpression( + 'x-injected', + 'Malformed control-character requests must not reach the origin server.', + ) + server.Streams.All += Testers.ExcludesExpression( + 'malformed-nul-value', + 'Malformed NUL request must not reach the origin server.', + ) + server.addResponse( + 'sessionlog.json', { + 'headers': 'GET / HTTP/1.1\r\nHost: www.example.com\r\n\r\n', + 'timestamp': '1469733493.993', + 'body': '', + }, { + 'headers': 'HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n', + 'timestamp': '1469733493.993', + 'body': '', + }) + + ts = urtest.MakeATSProcess('ts', enable_tls=True, enable_cache=False) + ts.addDefaultSSLFiles() + ts.Disk.ssl_multicert_yaml.AddLines( + """ + ssl_multicert: + - dest_ip: "*" + ssl_cert_name: server.pem + ssl_key_name: server.key + """.split('\n')) + ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'http', + 'proxy.config.ssl.server.cert.path': ts.Variables.SSLDir, + 'proxy.config.ssl.server.private_key.path': ts.Variables.SSLDir, + }) + ts.Disk.remap_config.AddLine(f'map / http://127.0.0.1:{server.Variables.Port}/') + + urtest.Setup.CopyAs('../connect/' + MALFORMED_CLIENT, urtest.RunDirectory) + + for i, case in enumerate(CONTROL_CHARACTER_CASES): + tr = urtest.AddTestRun(case['description']) + tr.Processes.Default.Command = (f'{sys.executable} {MALFORMED_CLIENT} {ts.Variables.ssl_port} {case["scenario"]}') + tr.Processes.Default.ReturnCode = 0 + if i == 0: + tr.Processes.Default.StartBefore(server) + tr.Processes.Default.StartBefore(ts) + tr.StillRunningAfter = ts + tr.Processes.Default.Streams.stdout += Testers.ContainsExpression( + r'Received (RST_STREAM|GOAWAY|HTTP/2 response with status 4\d\d)', + f'ATS should reject the request: {case["description"]}', + ) + urtest.execute() diff --git a/tests/uranium_tests/h2/test_http2_empty_data_frame.py b/tests/uranium_tests/h2/test_http2_empty_data_frame.py new file mode 100644 index 00000000000..7ccb9a60c4c --- /dev/null +++ b/tests/uranium_tests/h2/test_http2_empty_data_frame.py @@ -0,0 +1,96 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_http2_empty_data_frame(urtest: UraniumTest) -> None: + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import sys + + urtest.Sumary = ''' + Verify Empty DATA Frame Handling + ''' + + class Http2EmptyDataFrameTest: + + def __init__(self): + self.__setupOriginServer() + self.__setupTS() + self.__setupClient() + + def __setupOriginServer(self): + self._server = urtest.MakeHttpBinServer("httpbin") + + def __setupTS(self): + self._ts = urtest.MakeATSProcess(f"ts", enable_tls=True, enable_cache=True) + self._ts.addDefaultSSLFiles() + self._ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'http2', + 'proxy.config.ssl.server.cert.path': f"{self._ts.Variables.SSLDir}", + 'proxy.config.ssl.server.private_key.path': f"{self._ts.Variables.SSLDir}", + 'proxy.config.http.insert_response_via_str': 2, + 'proxy.config.http2.active_timeout_in': 3, + 'proxy.config.http2.stream_error_rate_threshold': 0.1 # default + }) + self._ts.Disk.remap_config.AddLine(f"map / http://127.0.0.1:{self._server.Variables.Port}") + self._ts.Disk.ssl_multicert_yaml.AddLines( + """ + ssl_multicert: + - dest_ip: "*" + ssl_cert_name: server.pem + ssl_key_name: server.key + """.split("\n")) + + def __setupClient(self): + self._ts.Setup.CopyAs("clients/h2empty_data_frame.py", urtest.RunDirectory) + + def run(self): + tr = urtest.AddTestRun("warm-up cache") + + tr.Processes.Default.StartBefore(self._ts) + tr.Processes.Default.StartBefore(self._server) + + # warm up the cache + tr.Processes.Default.Command = f"{sys.executable} h2empty_data_frame.py {self._ts.Variables.ssl_port} /cache/10 -n 1" + tr.Processes.Default.ReturnCode = 0 + + # verify 20 streams doesn't hit `proxy.config.http2.stream_error_rate_threshold` + tr = urtest.AddTestRun("open 20 streams") + tr.Processes.Default.Command = f"{sys.executable} h2empty_data_frame.py {self._ts.Variables.ssl_port} /cache/10 -n 20" + tr.Processes.Default.ReturnCode = 0 + + tr.StillRunningAfter = self._ts + + Http2EmptyDataFrameTest().run() + urtest.execute() diff --git a/tests/uranium_tests/h2/test_http2_flow_control.py b/tests/uranium_tests/h2/test_http2_flow_control.py new file mode 100644 index 00000000000..c0c3abb5fa0 --- /dev/null +++ b/tests/uranium_tests/h2/test_http2_flow_control.py @@ -0,0 +1,448 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_http2_flow_control(urtest: UraniumTest) -> None: + """Verify HTTP/2 flow control behavior.""" + + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import re + import sys + from enum import Enum + from typing import List, Optional + + urtest.Summary = __doc__ + + class Http2FlowControlTest: + """Define an object to test HTTP/2 flow control behavior.""" + + _replay_file: str = 'http2_flow_control.replay.yaml' + _replay_chunked_file: str = 'http2_flow_control_chunked.replay.yaml' + _valid_policy_values: List[int] = list(range(0, 3)) + _flow_control_policy: Optional[int] = None + _flow_control_policy_is_malformed: bool = False + + _default_initial_window_size: int = 65535 + _default_max_concurrent_streams: int = 100 + _default_flow_control_policy: int = 0 + + _dns_counter: int = 0 + _server_counter: int = 0 + _ts_counter: int = 0 + _client_counter: int = 0 + + IS_OUTBOUND = True + IS_INBOUND = False + + IS_HTTP2_TO_ORIGIN = True + IS_HTTP1_TO_ORIGIN = False + + class ServerType(Enum): + """Define the type of server to use in a TestRun.""" + + HTTP1_CONTENT_LENGTH = 0 + HTTP1_CHUNKED = 1 + HTTP2 = 2 + + def __init__( + self, + description: str, + initial_window_size: Optional[int] = None, + max_concurrent_streams: Optional[int] = None, + flow_control_policy: Optional[int] = None): + """Declare the various test Processes. + + :param description: A description of the test. + + :param initial_window_size: The value with which to configure the + proxy.config.http2.initial_window_size_(in|out) ATS parameter in the + records.yaml file. If the paramenter is None, then no window size + will be explicitly set and ATS will use the default value. + + :param max_concurrent_streams: The value with which to configure the + proxy.config.http2.max_concurrent_streams_(in|out) ATS parameter in the + records.yaml file. If the paramenter is None, then no window size + will be explicitly set and ATS will use the default value. + + :param flow_control_policy: The value with which to configure the + proxy.config.http2.flow_control.policy_(in|out) ATS parameter the + records.yaml file. If the paramenter is None, then no policy + configuration will be explicitly set and ATS will use the default + value. + """ + self._description = description + + self._initial_window_size = initial_window_size + self._expected_initial_stream_window_size = ( + initial_window_size if initial_window_size is not None else self._default_initial_window_size) + + self._max_concurrent_streams = max_concurrent_streams + self._expected_max_concurrent_streams = ( + max_concurrent_streams if max_concurrent_streams is not None else self._default_max_concurrent_streams) + + self._flow_control_policy = flow_control_policy + self._expected_flow_control_policy = ( + flow_control_policy if flow_control_policy is not None else self._default_flow_control_policy) + + self._flow_control_policy_is_malformed = ( + self._flow_control_policy is not None and self._flow_control_policy not in self._valid_policy_values) + + def _configure_dns(self, tr: 'TestRun') -> 'Process': + """Configure the DNS.""" + dns = tr.MakeDNServer(f'dns-{Http2FlowControlTest._dns_counter}') + Http2FlowControlTest._dns_counter += 1 + return dns + + def _configure_server(self, tr: 'TestRun', server_type: ServerType) -> Optional['Process']: + """Configure the test server.""" + if self._flow_control_policy_is_malformed: + return None + if server_type == self.ServerType.HTTP1_CHUNKED: + replay_file = self._replay_chunked_file + else: + replay_file = self._replay_file + + server = tr.AddVerifierServerProcess(f'server-{Http2FlowControlTest._server_counter}', replay_file) + Http2FlowControlTest._server_counter += 1 + return server + + def _configure_trafficserver(self, tr: 'TestRun', is_outbound: bool, server_type: ServerType) -> 'Process': + """Configure a Traffic Server process.""" + ts = tr.MakeATSProcess(f'ts-{Http2FlowControlTest._ts_counter}', enable_tls=True) + Http2FlowControlTest._ts_counter += 1 + + ts.addDefaultSSLFiles() + ts.Disk.records_config.update( + { + 'proxy.config.ssl.server.cert.path': f'{ts.Variables.SSLDir}', + 'proxy.config.ssl.server.private_key.path': f'{ts.Variables.SSLDir}', + 'proxy.config.ssl.client.verify.server.policy': 'PERMISSIVE', + 'proxy.config.dns.nameservers': '127.0.0.1:{0}'.format(self._dns.Variables.Port), + 'proxy.config.dns.resolv_conf': 'NULL', + 'proxy.config.http.insert_age_in_response': 0, + 'proxy.config.diags.debug.enabled': 3, + 'proxy.config.diags.debug.tags': 'http', + }) + + if server_type == self.ServerType.HTTP2: + ts.Disk.records_config.update({ + 'proxy.config.ssl.client.alpn_protocols': 'h2,http/1.1', + }) + + if self._initial_window_size is not None: + if is_outbound: + configuration = 'proxy.config.http2.initial_window_size_out' + else: + configuration = 'proxy.config.http2.initial_window_size_in' + ts.Disk.records_config.update({ + configuration: self._initial_window_size, + }) + + if self._flow_control_policy is not None: + if is_outbound: + configuration = 'proxy.config.http2.flow_control.policy_out' + else: + configuration = 'proxy.config.http2.flow_control.policy_in' + ts.Disk.records_config.update({ + configuration: self._flow_control_policy, + }) + + if self._max_concurrent_streams is not None: + if is_outbound: + configuration = 'proxy.config.http2.max_concurrent_streams_out' + else: + configuration = 'proxy.config.http2.max_concurrent_streams_in' + ts.Disk.records_config.update({ + configuration: self._max_concurrent_streams, + }) + + ts.Disk.ssl_multicert_yaml.AddLines( + """ + ssl_multicert: + - dest_ip: "*" + ssl_cert_name: server.pem + ssl_key_name: server.key + """.split("\n")) + + if self._server is not None: + ts.Disk.remap_config.AddLine(f'map / https://127.0.0.1:{self._server.Variables.https_port}') + + if self._flow_control_policy_is_malformed: + if is_outbound: + configuration = 'proxy.config.http2.flow_control.policy_out' + else: + configuration = 'proxy.config.http2.flow_control.policy_in' + ts.Disk.diags_log.Content = Testers.ContainsExpression( + f"ERROR.*{configuration}", "Expected an error about an invalid flow control policy.") + + return ts + + def _configure_client( + self, + tr, + ): + """Configure a client process. + + :param tr: The TestRun to associate the client with. + """ + tr.AddVerifierClientProcess( + f'client-{Http2FlowControlTest._client_counter}', self._replay_file, https_ports=[self._ts.Variables.ssl_port]) + Http2FlowControlTest._client_counter += 1 + + def _configure_log_expectations(self, host): + """Configure the log expectations for the client or server.""" + hostname = "server" if host == self._server else "client" + if self._flow_control_policy_is_malformed: + # Since we're just testing ATS configuration errors, there's no + # need to set up client expectations. + return + + # ATS currently always sends a MAX_CONCURRENT_STREAMS setting. + host.Streams.stdout += Testers.ContainsExpression( + f'MAX_CONCURRENT_STREAMS:{self._expected_max_concurrent_streams}', + f"{hostname} should receive a MAX_CONCURRENT_STREAMS setting.") + + if self._initial_window_size is not None: + host.Streams.stdout += Testers.ContainsExpression( + f'INITIAL_WINDOW_SIZE:{self._expected_initial_stream_window_size}', + f"{hostname} should receive an INITIAL_WINDOW_SIZE setting.") + + if self._expected_flow_control_policy == 0: + update_window_size = (self._expected_initial_stream_window_size - self._default_initial_window_size) + if update_window_size > 0: + host.Streams.stdout += Testers.ContainsExpression( + f'WINDOW_UPDATE.*id 0: {update_window_size}', f"{hostname} should receive a session WINDOW_UPDATE.") + + if self._expected_flow_control_policy in (1, 2): + # Verify the larger window size. + + session_window_size = (self._expected_initial_stream_window_size * self._expected_max_concurrent_streams) + + # ATS will send a WINDOW_UPDATE frame to the client to increase + # the session window size to the configured value from the default + # value. + update_window_size = (session_window_size - self._expected_initial_stream_window_size) + + # A WINDOW_UPDATE can only increase the window size. So make sure that + # the new window size is greater than the default window size. + if update_window_size > Http2FlowControlTest._default_initial_window_size: + host.Streams.stdout += Testers.ContainsExpression( + f'WINDOW_UPDATE.*id 0: {update_window_size}', + f"{hostname} should receive an initial session WINDOW_UPDATE.") + else: + # Our test traffic is large enough that eventually we should + # send a session WINDOW_UPDATE frame for the smaller window. + # It's not clear what it will be in advance though. A 100 byte + # session window may not receive a 100 byte WINDOW_UPDATE frame + # if the client is sending DATA frames in 10 byte chunks due to + # a smaller stream window. + host.Streams.stdout += Testers.ContainsExpression( + 'WINDOW_UPDATE.*id 0: ', f"{hostname} should receive a session WINDOW_UPDATE.") + + if self._expected_flow_control_policy == 2: + # Verify the streams window sizes get updated. + stream_window_1 = session_window_size + stream_window_2 = int(session_window_size / 2) + stream_window_3 = int(session_window_size / 3) + if self._server: + # Toward the server, there is a potential race condition + # between sending of first-request and the sending of the + # SETTINGS frame which reduces the stream window size. + # Allow for either scenario. + host.Streams.stdout += Testers.ContainsExpression( + (f'INITIAL_WINDOW_SIZE:{stream_window_1}.*' + f'INITIAL_WINDOW_SIZE:{stream_window_2}.*'), + f"{hostname} should stream receive window updates", + reflags=re.DOTALL | re.MULTILINE) + else: + host.Streams.stdout += Testers.ContainsExpression( + ( + f'INITIAL_WINDOW_SIZE:{stream_window_1}.*' + f'INITIAL_WINDOW_SIZE:{stream_window_2}.*' + f'INITIAL_WINDOW_SIZE:{stream_window_3}'), + f"{hostname} should stream receive window updates", + reflags=re.DOTALL | re.MULTILINE) + + if self._expected_initial_stream_window_size < 1000: + first_id = 5 if self._server else 3 + + if self._server and self._expected_flow_control_policy == 2: + # Toward the server, there is a potential race condition + # between sending of first-request and the sending of the + # SETTINGS frame which reduces the stream window size. Allow + # for either scenario. + window_update_size = f'33|{self._expected_initial_stream_window_size}' + else: + window_update_size = f'{self._expected_initial_stream_window_size}' + # For the smaller session window sizes, we expect WINDOW_UPDATE frames. + host.Streams.stdout += Testers.ContainsExpression( + f'WINDOW_UPDATE.*id {first_id}: {window_update_size}', f"{hostname} should receive a stream WINDOW_UPDATE.") + + host.Streams.stdout += Testers.ContainsExpression( + f'WINDOW_UPDATE.*id {first_id + 2}: {window_update_size}', f"{hostname} should receive a stream WINDOW_UPDATE.") + + host.Streams.stdout += Testers.ContainsExpression( + f'WINDOW_UPDATE.*id {first_id + 4}: {window_update_size}', f"{hostname} should receive a stream WINDOW_UPDATE.") + + def _configure_test_run_common(self, tr, is_outbound: bool, server_type: ServerType) -> None: + """Perform the common Process configuration.""" + self._dns = self._configure_dns(tr) + self._server = self._configure_server(tr, server_type) + self._ts = self._configure_trafficserver(tr, is_outbound, server_type) + if not self._flow_control_policy_is_malformed: + self._configure_client(tr) + tr.Processes.Default.StartBefore(self._dns) + tr.Processes.Default.StartBefore(self._server) + else: + tr.Processes.Default.Command = "true" + tr.Processes.Default.StartBefore(self._ts) + tr.TimeOut = 20 + + def _configure_inbound_http1_to_origin_test_run(self) -> None: + """Configure the TestRun for inbound stream configuration.""" + tr = urtest.AddTestRun(f'{self._description} - inbound, ' + 'HTTP/1 Content-Length origin') + self._configure_test_run_common(tr, self.IS_INBOUND, self.ServerType.HTTP1_CONTENT_LENGTH) + self._configure_log_expectations(tr.Processes.Default) + + tr = urtest.AddTestRun(f'{self._description} - inbound, ' + 'HTTP/1 chunked origin') + self._configure_test_run_common(tr, self.IS_INBOUND, self.ServerType.HTTP1_CHUNKED) + self._configure_log_expectations(tr.Processes.Default) + + def _configure_inbound_http2_to_origin_test_run(self) -> None: + """Configure the TestRun for inbound stream configuration.""" + tr = urtest.AddTestRun(f'{self._description} - inbound, HTTP/2 origin') + self._configure_test_run_common(tr, self.IS_INBOUND, self.ServerType.HTTP2) + self._configure_log_expectations(tr.Processes.Default) + + def _configure_outbound_test_run(self) -> None: + """Configure the TestRun outbound stream configuration.""" + tr = urtest.AddTestRun(f'{self._description} - outbound, HTTP/2 origin') + self._configure_test_run_common(tr, self.IS_OUTBOUND, self.ServerType.HTTP2) + self._configure_log_expectations(self._server) + + def run(self) -> None: + """Configure the test run for various origin side configurations.""" + self._configure_inbound_http1_to_origin_test_run() + self._configure_inbound_http2_to_origin_test_run() + self._configure_outbound_test_run() + + class Http2DynamicWindowSettingsCapTest: + """Verify dynamic stream windows cap unacknowledged SETTINGS frames.""" + + _replay_file: str = 'replay/http2_settings_ack_stall.replay.yaml' + + def run(self) -> None: + """Configure the test run.""" + tr = urtest.AddTestRun('Dynamic stream windows cap unacknowledged SETTINGS') + server = tr.AddVerifierServerProcess('server-settings-cap', self._replay_file) + ts = tr.MakeATSProcess('ts-settings-cap', enable_tls=True, enable_cache=False) + + ts.addDefaultSSLFiles() + ts.Setup.CopyAs('clients/h2_settings_ack_stall.py', urtest.RunDirectory) + ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'http2', + 'proxy.config.ssl.server.cert.path': f'{ts.Variables.SSLDir}', + 'proxy.config.ssl.server.private_key.path': f'{ts.Variables.SSLDir}', + 'proxy.config.http.insert_response_via_str': 2, + 'proxy.config.http2.active_timeout_in': 5, + 'proxy.config.http2.flow_control.policy_in': 2, + 'proxy.config.http2.max_concurrent_streams_in': 2, + }) + ts.Disk.remap_config.AddLine(f'map / http://127.0.0.1:{server.Variables.http_port}') + ts.Disk.ssl_multicert_yaml.AddLines( + """ + ssl_multicert: + - dest_ip: "*" + ssl_cert_name: server.pem + ssl_key_name: server.key + """.split("\n")) + + tr.Processes.Default.StartBefore(server) + tr.Processes.Default.StartBefore(ts) + tr.Processes.Default.Command = f'{sys.executable} h2_settings_ack_stall.py {ts.Variables.ssl_port}' + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.stdout += Testers.ContainsExpression( + 'GOAWAY error_code=4', 'ATS should close the connection with SETTINGS_TIMEOUT.') + ts.Disk.diags_log.Content = Testers.ContainsExpression( + 'ERROR: HTTP/2 connection error code=0x04.*send settings too many outstanding SETTINGS frames', + 'ATS should log the expected SETTINGS_TIMEOUT connection error.') + + # + # Default configuration. + # + test = Http2FlowControlTest("Default Configurations") + test.run() + + # + # Configuring max_concurrent_streams_(in|out). + # + test = Http2FlowControlTest(description="Configure max_concurrent_streams", max_concurrent_streams=53) + test.run() + + # + # Configuring initial_window_size. + # + test = Http2FlowControlTest(description="Configure a larger initial_window_size_(in|out)", initial_window_size=100123) + test.run() + + # + # Configuring flow_control_policy. + # + test = Http2FlowControlTest(description="Configure an unrecognized flow_control.in.policy", flow_control_policy=23) + test.run() + + test = Http2FlowControlTest( + description="Flow control policy 0 (default): small initial_window_size", + initial_window_size=500, # The default is 65 KB. + flow_control_policy=0) + test.run() + test = Http2FlowControlTest( + description="Flow control policy 1: 100 byte session, 10 byte streams", + max_concurrent_streams=10, + initial_window_size=10, + flow_control_policy=1) + test.run() + test = Http2FlowControlTest( + description="Flow control policy 2: 100 byte session, dynamic streams", + max_concurrent_streams=10, + initial_window_size=10, + flow_control_policy=2) + test.run() + + test = Http2DynamicWindowSettingsCapTest() + test.run() + urtest.execute() diff --git a/tests/uranium_tests/h2/test_http2_max_active_streams.py b/tests/uranium_tests/h2/test_http2_max_active_streams.py new file mode 100644 index 00000000000..4c8b79d927c --- /dev/null +++ b/tests/uranium_tests/h2/test_http2_max_active_streams.py @@ -0,0 +1,105 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_http2_max_active_streams(urtest: UraniumTest) -> None: + ''' + Verify proxy.config.http2.max_active_streams_policy_in enforces the global + active-streams cap and that HPACK stays in sync across refused streams. + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import sys + + urtest.Summary = ''' + HTTP/2 max_active_streams_policy_in enforcement and HPACK sync test. + ''' + + CLIENT_SCRIPT = 'h2_max_active_streams.py' + + class Http2MaxActiveStreamsTest: + """Drive concurrent inbound streams past max_active_streams_in.""" + + def __init__(self, name: str, replay_file: str, policy: int): + self._name = name + self._replay_file = replay_file + self._policy = policy + + def run(self) -> None: + tr = urtest.AddTestRun(self._name) + server = tr.AddVerifierServerProcess(f'server-{self._name}', self._replay_file) + ts = tr.MakeATSProcess(f'ts-{self._name}', enable_tls=True, enable_cache=False) + + ts.addDefaultSSLFiles() + ts.Setup.CopyAs(f'clients/{CLIENT_SCRIPT}', urtest.RunDirectory) + ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'http2', + 'proxy.config.ssl.server.cert.path': f'{ts.Variables.SSLDir}', + 'proxy.config.ssl.server.private_key.path': f'{ts.Variables.SSLDir}', + 'proxy.config.http2.max_active_streams_in': 2, + 'proxy.config.http2.max_active_streams_policy_in': self._policy, + 'proxy.config.http2.max_concurrent_streams_in': 100, + }) + ts.Disk.remap_config.AddLine(f'map / http://127.0.0.1:{server.Variables.http_port}') + ts.Disk.ssl_multicert_yaml.AddLines( + """ + ssl_multicert: + - dest_ip: "*" + ssl_cert_name: server.pem + ssl_key_name: server.key + """.split('\n')) + + tr.Processes.Default.StartBefore(server) + tr.Processes.Default.StartBefore(ts) + tr.Processes.Default.Command = (f'{sys.executable} {CLIENT_SCRIPT} {ts.Variables.ssl_port} --streams 4 --probe-from 5') + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.stdout += Testers.ExcludesExpression( + 'GOAWAY', 'ATS must not tear down the connection; HPACK dynamic table must stay in sync.') + + if self._policy == 1: + tr.Processes.Default.Streams.stdout += Testers.ContainsExpression( + r'stream 5: RST_STREAM error_code=7', 'stream 5 must be refused with REFUSED_STREAM under enforce policy.') + tr.Processes.Default.Streams.stdout += Testers.ContainsExpression( + r'stream 7: RST_STREAM error_code=7', + 'stream 7 must also be refused with REFUSED_STREAM, proving HPACK decode happened on stream 5.') + ts.Disk.traffic_out.Content += Testers.ContainsExpression( + r'HTTP/2 stream error code=0x07.*active streams cap reached', + 'ATS should log the cap-reached stream error under enforce policy.') + else: + tr.Processes.Default.Streams.stdout += Testers.ExcludesExpression( + r'RST_STREAM error_code=7', 'No stream should be refused under advisory policy.') + + Http2MaxActiveStreamsTest('enforce', 'replay/http2_max_active_streams_enforce.replay.yaml', policy=1).run() + Http2MaxActiveStreamsTest('advisory', 'replay/http2_max_active_streams_advisory.replay.yaml', policy=0).run() + urtest.execute() diff --git a/tests/uranium_tests/h2/test_http2_priority.py b/tests/uranium_tests/h2/test_http2_priority.py new file mode 100644 index 00000000000..45f8e5f6383 --- /dev/null +++ b/tests/uranium_tests/h2/test_http2_priority.py @@ -0,0 +1,107 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_http2_priority(urtest: UraniumTest) -> None: + ''' + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + urtest.Summary = ''' + Test a basic remap of a http connection with Stream Priority Feature + ''' + + urtest.SkipUnless( + Condition.HasCurlFeature('http2'), + Condition.HasProgram("cksum", "cksum need to be installed on system for this test to work"), + ) + urtest.ContinueOnFail = True + + # ---- + # Setup Origin Server + # ---- + server = urtest.MakeOriginServer("server") + + # Test Case 0: + server.addResponse( + "sessionlog.json", { + "headers": "GET /bigfile HTTP/1.1\r\nHost: www.example.com\r\n\r\n", + "timestamp": "1469733493.993", + "body": "" + }, { + "headers": + "HTTP/1.1 200 OK\r\nServer: microserver\r\nConnection: close\r\nCache-Control: max-age=3600\r\nContent-Length: 1048576\r\n\r\n", + "timestamp": "1469733493.993", + "body": "" + }) + + # ---- + # Setup ATS + # ---- + ts = urtest.MakeATSProcess("ts", enable_tls=True, enable_cache=False) + + ts.addDefaultSSLFiles() + + ts.Disk.remap_config.AddLine('map / http://127.0.0.1:{0}'.format(server.Variables.Port)) + ts.Disk.ssl_multicert_yaml.AddLines( + """ + ssl_multicert: + - dest_ip: "*" + ssl_cert_name: server.pem + ssl_key_name: server.key + """.split("\n")) + ts.Disk.records_config.update( + { + 'proxy.config.http2.stream_priority_enabled': 1, + 'proxy.config.http2.no_activity_timeout_in': 3, + 'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir), + 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts.Variables.SSLDir), + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'http2', + }) + + # ---- + # Test Cases + # ---- + + # Test Case 0: + tr = urtest.AddTestRun() + tr.MakeCurlCommand('-vs -k --http2 https://127.0.0.1:{0}/bigfile | cksum'.format(ts.Variables.ssl_port), ts=ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.TimeOut = 5 + tr.Processes.Default.StartBefore(server, ready=When.PortOpen(server.Variables.Port)) + tr.Processes.Default.StartBefore(urtest.Processes.ts) + tr.Processes.Default.Streams.stdout = "gold/priority_0_stdout.gold" + # Different versions of curl will have different cases for HTTP/2 field names. + tr.Processes.Default.Streams.stderr = Testers.GoldFile("gold/priority_0_stderr.gold", case_insensitive=True) + tr.StillRunningAfter = server + urtest.execute() diff --git a/tests/uranium_tests/h2/test_http2_rst_stream.py b/tests/uranium_tests/h2/test_http2_rst_stream.py new file mode 100644 index 00000000000..d4a708f4870 --- /dev/null +++ b/tests/uranium_tests/h2/test_http2_rst_stream.py @@ -0,0 +1,191 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_http2_rst_stream(urtest: UraniumTest) -> None: + ''' + Abort HTTP/2 connection using RST_STREAM frame. + ''' + + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + urtest.Summary = ''' + Abort HTTP/2 connection using RST_STREAM frame. + ''' + + urtest.SkipUnless(Condition.HasOpenSSLVersion('1.1.1'), Condition.HasProxyVerifierVersion('2.8.0')) + + # + # Client sends RST_STREAM after DATA frame + # + ts = urtest.MakeATSProcess("ts0", enable_tls=True) + replay_file = "replay_rst_stream/http2_rst_stream_client_after_data.yaml" + server = urtest.MakeVerifierServerProcess("server0", replay_file) + ts.addDefaultSSLFiles() + ts.Disk.records_config.update( + { + 'proxy.config.ssl.server.cert.path': f'{ts.Variables.SSLDir}', + 'proxy.config.ssl.server.private_key.path': f'{ts.Variables.SSLDir}', + 'proxy.config.ssl.client.verify.server.policy': 'PERMISSIVE', + 'proxy.config.diags.debug.enabled': 3, + 'proxy.config.diags.debug.tags': 'http', + 'proxy.config.exec_thread.autoconfig.enabled': 0, + 'proxy.config.exec_thread.limit': 4, + 'proxy.config.ssl.client.alpn_protocols': 'h2,http/1.1', + 'proxy.config.http.server_session_sharing.pool': 'thread', + 'proxy.config.http.server_session_sharing.match': 'ip,sni,cert', + }) + ts.Disk.remap_config.AddLine(f'map / https://127.0.0.1:{server.Variables.https_port}') + ts.Disk.ssl_multicert_yaml.AddLines( + """ + ssl_multicert: + - dest_ip: "*" + ssl_cert_name: server.pem + ssl_key_name: server.key + """.split("\n")) + + tr = urtest.AddTestRun('Client sends RST_STREAM after DATA frame') + tr.Processes.Default.StartBefore(server) + tr.Processes.Default.StartBefore(ts) + tr.AddVerifierClientProcess("client0", replay_file, http_ports=[ts.Variables.port], https_ports=[ts.Variables.ssl_port]) + + tr.Processes.Default.Streams.All += Testers.ContainsExpression( + 'Submitting RST_STREAM frame for key 1 after DATA frame with error code INTERNAL_ERROR.', 'Detect client abort flag.') + + tr.Processes.Default.Streams.All += Testers.ContainsExpression( + 'Submitted RST_STREAM frame for key 1 on stream 1.', 'Send RST_STREAM frame.') + + server.Streams.All += Testers.ExcludesExpression('RST_STREAM', 'Server is not affected.') + + ts.Disk.traffic_out.Content += Testers.ContainsExpression('Received HEADERS frame', 'Received HEADERS frame.') + + ts.Disk.traffic_out.Content += Testers.ContainsExpression('Received DATA frame', 'Received DATA frame.') + + ts.Disk.traffic_out.Content += Testers.ContainsExpression('Received RST_STREAM frame', 'Received RST_STREAM frame.') + + ts.Disk.traffic_out.Content += Testers.ContainsExpression('Parsed RST_STREAM frame: Error Code: 2', 'Error Code: ') + + # + # Client sends RST_STREAM after HEADERS frame + # + ts = urtest.MakeATSProcess("ts1", enable_tls=True) + replay_file = "replay_rst_stream/http2_rst_stream_client_after_headers.yaml" + server = urtest.MakeVerifierServerProcess("server1", replay_file) + ts.addDefaultSSLFiles() + ts.Disk.records_config.update( + { + 'proxy.config.ssl.server.cert.path': f'{ts.Variables.SSLDir}', + 'proxy.config.ssl.server.private_key.path': f'{ts.Variables.SSLDir}', + 'proxy.config.ssl.client.verify.server.policy': 'PERMISSIVE', + 'proxy.config.diags.debug.enabled': 3, + 'proxy.config.diags.debug.tags': 'http', + 'proxy.config.exec_thread.autoconfig.enabled': 0, + 'proxy.config.exec_thread.limit': 4, + 'proxy.config.ssl.client.alpn_protocols': 'h2,http/1.1', + 'proxy.config.http.server_session_sharing.pool': 'thread', + 'proxy.config.http.server_session_sharing.match': 'ip,sni,cert', + }) + ts.Disk.remap_config.AddLine(f'map / https://127.0.0.1:{server.Variables.https_port}') + ts.Disk.ssl_multicert_yaml.AddLines( + """ + ssl_multicert: + - dest_ip: "*" + ssl_cert_name: server.pem + ssl_key_name: server.key + """.split("\n")) + + tr = urtest.AddTestRun('Client sends RST_STREAM after HEADERS frame') + tr.Processes.Default.StartBefore(server) + tr.Processes.Default.StartBefore(ts) + tr.AddVerifierClientProcess("client1", replay_file, http_ports=[ts.Variables.port], https_ports=[ts.Variables.ssl_port]) + + tr.Processes.Default.Streams.All += Testers.ContainsExpression( + 'Submitting RST_STREAM frame for key 1 after HEADERS frame with error code STREAM_CLOSED.', 'Detect client abort flag.') + + tr.Processes.Default.Streams.All += Testers.ContainsExpression( + 'Submitted RST_STREAM frame for key 1 on stream 1', 'Send RST_STREAM frame.') + + server.Streams.All += Testers.ExcludesExpression('RST_STREAM', 'Server is not affected.') + + ts.Disk.traffic_out.Content += Testers.ContainsExpression('Received HEADERS frame', 'Received HEADERS frame.') + + ts.Disk.traffic_out.Content += Testers.ExcludesExpression('Received DATA frame', 'Received DATA frame.') + + ts.Disk.traffic_out.Content += Testers.ContainsExpression('Received RST_STREAM frame', 'Received RST_STREAM frame.') + + ts.Disk.traffic_out.Content += Testers.ContainsExpression('Parsed RST_STREAM frame: Error Code: 5', 'Error Code: ') + + # + # Server sends RST_STREAM after HEADERS frame + # + ts = urtest.MakeATSProcess("ts2", enable_tls=True) + replay_file = "replay_rst_stream/http2_rst_stream_server_after_headers.yaml" + server = urtest.MakeVerifierServerProcess("server2", replay_file) + ts.addDefaultSSLFiles() + ts.Disk.records_config.update( + { + 'proxy.config.ssl.server.cert.path': f'{ts.Variables.SSLDir}', + 'proxy.config.ssl.server.private_key.path': f'{ts.Variables.SSLDir}', + 'proxy.config.ssl.client.verify.server.policy': 'PERMISSIVE', + 'proxy.config.diags.debug.enabled': 3, + 'proxy.config.diags.debug.tags': 'http', + 'proxy.config.exec_thread.autoconfig.enabled': 0, + 'proxy.config.exec_thread.limit': 1, + 'proxy.config.ssl.client.alpn_protocols': 'h2,http/1.1', + 'proxy.config.http.server_session_sharing.pool': 'thread', + 'proxy.config.http.server_session_sharing.match': 'ip,sni,cert', + }) + ts.Disk.remap_config.AddLine(f'map / https://127.0.0.1:{server.Variables.https_port}') + ts.Disk.ssl_multicert_yaml.AddLines( + """ + ssl_multicert: + - dest_ip: "*" + ssl_cert_name: server.pem + ssl_key_name: server.key + """.split("\n")) + + tr = urtest.AddTestRun('Server sends RST_STREAM after HEADERS frame') + tr.Processes.Default.StartBefore(server) + tr.Processes.Default.StartBefore(ts) + tr.AddVerifierClientProcess("client2", replay_file, http_ports=[ts.Variables.port], https_ports=[ts.Variables.ssl_port]) + + tr.Processes.Default.Streams.All += Testers.ContainsExpression( + 'Received RST_STREAM frame with stream id 1, error code 0', 'Client received RST_STREAM frame.') + + server.Streams.All += "gold/server_after_headers.gold" + + ts.Disk.traffic_out.Content += Testers.ContainsExpression('Received RST_STREAM frame', 'Received RST_STREAM frame.') + + ts.Disk.traffic_out.Content += Testers.ContainsExpression('Parsed RST_STREAM frame: Error Code: 11', 'Error Code: ') + + ts.Disk.traffic_out.Content += Testers.ContainsExpression('Send RST_STREAM frame: Error Code: 0', 'Error Code: ') + urtest.execute() diff --git a/tests/uranium_tests/h2/test_http2_txn_start_read_gate.py b/tests/uranium_tests/h2/test_http2_txn_start_read_gate.py new file mode 100644 index 00000000000..a423a5aa58f --- /dev/null +++ b/tests/uranium_tests/h2/test_http2_txn_start_read_gate.py @@ -0,0 +1,84 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_http2_txn_start_read_gate(urtest: UraniumTest) -> None: + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import os + + urtest.Summary = ''' + Verify HTTP/2 read events do not bypass async TXN_START hooks. + ''' + + urtest.SkipUnless(Condition.HasProxyVerifierVersion('2.8.0')) + + replay_file = "replay/http2_txn_start_read_gate.replay.yaml" + server = urtest.MakeVerifierServerProcess("server", replay_file) + + ts = urtest.MakeATSProcess("ts", enable_tls=True, enable_cache=False) + ts.addDefaultSSLFiles() + ts.Disk.remap_config.AddLine(f"map / http://127.0.0.1:{server.Variables.http_port}") + ts.Disk.ssl_multicert_yaml.AddLines( + """ + ssl_multicert: + - dest_ip: "*" + ssl_cert_name: server.pem + ssl_key_name: server.key + """.split("\n")) + ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'delay_txn_start', + 'proxy.config.ssl.server.cert.path': f"{ts.Variables.SSLDir}", + 'proxy.config.ssl.server.private_key.path': f"{ts.Variables.SSLDir}", + }) + + urtest.PrepareTestPlugin(os.path.join(urtest.Variables.AtsTestPluginsDir, 'delay_txn_start.so'), ts, '500') + + tr = urtest.AddTestRun() + tr.Processes.Default.StartBefore(server) + tr.Processes.Default.StartBefore(ts) + tr.AddVerifierClientProcess("client", replay_file, http_ports=[ts.Variables.port], https_ports=[ts.Variables.ssl_port]) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.All += Testers.ExcludesExpression(r'\[ERROR\]', 'Proxy Verifier should not report errors.') + tr.Processes.Default.Streams.All += Testers.ContainsExpression( + 'Equals Success: Key: "1", Content Data: "body", Value: "response-body"', 'Client should receive the response body.') + + server.Streams.All += Testers.ContainsExpression( + 'Equals Success: Key: "1", Content Data: "body", Value: "request-body"', 'Origin should receive the request body.') + + ts.Disk.traffic_out.Content += Testers.ContainsExpression( + "delayed TXN_START reenable", "The test plugin should delay and then resume TXN_START.") + ts.Disk.traffic_out.Content += Testers.ExcludesExpression( + "READ_REQUEST_HDR before delayed TXN_START reenable", "READ_REQUEST_HDR must wait for TXN_START reenable.") + urtest.execute() diff --git a/tests/uranium_tests/h2/test_http2_write_threshold.py b/tests/uranium_tests/h2/test_http2_write_threshold.py new file mode 100644 index 00000000000..8c51bf84d79 --- /dev/null +++ b/tests/uranium_tests/h2/test_http2_write_threshold.py @@ -0,0 +1,163 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_http2_write_threshold(urtest: UraniumTest) -> None: + """Test proxy.config.http2.write_size_threshold.""" + + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import os + from ports import get_port + import sys + + class TestGrpc(): + """Test proxy.config.http2.write_size_threshold and its associated timeout.""" + + def __init__(self, description: str, write_threshold: int, write_timeout: int) -> None: + """Configure a TestRun for gRPC traffic. + + :param description: The description for the test runs. + """ + self._description = description + tr = urtest.AddTestRun(self._description) + dns = self._configure_dns(tr) + server = self._configure_h2_server(tr, write_timeout) + ts = self._configure_traffic_server(tr, dns.Variables.Port, server.Variables.port, write_threshold, write_timeout) + + ts.StartBefore(dns) + ts.StartBefore(server) + tr.Processes.Default.StartBefore(ts) + + tr.TimeOut = 10 + + self._configure_h2_client(tr, ts.Variables.ssl_port, write_timeout) + + def _configure_dns(self, tr: 'TestRun') -> 'Process': + """Configure a locally running MicroDNS server. + + :param tr: The TestRun with which to associate the MicroDNS server. + :return: The MicroDNS server process. + """ + self._dns = tr.MakeDNServer("dns", default=['127.0.0.1']) + return self._dns + + def _configure_h2_server(self, tr: 'TestRun', write_timeout: int) -> 'Process': + """Set up the go HTTP/2 server. + + :param tr: The TestRun with which to associate the server. + :param write_timeout: The expected maximum amount of time frames should be delivered. + :return: The server process. + """ + tr.Setup.Copy('trickle_server.py') + self._server = tr.Processes.Process('server') + + server_pem = os.path.join(urtest.Variables.AtsTestToolsDir, "ssl", "server.pem") + server_key = os.path.join(urtest.Variables.AtsTestToolsDir, "ssl", "server.key") + self._server.Setup.Copy(server_pem) + self._server.Setup.Copy(server_key) + + port = get_port(self._server, 'port') + command = (f'{sys.executable} {tr.RunDirectory}/trickle_server.py {port} ' + f'server.pem server.key {write_timeout}') + self._server.Command = command + self._server.ReturnCode = 0 + self._server.Ready = When.PortOpen(port) + return self._server + + def _configure_traffic_server( + self, tr: 'TestRun', dns_port: int, server_port: int, write_threshold: int, write_timeout: int) -> 'Process': + """Configure the traffic server process. + + :param tr: The TestRun with which to associate the traffic server. + :param dns_port: The MicroDNS server port that traffic server should connect to. + :param server_port: The server port that traffic server should connect to. + :param write_threshold: The value to set for proxy.config.http2.write_size_threshold. + :param write_timeout: The value to set for proxy.config.http2.write_time_threshold. + :return: The traffic server process. + """ + self._ts = tr.MakeATSProcess("ts", enable_tls=True, enable_cache=False) + + self._ts.addDefaultSSLFiles() + self._ts.Disk.ssl_multicert_yaml.AddLines( + """ + ssl_multicert: + - dest_ip: "*" + ssl_cert_name: server.pem + ssl_key_name: server.key + """.split("\n")) + + self._ts.Disk.remap_config.AddLine(f"map / https://example.com:{server_port}/") + + self._ts.Disk.records_config.update( + { + "proxy.config.ssl.server.cert.path": self._ts.Variables.SSLDir, + "proxy.config.ssl.server.private_key.path": self._ts.Variables.SSLDir, + 'proxy.config.ssl.client.alpn_protocols': 'h2,http/1.1', + 'proxy.config.http.server_session_sharing.pool': 'thread', + 'proxy.config.ssl.client.verify.server.policy': 'PERMISSIVE', + 'proxy.config.dns.nameservers': f"127.0.0.1:{dns_port}", + 'proxy.config.dns.resolv_conf': "NULL", + 'proxy.config.http2.write_size_threshold': write_threshold, + 'proxy.config.http2.write_time_threshold': write_timeout, + + # Only enable debug logging during manual exectution. All the + # DATA frames get multiple logs and it makes the traffic.out too + # unwieldy. + "proxy.config.diags.debug.enabled": 0, + "proxy.config.diags.debug.tags": "http", + }) + return self._ts + + def _configure_h2_client(self, tr: 'TestRun', proxy_port: int, write_timeout: int) -> None: + """Start the HTTP/2 client. + + :param tr: The TestRun with which to associate the client. + :param proxy_port: The proxy_port to which to connect. + """ + tr.Setup.Copy('trickle_client.py') + ca = os.path.join(urtest.Variables.AtsTestToolsDir, "ssl", "server.pem") + key = os.path.join(urtest.Variables.AtsTestToolsDir, "ssl", "server.key") + + self._server.Setup.Copy(ca) + self._server.Setup.Copy(key) + # The cert is for example.com, so we must use that domain. + hostname = 'example.com' + command = ( + f'{sys.executable} {tr.RunDirectory}/trickle_client.py ' + f'{hostname} {proxy_port} server.pem {write_timeout}') + p = tr.Processes.Default + p.Command = command + p.ReturnCode = 0 + + test = TestGrpc("Test proxy.config.http2.write_size_threshold", 0.5, 10) + urtest.execute() diff --git a/tests/uranium_tests/h2/test_httpbin.py b/tests/uranium_tests/h2/test_httpbin.py new file mode 100644 index 00000000000..4203aa4dbfc --- /dev/null +++ b/tests/uranium_tests/h2/test_httpbin.py @@ -0,0 +1,157 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_httpbin(urtest: UraniumTest) -> None: + ''' + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import os + import sys + + # ---- + # Setup Test + # ---- + urtest.Summary = ''' + Test HTTP/2 with httpbin origin server + ''' + # Require HTTP/2 enabled Curl + urtest.SkipUnless( + Condition.HasCurlFeature('http2'), + Condition.HasProgram("cksum", "cksum need to be installed on system for this test to work"), + ) + # Test.ContinueOnFail = True + + # ---- + # Setup httpbin Origin Server + # ---- + httpbin = urtest.MakeHttpBinServer("httpbin") + + # ---- + # Setup ATS + # ---- + ts = urtest.MakeATSProcess("ts", enable_tls=True, enable_cache=False) + + # add ssl materials like key, certificates for the server + ts.addDefaultSSLFiles() + + ts.Disk.remap_config.AddLine('map / http://127.0.0.1:{0}'.format(httpbin.Variables.Port)) + ts.Disk.ssl_multicert_yaml.AddLines( + """ + ssl_multicert: + - dest_ip: "*" + ssl_cert_name: server.pem + ssl_key_name: server.key + """.split("\n")) + ts.Disk.records_config.update( + { + 'proxy.config.http.insert_request_via_str': 1, + 'proxy.config.http.insert_response_via_str': 1, + 'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir), + 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts.Variables.SSLDir), + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'http', + }) + ts.Disk.logging_yaml.AddLines( + ''' + logging: + formats: + - name: access + format: '[%] % % % % % % % %' + + logs: + - filename: access + format: access + '''.split("\n")) + + urtest.Disk.File(os.path.join(ts.Variables.LOGDIR, 'access.log'), exists=True, content='gold/httpbin_access.gold') + + # TODO: when httpbin 0.8.0 or later is released, remove below json pretty print hack + json_printer = f''' + {sys.executable} -c "import sys,json; print(json.dumps(json.load(sys.stdin), indent=2, separators=(',', ': ')))" + ''' + + # ---- + # Test Cases + # ---- + + # Test Case 0: Basic request and response + test_run = urtest.AddTestRun() + test_run.MakeCurlCommand("-vs -k --http2 https://127.0.0.1:{0}/get | {1}".format(ts.Variables.ssl_port, json_printer), ts=ts) + test_run.Processes.Default.ReturnCode = 0 + test_run.Processes.Default.StartBefore(httpbin) + test_run.Processes.Default.StartBefore(urtest.Processes.ts) + test_run.Processes.Default.Streams.stdout = "gold/httpbin_0_stdout.gold" + # Different versions of curl will have different cases for HTTP/2 field names. + test_run.Processes.Default.Streams.stderr = Testers.GoldFile("gold/httpbin_0_stderr.gold", case_insensitive=True) + test_run.StillRunningAfter = httpbin + + # Test Case 1: Attempt an empty response body. + # This test case requires go-httpbin@v2.6.0 or later. + test_run = urtest.AddTestRun() + test_run.MakeCurlCommand('-vs -k --http2 https://127.0.0.1:{0}/bytes/0'.format(ts.Variables.ssl_port), ts=ts) + test_run.Processes.Default.ReturnCode = 0 + test_run.Processes.Default.Streams.stdout = "gold/httpbin_1_stdout.gold" + # Different versions of curl will have different cases for HTTP/2 field names. + test_run.Processes.Default.Streams.stderr = Testers.GoldFile("gold/httpbin_1_stderr.gold", case_insensitive=True) + test_run.StillRunningAfter = httpbin + + # Test Case 2: Chunked + test_run = urtest.AddTestRun() + test_run.MakeCurlCommand( + '-vs -k --http2 https://127.0.0.1:{0}/stream-bytes/102400?seed=0 | cksum'.format(ts.Variables.ssl_port), ts=ts) + test_run.Processes.Default.ReturnCode = 0 + test_run.Processes.Default.Streams.stdout = "gold/httpbin_2_stdout.gold" + # Different versions of curl will have different cases for HTTP/2 field names. + test_run.Processes.Default.Streams.stderr = Testers.GoldFile("gold/httpbin_2_stderr.gold", case_insensitive=True) + test_run.StillRunningAfter = httpbin + + # Test Case 3: Expect 100-Continue + test_run = urtest.AddTestRun() + test_run.MakeCurlCommand( + "-vs -k --http2 https://127.0.0.1:{0}/post --data 'key=value' -H 'Expect: 100-continue' --max-time 5 | {1}".format( + ts.Variables.ssl_port, json_printer), + ts=ts) + test_run.Processes.Default.ReturnCode = 0 + test_run.Processes.Default.Streams.stdout = "gold/httpbin_3_stdout.gold" + # Different versions of curl will have different cases for HTTP/2 field names. + test_run.Processes.Default.Streams.stderr = Testers.GoldFile("gold/httpbin_3_stderr.gold", case_insensitive=True) + test_run.StillRunningAfter = httpbin + + # Wait for the POST transaction to be logged. + urtest.AddAwaitFileContainsTestRun( + 'Await POST access log entry.', + os.path.join(ts.Variables.LOGDIR, 'access.log'), + r'POST .*?/post', + ) + urtest.execute() diff --git a/tests/uranium_tests/h2/test_nghttp.py b/tests/uranium_tests/h2/test_nghttp.py new file mode 100644 index 00000000000..0c4beab7adc --- /dev/null +++ b/tests/uranium_tests/h2/test_nghttp.py @@ -0,0 +1,118 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_nghttp(urtest: UraniumTest) -> None: + ''' + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import os + + urtest.Summary = ''' + Test with nghttp + ''' + + urtest.SkipUnless(Condition.HasProgram("nghttp", "Nghttp need to be installed on system for this test to work"),) + urtest.ContinueOnFail = True + + # ---- + # Setup Origin Server + # ---- + httpbin = urtest.MakeHttpBinServer("httpbin") + + # 128KB + post_body = "0123456789abcdef" * 8192 + post_body_file = open(os.path.join(urtest.RunDirectory, "post_body"), "w") + post_body_file.write(post_body) + post_body_file.close() + + # ---- + # Setup ATS + # ---- + ts = urtest.MakeATSProcess("ts", enable_tls=True, enable_cache=False) + + # add ssl materials like key, certificates for the server + ts.addDefaultSSLFiles() + + ts.Setup.CopyAs('rules/graceful_shutdown.conf', urtest.RunDirectory) + + ts.Disk.remap_config.AddLines( + [ + 'map /httpbin/ http://127.0.0.1:{0}/ @plugin=header_rewrite.so @pparam={1}/graceful_shutdown.conf'.format( + httpbin.Variables.Port, urtest.RunDirectory) + ]) + + ts.Disk.ssl_multicert_yaml.AddLines( + """ + ssl_multicert: + - dest_ip: "*" + ssl_cert_name: server.pem + ssl_key_name: server.key + """.split("\n")) + + ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'http2_cs', + 'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir), + 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts.Variables.SSLDir) + }) + + # ---- + # Test Cases + # ---- + + # Test Case 0: Trailer + tr = urtest.AddTestRun() + tr.TimeOut = 10 + tr.Processes.Default.Command = f"nghttp -vn --no-dep 'https://127.0.0.1:{ts.Variables.ssl_port}/httpbin/post' --trailer 'foo: bar' -d 'post_body'" + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.StartBefore(httpbin) + tr.Processes.Default.StartBefore(urtest.Processes.ts) + tr.Processes.Default.Streams.stdout = "gold/nghttp_0_stdout.gold" + tr.StillRunningAfter = httpbin + tr.StillRunningAfter = ts + + # Test Case 1: Graceful Shutdown + # - This test takes 3 seconds to make sure receiving 2 GOAWAY frames + # - TODO: add a test case of a client keeps the connection open ( -e.g. keep sending PING frame) + tr = urtest.AddTestRun() + tr.TimeOut = 10 + tr.Processes.Default.Command = f"nghttp -vn --no-dep 'https://127.0.0.1:{ts.Variables.ssl_port}/httpbin/drip?duration=3'" + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.stdout = "gold/nghttp_1_stdout.gold" + tr.StillRunningAfter = httpbin + tr.StillRunningAfter = ts + + ts.Disk.traffic_out.Content = "gold/nghttp_ts_stderr.gold" + urtest.execute() diff --git a/tests/gold_tests/h2/trickle_client.py b/tests/uranium_tests/h2/trickle_client.py similarity index 100% rename from tests/gold_tests/h2/trickle_client.py rename to tests/uranium_tests/h2/trickle_client.py diff --git a/tests/gold_tests/h2/trickle_server.py b/tests/uranium_tests/h2/trickle_server.py similarity index 100% rename from tests/gold_tests/h2/trickle_server.py rename to tests/uranium_tests/h2/trickle_server.py diff --git a/tests/gold_tests/h3/go_h3_client/go.mod b/tests/uranium_tests/h3/go_h3_client/go.mod similarity index 100% rename from tests/gold_tests/h3/go_h3_client/go.mod rename to tests/uranium_tests/h3/go_h3_client/go.mod diff --git a/tests/gold_tests/h3/go_h3_client/go.sum b/tests/uranium_tests/h3/go_h3_client/go.sum similarity index 100% rename from tests/gold_tests/h3/go_h3_client/go.sum rename to tests/uranium_tests/h3/go_h3_client/go.sum diff --git a/tests/gold_tests/h3/go_h3_client/main.go b/tests/uranium_tests/h3/go_h3_client/main.go similarity index 100% rename from tests/gold_tests/h3/go_h3_client/main.go rename to tests/uranium_tests/h3/go_h3_client/main.go diff --git a/tests/gold_tests/h3/h3_session_ticket.sh b/tests/uranium_tests/h3/h3_session_ticket.sh similarity index 100% rename from tests/gold_tests/h3/h3_session_ticket.sh rename to tests/uranium_tests/h3/h3_session_ticket.sh diff --git a/tests/gold_tests/h3/py_h3_client/h3_client.py b/tests/uranium_tests/h3/py_h3_client/h3_client.py similarity index 100% rename from tests/gold_tests/h3/py_h3_client/h3_client.py rename to tests/uranium_tests/h3/py_h3_client/h3_client.py diff --git a/tests/gold_tests/h3/replays/h3_active_timeout.test.yaml b/tests/uranium_tests/h3/replays/h3_active_timeout.test.yaml similarity index 99% rename from tests/gold_tests/h3/replays/h3_active_timeout.test.yaml rename to tests/uranium_tests/h3/replays/h3_active_timeout.test.yaml index 91df09ddf7e..f01566a3e20 100644 --- a/tests/gold_tests/h3/replays/h3_active_timeout.test.yaml +++ b/tests/uranium_tests/h3/replays/h3_active_timeout.test.yaml @@ -17,7 +17,7 @@ meta: version: "1.0" -autest: +urtest: description: "Verify HTTP/3 transaction cleanup on active timeout" requires: ats_features: [TS_USE_QUIC] diff --git a/tests/gold_tests/h3/replays/h3_flow_control.test.yaml b/tests/uranium_tests/h3/replays/h3_flow_control.test.yaml similarity index 99% rename from tests/gold_tests/h3/replays/h3_flow_control.test.yaml rename to tests/uranium_tests/h3/replays/h3_flow_control.test.yaml index 4ca067aa5a2..a84e0e6110a 100644 --- a/tests/gold_tests/h3/replays/h3_flow_control.test.yaml +++ b/tests/uranium_tests/h3/replays/h3_flow_control.test.yaml @@ -17,7 +17,7 @@ meta: version: "1.0" -autest: +urtest: description: "Verify HTTP/3 flow control with small QUIC windows" requires: ats_features: [TS_USE_QUIC] diff --git a/tests/gold_tests/h3/replays/h3_proxy_verifier.test.yaml b/tests/uranium_tests/h3/replays/h3_proxy_verifier.test.yaml similarity index 99% rename from tests/gold_tests/h3/replays/h3_proxy_verifier.test.yaml rename to tests/uranium_tests/h3/replays/h3_proxy_verifier.test.yaml index a437f0c58f8..26a7f21370b 100644 --- a/tests/gold_tests/h3/replays/h3_proxy_verifier.test.yaml +++ b/tests/uranium_tests/h3/replays/h3_proxy_verifier.test.yaml @@ -17,7 +17,7 @@ meta: version: "1.0" -autest: +urtest: description: "Verify HTTP/3 client interop with Proxy Verifier across multiple connections" requires: ats_features: [TS_USE_QUIC] diff --git a/tests/gold_tests/h3/replays/h3_server_for_go_client.replay.yaml b/tests/uranium_tests/h3/replays/h3_server_for_go_client.replay.yaml similarity index 100% rename from tests/gold_tests/h3/replays/h3_server_for_go_client.replay.yaml rename to tests/uranium_tests/h3/replays/h3_server_for_go_client.replay.yaml diff --git a/tests/gold_tests/h3/replays/h3_server_for_python_client.replay.yaml b/tests/uranium_tests/h3/replays/h3_server_for_python_client.replay.yaml similarity index 100% rename from tests/gold_tests/h3/replays/h3_server_for_python_client.replay.yaml rename to tests/uranium_tests/h3/replays/h3_server_for_python_client.replay.yaml diff --git a/tests/gold_tests/h3/replays/h3_sni.replay.yaml b/tests/uranium_tests/h3/replays/h3_sni.replay.yaml similarity index 100% rename from tests/gold_tests/h3/replays/h3_sni.replay.yaml rename to tests/uranium_tests/h3/replays/h3_sni.replay.yaml diff --git a/tests/gold_tests/h3/replays/h3_stream_lifetime.test.yaml b/tests/uranium_tests/h3/replays/h3_stream_lifetime.test.yaml similarity index 99% rename from tests/gold_tests/h3/replays/h3_stream_lifetime.test.yaml rename to tests/uranium_tests/h3/replays/h3_stream_lifetime.test.yaml index 36cc9cae464..19d52b87d27 100644 --- a/tests/gold_tests/h3/replays/h3_stream_lifetime.test.yaml +++ b/tests/uranium_tests/h3/replays/h3_stream_lifetime.test.yaml @@ -17,7 +17,7 @@ meta: version: "1.0" -autest: +urtest: description: "Verify HTTP/3 stream lifetime handling with concurrent streams" requires: ats_features: [TS_USE_QUIC] diff --git a/tests/uranium_tests/h3/test_h3_curl.py b/tests/uranium_tests/h3/test_h3_curl.py new file mode 100644 index 00000000000..83dd730be20 --- /dev/null +++ b/tests/uranium_tests/h3/test_h3_curl.py @@ -0,0 +1,162 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_h3_curl(urtest: UraniumTest) -> None: + ''' + Verify HTTP/3 client interop with curl. + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information regarding + # copyright ownership. The ASF licenses this file to you under + # the Apache License, Version 2.0 (the "License"); you may not + # use this file except in compliance with the License. You may + # obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import os + + urtest.Summary = ''' + This test is written specifically to verify that an HTTP/3 curl client can + complete a request through ATS. + ''' + + urtest.SkipUnless( + Condition.HasATSFeature('TS_USE_QUIC'), + Condition.HasCurlFeature('http3'), + Condition.HasCurlOption('--http3-only'), + ) + urtest.SkipIf(Condition.CurlUsingUnixDomainSocket()) + + class TestHttp3Curl: + """Configure a test to verify HTTP/3 curl client interoperability.""" + + response_body = "0123456789" * 30000 + + def __init__(self, name: str): + """Initialize the test. + + :param name: The name of the test. + """ + self.name = name + self._body_path = os.path.join(urtest.RunDirectory, "h3_curl_body.txt") + self._configure_server() + self._configure_traffic_server() + self._configure_client() + + def _configure_server(self): + """Configure the origin server.""" + server = urtest.MakeOriginServer("server") + server.addResponse( + "sessionlog.json", { + "headers": "GET /h3-curl HTTP/1.1\r\nHost: localhost\r\n\r\n", + "timestamp": "1469733493.993", + "body": "" + }, { + "headers": f"HTTP/1.1 200 OK\r\nConnection: close\r\nContent-Length: {len(self.response_body)}\r\n\r\n", + "timestamp": "1469733493.993", + "body": self.response_body + }) + + self._server = server + + def _configure_traffic_server(self): + """Configure Traffic Server.""" + ts = urtest.MakeATSProcess("ts", enable_tls=True, enable_quic=True, enable_cache=False) + ts.StartupTimeout = 60 + ts.addDefaultSSLFiles() + ts.addSSLfile("../tls/ssl/signed-foo.pem") + ts.addSSLfile("../tls/ssl/signed-foo.key") + ts.Disk.ssl_multicert_yaml.AddLines( + ''' + ssl_multicert: + - ssl_cert_name: signed-foo.pem + ssl_key_name: signed-foo.key + - ssl_cert_name: server.pem + ssl_key_name: server.key + dest_ip: "*" + '''.split('\n')) + ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'quic|http3', + 'proxy.config.quic.server.stateless_retry_enabled': 0, + 'proxy.config.ssl.server.cert.path': ts.Variables.SSLDir, + 'proxy.config.ssl.server.private_key.path': ts.Variables.SSLDir, + }) + ts.Disk.remap_config.AddLine(f'map / http://127.0.0.1:{self._server.Variables.Port}') + ts.Disk.logging_yaml.AddLines( + ''' + logging: + formats: + - name: h3_access + format: 'c_alpn=% client_version=% c_ssl_version=% c_method=% c_url=%' + + logs: + - filename: h3_access + format: h3_access + '''.split("\n")) + + self._access_log = urtest.Disk.File(os.path.join(ts.Variables.LOGDIR, 'h3_access.log'), exists=True) + self._access_log.Content = Testers.ContainsExpression( + r'c_alpn=h3 client_version=http/3 c_ssl_version=[^ ]+ c_method=GET c_url=https://foo.com:[0-9]+/h3-curl', + "ATS should log the curl request as HTTP/3") + + self._ts = ts + + def _check_curl_response(self, tr): + """Verify that curl received the response over HTTP/3.""" + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.stdout = Testers.ContainsExpression( + f"size_download={len(self.response_body)}", "curl should receive the complete HTTP/3 response body") + tr.Processes.Default.Streams.stdout += Testers.ContainsExpression("http_version=3", "curl should report HTTP/3") + + def _configure_client(self): + """Configure the curl client test runs.""" + tr = urtest.AddTestRun(self.name) + tr.Processes.Default.StartBefore(self._server) + tr.Processes.Default.StartBefore(self._ts) + tr.MakeCurlCommand( + '--silent --show-error --fail --ipv4 --http3-only --insecure ' + f'--resolve "foo.com:{self._ts.Variables.ssl_port}:127.0.0.1" ' + f'--output "{self._body_path}" ' + '--write-out "\\nhttp_version=%{http_version}\\nsize_download=%{size_download}\\n" ' + f'https://foo.com:{self._ts.Variables.ssl_port}/h3-curl', + ts=self._ts) + self._check_curl_response(tr) + tr.StillRunningAfter = self._server + tr.StillRunningAfter = self._ts + + tr = urtest.AddTestRun("Wait for HTTP/3 access log") + tr.Processes.Default.Command = ( + os.path.join(urtest.Variables.AtsTestToolsDir, 'condwait') + ' 60 1 -f ' + + os.path.join(self._ts.Variables.LOGDIR, 'h3_access.log')) + tr.Processes.Default.ReturnCode = 0 + tr.StillRunningAfter = self._server + tr.StillRunningAfter = self._ts + + TestHttp3Curl("curl forced HTTP/3 request") + urtest.execute() diff --git a/tests/uranium_tests/h3/test_h3_go_client.py b/tests/uranium_tests/h3/test_h3_go_client.py new file mode 100644 index 00000000000..a92c967a5a1 --- /dev/null +++ b/tests/uranium_tests/h3/test_h3_go_client.py @@ -0,0 +1,148 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_h3_go_client(urtest: UraniumTest) -> None: + ''' + Verify HTTP/3 client interop with a quic-go client. + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information regarding + # copyright ownership. The ASF licenses this file to you under + # the Apache License, Version 2.0 (the "License"); you may not + # use this file except in compliance with the License. You may + # obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import os + + urtest.Summary = ''' + Verify that a quic-go HTTP/3 client can complete sequential and concurrent + transactions through ATS. + ''' + + urtest.SkipUnless( + Condition.HasATSFeature('TS_USE_QUIC'), + Condition.HasGoVersion('1.24'), + ) + + def add_default_ssl_multicert(ts): + """Configure the default server certificate.""" + ts.Disk.ssl_multicert_yaml.AddLines( + """ + ssl_multicert: + - dest_ip: "*" + ssl_cert_name: server.pem + ssl_key_name: server.key + """.split("\n")) + + class TestHttp3GoClient: + """Configure a test to verify HTTP/3 quic-go client interoperability.""" + + replay_file = "replays/h3_server_for_go_client.replay.yaml" + + def __init__(self, name: str): + """Initialize the test.""" + self.name = name + self._configure_server() + self._configure_traffic_server() + self._configure_client() + + def _configure_server(self): + """Configure the Proxy Verifier origin server.""" + self._server = urtest.MakeVerifierServerProcess( + "server-go-h3-client", self.replay_file, verbose=False, other_args="--poll-timeout 30000") + + def _configure_traffic_server(self): + """Configure Traffic Server.""" + ts = urtest.MakeATSProcess("ts-go-h3-client", enable_tls=True, enable_quic=True, enable_cache=False) + ts.StartupTimeout = 60 + ts.addDefaultSSLFiles() + add_default_ssl_multicert(ts) + ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'quic|http3', + 'proxy.config.quic.initial_max_data_in': 1000000, + 'proxy.config.quic.initial_max_stream_data_bidi_remote_in': 1000000, + 'proxy.config.quic.server.stateless_retry_enabled': 0, + 'proxy.config.ssl.server.cert.path': ts.Variables.SSLDir, + 'proxy.config.ssl.server.private_key.path': ts.Variables.SSLDir, + }) + ts.Disk.remap_config.AddLine(f'map / http://127.0.0.1:{self._server.Variables.http_port}') + ts.Disk.logging_yaml.AddLines( + ''' + logging: + formats: + - name: h3_go_access + format: 'c_alpn=% client_version=% c_method=% c_url=%' + + logs: + - filename: h3_go_access + format: h3_go_access + '''.split("\n")) + + self._access_log = urtest.Disk.File(os.path.join(ts.Variables.LOGDIR, 'h3_go_access.log'), exists=True) + self._access_log.Content = Testers.ContainsExpression( + r'c_alpn=h3 client_version=http/3 c_method=GET c_url=https://go\.example\.com:[0-9]+/go-get-empty', + "ATS should log the quic-go request as HTTP/3") + self._access_log.Content += Testers.ContainsExpression( + r'c_alpn=h3 client_version=http/3 c_method=POST c_url=https://go\.example\.com:[0-9]+/go-post-large', + "ATS should log the quic-go large POST as HTTP/3") + + self._ts = ts + + def _configure_client(self): + """Configure the quic-go client test runs.""" + tr = urtest.AddTestRun(self.name) + tr.Setup.Copy("go_h3_client") + tr.Processes.Default.StartBefore(self._server) + tr.Processes.Default.StartBefore(self._ts) + tr.Processes.Default.Env['GOFLAGS'] = '-mod=readonly -modcacherw' + tr.Processes.Default.Env['GOCACHE'] = os.path.join(tr.RunDirectory, 'gocache') + tr.Processes.Default.Env['GOMODCACHE'] = os.path.join(tr.RunDirectory, 'gomodcache') + tr.Processes.Default.Env['GOTOOLCHAIN'] = 'local' + tr.Processes.Default.Command = ( + f'cd "{os.path.join(tr.RunDirectory, "go_h3_client")}" && ' + f'go run . --addr 127.0.0.1:{self._ts.Variables.ssl_port} ' + f'--authority go.example.com:{self._ts.Variables.ssl_port} ' + '--server-name go.example.com') + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.stdout = Testers.ContainsExpression( + "completed 13 HTTP/3 requests", "The quic-go client should complete all HTTP/3 requests.") + tr.StillRunningAfter = self._server + tr.StillRunningAfter = self._ts + + tr = urtest.AddTestRun("Wait for quic-go HTTP/3 access log") + tr.Processes.Default.Command = ( + os.path.join(urtest.Variables.AtsTestToolsDir, 'condwait') + ' 60 1 -f ' + + os.path.join(self._ts.Variables.LOGDIR, 'h3_go_access.log')) + tr.Processes.Default.ReturnCode = 0 + tr.StillRunningAfter = self._server + tr.StillRunningAfter = self._ts + + TestHttp3GoClient("quic-go HTTP/3 client requests") + urtest.execute() diff --git a/tests/uranium_tests/h3/test_h3_python_client.py b/tests/uranium_tests/h3/test_h3_python_client.py new file mode 100644 index 00000000000..fe3f453c24c --- /dev/null +++ b/tests/uranium_tests/h3/test_h3_python_client.py @@ -0,0 +1,150 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_h3_python_client(urtest: UraniumTest) -> None: + ''' + Verify HTTP/3 client interop with an aioquic Python client. + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information regarding + # copyright ownership. The ASF licenses this file to you under + # the Apache License, Version 2.0 (the "License"); you may not + # use this file except in compliance with the License. You may + # obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import os + import sys + + urtest.Summary = ''' + Verify that an aioquic HTTP/3 client can complete normal requests and selected + HTTP/3 edge-case probes through ATS. + ''' + + urtest.SkipUnless( + Condition.HasATSFeature('TS_USE_QUIC'), + Condition.HasProgram("python3", "python3 is required for the aioquic HTTP/3 client"), + ) + + def add_default_ssl_multicert(ts): + """Configure the default server certificate.""" + if hasattr(ts.Disk, "ssl_multicert_yaml"): + ts.Disk.ssl_multicert_yaml.AddLines( + """ + ssl_multicert: + - dest_ip: "*" + ssl_cert_name: server.pem + ssl_key_name: server.key + """.split("\n")) + else: + ts.Disk.ssl_multicert_config.AddLine("dest_ip=* ssl_cert_name=server.pem ssl_key_name=server.key") + + class TestHttp3PythonClient: + """Configure a test to verify HTTP/3 aioquic client interoperability.""" + + replay_file = "replays/h3_server_for_python_client.replay.yaml" + + def __init__(self, name: str): + """Initialize the test.""" + self.name = name + self._configure_server() + self._configure_traffic_server() + self._configure_client() + + def _configure_server(self): + """Configure the Proxy Verifier origin server.""" + self._server = urtest.MakeVerifierServerProcess( + "server-python-h3-client", self.replay_file, verbose=False, other_args="--poll-timeout 30000") + + def _configure_traffic_server(self): + """Configure Traffic Server.""" + ts = urtest.MakeATSProcess("ts-python-h3-client", enable_tls=True, enable_quic=True, enable_cache=False) + ts.StartupTimeout = 60 + ts.addDefaultSSLFiles() + add_default_ssl_multicert(ts) + ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'quic|http3', + 'proxy.config.quic.initial_max_data_in': 1000000, + 'proxy.config.quic.initial_max_stream_data_bidi_remote_in': 1000000, + 'proxy.config.quic.max_send_udp_payload_size_in': 1200, + 'proxy.config.quic.server.stateless_retry_enabled': 0, + 'proxy.config.ssl.server.cert.path': ts.Variables.SSLDir, + 'proxy.config.ssl.server.private_key.path': ts.Variables.SSLDir, + }) + ts.Disk.remap_config.AddLine(f'map / http://127.0.0.1:{self._server.Variables.http_port}') + ts.Disk.logging_yaml.AddLines( + ''' + logging: + formats: + - name: h3_python_access + format: 'c_alpn=% client_version=% c_method=% c_url=%' + + logs: + - filename: h3_python_access + format: h3_python_access + '''.split("\n")) + + self._access_log = urtest.Disk.File(os.path.join(ts.Variables.LOGDIR, 'h3_python_access.log'), exists=True) + self._access_log.Content = Testers.ContainsExpression( + r'c_alpn=h3 client_version=http/3 c_method=GET c_url=https://py\.example\.com:[0-9]+/py-get-empty', + "ATS should log the aioquic request as HTTP/3") + self._access_log.Content += Testers.ContainsExpression( + r'c_alpn=h3 client_version=http/3 c_method=PUT c_url=https://py\.example\.com:[0-9]+/py-put-large', + "ATS should log the aioquic large PUT as HTTP/3") + + self._ts = ts + + def _configure_client(self): + """Configure the aioquic client test runs.""" + tr = urtest.AddTestRun(self.name) + tr.Setup.Copy("py_h3_client") + tr.Processes.Default.StartBefore(self._server) + tr.Processes.Default.StartBefore(self._ts) + client_dir = os.path.join(tr.RunDirectory, "py_h3_client") + tr.Processes.Default.Command = ( + f'"{sys.executable}" "{os.path.join(client_dir, "h3_client.py")}" ' + f'--addr 127.0.0.1:{self._ts.Variables.ssl_port} ' + f'--authority py.example.com:{self._ts.Variables.ssl_port} ' + '--server-name py.example.com') + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.stdout = Testers.ContainsExpression( + "completed 18 Python HTTP/3 checks", "The aioquic client should complete all HTTP/3 checks.") + tr.StillRunningAfter = self._server + tr.StillRunningAfter = self._ts + + tr = urtest.AddTestRun("Wait for aioquic HTTP/3 access log") + tr.Processes.Default.Command = ( + os.path.join(urtest.Variables.AtsTestToolsDir, 'condwait') + ' 60 1 -f ' + + os.path.join(self._ts.Variables.LOGDIR, 'h3_python_access.log')) + tr.Processes.Default.ReturnCode = 0 + tr.StillRunningAfter = self._server + tr.StillRunningAfter = self._ts + + TestHttp3PythonClient("aioquic HTTP/3 client requests") + urtest.execute() diff --git a/tests/uranium_tests/h3/test_h3_range_cache.py b/tests/uranium_tests/h3/test_h3_range_cache.py new file mode 100644 index 00000000000..2d6b85c1c40 --- /dev/null +++ b/tests/uranium_tests/h3/test_h3_range_cache.py @@ -0,0 +1,159 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_h3_range_cache(urtest: UraniumTest) -> None: + ''' + Verify HTTP/3 range requests over cached content. + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information regarding + # copyright ownership. The ASF licenses this file to you under + # the Apache License, Version 2.0 (the "License"); you may not + # use this file except in compliance with the License. You may + # obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import os + + urtest.Summary = ''' + Verify that HTTP/3 clients can populate cache and receive range responses from + cached objects. + ''' + + urtest.SkipUnless( + Condition.HasATSFeature('TS_USE_QUIC'), + Condition.HasCurlFeature('http3'), + Condition.HasCurlOption('--http3-only'), + ) + urtest.SkipIf(Condition.CurlUsingUnixDomainSocket()) + + def add_default_ssl_multicert(ts): + """Configure the default server certificate.""" + if hasattr(ts.Disk, "ssl_multicert_yaml"): + ts.Disk.ssl_multicert_yaml.AddLines( + """ + ssl_multicert: + - dest_ip: "*" + ssl_cert_name: server.pem + ssl_key_name: server.key + """.split("\n")) + else: + ts.Disk.ssl_multicert_config.AddLine("dest_ip=* ssl_cert_name=server.pem ssl_key_name=server.key") + + class TestHttp3RangeCache: + """Configure an HTTP/3 range-over-cache test.""" + + response_body = "0123456789" * 30000 + range_body = "6789012345678901" + + def __init__(self): + """Initialize the test.""" + self._configure_server() + self._configure_traffic_server() + self._configure_clients() + + def _configure_server(self): + """Configure the origin server.""" + server = urtest.MakeOriginServer("server-h3-range-cache") + server.addResponse( + "sessionlog.json", { + "headers": "GET /h3-range-cache HTTP/1.1\r\nHost: localhost\r\n\r\n", + "timestamp": "1469733493.993", + "body": "" + }, { + "headers": + ( + "HTTP/1.1 200 OK\r\n" + "Connection: close\r\n" + "Cache-Control: public, max-age=60\r\n" + f"Content-Length: {len(self.response_body)}\r\n\r\n"), + "timestamp": "1469733493.993", + "body": self.response_body + }) + self._server = server + + def _configure_traffic_server(self): + """Configure Traffic Server.""" + ts = urtest.MakeATSProcess("ts-h3-range-cache", enable_tls=True, enable_quic=True, enable_cache=True) + ts.StartupTimeout = 60 + ts.addDefaultSSLFiles() + add_default_ssl_multicert(ts) + ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'quic|http3|http', + 'proxy.config.quic.server.stateless_retry_enabled': 0, + 'proxy.config.ssl.server.cert.path': ts.Variables.SSLDir, + 'proxy.config.ssl.server.private_key.path': ts.Variables.SSLDir, + }) + ts.Disk.remap_config.AddLine(f'map / http://127.0.0.1:{self._server.Variables.Port}') + self._ts = ts + + def _curl_base(self): + """Build the shared curl arguments.""" + return ( + '--silent --show-error --fail --ipv4 --http3-only --insecure ' + f'--resolve "range.example.com:{self._ts.Variables.ssl_port}:127.0.0.1" ' + f'https://range.example.com:{self._ts.Variables.ssl_port}/h3-range-cache') + + def _configure_clients(self): + """Configure the cache fill and range request clients.""" + full_body_path = os.path.join(urtest.RunDirectory, "h3-range-full.txt") + range_body_path = os.path.join(urtest.RunDirectory, "h3-range-part.txt") + + tr = urtest.AddTestRun("HTTP/3 cache fill") + tr.Processes.Default.StartBefore(self._server) + tr.Processes.Default.StartBefore(self._ts) + tr.MakeCurlCommand( + f'{self._curl_base()} --output "{full_body_path}" ' + '--write-out "\\nhttp_code=%{http_code}\\nsize_download=%{size_download}\\n"', + ts=self._ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.stdout = Testers.ContainsExpression("http_code=200", "The fill request should return 200.") + tr.Processes.Default.Streams.stdout += Testers.ContainsExpression( + f"size_download={len(self.response_body)}", "The fill request should receive the full object.") + tr.StillRunningAfter = self._server + tr.StillRunningAfter = self._ts + + tr = urtest.AddTestRun("HTTP/3 cached range request") + tr.MakeCurlCommand( + f'{self._curl_base()} --header "Range: bytes=16-31" --output "{range_body_path}" ' + '--write-out "\\nhttp_code=%{http_code}\\nsize_download=%{size_download}\\n"', + ts=self._ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.stdout = Testers.ContainsExpression( + "http_code=206", "The range request should return 206.") + tr.Processes.Default.Streams.stdout += Testers.ContainsExpression( + "size_download=16", "The range request should receive 16 bytes.") + urtest.Disk.File( + range_body_path, exists=True).Content = Testers.ContainsExpression( + self.range_body, "The cached range response body should match the requested byte range.") + tr.StillRunningAfter = self._server + tr.StillRunningAfter = self._ts + + TestHttp3RangeCache() + urtest.execute() diff --git a/tests/uranium_tests/h3/test_h3_session_ticket.py b/tests/uranium_tests/h3/test_h3_session_ticket.py new file mode 100644 index 00000000000..d121dd6559e --- /dev/null +++ b/tests/uranium_tests/h3/test_h3_session_ticket.py @@ -0,0 +1,128 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_h3_session_ticket(urtest: UraniumTest) -> None: + ''' + Verify HTTP/3 QUIC TLS session ticket handling. + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information regarding + # copyright ownership. The ASF licenses this file to you under + # the Apache License, Version 2.0 (the "License"); you may not + # use this file except in compliance with the License. You may + # obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import os + import shlex + + urtest.Summary = ''' + Verify that HTTP/3 QUIC connections can receive and offer TLS session tickets. + ''' + + urtest.SkipUnless( + Condition.HasATSFeature('TS_USE_QUIC'), + Condition.HasOpenSSLVersion('3.5.0'), + Condition.HasOpenSSLQuicClient(), + ) + urtest.Setup.Copy('../tls/file.ticket') + + def add_default_ssl_multicert(ts): + """Configure the default server certificate.""" + if hasattr(ts.Disk, "ssl_multicert_yaml"): + ts.Disk.ssl_multicert_yaml.AddLines( + """ + ssl_multicert: + - dest_ip: "*" + ssl_cert_name: server.pem + ssl_key_name: server.key + """.split("\n")) + else: + ts.Disk.ssl_multicert_config.AddLine("dest_ip=* ssl_cert_name=server.pem ssl_key_name=server.key") + + class TestHttp3SessionTicket: + """Configure an HTTP/3 QUIC TLS session ticket test.""" + + def __init__(self, name: str): + """Initialize the test.""" + self.name = name + self.session_file = os.path.join(urtest.RunDirectory, "h3-quic-session.pem") + self.ticket_file = os.path.join(urtest.RunDirectory, "file.ticket") + self._configure_traffic_server() + self._configure_ticket_save() + self._configure_ticket_reuse() + + def _configure_traffic_server(self): + """Configure Traffic Server.""" + ts = urtest.MakeATSProcess("ts", enable_tls=True, enable_quic=True, enable_cache=False) + ts.StartupTimeout = 60 + ts.addDefaultSSLFiles() + add_default_ssl_multicert(ts) + ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'quic|ssl', + 'proxy.config.quic.server.stateless_retry_enabled': 0, + 'proxy.config.ssl.server.cert.path': ts.Variables.SSLDir, + 'proxy.config.ssl.server.private_key.path': ts.Variables.SSLDir, + 'proxy.config.ssl.server.session_ticket.enable': 1, + 'proxy.config.ssl.server.session_ticket.number': 2, + 'proxy.config.ssl.server.ticket_key.filename': self.ticket_file, + }) + + self._ts = ts + + def _s_client_command(self, session_option: str): + """Build an OpenSSL QUIC client command for ticket save or reuse.""" + script = os.path.join(urtest.TestDirectory, "h3_session_ticket.sh") + return f"{shlex.quote(script)} {session_option} {shlex.quote(self.session_file)} {self._ts.Variables.ssl_port}" + + def _check_s_client_handshake(self, tr): + """Verify that OpenSSL completed the QUIC handshake.""" + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.All = Testers.ContainsExpression( + "CONNECTION ESTABLISHED", "OpenSSL should complete the QUIC handshake.") + tr.Processes.Default.Streams.All += Testers.ContainsExpression( + "Protocol version: QUICv1", "OpenSSL should negotiate QUICv1.") + + def _configure_ticket_save(self): + """Configure the ticket save test run.""" + tr = urtest.AddTestRun(self.name) + tr.Processes.Default.StartBefore(self._ts) + tr.Processes.Default.Command = f"rm -f {shlex.quote(self.session_file)}; {self._s_client_command('-sess_out')}" + self._check_s_client_handshake(tr) + tr.StillRunningAfter = self._ts + + def _configure_ticket_reuse(self): + """Configure the ticket reuse test run.""" + tr = urtest.AddTestRun("OpenSSL QUIC offers saved session ticket") + tr.Processes.Default.Command = self._s_client_command("-sess_in") + self._check_s_client_handshake(tr) + tr.StillRunningAfter = self._ts + + TestHttp3SessionTicket("OpenSSL QUIC saves session ticket") + urtest.execute() diff --git a/tests/uranium_tests/h3/test_h3_sni_check.py b/tests/uranium_tests/h3/test_h3_sni_check.py new file mode 100644 index 00000000000..c227b79f841 --- /dev/null +++ b/tests/uranium_tests/h3/test_h3_sni_check.py @@ -0,0 +1,151 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_h3_sni_check(urtest: UraniumTest) -> None: + ''' + Verify h3 SNI checking behavior. + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + urtest.Summary = ''' + Verify h3 SNI checking behavior. + ''' + + urtest.SkipUnless(Condition.HasATSFeature('TS_USE_QUIC')) + + urtest.ContinueOnFail = True + + class Test_sni_check: + """Configure a test to verify SNI checking behavior for h3 connections.""" + + replay_file = "replays/h3_sni.replay.yaml" + client_counter: int = 0 + ts_counter: int = 0 + server_counter: int = 0 + + def __init__(self, name: str, gold_file="", replay_keys="", expect_request_rejected=False): + """Initialize the test. + + :param name: The name of the test. + :param gold_file: (Optional) Gold file to be checked. + :param replay_keys: (Optional) Keys to be used by pv. + :param expect_request_rejected: (Optional) Whether or not the client request is expected to be rejected. + """ + self.name = name + self.gold_file = gold_file + self.replay_keys = replay_keys + self.expect_request_rejected = expect_request_rejected + + def _configure_server(self, tr: 'TestRun'): + """Configure the server. + + :param tr: The TestRun object to associate the server process with. + """ + server = tr.AddVerifierServerProcess(f"server_{Test_sni_check.server_counter}", self.replay_file) + Test_sni_check.server_counter += 1 + self._server = server + + def _configure_traffic_server(self, tr: 'TestRun'): + """Configure Traffic Server. + + :param tr: The TestRun object to associate the ts process with. + """ + ts = tr.MakeATSProcess(f"ts-{Test_sni_check.ts_counter}", enable_quic=True, enable_tls=True) + ts.StartupTimeout = 60 + + Test_sni_check.ts_counter += 1 + self._ts = ts + # Configure TLS for Traffic Server. + self._ts.addDefaultSSLFiles() + self._ts.addSSLfile("../tls/ssl/signed-foo.pem") + self._ts.addSSLfile("../tls/ssl/signed-foo.key") + self._ts.Disk.ssl_multicert_yaml.AddLines( + """ + ssl_multicert: + - ssl_cert_name: signed-foo.pem + ssl_key_name: signed-foo.key + - dest_ip: "*" + ssl_cert_name: server.pem + ssl_key_name: server.key + """.split("\n")) + self._ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'http', + 'proxy.config.ssl.server.cert.path': ts.Variables.SSLDir, + 'proxy.config.quic.no_activity_timeout_in': 0, + 'proxy.config.ssl.server.private_key.path': ts.Variables.SSLDir, + 'proxy.config.ssl.client.verify.server.policy': 'PERMISSIVE', + }) + + self._ts.Disk.remap_config.AddLine(f'map / http://127.0.0.1:{self._server.Variables.http_port}') + + def run(self): + """Run the test.""" + tr = urtest.AddTestRun(self.name) + self._configure_server(tr) + self._configure_traffic_server(tr) + + tr.Processes.Default.StartBefore(self._server) + tr.Processes.Default.StartBefore(self._ts) + + tr.AddVerifierClientProcess( + f'client-{Test_sni_check.client_counter}', + self.replay_file, + http3_ports=[self._ts.Variables.ssl_port], + keys=self.replay_keys) + Test_sni_check.client_counter += 1 + + if self.expect_request_rejected: + # The client request should time out because ATS rejects it and does + # not send a response. + tr.Processes.Default.ReturnCode = 1 + self._ts.Disk.traffic_out.Content += Testers.ContainsExpression( + "SNI not found", "ATS should detect the missing SNI.") + else: + # Verify the client request is successful. + tr.Processes.Default.ReturnCode = 0 + self._ts.Disk.traffic_out.Content += Testers.ExcludesExpression( + "SNI not found", "ATS should see the SNI presented by client.") + + if self.gold_file: + tr.Processes.Default.Streams.all = self.gold_file + + # TEST 1: Client request with SNI. + test0 = Test_sni_check("", replay_keys="has_sni", expect_request_rejected=False) + test0.run() + + # TEST 2: Client request without SNI. + test1 = Test_sni_check("", replay_keys="no_sni", expect_request_rejected=True) + test1.run() + urtest.execute() diff --git a/tests/gold_tests/headers/cache_and_error_nobody.gold b/tests/uranium_tests/headers/cache_and_error_nobody.gold similarity index 100% rename from tests/gold_tests/headers/cache_and_error_nobody.gold rename to tests/uranium_tests/headers/cache_and_error_nobody.gold diff --git a/tests/gold_tests/headers/cache_and_req_body-hit-stale-206-etag.gold b/tests/uranium_tests/headers/cache_and_req_body-hit-stale-206-etag.gold similarity index 100% rename from tests/gold_tests/headers/cache_and_req_body-hit-stale-206-etag.gold rename to tests/uranium_tests/headers/cache_and_req_body-hit-stale-206-etag.gold diff --git a/tests/gold_tests/headers/cache_and_req_body-hit-stale-206.gold b/tests/uranium_tests/headers/cache_and_req_body-hit-stale-206.gold similarity index 100% rename from tests/gold_tests/headers/cache_and_req_body-hit-stale-206.gold rename to tests/uranium_tests/headers/cache_and_req_body-hit-stale-206.gold diff --git a/tests/gold_tests/headers/cache_and_req_body-hit-stale-INM.gold b/tests/uranium_tests/headers/cache_and_req_body-hit-stale-INM.gold similarity index 100% rename from tests/gold_tests/headers/cache_and_req_body-hit-stale-INM.gold rename to tests/uranium_tests/headers/cache_and_req_body-hit-stale-INM.gold diff --git a/tests/gold_tests/headers/cache_and_req_body-hit-stale.gold b/tests/uranium_tests/headers/cache_and_req_body-hit-stale.gold similarity index 100% rename from tests/gold_tests/headers/cache_and_req_body-hit-stale.gold rename to tests/uranium_tests/headers/cache_and_req_body-hit-stale.gold diff --git a/tests/gold_tests/headers/cache_and_req_body-hit.gold b/tests/uranium_tests/headers/cache_and_req_body-hit.gold similarity index 100% rename from tests/gold_tests/headers/cache_and_req_body-hit.gold rename to tests/uranium_tests/headers/cache_and_req_body-hit.gold diff --git a/tests/gold_tests/headers/cache_and_req_body-hit_close.gold b/tests/uranium_tests/headers/cache_and_req_body-hit_close.gold similarity index 100% rename from tests/gold_tests/headers/cache_and_req_body-hit_close.gold rename to tests/uranium_tests/headers/cache_and_req_body-hit_close.gold diff --git a/tests/gold_tests/headers/cache_and_req_body-miss.gold b/tests/uranium_tests/headers/cache_and_req_body-miss.gold similarity index 100% rename from tests/gold_tests/headers/cache_and_req_body-miss.gold rename to tests/uranium_tests/headers/cache_and_req_body-miss.gold diff --git a/tests/gold_tests/headers/cache_and_req_nobody-hit-stale.gold b/tests/uranium_tests/headers/cache_and_req_nobody-hit-stale.gold similarity index 100% rename from tests/gold_tests/headers/cache_and_req_nobody-hit-stale.gold rename to tests/uranium_tests/headers/cache_and_req_nobody-hit-stale.gold diff --git a/tests/gold_tests/headers/clients/oversized_field_h2_client.py b/tests/uranium_tests/headers/clients/oversized_field_h2_client.py similarity index 97% rename from tests/gold_tests/headers/clients/oversized_field_h2_client.py rename to tests/uranium_tests/headers/clients/oversized_field_h2_client.py index 83d9b3feec0..bc1601ab944 100644 --- a/tests/gold_tests/headers/clients/oversized_field_h2_client.py +++ b/tests/uranium_tests/headers/clients/oversized_field_h2_client.py @@ -15,7 +15,7 @@ # See the License for the specific language governing permissions and # limitations under the License. """ -Minimal raw HTTP/2 client for the oversized-header autest. +Minimal raw HTTP/2 client for the oversized-header Uranium test. Why this exists: curl and nghttp both refuse to *send* a header set larger than ~60000 bytes (an nghttp2 client-side guard), so they cannot drive the oversized @@ -93,7 +93,7 @@ def frame(ftype, flags, sid, payload): dec = Decoder() status = rst = goaway = None -# Generous read deadline: on a loaded CI host (parallel autest shards under ASAN) +# Generous read deadline: on a loaded CI host (parallel end-to-end shards under ASAN) # ATS can take several seconds to receive the oversized CONTINUATION-framed # HEADERS, reject it, and send GOAWAY. A tight timeout turns that latency into a # spurious "wrong code" failure, so allow ample time and report a distinct diff --git a/tests/gold_tests/headers/data/www.http408.test.txt b/tests/uranium_tests/headers/data/www.http408.test.txt similarity index 100% rename from tests/gold_tests/headers/data/www.http408.test.txt rename to tests/uranium_tests/headers/data/www.http408.test.txt diff --git a/tests/gold_tests/headers/data/www.passthrough.test_get.txt b/tests/uranium_tests/headers/data/www.passthrough.test_get.txt similarity index 100% rename from tests/gold_tests/headers/data/www.passthrough.test_get.txt rename to tests/uranium_tests/headers/data/www.passthrough.test_get.txt diff --git a/tests/gold_tests/headers/data/www.redirect0.test_get.txt b/tests/uranium_tests/headers/data/www.redirect0.test_get.txt similarity index 100% rename from tests/gold_tests/headers/data/www.redirect0.test_get.txt rename to tests/uranium_tests/headers/data/www.redirect0.test_get.txt diff --git a/tests/gold_tests/headers/data/www.redirect301.test_get.txt b/tests/uranium_tests/headers/data/www.redirect301.test_get.txt similarity index 100% rename from tests/gold_tests/headers/data/www.redirect301.test_get.txt rename to tests/uranium_tests/headers/data/www.redirect301.test_get.txt diff --git a/tests/gold_tests/headers/data/www.redirect302.test_get.txt b/tests/uranium_tests/headers/data/www.redirect302.test_get.txt similarity index 100% rename from tests/gold_tests/headers/data/www.redirect302.test_get.txt rename to tests/uranium_tests/headers/data/www.redirect302.test_get.txt diff --git a/tests/gold_tests/headers/data/www.redirect307.test_get.txt b/tests/uranium_tests/headers/data/www.redirect307.test_get.txt similarity index 100% rename from tests/gold_tests/headers/data/www.redirect307.test_get.txt rename to tests/uranium_tests/headers/data/www.redirect307.test_get.txt diff --git a/tests/gold_tests/headers/data/www.redirect308.test_get.txt b/tests/uranium_tests/headers/data/www.redirect308.test_get.txt similarity index 100% rename from tests/gold_tests/headers/data/www.redirect308.test_get.txt rename to tests/uranium_tests/headers/data/www.redirect308.test_get.txt diff --git a/tests/gold_tests/headers/forwarded-observer.py b/tests/uranium_tests/headers/forwarded-observer.py similarity index 100% rename from tests/gold_tests/headers/forwarded-observer.py rename to tests/uranium_tests/headers/forwarded-observer.py diff --git a/tests/gold_tests/headers/forwarded.gold b/tests/uranium_tests/headers/forwarded.gold similarity index 100% rename from tests/gold_tests/headers/forwarded.gold rename to tests/uranium_tests/headers/forwarded.gold diff --git a/tests/gold_tests/headers/general-connection-failure-502.gold b/tests/uranium_tests/headers/general-connection-failure-502.gold similarity index 100% rename from tests/gold_tests/headers/general-connection-failure-502.gold rename to tests/uranium_tests/headers/general-connection-failure-502.gold diff --git a/tests/gold_tests/headers/gold/bad_good_request.gold b/tests/uranium_tests/headers/gold/bad_good_request.gold similarity index 100% rename from tests/gold_tests/headers/gold/bad_good_request.gold rename to tests/uranium_tests/headers/gold/bad_good_request.gold diff --git a/tests/gold_tests/headers/gold/bad_good_request_header.gold b/tests/uranium_tests/headers/gold/bad_good_request_header.gold similarity index 100% rename from tests/gold_tests/headers/gold/bad_good_request_header.gold rename to tests/uranium_tests/headers/gold/bad_good_request_header.gold diff --git a/tests/gold_tests/headers/gold/bad_good_request_http1.gold b/tests/uranium_tests/headers/gold/bad_good_request_http1.gold similarity index 100% rename from tests/gold_tests/headers/gold/bad_good_request_http1.gold rename to tests/uranium_tests/headers/gold/bad_good_request_http1.gold diff --git a/tests/gold_tests/headers/gold/bad_method.gold b/tests/uranium_tests/headers/gold/bad_method.gold similarity index 100% rename from tests/gold_tests/headers/gold/bad_method.gold rename to tests/uranium_tests/headers/gold/bad_method.gold diff --git a/tests/gold_tests/headers/gold/bad_protocol_number.gold b/tests/uranium_tests/headers/gold/bad_protocol_number.gold similarity index 100% rename from tests/gold_tests/headers/gold/bad_protocol_number.gold rename to tests/uranium_tests/headers/gold/bad_protocol_number.gold diff --git a/tests/gold_tests/headers/gold/bad_te_value.gold b/tests/uranium_tests/headers/gold/bad_te_value.gold similarity index 100% rename from tests/gold_tests/headers/gold/bad_te_value.gold rename to tests/uranium_tests/headers/gold/bad_te_value.gold diff --git a/tests/gold_tests/headers/gold/field_name_space.gold b/tests/uranium_tests/headers/gold/field_name_space.gold similarity index 100% rename from tests/gold_tests/headers/gold/field_name_space.gold rename to tests/uranium_tests/headers/gold/field_name_space.gold diff --git a/tests/gold_tests/headers/gold/invalid_character_in_te_value.gold b/tests/uranium_tests/headers/gold/invalid_character_in_te_value.gold similarity index 100% rename from tests/gold_tests/headers/gold/invalid_character_in_te_value.gold rename to tests/uranium_tests/headers/gold/invalid_character_in_te_value.gold diff --git a/tests/gold_tests/headers/hsts.200.gold b/tests/uranium_tests/headers/hsts.200.gold similarity index 100% rename from tests/gold_tests/headers/hsts.200.gold rename to tests/uranium_tests/headers/hsts.200.gold diff --git a/tests/gold_tests/headers/hsts.404.gold b/tests/uranium_tests/headers/hsts.404.gold similarity index 100% rename from tests/gold_tests/headers/hsts.404.gold rename to tests/uranium_tests/headers/hsts.404.gold diff --git a/tests/gold_tests/headers/http408.gold b/tests/uranium_tests/headers/http408.gold similarity index 100% rename from tests/gold_tests/headers/http408.gold rename to tests/uranium_tests/headers/http408.gold diff --git a/tests/gold_tests/headers/maps.reg b/tests/uranium_tests/headers/maps.reg similarity index 100% rename from tests/gold_tests/headers/maps.reg rename to tests/uranium_tests/headers/maps.reg diff --git a/tests/gold_tests/headers/normalize_ae.gold b/tests/uranium_tests/headers/normalize_ae.gold similarity index 100% rename from tests/gold_tests/headers/normalize_ae.gold rename to tests/uranium_tests/headers/normalize_ae.gold diff --git a/tests/gold_tests/headers/normalize_ae_observer.py b/tests/uranium_tests/headers/normalize_ae_observer.py similarity index 100% rename from tests/gold_tests/headers/normalize_ae_observer.py rename to tests/uranium_tests/headers/normalize_ae_observer.py diff --git a/tests/gold_tests/headers/passthrough_get.gold b/tests/uranium_tests/headers/passthrough_get.gold similarity index 100% rename from tests/gold_tests/headers/passthrough_get.gold rename to tests/uranium_tests/headers/passthrough_get.gold diff --git a/tests/gold_tests/headers/redirect0_get.gold b/tests/uranium_tests/headers/redirect0_get.gold similarity index 100% rename from tests/gold_tests/headers/redirect0_get.gold rename to tests/uranium_tests/headers/redirect0_get.gold diff --git a/tests/gold_tests/headers/redirect301_get.gold b/tests/uranium_tests/headers/redirect301_get.gold similarity index 100% rename from tests/gold_tests/headers/redirect301_get.gold rename to tests/uranium_tests/headers/redirect301_get.gold diff --git a/tests/gold_tests/headers/redirect302_get.gold b/tests/uranium_tests/headers/redirect302_get.gold similarity index 100% rename from tests/gold_tests/headers/redirect302_get.gold rename to tests/uranium_tests/headers/redirect302_get.gold diff --git a/tests/gold_tests/headers/redirect307_get.gold b/tests/uranium_tests/headers/redirect307_get.gold similarity index 100% rename from tests/gold_tests/headers/redirect307_get.gold rename to tests/uranium_tests/headers/redirect307_get.gold diff --git a/tests/gold_tests/headers/redirect308_get.gold b/tests/uranium_tests/headers/redirect308_get.gold similarity index 100% rename from tests/gold_tests/headers/redirect308_get.gold rename to tests/uranium_tests/headers/redirect308_get.gold diff --git a/tests/gold_tests/headers/replay/oversized_field.test.yaml b/tests/uranium_tests/headers/replay/oversized_field.test.yaml similarity index 99% rename from tests/gold_tests/headers/replay/oversized_field.test.yaml rename to tests/uranium_tests/headers/replay/oversized_field.test.yaml index ac6690fdf9a..affefd61f9c 100644 --- a/tests/gold_tests/headers/replay/oversized_field.test.yaml +++ b/tests/uranium_tests/headers/replay/oversized_field.test.yaml @@ -17,7 +17,7 @@ meta: version: "1.0" -autest: +urtest: description: 'Verify ATS rejects a header field longer than the uint16_t field-length limit with a 400.' dns: diff --git a/tests/gold_tests/headers/replay/oversized_field_h2.replay.yaml b/tests/uranium_tests/headers/replay/oversized_field_h2.replay.yaml similarity index 100% rename from tests/gold_tests/headers/replay/oversized_field_h2.replay.yaml rename to tests/uranium_tests/headers/replay/oversized_field_h2.replay.yaml diff --git a/tests/gold_tests/headers/replays/accept_webp.test.yaml b/tests/uranium_tests/headers/replays/accept_webp.test.yaml similarity index 99% rename from tests/gold_tests/headers/replays/accept_webp.test.yaml rename to tests/uranium_tests/headers/replays/accept_webp.test.yaml index 729d28d118d..7c8dbd4b345 100644 --- a/tests/gold_tests/headers/replays/accept_webp.test.yaml +++ b/tests/uranium_tests/headers/replays/accept_webp.test.yaml @@ -29,7 +29,7 @@ meta: encoding: plain data: "origin-should-not" -autest: +urtest: description: 'Verify cached image/webp responses are not reused for non-webp clients' server: diff --git a/tests/gold_tests/headers/replays/cache-test.test.yaml b/tests/uranium_tests/headers/replays/cache-test.test.yaml similarity index 99% rename from tests/gold_tests/headers/replays/cache-test.test.yaml rename to tests/uranium_tests/headers/replays/cache-test.test.yaml index 50d5e2913f6..5afb6f99a77 100644 --- a/tests/gold_tests/headers/replays/cache-test.test.yaml +++ b/tests/uranium_tests/headers/replays/cache-test.test.yaml @@ -17,7 +17,7 @@ meta: version: "1.0" -autest: +urtest: description: 'Verify cached duplicate headers are preserved across stale revalidation' server: diff --git a/tests/gold_tests/headers/replays/cached_ims_range.test.yaml b/tests/uranium_tests/headers/replays/cached_ims_range.test.yaml similarity index 99% rename from tests/gold_tests/headers/replays/cached_ims_range.test.yaml rename to tests/uranium_tests/headers/replays/cached_ims_range.test.yaml index 3911c76298b..201d38ae1ca 100644 --- a/tests/gold_tests/headers/replays/cached_ims_range.test.yaml +++ b/tests/uranium_tests/headers/replays/cached_ims_range.test.yaml @@ -29,7 +29,7 @@ meta: encoding: plain data: "origin-should-not" -autest: +urtest: description: 'Verify IMS and INM cache revalidation, range revalidation, and regex-remap 304 handling' server: diff --git a/tests/gold_tests/headers/replays/domain-blacklist-30x.test.yaml b/tests/uranium_tests/headers/replays/domain-blacklist-30x.test.yaml similarity index 99% rename from tests/gold_tests/headers/replays/domain-blacklist-30x.test.yaml rename to tests/uranium_tests/headers/replays/domain-blacklist-30x.test.yaml index 32f2616be0d..202255bc600 100644 --- a/tests/gold_tests/headers/replays/domain-blacklist-30x.test.yaml +++ b/tests/uranium_tests/headers/replays/domain-blacklist-30x.test.yaml @@ -29,7 +29,7 @@ meta: encoding: plain data: "origin-should-not" -autest: +urtest: description: 'Verify header_rewrite redirects are returned for configured hosts' server: diff --git a/tests/gold_tests/headers/replays/hopbyhop_connection.test.yaml b/tests/uranium_tests/headers/replays/hopbyhop_connection.test.yaml similarity index 99% rename from tests/gold_tests/headers/replays/hopbyhop_connection.test.yaml rename to tests/uranium_tests/headers/replays/hopbyhop_connection.test.yaml index 2b084f747ab..72e6f966310 100644 --- a/tests/gold_tests/headers/replays/hopbyhop_connection.test.yaml +++ b/tests/uranium_tests/headers/replays/hopbyhop_connection.test.yaml @@ -17,7 +17,7 @@ meta: version: "1.0" -autest: +urtest: description: "Verify dynamic Connection-token hop-by-hop header stripping (forward-path)" server: diff --git a/tests/gold_tests/headers/replays/hsts.test.yaml b/tests/uranium_tests/headers/replays/hsts.test.yaml similarity index 99% rename from tests/gold_tests/headers/replays/hsts.test.yaml rename to tests/uranium_tests/headers/replays/hsts.test.yaml index 477a2ff973a..3e1d79a50d1 100644 --- a/tests/gold_tests/headers/replays/hsts.test.yaml +++ b/tests/uranium_tests/headers/replays/hsts.test.yaml @@ -26,7 +26,7 @@ meta: fields: - [Content-Length, 0] -autest: +urtest: description: 'Verify HSTS is added on mapped HTTPS traffic but not accelerator 404 responses' server: diff --git a/tests/gold_tests/headers/replays/invalid_range_request.test.yaml b/tests/uranium_tests/headers/replays/invalid_range_request.test.yaml similarity index 99% rename from tests/gold_tests/headers/replays/invalid_range_request.test.yaml rename to tests/uranium_tests/headers/replays/invalid_range_request.test.yaml index 2e68f97296b..e184b238ea7 100644 --- a/tests/gold_tests/headers/replays/invalid_range_request.test.yaml +++ b/tests/uranium_tests/headers/replays/invalid_range_request.test.yaml @@ -17,7 +17,7 @@ meta: version: "1.0" -autest: +urtest: description: 'Verify invalid Range requests are forwarded to origin and return 416' server: diff --git a/tests/gold_tests/headers/replays/malformed_host_port.test.yaml b/tests/uranium_tests/headers/replays/malformed_host_port.test.yaml similarity index 99% rename from tests/gold_tests/headers/replays/malformed_host_port.test.yaml rename to tests/uranium_tests/headers/replays/malformed_host_port.test.yaml index 0201f9b89c4..c061166cc78 100644 --- a/tests/gold_tests/headers/replays/malformed_host_port.test.yaml +++ b/tests/uranium_tests/headers/replays/malformed_host_port.test.yaml @@ -17,7 +17,7 @@ meta: version: "1.0" -autest: +urtest: description: 'Reject malformed Host headers with multiple port separators' server: diff --git a/tests/gold_tests/headers/replays/normalized_ae_varied_transactions.test.yaml b/tests/uranium_tests/headers/replays/normalized_ae_varied_transactions.test.yaml similarity index 99% rename from tests/gold_tests/headers/replays/normalized_ae_varied_transactions.test.yaml rename to tests/uranium_tests/headers/replays/normalized_ae_varied_transactions.test.yaml index bb0f010ae83..688acf335c2 100644 --- a/tests/gold_tests/headers/replays/normalized_ae_varied_transactions.test.yaml +++ b/tests/uranium_tests/headers/replays/normalized_ae_varied_transactions.test.yaml @@ -26,7 +26,7 @@ meta: fields: - [ Content-Length, 0 ] -autest: +urtest: description: 'Verify cache matching with normalized Accept-Encoding and Vary' server: diff --git a/tests/gold_tests/headers/replays/range.test.yaml b/tests/uranium_tests/headers/replays/range.test.yaml similarity index 99% rename from tests/gold_tests/headers/replays/range.test.yaml rename to tests/uranium_tests/headers/replays/range.test.yaml index 0355f8c6366..fffde17c894 100644 --- a/tests/gold_tests/headers/replays/range.test.yaml +++ b/tests/uranium_tests/headers/replays/range.test.yaml @@ -61,7 +61,7 @@ meta: encoding: plain data: "origin-should-not" -autest: +urtest: description: 'Verify range request handling for hits, misses, and stale cache revalidation' server: diff --git a/tests/gold_tests/headers/replays/range_transform.test.yaml b/tests/uranium_tests/headers/replays/range_transform.test.yaml similarity index 99% rename from tests/gold_tests/headers/replays/range_transform.test.yaml rename to tests/uranium_tests/headers/replays/range_transform.test.yaml index 794ea02000e..de60a72d052 100644 --- a/tests/gold_tests/headers/replays/range_transform.test.yaml +++ b/tests/uranium_tests/headers/replays/range_transform.test.yaml @@ -25,7 +25,7 @@ meta: version: '1.0' -autest: +urtest: description: 'Verify RangeTransform' dns: diff --git a/tests/gold_tests/headers/rewrite_rules/header_rewrite_rules_0.conf b/tests/uranium_tests/headers/rewrite_rules/header_rewrite_rules_0.conf similarity index 100% rename from tests/gold_tests/headers/rewrite_rules/header_rewrite_rules_0.conf rename to tests/uranium_tests/headers/rewrite_rules/header_rewrite_rules_0.conf diff --git a/tests/gold_tests/headers/rewrite_rules/header_rewrite_rules_301.conf b/tests/uranium_tests/headers/rewrite_rules/header_rewrite_rules_301.conf similarity index 100% rename from tests/gold_tests/headers/rewrite_rules/header_rewrite_rules_301.conf rename to tests/uranium_tests/headers/rewrite_rules/header_rewrite_rules_301.conf diff --git a/tests/gold_tests/headers/rewrite_rules/header_rewrite_rules_302.conf b/tests/uranium_tests/headers/rewrite_rules/header_rewrite_rules_302.conf similarity index 100% rename from tests/gold_tests/headers/rewrite_rules/header_rewrite_rules_302.conf rename to tests/uranium_tests/headers/rewrite_rules/header_rewrite_rules_302.conf diff --git a/tests/gold_tests/headers/rewrite_rules/header_rewrite_rules_307.conf b/tests/uranium_tests/headers/rewrite_rules/header_rewrite_rules_307.conf similarity index 100% rename from tests/gold_tests/headers/rewrite_rules/header_rewrite_rules_307.conf rename to tests/uranium_tests/headers/rewrite_rules/header_rewrite_rules_307.conf diff --git a/tests/gold_tests/headers/rewrite_rules/header_rewrite_rules_308.conf b/tests/uranium_tests/headers/rewrite_rules/header_rewrite_rules_308.conf similarity index 100% rename from tests/gold_tests/headers/rewrite_rules/header_rewrite_rules_308.conf rename to tests/uranium_tests/headers/rewrite_rules/header_rewrite_rules_308.conf diff --git a/tests/gold_tests/headers/syntax.200.gold b/tests/uranium_tests/headers/syntax.200.gold similarity index 100% rename from tests/gold_tests/headers/syntax.200.gold rename to tests/uranium_tests/headers/syntax.200.gold diff --git a/tests/gold_tests/headers/syntax.400.gold b/tests/uranium_tests/headers/syntax.400.gold similarity index 100% rename from tests/gold_tests/headers/syntax.400.gold rename to tests/uranium_tests/headers/syntax.400.gold diff --git a/tests/uranium_tests/headers/test_cache_and_req_body.py b/tests/uranium_tests/headers/test_cache_and_req_body.py new file mode 100644 index 00000000000..0270c81196a --- /dev/null +++ b/tests/uranium_tests/headers/test_cache_and_req_body.py @@ -0,0 +1,180 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_cache_and_req_body(urtest: UraniumTest) -> None: + ''' + Test cached responses and requests with bodies using CurlHeader tester + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + urtest.Summary = ''' + Test cached responses and requests with bodies using CurlHeader tester + ''' + urtest.SkipIf(Condition.CurlUsingUnixDomainSocket()) + urtest.ContinueOnFail = True + + # Define default ATS + ts = urtest.MakeATSProcess("ts") + server = urtest.MakeOriginServer("server") + + # **testname is required** + testName = "" + request_header = {"headers": "GET / HTTP/1.1\r\nHost: www.example.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + response_header = { + "headers": + "HTTP/1.1 200 OK\r\nConnection: close\r\nLast-Modified: Tue, 08 May 2018 15:49:41 GMT\r\nCache-Control: max-age=1\r\n\r\n", + "timestamp": "1469733493.993", + "body": "xxx" + } + server.addResponse("sessionlog.json", request_header, response_header) + + # ATS Configuration + ts.Disk.plugin_config.AddLine('xdebug.so --enable=x-cache,x-cache-key,via') + ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'http', + 'proxy.config.http.response_via_str': 3, + }) + + ts.Disk.remap_config.AddLine('map / http://127.0.0.1:{0}'.format(server.Variables.Port)) + + cache_and_req_body_miss = { + 'Connection': 'keep-alive', + 'Via': { + 'equal_re': None + }, + 'Server': { + 'equal_re': '.*' + }, + 'X-Cache-Key': { + 'equal_re': 'http://127.0.0.1.*' + }, + 'X-Cache': 'miss', + 'Last-Modified': { + 'equal_re': '.*' + }, + 'cache-control': 'max-age=1', + 'Content-Length': '3', + 'Date': { + 'equal_re': '.*' + }, + 'Age': { + 'equal_re': '.*' + } + } + + cache_and_req_body_hit = { + 'Last-Modified': { + 'equal_re': '.*' + }, + 'cache-control': 'max-age=1', + 'Content-Length': '3', + 'Date': { + 'equal_re': '.*' + }, + 'Age': { + 'equal_re': '.*' + }, + 'Connection': 'keep-alive', + 'Via': { + 'equal_re': '.*' + }, + 'Server': { + 'equal_re': '.*' + }, + 'X-Cache': 'hit-fresh', + 'HTTP/1.1 200 OK': '' + } + + cache_and_req_body_hit_close = { + 'Last-Modified': { + 'equal_re': '.*' + }, + 'cache-control': 'max-age=1', + 'Content-Length': '3', + 'Date': { + 'equal_re': '.*' + }, + 'Age': { + 'equal_re': '.*' + }, + 'Connection': 'close', + 'Via': { + 'equal_re': '.*' + }, + 'Server': { + 'equal_re': '.*' + }, + 'X-Cache': 'hit-fresh', + 'HTTP/1.1 200 OK': '' + } + + # Test 1 - 200 response and cache fill + tr = urtest.AddTestRun() + tr.Processes.Default.StartBefore(server) + tr.Processes.Default.StartBefore(ts) + tr.MakeCurlCommand( + '-s -D - -v --ipv4 --http1.1 -H "x-debug: x-cache,x-cache-key,via" -H "Host: www.example.com" http://localhost:{port}/' + .format(port=ts.Variables.port), + ts=ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.stdout = Testers.CurlHeader(cache_and_req_body_miss) + tr.StillRunningAfter = ts + + # Test 2 - 200 cached response and using netcat + tr = urtest.AddTestRun() + tr.MakeCurlCommand( + '-s -D - -v --ipv4 --http1.1 -H "x-debug: x-cache,x-cache-key,via" -H "Host: www.example.com" http://localhost:{}'.format( + ts.Variables.port), + ts=ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.stdout = Testers.CurlHeader(cache_and_req_body_hit) + tr.StillRunningAfter = ts + + # Test 3 - 200 cached response and trying to hide a request in the body + tr = urtest.AddTestRun() + tr.Processes.Default.Command = "printf 'GET / HTTP/1.1\r\n''x-debug: x-cache,x-cache-key,via\r\n''Host: www.example.com\r\n''Content-Length: 71\r\n''\r\n''GET /index.html?evil=zorg810 HTTP/1.1\r\n''Host: dummy-host.example.com\r\n''\r\n'|nc 127.0.0.1 -w 1 {port}".format( + port=ts.Variables.port) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.stdout = Testers.CurlHeader(cache_and_req_body_hit) + tr.StillRunningAfter = ts + + # Test 4 - 200 cached response and Content-Length larger than bytes sent, MUST close + tr = urtest.AddTestRun() + tr.Processes.Default.Command = "printf 'GET / HTTP/1.1\r\n''x-debug: x-cache,x-cache-key,via\r\n''Host: dummy-host.example.com\r\n''Cache-control: max-age=300\r\n''Content-Length: 100\r\n''\r\n''GET /index.html?evil=zorg810 HTTP/1.1\r\n''Host: dummy-host.example.com\r\n''\r\n'|nc 127.0.0.1 -w 1 {port}".format( + port=ts.Variables.port) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.stdout = Testers.CurlHeader(cache_and_req_body_hit_close) + tr.StillRunningAfter = ts + urtest.execute() diff --git a/tests/uranium_tests/headers/test_field_name_space.py b/tests/uranium_tests/headers/test_field_name_space.py new file mode 100644 index 00000000000..7af5e2fe0c8 --- /dev/null +++ b/tests/uranium_tests/headers/test_field_name_space.py @@ -0,0 +1,74 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_field_name_space(urtest: UraniumTest) -> None: + ''' + Test on handling spaces after the field name and before the colon + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + urtest.Summary = ''' + Checking on handling spaces after the field name and before the colon + ''' + + urtest.ContinueOnFail = True + + # Define default ATS + ts = urtest.MakeATSProcess("ts") + server = urtest.MakeOriginServer("server") + + testName = "field_name_space" + request_header = {"headers": "GET / HTTP/1.1\r\nHost: www.example.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + response_header = { + "headers": "HTTP/1.1 200 OK\r\nConnection: close\r\nFoo : 123\r\nFoo: 456\r\n", + "timestamp": "1469733493.993", + "body": "xxx" + } + server.addResponse("sessionlog.json", request_header, response_header) + + ts.Disk.remap_config.AddLine('map http://www.example.com http://127.0.0.1:{0}'.format(server.Variables.Port)) + + ipv4flag = "" + if not Condition.CurlUsingUnixDomainSocket(): + ipv4flag = "--ipv4" + + # Test spaces at the end of the field name and before the : + tr = urtest.AddTestRun() + tr.Processes.Default.StartBefore(server, ready=When.PortOpen(server.Variables.Port)) + tr.Processes.Default.StartBefore(urtest.Processes.ts) + tr.MakeCurlCommand( + '-s -D - -v {0} --http1.1 -H "Host: www.example.com" http://localhost:{1}/'.format(ipv4flag, ts.Variables.port), ts=ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.stderr = "gold/field_name_space.gold" + tr.StillRunningAfter = ts + urtest.execute() diff --git a/tests/uranium_tests/headers/test_forwarded.py b/tests/uranium_tests/headers/test_forwarded.py new file mode 100644 index 00000000000..4ecb4ec175e --- /dev/null +++ b/tests/uranium_tests/headers/test_forwarded.py @@ -0,0 +1,307 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_forwarded(urtest: UraniumTest) -> None: + ''' + Test the Forwarded header and related configuration.. + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import os + + urtest.Summary = ''' + Test FORWARDED header. + ''' + urtest.SkipIf(Condition.CurlUsingUnixDomainSocket()) + + urtest.SkipUnless( + Condition.HasCurlFeature('http2'), + Condition.HasCurlFeature('IPv6'), + ) + urtest.ContinueOnFail = True + + testName = "FORWARDED" + + server = urtest.MakeOriginServer("server", options={'--load': os.path.join(urtest.TestDirectory, 'forwarded-observer.py')}) + + request_header = {"headers": "GET / HTTP/1.1\r\nHost: www.no-oride.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + server.addResponse("sessionlog.json", request_header, response_header) + request_header = { + "headers": "GET / HTTP/1.1\r\nHost: www.forwarded-none.com\r\n\r\n", + "timestamp": "1469733493.993", + "body": "" + } + server.addResponse("sessionlog.json", request_header, response_header) + request_header = {"headers": "GET / HTTP/1.1\r\nHost: www.forwarded-for.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + server.addResponse("sessionlog.json", request_header, response_header) + request_header = { + "headers": "GET / HTTP/1.1\r\nHost: www.forwarded-by-ip.com\r\n\r\n", + "timestamp": "1469733493.993", + "body": "" + } + server.addResponse("sessionlog.json", request_header, response_header) + request_header = { + "headers": "GET / HTTP/1.1\r\nHost: www.forwarded-by-unknown.com\r\n\r\n", + "timestamp": "1469733493.993", + "body": "" + } + server.addResponse("sessionlog.json", request_header, response_header) + request_header = { + "headers": "GET / HTTP/1.1\r\nHost: www.forwarded-by-server-name.com\r\n\r\n", + "timestamp": "1469733493.993", + "body": "" + } + server.addResponse("sessionlog.json", request_header, response_header) + request_header = { + "headers": "GET / HTTP/1.1\r\nHost: www.forwarded-by-uuid.com\r\n\r\n", + "timestamp": "1469733493.993", + "body": "" + } + server.addResponse("sessionlog.json", request_header, response_header) + request_header = { + "headers": "GET / HTTP/1.1\r\nHost: www.forwarded-proto.com\r\n\r\n", + "timestamp": "1469733493.993", + "body": "" + } + server.addResponse("sessionlog.json", request_header, response_header) + request_header = { + "headers": "GET / HTTP/1.1\r\nHost: www.forwarded-host.com\r\n\r\n", + "timestamp": "1469733493.993", + "body": "" + } + server.addResponse("sessionlog.json", request_header, response_header) + request_header = { + "headers": "GET / HTTP/1.1\r\nHost: www.forwarded-connection-compact.com\r\n\r\n", + "timestamp": "1469733493.993", + "body": "" + } + server.addResponse("sessionlog.json", request_header, response_header) + request_header = { + "headers": "GET / HTTP/1.1\r\nHost: www.forwarded-connection-std.com\r\n\r\n", + "timestamp": "1469733493.993", + "body": "" + } + server.addResponse("sessionlog.json", request_header, response_header) + request_header = { + "headers": "GET / HTTP/1.1\r\nHost: www.forwarded-connection-full.com\r\n\r\n", + "timestamp": "1469733493.993", + "body": "" + } + server.addResponse("sessionlog.json", request_header, response_header) + + # Set up to check the output after the tests have run. + # + forwarded_log_id = urtest.Disk.File("forwarded.log") + forwarded_log_id.Content = "forwarded.gold" + + def baselineTsSetup(ts): + + ts.addDefaultSSLFiles() + + ts.Disk.records_config.update( + { + # 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.url_remap.pristine_host_hdr': 1, # Retain Host header in original incoming client request. + 'proxy.config.proxy_name': 'Poxy_Proxy', # This will be the server name. + 'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir), + 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts.Variables.SSLDir) + }) + + ts.Disk.ssl_multicert_yaml.AddLines( + """ + ssl_multicert: + - dest_ip: "*" + ssl_cert_name: server.pem + ssl_key_name: server.key + """.split("\n")) + + ts.Disk.remap_config.AddLine('map http://www.no-oride.com http://127.0.0.1:{0}'.format(server.Variables.Port)) + + # Disable the cache to make sure each request is forwarded to the origin + # server. + ts = urtest.MakeATSProcess("ts", enable_tls=True, enable_cache=False) + + baselineTsSetup(ts) + + ts.Disk.remap_config.AddLine( + 'map http://www.forwarded-none.com http://127.0.0.1:{0}'.format(server.Variables.Port) + + ' @plugin=conf_remap.so @pparam=proxy.config.http.insert_forwarded=none') + ts.Disk.remap_config.AddLine( + 'map http://www.forwarded-for.com http://127.0.0.1:{0}'.format(server.Variables.Port) + + ' @plugin=conf_remap.so @pparam=proxy.config.http.insert_forwarded=for') + ts.Disk.remap_config.AddLine( + 'map http://www.forwarded-by-ip.com http://127.0.0.1:{0}'.format(server.Variables.Port) + + ' @plugin=conf_remap.so @pparam=proxy.config.http.insert_forwarded=by=ip') + ts.Disk.remap_config.AddLine( + 'map http://www.forwarded-by-unknown.com http://127.0.0.1:{0}'.format(server.Variables.Port) + + ' @plugin=conf_remap.so @pparam=proxy.config.http.insert_forwarded=by=unknown') + ts.Disk.remap_config.AddLine( + 'map http://www.forwarded-by-server-name.com http://127.0.0.1:{0}'.format(server.Variables.Port) + + ' @plugin=conf_remap.so @pparam=proxy.config.http.insert_forwarded=by=serverName') + ts.Disk.remap_config.AddLine( + 'map http://www.forwarded-by-uuid.com http://127.0.0.1:{0}'.format(server.Variables.Port) + + ' @plugin=conf_remap.so @pparam=proxy.config.http.insert_forwarded=by=uuid') + ts.Disk.remap_config.AddLine( + 'map http://www.forwarded-proto.com http://127.0.0.1:{0}'.format(server.Variables.Port) + + ' @plugin=conf_remap.so @pparam=proxy.config.http.insert_forwarded=proto') + ts.Disk.remap_config.AddLine( + 'map http://www.forwarded-host.com http://127.0.0.1:{0}'.format(server.Variables.Port) + + ' @plugin=conf_remap.so @pparam=proxy.config.http.insert_forwarded=host') + ts.Disk.remap_config.AddLine( + 'map http://www.forwarded-connection-compact.com http://127.0.0.1:{0}'.format(server.Variables.Port) + + ' @plugin=conf_remap.so @pparam=proxy.config.http.insert_forwarded=connection=compact') + ts.Disk.remap_config.AddLine( + 'map http://www.forwarded-connection-std.com http://127.0.0.1:{0}'.format(server.Variables.Port) + + ' @plugin=conf_remap.so @pparam=proxy.config.http.insert_forwarded=connection=std') + ts.Disk.remap_config.AddLine( + 'map http://www.forwarded-connection-full.com http://127.0.0.1:{0}'.format(server.Variables.Port) + + ' @plugin=conf_remap.so @pparam=proxy.config.http.insert_forwarded=connection=full') + + # Basic HTTP 1.1 -- No Forwarded by default + tr = urtest.AddTestRun() + # Wait for the micro server + tr.Processes.Default.StartBefore(server, ready=When.PortOpen(server.Variables.Port)) + # Delay on readiness of our ssl ports + tr.Processes.Default.StartBefore(urtest.Processes.ts) + # + tr.MakeCurlCommand('--verbose --ipv4 --http1.1 --proxy localhost:{} http://www.no-oride.com'.format(ts.Variables.port), ts=ts) + tr.Processes.Default.ReturnCode = 0 + + def TestHttp1_1(host): + + tr = urtest.AddTestRun() + tr.MakeCurlCommand('--verbose --ipv4 --http1.1 --proxy localhost:{} http://{}'.format(ts.Variables.port, host), ts=ts) + tr.Processes.Default.ReturnCode = 0 + + # Basic HTTP 1.1 -- No Forwarded -- explicit configuration. + # + TestHttp1_1('www.forwarded-none.com') + + # Test enabling of each forwarded parameter singly. + + TestHttp1_1('www.forwarded-for.com') + + # Note: forwarded-observer.py counts on the "by" tests being done in the order below. + + TestHttp1_1('www.forwarded-by-ip.com') + TestHttp1_1('www.forwarded-by-unknown.com') + TestHttp1_1('www.forwarded-by-server-name.com') + TestHttp1_1('www.forwarded-by-uuid.com') + + TestHttp1_1('www.forwarded-proto.com') + TestHttp1_1('www.forwarded-host.com') + TestHttp1_1('www.forwarded-connection-compact.com') + TestHttp1_1('www.forwarded-connection-std.com') + TestHttp1_1('www.forwarded-connection-full.com') + + ts2 = urtest.MakeATSProcess("ts2", enable_tls=True) + + baselineTsSetup(ts2) + + ts2.Disk.records_config.update( + { + # 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.url_remap.pristine_host_hdr': 1, # Retain Host header in original incoming client request. + 'proxy.config.http.insert_forwarded': 'by=uuid' + }) + + ts2.Disk.remap_config.AddLine('map https://www.no-oride.com http://127.0.0.1:{0}'.format(server.Variables.Port)) + + # Forwarded header with UUID of 2nd ATS. + tr = urtest.AddTestRun() + # Delay on readiness of our ssl ports + tr.Processes.Default.StartBefore(urtest.Processes.ts2) + # + tr.MakeCurlCommand('--verbose --ipv4 --http1.1 --proxy localhost:{} http://www.no-oride.com'.format(ts2.Variables.port), ts=ts2) + tr.Processes.Default.ReturnCode = 0 + + # Call traffic_ctrl to set insert_forwarded + tr = urtest.AddTestRun() + tr.Processes.Default.Command = ( + 'traffic_ctl --debug config set proxy.config.http.insert_forwarded' + + ' "for|by=ip|by=unknown|by=servername|by=uuid|proto|host|connection=compact|connection=std|connection=full"') + tr.Processes.Default.ForceUseShell = False + tr.Processes.Default.Env = ts2.Env + tr.Processes.Default.ReturnCode = 0 + + # HTTP 1.1 + tr = urtest.AddTestRun() + # Delay to give traffic_ctl config change time to take effect. + tr.DelayStart = 15 + tr.MakeCurlCommand('--verbose --ipv4 --http1.1 --proxy localhost:{} http://www.no-oride.com'.format(ts2.Variables.port), ts=ts2) + tr.Processes.Default.ReturnCode = 0 + + # HTTP 1.0 + tr = urtest.AddTestRun() + tr.MakeCurlCommand('--verbose --ipv4 --http1.0 --proxy localhost:{} http://www.no-oride.com'.format(ts2.Variables.port), ts=ts2) + tr.Processes.Default.ReturnCode = 0 + + # HTTP 1.0 -- Forwarded headers already present + tr = urtest.AddTestRun() + tr.MakeCurlCommand( + "--verbose -H 'forwarded:for=0.6.6.6' -H 'forwarded:for=_argh' --ipv4 --http1.0" + + " --proxy localhost:{} http://www.no-oride.com".format(ts2.Variables.port), + ts=ts2) + tr.Processes.Default.ReturnCode = 0 + + # HTTP 2 + tr = urtest.AddTestRun() + tr.MakeCurlCommand( + '--verbose --ipv4 --http2 --insecure --header "Host: www.no-oride.com"' + + ' https://localhost:{}'.format(ts2.Variables.ssl_port), + ts=ts2) + tr.Processes.Default.ReturnCode = 0 + + # TLS + tr = urtest.AddTestRun() + tr.MakeCurlCommand( + '--verbose --ipv4 --http1.1 --insecure --header "Host: www.no-oride.com" https://localhost:{}'.format( + ts2.Variables.ssl_port), + ts=ts2) + tr.Processes.Default.ReturnCode = 0 + + # IPv6 + + tr = urtest.AddTestRun() + tr.MakeCurlCommand( + '--verbose --ipv6 --http1.1 --proxy localhost:{} http://www.no-oride.com'.format(ts2.Variables.portv6), ts=ts2) + tr.Processes.Default.ReturnCode = 0 + + tr = urtest.AddTestRun() + tr.MakeCurlCommand( + '--verbose --ipv6 --http1.1 --insecure --header "Host: www.no-oride.com" https://localhost:{}'.format( + ts2.Variables.ssl_portv6), + ts=ts2) + tr.Processes.Default.ReturnCode = 0 + urtest.execute() diff --git a/tests/uranium_tests/headers/test_general_connection_failure_502.py b/tests/uranium_tests/headers/test_general_connection_failure_502.py new file mode 100644 index 00000000000..7e4a1791247 --- /dev/null +++ b/tests/uranium_tests/headers/test_general_connection_failure_502.py @@ -0,0 +1,67 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_general_connection_failure_502(urtest: UraniumTest) -> None: + ''' + Test response when connection to origin fails + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import os + import sys + + urtest.Summary = ''' + Test response when connection to origin fails + ''' + ts = urtest.MakeATSProcess("ts") + + HOST = 'www.connectfail502.test' + server = urtest.MakeOriginServer("server", ssl=False) # Reserves a port across autest. + + ts.Disk.remap_config.AddLine( + 'map http://{host} http://{ip}:{uport}'.format(host=HOST, ip='127.0.0.1', uport=server.Variables.Port)) + + urtest.Setup.Copy(os.path.join(urtest.Variables.AtsTestToolsDir, 'tcp_client.py')) + + data_file = urtest.Disk.File("www.connectfail502.test-get.txt", id="datafile") + data_file.WriteOn("GET / HTTP/1.1\r\nHost: {host}\r\n\r\n".format(host=HOST)) + + tr = urtest.AddTestRun() + tr.Processes.Default.StartBefore(urtest.Processes.ts) + # Do not start the origin server: We wish to simulate connection refused while hopefully no one else uses this port. + tr.Processes.Default.Command = \ + (f"{sys.executable} tcp_client.py 127.0.0.1 {ts.Variables.port} www.connectfail502.test-get.txt | " + r"sed -e '/^Date: /d' -e '/^Server: ATS\//d'") + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.stdout = 'general-connection-failure-502.gold' + urtest.execute() diff --git a/tests/uranium_tests/headers/test_good_request_after_bad.py b/tests/uranium_tests/headers/test_good_request_after_bad.py new file mode 100644 index 00000000000..94a5af9ba06 --- /dev/null +++ b/tests/uranium_tests/headers/test_good_request_after_bad.py @@ -0,0 +1,213 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_good_request_after_bad(urtest: UraniumTest) -> None: + ''' + Verify that request following a ill-formed request is not processed + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import os + + urtest.Summary = ''' + Verify that request following a ill-formed request is not processed + ''' + urtest.ContinueOnFail = True + ts = urtest.MakeATSProcess("ts") + urtest.ContinueOnFail = True + ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.tags': 'http', + 'proxy.config.diags.debug.enabled': 0, + 'proxy.config.http.strict_uri_parsing': 1 + }) + + ts2 = urtest.MakeATSProcess("ts2") + + ts2.Disk.records_config.update( + { + 'proxy.config.diags.debug.tags': 'http', + 'proxy.config.diags.debug.enabled': 0, + 'proxy.config.http.strict_uri_parsing': 2 + }) + + server = urtest.MakeOriginServer("server") + request_header = {"headers": "GET / HTTP/1.1\r\nHost: www.example.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + response_header = { + "headers": + "HTTP/1.1 200 OK\r\nConnection: close\r\nLast-Modified: Tue, 08 May 2018 15:49:41 GMT\r\nCache-Control: max-age=1000\r\n\r\n", + "timestamp": "1469733493.993", + "body": "xxx" + } + server.addResponse("sessionlog.json", request_header, response_header) + + ts.Disk.remap_config.AddLine('map / http://127.0.0.1:{0}'.format(server.Variables.Port)) + ts.Disk.remap_config.AddLine('map /bob<> http://127.0.0.1:{0}'.format(server.Variables.Port)) + ts2.Disk.remap_config.AddLine('map / http://127.0.0.1:{0}'.format(server.Variables.Port)) + ts2.Disk.remap_config.AddLine('map /bob<> http://127.0.0.1:{0}'.format(server.Variables.Port)) + + trace_out = urtest.Disk.File("trace_curl.txt") + + # Make a good request to get item in the cache for later tests + tr = urtest.AddTestRun("Good control") + tr.Processes.Default.StartBefore(server) + tr.Processes.Default.StartBefore(urtest.Processes.ts) + tr.Processes.Default.Command = 'printf "GET / HTTP/1.1\r\nHost: bob\r\n\r\n" | nc 127.0.0.1 {}'.format(ts.Variables.port) + tr.Processes.Default.ReturnCode = 0 + + tr = urtest.AddTestRun("Good control") + tr.Processes.Default.StartBefore(server) + tr.Processes.Default.StartBefore(urtest.Processes.ts2) + tr.Processes.Default.Command = 'printf "GET / HTTP/1.1\r\nHost: bob\r\n\r\n" | nc 127.0.0.1 {}'.format(ts2.Variables.port) + tr.Processes.Default.ReturnCode = 0 + + tr = urtest.AddTestRun("space after header name") + tr.Processes.Default.Command = 'printf "GET / HTTP/1.1\r\nHost : bob\r\n\r\nGET / HTTP/1.1\r\nHost: boa\r\n\r\n" | nc 127.0.0.1 {}'.format( + ts.Variables.port) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.stdout = 'gold/bad_good_request.gold' + + tr = urtest.AddTestRun("Bad protocol number") + tr.Processes.Default.Command = 'printf "GET / HTTP/11.1\r\nhost: bob\r\n\r\nGET / HTTP/1.1\r\nHost: boa\r\n\r\n" | nc 127.0.0.1 {}'.format( + ts.Variables.port) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.stdout = 'gold/bad_protocol_number.gold' + + tr = urtest.AddTestRun("Unsupported Transfer Encoding value") + tr.Processes.Default.Command = 'printf "GET / HTTP/1.1\r\nhost: bob\r\ntransfer-encoding: random\r\n\r\nGET / HTTP/1.1\r\nHost: boa\r\n\r\n" | nc 127.0.0.1 {}'.format( + ts.Variables.port) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.stdout = 'gold/bad_te_value.gold' + + tr = urtest.AddTestRun("Another unsupported Transfer Encoding value") + tr.Processes.Default.Command = 'printf "GET / HTTP/1.1\r\nhost: bob\r\ntransfer-encoding: \x08chunked\r\n\r\nGET / HTTP/1.1\r\nHost: boa\r\n\r\n" | nc 127.0.0.1 {}'.format( + ts.Variables.port) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.stdout = 'gold/invalid_character_in_te_value.gold' + + tr = urtest.AddTestRun("Extra characters in content-length") + tr.Processes.Default.Command = 'printf "GET / HTTP/1.1\r\nhost: bob\r\ncontent-length:+3\r\n\r\nGET / HTTP/1.1\r\nHost: boa\r\n\r\n" | nc 127.0.0.1 {}'.format( + ts.Variables.port) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.stdout = 'gold/bad_good_request_header.gold' + + tr = urtest.AddTestRun("Different extra characters in content-length") + tr.Processes.Default.Command = 'printf "GET / HTTP/1.1\r\nhost: bob\r\ncontent-length:\x0c3\r\n\r\nGET / HTTP/1.1\r\nHost: boa\r\n\r\n" | nc 127.0.0.1 {}'.format( + ts.Variables.port) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.stdout = 'gold/bad_good_request_header.gold' + + # TRACE request with a body + tr = urtest.AddTestRun("Trace request with a body") + tr.Processes.Default.Command = 'printf "TRACE /foo HTTP/1.1\r\nHost: bob\r\nContent-length:2\r\n\r\nokGET / HTTP/1.1\r\nHost: boa\r\n\r\n" | nc 127.0.0.1 {}'.format( + ts.Variables.port) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.stdout = 'gold/bad_good_request.gold' + + tr = urtest.AddTestRun("Trace request with a chunked body") + tr.Processes.Default.Command = 'printf "TRACE /foo HTTP/1.1\r\nHost: bob\r\ntransfer-encoding: chunked\r\n\r\n2\r\nokGGET / HTTP/1.1\r\nHost: boa\r\n\r\n" | nc 127.0.0.1 {}'.format( + ts.Variables.port) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.stdout = 'gold/bad_good_request.gold' + + tr = urtest.AddTestRun("Trace request with a chunked body via curl") + tr.MakeCurlCommand( + '-v --http1.1 --header "Transfer-Encoding: chunked" -d aaa -X TRACE -o trace_curl.txt -k http://127.0.0.1:{}/foo'.format( + ts.Variables.port), + ts=ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.All = 'gold/bad_good_request_header.gold' + trace_out.Content = Testers.ContainsExpression("Bad Request", "ATS error msg") + trace_out.Content += Testers.ContainsExpression("Description: Could not process this request.", "ATS error msg") + + tr = urtest.AddTestRun("Trace request via curl") + tr.MakeCurlCommand('-v --http1.1 -X TRACE -k http://127.0.0.1:{}/bar'.format(ts.Variables.port), ts=ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.All = Testers.ContainsExpression( + r"HTTP/1.1 501 Unsupported method \('TRACE'\)", "microserver does not support TRACE") + + # Methods are case sensitive. Verify that "gET" is not confused with "GET". + tr = urtest.AddTestRun("mixed case method") + tr.Processes.Default.Command = 'printf "gET / HTTP/1.1\r\nHost:bob\r\n\r\nGET / HTTP/1.1\r\nHost: boa\r\n\r\n" | nc 127.0.0.1 {}'.format( + ts.Variables.port) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.stdout = 'gold/bad_method.gold' + + # mangled termination + tr = urtest.AddTestRun("mangled line termination") + tr.Processes.Default.Command = 'printf "GET / HTTP/1.1\r\nHost:bob\r\n \r\nGET / HTTP/1.1\r\nHost: boa\r\n\r\n" | nc 127.0.0.1 {}'.format( + ts.Variables.port) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.stdout = 'gold/bad_good_request.gold' + + tr = urtest.AddTestRun("Catch bad URL characters") + tr.Processes.Default.Command = 'printf "GET /bob<> HTTP/1.1\r\nhost: bob\r\n\r\nGET / HTTP/1.1\r\nHost: boa\r\n\r\n" | nc 127.0.0.1 {}'.format( + ts.Variables.port) + tr.Processes.Default.ReturnCode = 0 + # Since the request line is messsed up ATS will reply with HTTP/1.0 + tr.Processes.Default.Streams.stdout = 'gold/bad_good_request_http1.gold' + + tr = urtest.AddTestRun("Catch whitespace in URL") + tr.Processes.Default.Command = 'printf "GET /bob foo HTTP/1.1\r\nhost: bob\r\n\r\nGET / HTTP/1.1\r\nHost: boa\r\n\r\n" | nc 127.0.0.1 {}'.format( + ts.Variables.port) + tr.Processes.Default.ReturnCode = 0 + # Since the request line is messsed up ATS will reply with HTTP/1.0 + tr.Processes.Default.Streams.stdout = 'gold/bad_good_request_http1.gold' + + tr = urtest.AddTestRun("Extra characters in protocol") + tr.Processes.Default.Command = 'printf "GET / HTP/1.1\r\nhost: bob\r\n\r\nGET / HTTP/1.1\r\nHost: boa\r\n\r\n" | nc 127.0.0.1 {}'.format( + ts.Variables.port) + tr.Processes.Default.ReturnCode = 0 + # Since the request line is messsed up ATS will reply with HTTP/1.0 + tr.Processes.Default.Streams.stdout = 'gold/bad_good_request_http1.gold' + + tr = urtest.AddTestRun("Characters that are strict but not case 2 bad") + tr.Processes.Default.Command = 'printf "GET /bob<> HTTP/1.1\r\nhost: bob\r\n\r\nGET / HTTP/1.1\r\nHost: boa\r\n\r\n" | nc 127.0.0.1 {}'.format( + ts2.Variables.port) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.All = Testers.ContainsExpression("HTTP/1.1 200 OK", "Success") + + tr = urtest.AddTestRun("Catch whitespace in URL") + tr.Processes.Default.Command = 'printf "GET /bob foo HTTP/1.1\r\nhost: bob\r\n\r\nGET / HTTP/1.1\r\nHost: boa\r\n\r\n" | nc 127.0.0.1 {}'.format( + ts2.Variables.port) + tr.Processes.Default.ReturnCode = 0 + # Since the request line is messsed up ATS will reply with HTTP/1.0 + tr.Processes.Default.Streams.stdout = 'gold/bad_good_request_http1.gold' + + tr = urtest.AddTestRun("Extra characters in protocol") + tr.Processes.Default.Command = 'printf "GET / HTP/1.1\r\nhost: bob\r\n\r\nGET / HTTP/1.1\r\nHost: boa\r\n\r\n" | nc 127.0.0.1 {}'.format( + ts2.Variables.port) + tr.Processes.Default.ReturnCode = 0 + # Since the request line is messsed up ATS will reply with HTTP/1.0 + tr.Processes.Default.Streams.stdout = 'gold/bad_good_request_http1.gold' + urtest.execute() diff --git a/tests/uranium_tests/headers/test_http408.py b/tests/uranium_tests/headers/test_http408.py new file mode 100644 index 00000000000..6e077bc1e21 --- /dev/null +++ b/tests/uranium_tests/headers/test_http408.py @@ -0,0 +1,81 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_http408(urtest: UraniumTest) -> None: + ''' + Test the 408 response header. + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import os + import sys + + urtest.Summary = ''' + Check 408 response header for protocol stack data. + ''' + + urtest.ContinueOnFail = True + + # Define default ATS + ts = urtest.MakeATSProcess("ts") + server = urtest.MakeOriginServer("server") + + HTTP_408_HOST = 'www.http408.test' + + request_header = { + "headers": "GET / HTTP/1.1\r\nHost: {}\r\n\r\n".format(HTTP_408_HOST), + "timestamp": "1469733493.993", + "body": "" + } + response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + server.addResponse("sessionlog.json", request_header, response_header) + + ts.Disk.remap_config.AddLine('map http://{0} http://127.0.0.1:{1}'.format(HTTP_408_HOST, server.Variables.Port)) + + TIMEOUT = 2 + ts.Disk.records_config.update({ + 'proxy.config.http.transaction_no_activity_timeout_in': TIMEOUT, + }) + + urtest.Setup.Copy(os.path.join(os.pardir, os.pardir, 'tools', 'tcp_client.py')) + urtest.Setup.Copy('data') + + tr = urtest.AddTestRun() + tr.Processes.Default.StartBefore(server) + tr.Processes.Default.StartBefore(urtest.Processes.ts) + DELAY = TIMEOUT + 2 + tr.Processes.Default.Command = f'{sys.executable} tcp_client.py 127.0.0.1 {ts.Variables.port} data/{HTTP_408_HOST}.txt --delay-after-send {DELAY}' + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.TimeOut = 10 + tr.Processes.Default.Streams.stdout = "http408.gold" + urtest.execute() diff --git a/tests/uranium_tests/headers/test_normalize_ae.py b/tests/uranium_tests/headers/test_normalize_ae.py new file mode 100644 index 00000000000..bc03be5ff23 --- /dev/null +++ b/tests/uranium_tests/headers/test_normalize_ae.py @@ -0,0 +1,216 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_normalize_ae(urtest: UraniumTest) -> None: + ''' + Test normalizations of the Accept-Encoding header field. + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import os + + urtest.Summary = ''' + Test normalizations of the Accept-Encoding header field. + ''' + + urtest.SkipUnless(Condition.HasATSFeature('TS_HAS_BROTLI')) + + urtest.ContinueOnFail = True + + server = urtest.MakeOriginServer("server", options={'--load': os.path.join(urtest.TestDirectory, 'normalize_ae_observer.py')}) + + testName = "NORMALIZE_AE" + + request_header = {"headers": "GET / HTTP/1.1\r\nHost: www.no-oride.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + server.addResponse("sessionlog.json", request_header, response_header) + request_header = {"headers": "GET / HTTP/1.1\r\nHost: www.ae-0.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + server.addResponse("sessionlog.json", request_header, response_header) + request_header = {"headers": "GET / HTTP/1.1\r\nHost: www.ae-1.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + server.addResponse("sessionlog.json", request_header, response_header) + request_header = {"headers": "GET / HTTP/1.1\r\nHost: www.ae-2.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + server.addResponse("sessionlog.json", request_header, response_header) + request_header = {"headers": "GET / HTTP/1.1\r\nHost: www.ae-4.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + server.addResponse("sessionlog.json", request_header, response_header) + request_header = {"headers": "GET / HTTP/1.1\r\nHost: www.ae-5.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + server.addResponse("sessionlog.json", request_header, response_header) + + # Define first ATS. Disable the cache to make sure each request is sent to the + # origin server. + ts = urtest.MakeATSProcess("ts", enable_cache=False) + + def baselineTsSetup(ts): + + ts.Disk.records_config.update({ + # 'proxy.config.diags.debug.enabled': 1, + }) + + ts.Disk.remap_config.AddLine('map http://www.no-oride.com http://127.0.0.1:{0}'.format(server.Variables.Port)) + ts.Disk.remap_config.AddLine( + 'map http://www.ae-0.com http://127.0.0.1:{0}'.format(server.Variables.Port) + + ' @plugin=conf_remap.so @pparam=proxy.config.http.normalize_ae=0') + ts.Disk.remap_config.AddLine( + 'map http://www.ae-1.com http://127.0.0.1:{0}'.format(server.Variables.Port) + + ' @plugin=conf_remap.so @pparam=proxy.config.http.normalize_ae=1') + ts.Disk.remap_config.AddLine( + 'map http://www.ae-2.com http://127.0.0.1:{0}'.format(server.Variables.Port) + + ' @plugin=conf_remap.so @pparam=proxy.config.http.normalize_ae=2') + ts.Disk.remap_config.AddLine( + 'map http://www.ae-3.com http://127.0.0.1:{0}'.format(server.Variables.Port) + + ' @plugin=conf_remap.so @pparam=proxy.config.http.normalize_ae=3') + ts.Disk.remap_config.AddLine( + 'map http://www.ae-4.com http://127.0.0.1:{0}'.format(server.Variables.Port) + + ' @plugin=conf_remap.so @pparam=proxy.config.http.normalize_ae=4') + ts.Disk.remap_config.AddLine( + 'map http://www.ae-5.com http://127.0.0.1:{0}'.format(server.Variables.Port) + + ' @plugin=conf_remap.so @pparam=proxy.config.http.normalize_ae=5') + + baselineTsSetup(ts) + + # set up to check the output after the tests have run. + # + normalize_ae_log_id = urtest.Disk.File("normalize_ae.log") + normalize_ae_log_id.Content = "normalize_ae.gold" + + # Try various Accept-Encoding header fields for a particular traffic server and host. + + ipv4flag = "" + if not Condition.CurlUsingUnixDomainSocket(): + ipv4flag = "--ipv4" + + def allAEHdrs(shouldWaitForUServer, shouldWaitForTs, ts, host): + + tr = urtest.AddTestRun() + + if shouldWaitForUServer: + # wait for the micro server + tr.Processes.Default.StartBefore(server) + + if shouldWaitForTs: + # wait for the micro server + # delay on readiness of port + tr.Processes.Default.StartBefore(ts) + + baseCurl = '--verbose {0} --http1.1 --proxy localhost:{1} '.format(ipv4flag, ts.Variables.port) + + # No Accept-Encoding header. + # + tr.MakeCurlCommand(baseCurl + '--header "X-Au-Test: {0}" http://{0}'.format(host), ts=ts) + tr.Processes.Default.ReturnCode = 0 + + def curlTail(hdrValue): + return '--header "Accept-Encoding: {}" http://'.format(hdrValue) + host + + tr = urtest.AddTestRun() + tr.MakeCurlCommand(baseCurl + curlTail('gzip'), ts=ts) + tr.Processes.Default.ReturnCode = 0 + + tr = urtest.AddTestRun() + tr.MakeCurlCommand(baseCurl + curlTail('x-gzip'), ts=ts) + tr.Processes.Default.ReturnCode = 0 + + tr = urtest.AddTestRun() + tr.MakeCurlCommand(baseCurl + curlTail('br'), ts=ts) + tr.Processes.Default.ReturnCode = 0 + + tr = urtest.AddTestRun() + tr.MakeCurlCommand(baseCurl + curlTail('gzip, br'), ts=ts) + tr.Processes.Default.ReturnCode = 0 + + tr = urtest.AddTestRun() + tr.MakeCurlCommand(baseCurl + curlTail('gzip;q=0.3, whatever;q=0.666, br;q=0.7'), ts=ts) + tr.Processes.Default.ReturnCode = 0 + + # ZSTD-related tests for normalize_ae modes 4 and 5 + tr = urtest.AddTestRun() + tr.MakeCurlCommand(baseCurl + curlTail('zstd')) + tr.Processes.Default.ReturnCode = 0 + + tr = urtest.AddTestRun() + tr.MakeCurlCommand(baseCurl + curlTail('zstd, gzip')) + tr.Processes.Default.ReturnCode = 0 + + tr = urtest.AddTestRun() + tr.MakeCurlCommand(baseCurl + curlTail('zstd, br')) + tr.Processes.Default.ReturnCode = 0 + + tr = urtest.AddTestRun() + tr.MakeCurlCommand(baseCurl + curlTail('zstd, br, gzip')) + tr.Processes.Default.ReturnCode = 0 + + tr = urtest.AddTestRun() + tr.MakeCurlCommand(baseCurl + curlTail('gzip, zstd, br')) + tr.Processes.Default.ReturnCode = 0 + + tr = urtest.AddTestRun() + tr.MakeCurlCommand(baseCurl + curlTail('br, zstd')) + tr.Processes.Default.ReturnCode = 0 + + tr = urtest.AddTestRun() + tr.MakeCurlCommand(baseCurl + curlTail('zstd;q=0.8, br;q=0.7, gzip;q=0.6')) + tr.Processes.Default.ReturnCode = 0 + + tr = urtest.AddTestRun() + tr.MakeCurlCommand(baseCurl + curlTail('deflate, zstd')) + tr.Processes.Default.ReturnCode = 0 + + tr = urtest.AddTestRun() + tr.MakeCurlCommand(baseCurl + curlTail('identity, zstd, compress')) + tr.Processes.Default.ReturnCode = 0 + + tr = urtest.AddTestRun() + tr.MakeCurlCommand(baseCurl + curlTail('br, compress')) + tr.Processes.Default.ReturnCode = 0 + + def perTsTest(shouldWaitForUServer, ts): + allAEHdrs(shouldWaitForUServer, True, ts, 'www.no-oride.com') + allAEHdrs(False, False, ts, 'www.ae-0.com') + allAEHdrs(False, False, ts, 'www.ae-1.com') + allAEHdrs(False, False, ts, 'www.ae-2.com') + allAEHdrs(False, False, ts, 'www.ae-3.com') + allAEHdrs(False, False, ts, 'www.ae-4.com') + allAEHdrs(False, False, ts, 'www.ae-5.com') + + perTsTest(True, ts) + + # Define second ATS + ts2 = urtest.MakeATSProcess("ts2") + + baselineTsSetup(ts2) + + ts2.Disk.records_config.update({ + 'proxy.config.http.normalize_ae': 0, + }) + + perTsTest(False, ts2) + urtest.execute() diff --git a/tests/uranium_tests/headers/test_oversized_field_h2.py b/tests/uranium_tests/headers/test_oversized_field_h2.py new file mode 100644 index 00000000000..2f7643e5ae5 --- /dev/null +++ b/tests/uranium_tests/headers/test_oversized_field_h2.py @@ -0,0 +1,162 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_oversized_field_h2(urtest: UraniumTest) -> None: + ''' + Verify that an HTTP/2 header whose name or value length exceeds the uint16_t + field-length limit (>= 65535 bytes) is treated as an HPACK connection error + (GOAWAY with COMPRESSION_ERROR, code 0x9) and is never forwarded to the origin. + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import sys + + urtest.Summary = ''' + Verify that an HTTP/2 header field whose name or value length exceeds the + uint16_t field-length limit (>= 65535 bytes) is rejected as an HPACK connection + error (GOAWAY COMPRESSION_ERROR, code 0x9) rather than stored with a truncated + length, and is never forwarded to the origin. + ''' + + urtest.SkipUnless(Condition.HasOpenSSLVersion('1.1.1'), Condition.HasProxyVerifierVersion('2.8.0')) + + class OversizedFieldH2Test: + '''Drive an oversized HTTP/2 header with a raw HPACK client.''' + + replayFile = "replay/oversized_field_h2.replay.yaml" + clientScript = "oversized_field_h2_client.py" + + # Header sizes large enough to exceed the uint16_t (65535) field-length limit. + oversizedSize = 70000 + + def __init__(self): + self.__setupOriginServer() + self.__setupTS() + self.__setupClient() + + def __setupOriginServer(self): + self._server = urtest.MakeVerifierServerProcess("verifier-server", self.replayFile) + # The origin must never receive the oversized requests. If ATS forwarded + # them (the pre-fix behavior), the verifier server would log a request + # for these paths / serve the marker bodies. + self._server.Streams.All += Testers.ExcludesExpression( + 'h2-oversized-value', 'Origin must not receive the oversized-value request.') + self._server.Streams.All += Testers.ExcludesExpression( + 'h2-oversized-name', 'Origin must not receive the oversized-name request.') + # Regression guard: the normal, under-limit request MUST reach the origin. + self._server.Streams.All += Testers.ContainsExpression( + 'h2-normal', 'Origin must receive the normal under-limit request.') + + def __setupTS(self): + self._ts = urtest.MakeATSProcess("ts", enable_tls=True, enable_cache=False) + self._ts.addDefaultSSLFiles() + self._ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'http2|hpack', + 'proxy.config.ssl.server.cert.path': f"{self._ts.Variables.SSLDir}", + 'proxy.config.ssl.server.private_key.path': f"{self._ts.Variables.SSLDir}", + # max_header_list_size is raised well above the oversized field so the + # request is not rejected at the HTTP/2 header-list-size level first. + # header_field_max_size is set to its maximum (65535, the uint16_t + # ceiling enforced by the records range check); a field larger than that + # is rejected by the configured field-size limit with a COMPRESSION_ERROR. + # The uint16_t storage limit in the MIME setters is exercised directly by + # the HpackIndexingTable unit test: header_field_max_size can no longer be + # configured above 65535, so an oversized field can no longer reach the + # storage path end to end through config. + 'proxy.config.http.header_field_max_size': 65535, + 'proxy.config.http2.max_header_list_size': 8 * 1024 * 1024, + }) + # Rejecting the oversized field is an HPACK connection error, so ATS + # intentionally logs "ERROR: HTTP/2 connection error code=0x09 ... + # compression error". Whitelist exactly that line; the default check + # treats any "ERROR:" in diags.log as a failure, so assert this + # expected line is present instead. + self._ts.Disk.diags_log.Content = Testers.ContainsExpression( + r"ERROR: HTTP/2 connection error code=0x09 .* compression error", + "ATS must log the expected HTTP/2 COMPRESSION_ERROR for the oversized field.") + self._ts.Disk.remap_config.AddLine(f"map / http://127.0.0.1:{self._server.Variables.http_port}") + self._ts.Disk.ssl_multicert_yaml.AddLines( + """ + ssl_multicert: + - dest_ip: "*" + ssl_cert_name: server.pem + ssl_key_name: server.key + """.split("\n")) + + def __setupClient(self): + self._ts.Setup.CopyAs(f"clients/{self.clientScript}", urtest.RunDirectory) + + def run(self): + # Case 1: oversized header VALUE. + tr = urtest.AddTestRun("oversized H2 header value") + tr.Processes.Default.StartBefore(self._server) + tr.Processes.Default.StartBefore(self._ts) + port = self._ts.Variables.ssl_port + tr.Processes.Default.Command = ( + f"{sys.executable} {self.clientScript} /h2-oversized-value 0 {self.oversizedSize} 127.0.0.1 {port} example.com") + tr.Processes.Default.ReturnCode = 0 + # No :status (connection error, not a response) and GOAWAY COMPRESSION_ERROR (0x9). + tr.Processes.Default.Streams.All += Testers.ContainsExpression( + 'status=None', 'Client must not get an HTTP response for the oversized header.') + tr.Processes.Default.Streams.All += Testers.ContainsExpression( + 'goaway_error=9', 'ATS must send GOAWAY with COMPRESSION_ERROR (0x9).') + tr.StillRunningAfter = self._ts + tr.StillRunningAfter = self._server + + # Case 2: oversized header NAME. + tr = urtest.AddTestRun("oversized H2 header name") + tr.Processes.Default.Command = ( + f"{sys.executable} {self.clientScript} /h2-oversized-name {self.oversizedSize} 0 127.0.0.1 {port} example.com") + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.All += Testers.ContainsExpression( + 'status=None', 'Client must not get an HTTP response for the oversized header.') + tr.Processes.Default.Streams.All += Testers.ContainsExpression( + 'goaway_error=9', 'ATS must send GOAWAY with COMPRESSION_ERROR (0x9).') + tr.StillRunningAfter = self._ts + tr.StillRunningAfter = self._server + + # Case 3: NORMAL, under-limit request (regression guard against + # over-rejection). Sanity mode "0 0" sends a plain GET; the client must + # get a 200 and the origin must receive it. + tr = urtest.AddTestRun("normal under-limit H2 request") + tr.Processes.Default.Command = (f"{sys.executable} {self.clientScript} /h2-normal 0 0 127.0.0.1 {port} example.com") + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.All += Testers.ContainsExpression( + 'status=200', 'Client must get a 200 for the normal under-limit request.') + tr.StillRunningAfter = self._ts + tr.StillRunningAfter = self._server + + OversizedFieldH2Test().run() + urtest.execute() diff --git a/tests/uranium_tests/headers/test_syntax.py b/tests/uranium_tests/headers/test_syntax.py new file mode 100644 index 00000000000..0a0327bb95f --- /dev/null +++ b/tests/uranium_tests/headers/test_syntax.py @@ -0,0 +1,123 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_syntax(urtest: UraniumTest) -> None: + ''' + Test whitespace between field name and colon in the header + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + urtest.Summary = ''' + Test whitespace between field name and colon in the header + ''' + + urtest.ContinueOnFail = True + + # Define default ATS + ts = urtest.MakeATSProcess("ts") + server = urtest.MakeOriginServer("server") + + # **testname is required** + testName = "" + request_header = {"headers": "GET / HTTP/1.1\r\nHost: www.example.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + server.addResponse("sessionlog.json", request_header, response_header) + + ts.Disk.remap_config.AddLine('map http://www.example.com http://127.0.0.1:{0}'.format(server.Variables.Port)) + + ipv4flag = "" + if not Condition.CurlUsingUnixDomainSocket(): + ipv4flag = "--ipv4" + + # Test 0 - 200 Response + tr = urtest.AddTestRun() + tr.Processes.Default.StartBefore(server, ready=When.PortOpen(server.Variables.Port)) + tr.Processes.Default.StartBefore(urtest.Processes.ts) + tr.MakeCurlCommand( + '-s -D - -v {0} --http1.1 -H " foo: bar" -H "Host: www.example.com" http://localhost:{1}/'.format( + ipv4flag, ts.Variables.port), + ts=ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.stdout = "syntax.200.gold" + tr.StillRunningAfter = ts + + # Test 1 - 400 Response - Single space after field name + tr = urtest.AddTestRun() + tr.MakeCurlCommand( + '-s -D - -v {0} --http1.1 -H "foo : bar" -H "Host: www.example.com" http://localhost:{1}/'.format( + ipv4flag, ts.Variables.port), + ts=ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.stdout = "syntax.400.gold" + tr.StillRunningAfter = ts + + # Test 2 - 400 Response - Double space after field name + tr = urtest.AddTestRun() + tr.MakeCurlCommand( + '-s -D - -v {0} --http1.1 -H "foo : bar" -H "Host: www.example.com" http://localhost:{1}/'.format( + ipv4flag, ts.Variables.port), + ts=ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.stdout = "syntax.400.gold" + tr.StillRunningAfter = ts + + # Test 3 - 400 Response - Three different Content-Length headers + tr = urtest.AddTestRun() + tr.MakeCurlCommand( + '-s -D - -v {0} --http1.1 -d "hello world" -H "Content-Length: 11" -H "Content-Length: 10" -H "Content-Length: 9" -H "Host: www.example.com" http://localhost:{1}/' + .format(ipv4flag, ts.Variables.port), + ts=ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.stdout = "syntax.400.gold" + tr.StillRunningAfter = ts + + # Test 4 - 200 Response - Three same Content-Length headers + tr = urtest.AddTestRun() + tr.MakeCurlCommand( + '-s -D - -v {0} --http1.1 -d "hello world" -H "Content-Length: 11" -H "Content-Length: 11" -H "Content-Length: 11" -H "Host: www.example.com" http://localhost:{1}/' + .format(ipv4flag, ts.Variables.port), + ts=ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.stdout = "syntax.200.gold" + tr.StillRunningAfter = ts + + # Test 5 - 200 Response - Three different Content-Length headers with a Transfer encoding header + tr = urtest.AddTestRun() + tr.MakeCurlCommand( + '-s -D - -v {0} --http1.1 -d "hello world" -H "Transfer-Encoding: chunked" -H "Content-Length: 11" -H "Content-Length: 10" -H "Content-Length: 9" -H "Host: www.example.com" http://localhost:{1}/' + .format(ipv4flag, ts.Variables.port), + ts=ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.stdout = "syntax.200.gold" + tr.StillRunningAfter = ts + urtest.execute() diff --git a/tests/uranium_tests/headers/test_via.py b/tests/uranium_tests/headers/test_via.py new file mode 100644 index 00000000000..921666c1753 --- /dev/null +++ b/tests/uranium_tests/headers/test_via.py @@ -0,0 +1,178 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_via(urtest: UraniumTest) -> None: + ''' + Test the VIA header. This runs several requests through ATS and extracts the upstream VIA headers. + Those are then checked against a gold file to verify the protocol stack based output is correct. + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import os + + urtest.Summary = ''' + Check VIA header for protocol stack data. + ''' + + urtest.SkipUnless(Condition.HasCurlFeature('http2'), Condition.HasCurlFeature('IPv6')) + urtest.ContinueOnFail = True + + # Define default ATS + if Condition.HasATSFeature('TS_USE_QUIC') and Condition.HasCurlFeature('http3'): + ts = urtest.MakeATSProcess("ts", enable_tls=True, enable_quic=True) + else: + ts = urtest.MakeATSProcess("ts", enable_tls=True) + + server = urtest.MakeOriginServer("server", options={'--load': os.path.join(urtest.TestDirectory, 'via-observer.py')}) + + testName = "VIA" + + # We only need one transaction as only the VIA header will be checked. + request_header = {"headers": "GET / HTTP/1.1\r\nHost: www.example.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + server.addResponse("sessionlog.json", request_header, response_header) + + # These should be promoted rather than other tests like this reaching around. + ts.addDefaultSSLFiles() + + ts.Disk.records_config.update( + { + 'proxy.config.http.insert_request_via_str': 4, + 'proxy.config.http.insert_response_via_str': 4, + 'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir), + 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts.Variables.SSLDir), + }) + + ts.Disk.remap_config.AddLine('map http://www.example.com http://127.0.0.1:{0}'.format(server.Variables.Port)) + ts.Disk.remap_config.AddLine( + 'map https://www.example.com http://127.0.0.1:{0}'.format(server.Variables.Port, ts.Variables.ssl_port)) + + ts.Disk.ssl_multicert_yaml.AddLines( + """ + ssl_multicert: + - dest_ip: "*" + ssl_cert_name: server.pem + ssl_key_name: server.key + """.split("\n")) + + # Set up to check the output after the tests have run. + via_log_id = urtest.Disk.File("via.log") + if Condition.HasATSFeature('TS_USE_QUIC') and Condition.HasCurlFeature('http3'): + via_log_id.Content = "via_h3.gold" + elif Condition.CurlUsingUnixDomainSocket(): + via_log_id.Content = "via_uds.gold" + else: + via_log_id.Content = "via.gold" + + ipv4flag = "" + ipv6flag = "" + if not Condition.CurlUsingUnixDomainSocket(): + ipv4flag = "--ipv4" + ipv6flag = "--ipv6" + + # Basic HTTP 1.1 + tr = urtest.AddTestRun() + # Wait for the micro server + tr.Processes.Default.StartBefore(server, ready=When.PortOpen(server.Variables.Port)) + # Delay on readiness of our ssl ports + tr.Processes.Default.StartBefore(urtest.Processes.ts) + + tr.MakeCurlCommand( + '--verbose {0} --http1.1 --proxy localhost:{1} http://www.example.com'.format(ipv4flag, ts.Variables.port), ts=ts) + tr.Processes.Default.ReturnCode = 0 + + tr.StillRunningAfter = server + tr.StillRunningAfter = ts + + # HTTP 1.0 + tr = urtest.AddTestRun() + tr.MakeCurlCommand( + '--verbose {0} --http1.0 --proxy localhost:{1} http://www.example.com'.format(ipv4flag, ts.Variables.port), ts=ts) + tr.Processes.Default.ReturnCode = 0 + + tr.StillRunningAfter = server + tr.StillRunningAfter = ts + + if not Condition.CurlUsingUnixDomainSocket(): + # HTTP 2 + tr = urtest.AddTestRun() + tr.MakeCurlCommand( + '--verbose --ipv4 --http2 --insecure --header "Host: www.example.com" https://localhost:{}'.format( + ts.Variables.ssl_port), + ts=ts) + tr.Processes.Default.ReturnCode = 0 + + tr.StillRunningAfter = server + tr.StillRunningAfter = ts + + # HTTP 3 + if Condition.HasATSFeature('TS_USE_QUIC') and Condition.HasCurlFeature('http3'): + tr = urtest.AddTestRun() + tr.MakeCurlCommand( + '--verbose --ipv4 --http3 --insecure --header "Host: www.example.com" https://localhost:{}'.format( + ts.Variables.ssl_port), + ts=ts) + tr.Processes.Default.ReturnCode = 0 + tr.StillRunningAfter = server + tr.StillRunningAfter = ts + + # TLS + tr = urtest.AddTestRun() + tr.MakeCurlCommand( + '--verbose --ipv4 --http1.1 --insecure --header "Host: www.example.com" https://localhost:{}'.format( + ts.Variables.ssl_port), + ts=ts) + tr.Processes.Default.ReturnCode = 0 + + tr.StillRunningAfter = server + tr.StillRunningAfter = ts + + # IPv6 + tr = urtest.AddTestRun() + tr.MakeCurlCommand( + '--verbose {0} --http1.1 --proxy localhost:{1} http://www.example.com'.format(ipv6flag, ts.Variables.portv6), ts=ts) + tr.Processes.Default.ReturnCode = 0 + tr.StillRunningAfter = server + tr.StillRunningAfter = ts + + if not Condition.CurlUsingUnixDomainSocket(): + tr = urtest.AddTestRun() + tr.MakeCurlCommand( + '--verbose --ipv6 --http1.1 --insecure --header "Host: www.example.com" https://localhost:{}'.format( + ts.Variables.ssl_portv6), + ts=ts) + tr.Processes.Default.ReturnCode = 0 + + tr.StillRunningAfter = server + tr.StillRunningAfter = ts + urtest.execute() diff --git a/tests/gold_tests/headers/via-observer.py b/tests/uranium_tests/headers/via-observer.py similarity index 100% rename from tests/gold_tests/headers/via-observer.py rename to tests/uranium_tests/headers/via-observer.py diff --git a/tests/gold_tests/headers/via.gold b/tests/uranium_tests/headers/via.gold similarity index 100% rename from tests/gold_tests/headers/via.gold rename to tests/uranium_tests/headers/via.gold diff --git a/tests/gold_tests/headers/via_h3.gold b/tests/uranium_tests/headers/via_h3.gold similarity index 100% rename from tests/gold_tests/headers/via_h3.gold rename to tests/uranium_tests/headers/via_h3.gold diff --git a/tests/gold_tests/headers/via_uds.gold b/tests/uranium_tests/headers/via_uds.gold similarity index 100% rename from tests/gold_tests/headers/via_uds.gold rename to tests/uranium_tests/headers/via_uds.gold diff --git a/tests/gold_tests/ip_allow/gold/200.gold b/tests/uranium_tests/ip_allow/gold/200.gold similarity index 100% rename from tests/gold_tests/ip_allow/gold/200.gold rename to tests/uranium_tests/ip_allow/gold/200.gold diff --git a/tests/gold_tests/ip_allow/gold/403.gold b/tests/uranium_tests/ip_allow/gold/403.gold similarity index 100% rename from tests/gold_tests/ip_allow/gold/403.gold rename to tests/uranium_tests/ip_allow/gold/403.gold diff --git a/tests/gold_tests/ip_allow/gold/403_h2.gold b/tests/uranium_tests/ip_allow/gold/403_h2.gold similarity index 100% rename from tests/gold_tests/ip_allow/gold/403_h2.gold rename to tests/uranium_tests/ip_allow/gold/403_h2.gold diff --git a/tests/gold_tests/ip_allow/gold/log.gold b/tests/uranium_tests/ip_allow/gold/log.gold similarity index 100% rename from tests/gold_tests/ip_allow/gold/log.gold rename to tests/uranium_tests/ip_allow/gold/log.gold diff --git a/tests/gold_tests/ip_allow/replay/ip_allow_subjects_overflow.test.yaml b/tests/uranium_tests/ip_allow/replay/ip_allow_subjects_overflow.test.yaml similarity index 99% rename from tests/gold_tests/ip_allow/replay/ip_allow_subjects_overflow.test.yaml rename to tests/uranium_tests/ip_allow/replay/ip_allow_subjects_overflow.test.yaml index 1d66dce38a8..fbd9cda3ad7 100644 --- a/tests/gold_tests/ip_allow/replay/ip_allow_subjects_overflow.test.yaml +++ b/tests/uranium_tests/ip_allow/replay/ip_allow_subjects_overflow.test.yaml @@ -17,7 +17,7 @@ meta: version: "1.0" -autest: +urtest: description: 'Verify that exceeding MAX_SUBJECTS (4 subjects) logs an error but does not crash' server: diff --git a/tests/gold_tests/ip_allow/replay/ip_allow_subjects_valid.test.yaml b/tests/uranium_tests/ip_allow/replay/ip_allow_subjects_valid.test.yaml similarity index 99% rename from tests/gold_tests/ip_allow/replay/ip_allow_subjects_valid.test.yaml rename to tests/uranium_tests/ip_allow/replay/ip_allow_subjects_valid.test.yaml index 6e9351aed1f..921907413d9 100644 --- a/tests/gold_tests/ip_allow/replay/ip_allow_subjects_valid.test.yaml +++ b/tests/uranium_tests/ip_allow/replay/ip_allow_subjects_valid.test.yaml @@ -17,7 +17,7 @@ meta: version: "1.0" -autest: +urtest: description: 'Verify that exactly MAX_SUBJECTS (3 subjects) works without error' server: diff --git a/tests/gold_tests/ip_allow/replays/h3.replay.yaml b/tests/uranium_tests/ip_allow/replays/h3.replay.yaml similarity index 100% rename from tests/gold_tests/ip_allow/replays/h3.replay.yaml rename to tests/uranium_tests/ip_allow/replays/h3.replay.yaml diff --git a/tests/gold_tests/ip_allow/replays/http_proxy_protocol.replay.yaml b/tests/uranium_tests/ip_allow/replays/http_proxy_protocol.replay.yaml similarity index 100% rename from tests/gold_tests/ip_allow/replays/http_proxy_protocol.replay.yaml rename to tests/uranium_tests/ip_allow/replays/http_proxy_protocol.replay.yaml diff --git a/tests/gold_tests/ip_allow/replays/https_categories_all.replay.yaml b/tests/uranium_tests/ip_allow/replays/https_categories_all.replay.yaml similarity index 100% rename from tests/gold_tests/ip_allow/replays/https_categories_all.replay.yaml rename to tests/uranium_tests/ip_allow/replays/https_categories_all.replay.yaml diff --git a/tests/gold_tests/ip_allow/replays/https_categories_external.replay.yaml b/tests/uranium_tests/ip_allow/replays/https_categories_external.replay.yaml similarity index 100% rename from tests/gold_tests/ip_allow/replays/https_categories_external.replay.yaml rename to tests/uranium_tests/ip_allow/replays/https_categories_external.replay.yaml diff --git a/tests/gold_tests/ip_allow/replays/https_categories_external_remap.replay.yaml b/tests/uranium_tests/ip_allow/replays/https_categories_external_remap.replay.yaml similarity index 100% rename from tests/gold_tests/ip_allow/replays/https_categories_external_remap.replay.yaml rename to tests/uranium_tests/ip_allow/replays/https_categories_external_remap.replay.yaml diff --git a/tests/gold_tests/ip_allow/replays/https_categories_internal.replay.yaml b/tests/uranium_tests/ip_allow/replays/https_categories_internal.replay.yaml similarity index 100% rename from tests/gold_tests/ip_allow/replays/https_categories_internal.replay.yaml rename to tests/uranium_tests/ip_allow/replays/https_categories_internal.replay.yaml diff --git a/tests/gold_tests/ip_allow/replays/https_categories_server.replay.yaml b/tests/uranium_tests/ip_allow/replays/https_categories_server.replay.yaml similarity index 100% rename from tests/gold_tests/ip_allow/replays/https_categories_server.replay.yaml rename to tests/uranium_tests/ip_allow/replays/https_categories_server.replay.yaml diff --git a/tests/gold_tests/ip_allow/replays/https_multiple_methods.replay.yaml b/tests/uranium_tests/ip_allow/replays/https_multiple_methods.replay.yaml similarity index 100% rename from tests/gold_tests/ip_allow/replays/https_multiple_methods.replay.yaml rename to tests/uranium_tests/ip_allow/replays/https_multiple_methods.replay.yaml diff --git a/tests/gold_tests/ip_allow/run_sed.sh b/tests/uranium_tests/ip_allow/run_sed.sh similarity index 100% rename from tests/gold_tests/ip_allow/run_sed.sh rename to tests/uranium_tests/ip_allow/run_sed.sh diff --git a/tests/uranium_tests/ip_allow/test_connect_destination_acl.py b/tests/uranium_tests/ip_allow/test_connect_destination_acl.py new file mode 100644 index 00000000000..ac00812021f --- /dev/null +++ b/tests/uranium_tests/ip_allow/test_connect_destination_acl.py @@ -0,0 +1,191 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_connect_destination_acl(urtest: UraniumTest) -> None: + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + ''' + Verify outbound ip_allow filtering for CONNECT destinations. + ''' + + urtest.Summary = ''' + Verify outbound ip_allow filtering for CONNECT destinations. + ''' + + urtest.SkipIf(Condition.CurlUsingUnixDomainSocket()) + urtest.ContinueOnFail = True + + server = urtest.MakeOriginServer("server", ssl=True) + + request = { + "headers": f"GET / HTTP/1.1\r\nHost: 127.0.0.1:{server.Variables.SSL_Port}\r\n\r\n", + "timestamp": "1469733493.993", + "body": "", + } + response = { + "headers": "HTTP/1.1 200 OK\r\nContent-Length: 0\r\nConnection: close\r\n\r\n", + "timestamp": "1469733493.993", + "body": "", + } + server.addResponse("sessionlog.json", request, response) + + def configure_ts(name): + ts = urtest.MakeATSProcess(name, enable_cache=False) + ts.Disk.records_config.update( + { + "proxy.config.diags.debug.enabled": 1, + "proxy.config.diags.debug.tags": "http|ip_allow", + "proxy.config.http.connect_ports": f"{server.Variables.SSL_Port}", + "proxy.config.url_remap.remap_required": 0, + }) + return ts + + ts_default = configure_ts("ts-default") + ts_allowed = configure_ts("ts-allowed") + ts_allowed.Disk.ip_allow_yaml.AddLines( + [ + "ip_allow:", + " - apply: in", + " ip_addrs: 127.0.0.1", + " action: allow", + " methods: ALL", + " - apply: out", + " ip_addrs: 127.0.0.1", + " action: allow", + " methods: CONNECT", + " - apply: out", + " ip_addrs:", + " - 0.0.0.0/8", + " - 127.0.0.0/8", + " - \"::\"", + " - ::1", + " - 10.0.0.0/8", + " - 172.16.0.0/12", + " - 192.168.0.0/16", + " - 169.254.0.0/16", + " - ::/96", + " - fc00::/7", + " - fe80::/10", + " - ::ffff:0:0/96", + " action: deny", + " methods: CONNECT", + ]) + + ts_sni_default = urtest.MakeATSProcess("ts-sni-default", enable_cache=False, enable_tls=True) + ts_sni_default.addDefaultSSLFiles() + ts_sni_default.Disk.records_config.update( + { + "proxy.config.diags.debug.enabled": 1, + "proxy.config.diags.debug.tags": "http|ip_allow|ssl|sni", + "proxy.config.http.connect_ports": f"{server.Variables.SSL_Port}", + "proxy.config.ssl.server.cert.path": ts_sni_default.Variables.SSLDir, + "proxy.config.ssl.server.private_key.path": ts_sni_default.Variables.SSLDir, + }) + ts_sni_default.Disk.ssl_multicert_yaml.AddLines( + """ + ssl_multicert: + - dest_ip: "*" + ssl_cert_name: server.pem + ssl_key_name: server.key + """.split("\n")) + ts_sni_default.Disk.sni_yaml.AddLines( + [ + "sni:", + "- fqdn: sni-denied.example.com", + f" tunnel_route: 127.0.0.1:{server.Variables.SSL_Port}", + ]) + ts_sni_default.Disk.diags_log.Content += Testers.ContainsExpression( + r"server '127\.0\.0\.1.*' prohibited by ip-allow policy", "SNI tunnel_route should be denied by outbound ip_allow.") + + loopback_url = f"https://127.0.0.1:{server.Variables.SSL_Port}/" + unspecified_url = f"https://0.0.0.0:{server.Variables.SSL_Port}/" + reserved_ipv4_url = f"https://0.1.2.3:{server.Variables.SSL_Port}/" + unspecified_v6_url = f"https://[::]:{server.Variables.SSL_Port}/" + compatible_loopback_url = f"https://[::7f00:1]:{server.Variables.SSL_Port}/" + mapped_loopback_url = f"https://[::ffff:127.0.0.1]:{server.Variables.SSL_Port}/" + mapped_loopback_hex_url = f"https://[::ffff:7f00:1]:{server.Variables.SSL_Port}/" + + def add_denied_connect_run(name, url, http_connect="403"): + tr = urtest.AddTestRun(name) + tr.MakeCurlCommand( + f'-sk --noproxy does-not-match --proxy http://127.0.0.1:{ts_default.Variables.port} ' + f'-o /dev/null -w "http_code=%{{http_code}} http_connect=%{{http_connect}}\\n" {url}', + ts=ts_default) + tr.Processes.Default.ReturnCode = Any(7, 56) + tr.Processes.Default.Streams.stdout = Testers.ContainsExpression( + f"http_code=000 http_connect={http_connect}", "CONNECT should be rejected before tunneling.") + tr.StillRunningAfter = server + tr.StillRunningAfter = ts_default + return tr + + tr = urtest.AddTestRun("Default policy denies CONNECT to loopback") + tr.Processes.Default.StartBefore(server, ready=When.PortOpen(server.Variables.SSL_Port)) + tr.Processes.Default.StartBefore(ts_default) + tr.MakeCurlCommand( + f'-sk --noproxy does-not-match --proxy http://127.0.0.1:{ts_default.Variables.port} ' + f'-o /dev/null -w "http_code=%{{http_code}} http_connect=%{{http_connect}}\\n" {loopback_url}', + ts=ts_default) + tr.Processes.Default.ReturnCode = Any(7, 56) + tr.Processes.Default.Streams.stdout = Testers.ContainsExpression( + "http_code=000 http_connect=403", "CONNECT to loopback should be rejected by the default outbound policy.") + tr.StillRunningAfter = server + tr.StillRunningAfter = ts_default + + add_denied_connect_run("ATS rejects CONNECT to unspecified IPv4 before outbound policy", unspecified_url, http_connect="400") + add_denied_connect_run("Default policy denies CONNECT to 0.0.0.0/8", reserved_ipv4_url) + add_denied_connect_run("ATS rejects CONNECT to unspecified IPv6 before outbound policy", unspecified_v6_url, http_connect="400") + add_denied_connect_run("Default policy denies CONNECT to IPv4-compatible loopback", compatible_loopback_url) + add_denied_connect_run("Default policy denies CONNECT to IPv4-mapped loopback", mapped_loopback_url) + add_denied_connect_run("Default policy denies CONNECT to hex IPv4-mapped loopback", mapped_loopback_hex_url) + + tr = urtest.AddTestRun("Default policy denies SNI tunnel_route to loopback") + tr.Processes.Default.StartBefore(ts_sni_default) + tr.MakeCurlCommand( + f"-skv --resolve sni-denied.example.com:{ts_sni_default.Variables.ssl_port}:127.0.0.1 " + f"https://sni-denied.example.com:{ts_sni_default.Variables.ssl_port}/", + ts=ts_sni_default) + tr.Processes.Default.ReturnCode = Any(35, 52, 56) + tr.StillRunningAfter = server + tr.StillRunningAfter = ts_sni_default + + tr = urtest.AddTestRun("Explicit outbound allow permits CONNECT to loopback") + tr.Processes.Default.StartBefore(ts_allowed) + tr.MakeCurlCommand( + f'-sk --noproxy does-not-match --proxy http://127.0.0.1:{ts_allowed.Variables.port} ' + f'-o /dev/null -w "http_code=%{{http_code}} http_connect=%{{http_connect}}\\n" {loopback_url}', + ts=ts_allowed) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.stdout = Testers.ContainsExpression( + "http_code=200 http_connect=200", "Explicit outbound allow should permit the CONNECT tunnel.") + tr.StillRunningAfter = server + tr.StillRunningAfter = ts_allowed + urtest.execute() diff --git a/tests/uranium_tests/ip_allow/test_ip_allow.py b/tests/uranium_tests/ip_allow/test_ip_allow.py new file mode 100644 index 00000000000..fe2bc0d31c4 --- /dev/null +++ b/tests/uranium_tests/ip_allow/test_ip_allow.py @@ -0,0 +1,387 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_ip_allow(urtest: UraniumTest) -> None: + ''' + Verify ip_allow filtering behavior. + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import os + + urtest.Summary = ''' + Verify ip_allow filtering behavior. + ''' + urtest.SkipIf(Condition.CurlUsingUnixDomainSocket()) + urtest.ContinueOnFail = True + + # Define default ATS + ts = urtest.MakeATSProcess("ts", enable_tls=True, enable_cache=False) + server = urtest.MakeOriginServer("server", ssl=True) + + testName = "" + request = { + "headers": "GET /get HTTP/1.1\r\n" + "Host: www.example.com:80\r\n\r\n", + "timestamp": "1469733493.993", + "body": "" + } + response = { + "headers": "HTTP/1.1 200 OK\r\n" + "Content-Length: 3\r\n" + "Connection: close\r\n\r\n", + "timestamp": "1469733493.993", + "body": "xxx" + } + server.addResponse("sessionlog.json", request, response) + + # The following shouldn't come to the server, but in the event that there is a + # bug in ip_allow and they are sent through, have them return a 200 OK. This + # will fail the match with the gold file which expects a 403. + request = { + "headers": "CONNECT www.example.com:80/connect HTTP/1.1\r\n" + "Host: www.example.com:80\r\n\r\n", + "timestamp": "1469733493.993", + "body": "" + } + response = { + "headers": "HTTP/1.1 200 OK\r\n" + "Content-Length: 3\r\n" + "Connection: close\r\n\r\n", + "timestamp": "1469733493.993", + "body": "xxx" + } + server.addResponse("sessionlog.json", request, response) + request = { + "headers": "PUSH www.example.com:80/h2_push HTTP/2\r\n" + "Host: www.example.com:80\r\n\r\n", + "timestamp": "1469733493.993", + "body": "" + } + response = { + "headers": "HTTP/2 200 OK\r\n" + "Content-Length: 3\r\n" + "Connection: close\r\n\r\n", + "timestamp": "1469733493.993", + "body": "xxx" + } + server.addResponse("sessionlog.json", request, response) + + # Configure TLS for Traffic Server for HTTP/2. + ts.addDefaultSSLFiles() + + ts.Disk.ssl_multicert_yaml.AddLines( + """ + ssl_multicert: + - dest_ip: "*" + ssl_cert_name: server.pem + ssl_key_name: server.key + """.split("\n")) + + ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'ip_allow|http|url_rewrite', + 'proxy.config.http.push_method_enabled': 1, + 'proxy.config.http.connect_ports': '{0}'.format(server.Variables.SSL_Port), + 'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir), + 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts.Variables.SSLDir), + 'proxy.config.ssl.client.verify.server.policy': 'PERMISSIVE', + 'proxy.config.http2.active_timeout_in': 3, + 'proxy.config.http2.max_concurrent_streams_in': 65535, + }) + + format_string = ( + 'scheme=% %-% % % % %/% % ' + '% % % % %<{Y-RID}pqh> ' + '%<{Y-YPCS}pqh> %<{Host}cqh> %<{CHAD}pqh> ' + 'sftover=%<{x-safet-overlimit-rules}cqh> sftmat=%<{x-safet-matched-rules}cqh> ' + 'sftcls=%<{x-safet-classification}cqh> ' + 'sftbadclf=%<{x-safet-bad-classifiers}cqh> yra=%<{Y-RA}cqh> status_setter=%') + + ts.Disk.logging_yaml.AddLines( + f''' logging: + formats: + - name: custom + format: '{format_string}' + logs: + - filename: squid.log + format: custom + '''.split("\n")) + + ts.Disk.remap_config.AddLine('map / https://127.0.0.1:{0}'.format(server.Variables.SSL_Port)) + + # Note that CONNECT is not in the allowed list. + ts.Disk.ip_allow_yaml.AddLines( + '''ip_allow: + - apply: in + ip_addrs: 0/0 + action: allow + methods: [GET, HEAD, POST ] + - apply: in + ip_addrs: ::/0 + action: allow + methods: [GET, HEAD, POST ] + + '''.split("\n")) + + ts.Disk.traffic_out.Content += Testers.ContainsExpression( + "Line 1 denial for 'CONNECT' from 127.0.0.1", "The CONNECT request should be denied by ip_allow") + ts.Disk.traffic_out.Content += Testers.ContainsExpression( + "Line 1 denial for 'PUSH' from 127.0.0.1", "The PUSH request should be denied by ip_allow") + + # + # TEST 1: Perform a GET request. Should be allowed because GET is in the allowlist. + # + tr = urtest.AddTestRun('Allowed GET request') + tr.Processes.Default.StartBefore(server, ready=When.PortOpen(server.Variables.SSL_Port)) + tr.Processes.Default.StartBefore(urtest.Processes.ts) + + tr.MakeCurlCommand( + '--verbose -H "Host: www.example.com" http://localhost:{ts_port}/get'.format(ts_port=ts.Variables.port), ts=ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.stderr = 'gold/200.gold' + tr.StillRunningAfter = ts + tr.StillRunningAfter = server + + # + # TEST 2: Perform a CONNECT request. Should not be allowed because CONNECT is + # not in the allowlist. + # + tr = urtest.AddTestRun('Denied CONNECT request') + tr.MakeCurlCommand( + '--verbose -X CONNECT -H "Host: localhost" http://localhost:{ts_port}/connect'.format(ts_port=ts.Variables.port), ts=ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.stderr = 'gold/403.gold' + tr.StillRunningAfter = ts + tr.StillRunningAfter = server + + # + # TEST 3: Perform a PUSH request over HTTP/2. Should not be allowed because + # PUSH is not in the allowlist. + # + tr = urtest.AddTestRun('Denied PUSH request over HTTP/2') + tr.MakeCurlCommand( + '--http2 --verbose -k -X PUSH -H "Host: localhost" https://localhost:{ts_port}/h2_push'.format( + ts_port=ts.Variables.ssl_port), + ts=ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.stderr = 'gold/403_h2.gold' + tr.StillRunningAfter = ts + tr.StillRunningAfter = server + + tr = urtest.AddTestRun('Await and verify the transaction log file') + squid_log = os.path.join(ts.Variables.LOGDIR, 'squid.log') + tr.Processes.Default.Command = ( + os.path.join(urtest.Variables.AtsTestToolsDir, 'stdout_wait') + ' 60 "{} {}" {}'.format( + os.path.join(urtest.TestDirectory, 'run_sed.sh'), squid_log, os.path.join(urtest.TestDirectory, 'gold/log.gold'))) + tr.Processes.Default.ReturnCode = 0 + + IP_ALLOW_CONFIG_ALLOW_ALL = '''ip_allow: + - apply: in + ip_addrs: 0/0 + action: allow + methods: ALL + ''' + + IP_ALLOW_CONFIG_DENY_ALL = '''ip_allow: + - apply: in + ip_addrs: 0/0 + action: deny + methods: ALL + ''' + + class Test_ip_allow: + """Configure a test to verify ip_allow behavior.""" + + client_counter: int = 0 + ts_counter: int = 0 + server_counter: int = 0 + + def __init__( + self, + name: str, + replay_file: str, + ip_allow_config: str, + gold_file="", + replay_keys="", + is_h3=False, + expect_request_rejected=False): + """Initialize the test. + + :param name: The name of the test. + :param ip_allow_config: The ip_allow configuration to be used. + :param replay_file: The replay file to be used. + :param gold_file: (Optional) Gold file to be checked. + :param replay_keys: (Optional) Keys to be used by pv. + :param expect_request_rejected: (Optional) Whether or not the client request is expected to be rejected. + """ + self.name = name + self.replay_file = replay_file + self.ip_allow_config = ip_allow_config + self.gold_file = gold_file + self.replay_keys = replay_keys + self.is_h3 = is_h3 + self.expect_request_rejected = expect_request_rejected + + def _configure_server(self, tr: 'TestRun'): + """Configure the server. + + :param tr: The TestRun object to associate the server process with. + """ + server = tr.AddVerifierServerProcess(f"server_{Test_ip_allow.server_counter}", self.replay_file) + Test_ip_allow.server_counter += 1 + self._server = server + + def _configure_traffic_server(self, tr: 'TestRun'): + """Configure Traffic Server. + + :param tr: The TestRun object to associate the ts process with. + """ + ts = tr.MakeATSProcess( + f"ts-{Test_ip_allow.ts_counter}", enable_quic=self.is_h3, enable_tls=True, enable_proxy_protocol=True) + + Test_ip_allow.ts_counter += 1 + self._ts = ts + # Configure TLS for Traffic Server. + self._ts.addDefaultSSLFiles() + self._ts.Disk.ssl_multicert_yaml.AddLines( + """ + ssl_multicert: + - dest_ip: "*" + ssl_cert_name: server.pem + ssl_key_name: server.key + """.split("\n")) + self._ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'v_quic|quic|http|ip_allow|proxyprotocol', + 'proxy.config.http.push_method_enabled': 1, + 'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir), + 'proxy.config.quic.no_activity_timeout_in': 0, + 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts.Variables.SSLDir), + 'proxy.config.ssl.client.verify.server.policy': 'PERMISSIVE', + 'proxy.config.http.connect_ports': f"{self._server.Variables.http_port}", + 'proxy.config.acl.subjects': 'PROXY,PEER', + }) + + self._ts.Disk.remap_config.AddLine(f'map / http://127.0.0.1:{self._server.Variables.http_port}') + + # Set ip_allow policy based on the input configuration. + self._ts.Disk.ip_allow_yaml.AddLines(self.ip_allow_config.split("\n")) + + def run(self): + """Run the test.""" + tr = urtest.AddTestRun(self.name) + self._configure_server(tr) + self._configure_traffic_server(tr) + + tr.Processes.Default.StartBefore(self._server) + tr.Processes.Default.StartBefore(self._ts) + + tr.AddVerifierClientProcess( + f'client-{Test_ip_allow.client_counter}', + self.replay_file, + http_ports=[self._ts.Variables.proxy_protocol_port], + https_ports=[self._ts.Variables.ssl_port], + http3_ports=[self._ts.Variables.ssl_port], + keys=self.replay_keys) + Test_ip_allow.client_counter += 1 + + if self.expect_request_rejected: + # The client request should time out because ATS rejects it and does + # not send a response. + tr.Processes.Default.ReturnCode = 1 + self._ts.Disk.diags_log.Content += Testers.ContainsExpression( + "client.*prohibited by ip-allow policy", "Request should be rejected by ip_allow") + else: + # Verify the client request is successful. + tr.Processes.Default.ReturnCode = 0 + self._ts.Disk.diags_log.Content += Testers.ExcludesExpression( + "client.*allowed by ip-allow policy", "Request should be allowed by ip_allow") + + if self.gold_file: + tr.Processes.Default.Streams.all = self.gold_file + + # ip_allow tests for h3. + if Condition.HasATSFeature('TS_USE_QUIC') and Condition.HasCurlFeature('http3'): + + # TEST 4: Perform a request in h3 with ip_allow configured to allow all IPs. + test0 = Test_ip_allow( + "h3_allow_all", + replay_file='replays/h3.replay.yaml', + ip_allow_config=IP_ALLOW_CONFIG_ALLOW_ALL, + is_h3=True, + expect_request_rejected=False) + test0.run() + + # TEST 5: Perform a request in h3 with ip_allow configured to deny all IPs. + test1 = Test_ip_allow( + "h3_deny_all", + replay_file='replays/h3.replay.yaml', + ip_allow_config=IP_ALLOW_CONFIG_DENY_ALL, + is_h3=True, + expect_request_rejected=True) + test1.run() + + # TEST 6: Verify rules are applied to all methods if methods is not specified. + IP_ALLOW_CONFIG_METHODS_UNSPECIFIED = '''ip_allow: + - apply: in + ip_addrs: 0/0 + action: allow + ''' + test_ip_allow_optional_methods = Test_ip_allow( + "ip_allow_optional_methods", + replay_file='replays/https_multiple_methods.replay.yaml', + ip_allow_config=IP_ALLOW_CONFIG_METHODS_UNSPECIFIED, + is_h3=False, + expect_request_rejected=False) + test_ip_allow_optional_methods.run() + + # TEST 7: Verify IP address from PROXY protocol is used. + IP_ALLOW_CONFIG_PROXY_PROTOCOL = '''ip_allow: + - apply: in + ip_addrs: 1.2.3.4 + action: allow + - apply: in + ip_addrs: 0/0 + action: deny + ''' + test_ip_allow_proxy_protocol = Test_ip_allow( + "ip_allow_proxy_protocol", + replay_file='replays/http_proxy_protocol.replay.yaml', + ip_allow_config=IP_ALLOW_CONFIG_PROXY_PROTOCOL, + is_h3=False, + expect_request_rejected=False) + test_ip_allow_proxy_protocol.run() + urtest.execute() diff --git a/tests/uranium_tests/ip_allow/test_ip_allow_reload_triggered.py b/tests/uranium_tests/ip_allow/test_ip_allow_reload_triggered.py new file mode 100644 index 00000000000..4f9988aa3ce --- /dev/null +++ b/tests/uranium_tests/ip_allow/test_ip_allow_reload_triggered.py @@ -0,0 +1,240 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_ip_allow_reload_triggered(urtest: UraniumTest) -> None: + ''' + Test ip_allow and ip_categories reload via ConfigRegistry. + + Verifies that: + 1. ip_allow.yaml touch triggers ip_allow reload + 2. ip_categories touch triggers ip_allow reload (via add_file_dependency) + 3. Unrelated config touch (hosting.config) does NOT trigger ip_allow reload + 4. ip_categories content change causes actual behavior change after reload + 5. Changing ip_categories record value (traffic_ctl config set) triggers ip_allow reload + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import os + import shutil + + urtest.Summary = ''' + Test ip_allow and ip_categories reload via ConfigRegistry add_file_dependency. + ''' + + urtest.ContinueOnFail = True + + # --- Setup: origin server --- + server = urtest.MakeOriginServer("server", ssl=False) + request = {"headers": "GET /test HTTP/1.1\r\nHost: www.example.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + response = { + "headers": "HTTP/1.1 200 OK\r\nContent-Length: 2\r\nConnection: close\r\n\r\n", + "timestamp": "1469733493.993", + "body": "ok" + } + server.addResponse("sessionlog.json", request, response) + + # --- Setup: ip_categories file variants --- + + # Version A: 127.0.0.1 is INTERNAL (requests allowed) + categories_allow = os.path.join(urtest.RunDirectory, 'categories_allow.yaml') + with open(categories_allow, 'w') as f: + f.write('ip_categories:\n - name: INTERNAL\n ip_addrs: 127.0.0.1\n') + + # Version B: 127.0.0.1 is NOT INTERNAL (GET denied, only HEAD allowed by catch-all) + categories_deny = os.path.join(urtest.RunDirectory, 'categories_deny.yaml') + with open(categories_deny, 'w') as f: + f.write('ip_categories:\n - name: INTERNAL\n ip_addrs: 1.2.3.4\n') + + # Version C: 127.0.0.1 back in INTERNAL (for record value change test) + categories_restore = os.path.join(urtest.RunDirectory, 'categories_restore.yaml') + with open(categories_restore, 'w') as f: + f.write('ip_categories:\n - name: INTERNAL\n ip_addrs: 127.0.0.1\n') + + # Active ip_categories file that the record points to (start with allow version) + categories_file = os.path.join(urtest.RunDirectory, 'ip_categories.yaml') + shutil.copy(categories_allow, categories_file) + + # --- Setup: ATS --- + ts = urtest.MakeATSProcess("ts", enable_cache=True) + + ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'ip_allow|config', + 'proxy.config.cache.ip_categories.filename': categories_file, + }) + + # ip_allow config: + # Rule 1: INTERNAL category → allow ALL methods + # Rule 2: catch-all 0/0 → allow only HEAD + # + # Effect: + # 127.0.0.1 in INTERNAL → GET /test → 200 (rule 1 matches) + # 127.0.0.1 NOT in INTERNAL → GET /test → 403 (rule 2 matches, GET not allowed) + ts.Disk.ip_allow_yaml.AddLines( + '''ip_allow: + - apply: in + ip_categories: INTERNAL + action: allow + methods: ALL + - apply: in + ip_addrs: 0/0 + action: allow + methods: + - HEAD + '''.split("\n")) + + ts.Disk.remap_config.AddLine(f'map / http://127.0.0.1:{server.Variables.Port}') + + config_dir = ts.Variables.CONFIGDIR + reload_counter = 0 + + # ================================================================ + # Test 1: Touch ip_allow.yaml → reload → ip_allow reloads (count 2) + # ================================================================ + + tr = urtest.AddTestRun("Touch ip_allow.yaml") + tr.Processes.Default.StartBefore(server, ready=When.PortOpen(server.Variables.Port)) + tr.Processes.Default.StartBefore(ts) + tr.Processes.Default.Command = f"sleep 3 && touch {os.path.join(config_dir, 'ip_allow.yaml')} && sleep 1" + tr.Processes.Default.ReturnCode = 0 + tr.StillRunningAfter = ts + tr.StillRunningAfter = server + + reload_counter += 1 + tr = urtest.AddConfigReload(ts, expect_tasks=["ip_allow.yaml"], description="Reload after ip_allow.yaml touch") + tr.StillRunningAfter = ts + + # ================================================================ + # Test 2: Touch ip_categories → reload → ip_allow reloads (count 3) + # Verifies add_file_dependency() correctly wired ip_categories + # to trigger ip_allow reload via FileManager mtime detection. + # ================================================================ + + tr = urtest.AddTestRun("Touch ip_categories") + tr.Processes.Default.Command = f"touch {categories_file} && sleep 1" + tr.Processes.Default.ReturnCode = 0 + tr.StillRunningAfter = ts + + reload_counter += 1 + tr = urtest.AddConfigReload(ts, expect_tasks=["ip_allow.yaml"], description="Reload after ip_categories touch") + tr.StillRunningAfter = ts + + # ================================================================ + # Test 3: Touch hosting.config → reload → ip_allow NOT triggered + # Verifies the fix for the false trigger bug where changing + # any file in the config directory spuriously triggered + # ip_allow reload (due to FileManager watching the directory + # instead of ip_categories.yaml when the record was ""). + # ================================================================ + + tr = urtest.AddTestRun("Touch hosting.config before reload") + tr.Processes.Default.Command = f"touch {os.path.join(config_dir, 'hosting.config')}" + tr.Processes.Default.ReturnCode = 0 + tr.StillRunningAfter = ts + + tr = urtest.AddConfigReload(ts, expect_absent_tasks=["ip_allow.yaml"], description="Reload (should NOT trigger ip_allow)") + + # ================================================================ + # Test 4: Functional — change ip_categories content, verify behavior + # Proves that add_file_dependency() not only triggers the + # reload but that the handler actually re-reads the updated + # ip_categories file. + # ================================================================ + + # 4a: Verify initial state: GET → 200 (127.0.0.1 is in INTERNAL) + tr = urtest.AddTestRun("GET should succeed (127.0.0.1 in INTERNAL category)") + tr.Processes.Default.Command = (f"curl -s -o /dev/null -w '%{{http_code}}' " + f"http://127.0.0.1:{ts.Variables.port}/test") + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.stdout = Testers.ContainsExpression("200", "Should get 200 when 127.0.0.1 is in INTERNAL category") + tr.StillRunningAfter = ts + tr.StillRunningAfter = server + + # 4b: Swap ip_categories to deny version (127.0.0.1 NOT in INTERNAL) + tr = urtest.AddTestRun("Change ip_categories to deny 127.0.0.1") + tr.Processes.Default.Command = f"cp {categories_deny} {categories_file}" + tr.Processes.Default.ReturnCode = 0 + tr.StillRunningAfter = ts + + # 4c: Reload and wait for ip_allow to pick up the change + reload_counter += 1 + tr = urtest.AddConfigReload(ts, expect_tasks=["ip_allow.yaml"], description="Reload after ip_categories content change") + tr.StillRunningAfter = ts + + # 4d: GET should now be denied (falls to catch-all: only HEAD allowed) + tr = urtest.AddTestRun("GET should be denied (127.0.0.1 NOT in INTERNAL)") + tr.Processes.Default.Command = (f"curl -s -o /dev/null -w '%{{http_code}}' " + f"http://127.0.0.1:{ts.Variables.port}/test") + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.stdout = Testers.ContainsExpression( + "403", "Should get 403 when 127.0.0.1 is not in INTERNAL category") + tr.StillRunningAfter = ts + tr.StillRunningAfter = server + + # ================================================================ + # Test 5: Record value change triggers ip_allow reload + # Changes the ip_categories filename record via traffic_ctl + # config set (no explicit config reload). The callback + # registered by add_file_dependency() fires when the record + # value changes, triggering ip_allow reload with the new file. + # ================================================================ + + # 5a: Change the ip_categories filename record to point to categories_restore + # (which has 127.0.0.1 back in INTERNAL). + # No traffic_ctl config reload — the RecRegisterConfigUpdateCb fires + # automatically via config_update_cont when the record value changes. + reload_counter += 1 + tr = urtest.AddTestRun("Change ip_categories record value to new file") + p = tr.Processes.Process(f"reload-{reload_counter}") + p.Command = (f"traffic_ctl config set proxy.config.cache.ip_categories.filename " + f"'{categories_restore}'; sleep 30") + p.Env = ts.Env + p.ReturnCode = Any(0, -2) + p.Ready = When.FileContains(ts.Disk.diags_log.Name, "ip_allow.yaml finished loading", 1 + reload_counter) + p.Timeout = 20 + tr.Processes.Default.StartBefore(p) + tr.Processes.Default.Command = 'echo "waiting for ip_allow reload after record value change"' + tr.TimeOut = 25 + tr.StillRunningAfter = ts + + # 5b: GET should succeed again (new file has 127.0.0.1 in INTERNAL) + tr = urtest.AddTestRun("GET should succeed after record value change") + tr.Processes.Default.Command = (f"curl -s -o /dev/null -w '%{{http_code}}' " + f"http://127.0.0.1:{ts.Variables.port}/test") + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.stdout = Testers.ContainsExpression( + "200", "Should get 200 after restoring INTERNAL category via record change") + tr.StillRunningAfter = ts + tr.StillRunningAfter = server + urtest.execute() diff --git a/tests/uranium_tests/ip_allow/test_ip_category.py b/tests/uranium_tests/ip_allow/test_ip_category.py new file mode 100644 index 00000000000..2b5fddc672e --- /dev/null +++ b/tests/uranium_tests/ip_allow/test_ip_category.py @@ -0,0 +1,342 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_ip_category(urtest: UraniumTest) -> None: + ''' + Verify IP allow ip_category behavior. + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import os + import re + + from typing import List + + urtest.Summary = ''' + Verify IP allow ip_category behavior. + ''' + + class CategoryFile: + """Encapsulate the various ip_category.yaml contents.""" + + contents: List['CategoryFile'] = [] + parent_directory: str = urtest.RunDirectory + _index: int = 0 + + def __init__(self, content: str): + """Initialize the object. + + :param content: The content of the ip_category.yaml file. + """ + self._content = content + self._index = len(CategoryFile.contents) + self._filename = os.path.join(CategoryFile.parent_directory, f'categories{self._index}.yaml') + CategoryFile.contents.append(self) + + def _write(self): + with open(self._filename, 'w') as f: + f.write(self._content) + + def get_path(self): + return self._filename + + @classmethod + def write_all(cls): + for content in cls.contents: + content._write() + + localhost_is_internal_and_external = CategoryFile( + ''' + ip_categories: + - name: ACME_INTERNAL + ip_addrs: 127.0.0.1 + - name: ACME_EXTERNAL + ip_addrs: 127.0.0.1 + - name: ACME_ALL + ip_addrs: 127.0.0.1 + - name: ALL + ip_addrs: 127.0.0.1 + ''') + + localhost_is_external = CategoryFile( + ''' + ip_categories: + - name: ACME_INTERNAL + ip_addrs: 1.2.3.4 + - name: ACME_REMAP_EXTERNAL + ip_addrs: 127.0.0.1 + - name: ACME_EXTERNAL + ip_addrs: 127.0.0.1 + - name: ACME_ALL + ip_addrs: + - 1.2.3.4 + - 127.0.0.1 + - name: ALL + ip_addrs: + - 1.2.3.4 + - 127.0.0.1 + ''') + + localhost_is_neither = CategoryFile( + ''' + ip_categories: + - name: ACME_INTERNAL + ip_addrs: 1.2.3.4 + - name: ACME_EXTERNAL + ip_addrs: 1.2.3.4 + - name: ACME_ALL + ip_addrs: 1.2.3.4 + - name: ALL + ip_addrs: + - 1.2.3.4 + - 127.0.0.1 + ''') + + # Keep this below the above content instantiations. + CategoryFile.write_all() + + class Test_ip_category: + """Configure a test to verify ip_category behavior.""" + + _client_counter: int = 0 + _ts_is_started: bool = False + _reload_server_is_started: bool = False + _server_is_started: bool = False + _reload_counter: int = 0 + + _ts: 'TestProcess' = None + _server: 'TestProcess' = None + _reload_server: 'TestProcess' = None + + _categories_filename: str = f'{urtest.RunDirectory}/categories.yaml' + _category_files_are_written: bool = False + + _server_replay = 'replays/https_categories_server.replay.yaml' + + def __init__( + self, name: str, replay_file: str, ip_allow_config: str, ip_category_config: 'CategoryFile', acl_configuration: str, + expected_responses: List[int]): + """Initialize the test. + + :param name: The name of the test. + :param replay_file: The replay file to be used. + :param ip_allow_config: The ip_allow configuration to be used. + :param ip_category_config: The ip_category.yaml configuration to be used. + :param acl_configuration: The ACL configuration to be used. + :param expect_responses: The in-order expected responses from the proxy. + """ + self._replay_file = replay_file + self._ip_allow_config = ip_allow_config + self._acl_configuration = acl_configuration + self._expected_responses = expected_responses + + self._update_categories_file(ip_category_config) + self._update_remap_with_acl() + + self._configure_server() + self._configure_traffic_server() + + tr = urtest.AddTestRun(name) + self._configure_client(tr) + + def _update_remap_with_acl(self) -> None: + """Update the remap.config file with the ACL configuration.""" + if Test_ip_category._ts: + if self._acl_configuration: + tr = urtest.AddTestRun(f"remap.config file update with acl: {self._acl_configuration}") + p = tr.Processes.Default + destination = os.path.join(Test_ip_category._ts.Variables.CONFIGDIR, 'remap.config') + common = f'map / http://127.0.0.1:{Test_ip_category._server.Variables.http_port} ' + p.Command = f'echo {common} {self._acl_configuration} > {destination}; cat {destination}' + p.ReturnCode = 0 + else: + tr = urtest.AddTestRun(f"remap.config file update with no acl") + p = tr.Processes.Default + destination = os.path.join(Test_ip_category._ts.Variables.CONFIGDIR, 'remap.config') + common = f'map / http://127.0.0.1:{Test_ip_category._server.Variables.http_port} ' + p.Command = f'echo {common} > {destination}; cat {destination}' + p.ReturnCode = 0 + + def _update_categories_file(self, category_content: 'CategoryFile') -> None: + """Update the categories file. + + :param category_content: The content of the categories file. + """ + tr = urtest.AddTestRun(f"Categories file update: {category_content.get_path()}") + p = tr.Processes.Default + destination = Test_ip_category._categories_filename + p.Command = f'cp {category_content.get_path()} {destination}; cat {destination}; ls -ltr {destination}' + p.ReturnCode = 0 + + def _configure_server(self) -> None: + """Configure the server.""" + if Test_ip_category._server: + # All test runs share a single server instance. + return + server = urtest.MakeVerifierServerProcess(f"server", self._server_replay) + Test_ip_category._server = server + + def _configure_traffic_server(self) -> None: + """Configure Traffic Server.""" + + if Test_ip_category._ts: + # All test runs share a single Traffic Server instance. + + # Reload the ip_allow.yaml file. + ts = Test_ip_category._ts + Test_ip_category._reload_counter += 1 + tr = urtest.AddConfigReload(ts, expect_tasks=["ip_allow.yaml"], description="Reload the configuration") + tr.StillRunningAfter = ts + + return + ts = urtest.MakeATSProcess("ts", enable_cache=False, enable_tls=True) + Test_ip_category._ts = ts + + ts.addDefaultSSLFiles() + ts.Disk.ssl_multicert_yaml.AddLines( + """ + ssl_multicert: + - dest_ip: "*" + ssl_cert_name: server.pem + ssl_key_name: server.key + """.split("\n")) + ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'http|ip_allow|config.reload', + 'proxy.config.cache.ip_categories.filename': Test_ip_category._categories_filename, + 'proxy.config.http.push_method_enabled': 1, + 'proxy.config.ssl.server.cert.path': ts.Variables.SSLDir, + 'proxy.config.quic.no_activity_timeout_in': 0, + 'proxy.config.ssl.server.private_key.path': ts.Variables.SSLDir, + 'proxy.config.ssl.client.verify.server.policy': 'PERMISSIVE', + 'proxy.config.http.connect_ports': Test_ip_category._server.Variables.http_port, + 'proxy.config.url_remap.acl_behavior_policy': 1, # TODO: adjust expected_responses with the default config + }) + + ts.Disk.remap_config.AddLine( + f'map / http://127.0.0.1:{Test_ip_category._server.Variables.http_port} {self._acl_configuration}') + ts.Disk.ip_allow_yaml.AddLines(self._ip_allow_config.split("\n")) + + def _configure_client(self, tr: 'TestRun') -> None: + """Run the test. + + :param tr: The TestRun object to associate the client process with. + """ + + if not Test_ip_category._server_is_started: + tr.Processes.Default.StartBefore(Test_ip_category._server) + Test_ip_category._server_is_started = True + if not Test_ip_category._ts_is_started: + tr.Processes.Default.StartBefore(Test_ip_category._ts) + Test_ip_category._ts_is_started = True + + p = tr.AddVerifierClientProcess( + f'client-{Test_ip_category._client_counter}', + self._replay_file, + https_ports=[Test_ip_category._ts.Variables.ssl_port]) + Test_ip_category._client_counter += 1 + + if self._expected_responses: + codes = [str(code) for code in self._expected_responses] + p.Streams.stdout += Testers.ContainsExpression( + '.*'.join(codes), "Verifying the expected order of responses", reflags=re.DOTALL | re.MULTILINE) + else: + # If there are no expected responses, expect the Warning about the rejected ip. + self._ts.Disk.diags_log.Content += Testers.ContainsExpression( + "client '127.0.0.1' prohibited by ip-allow policy", "Verify the client rejection warning message.") + + # Also, the client will complain about the broken connections. + p.ReturnCode = 1 + + IP_ALLOW_CONTENT = f''' + ip_allow: + - apply: in + ip_categories: ACME_INTERNAL + action: set_allow + methods: + - GET + - HEAD + - POST + - PUSH + - apply: in + ip_categories: ACME_EXTERNAL + action: set_allow + methods: + - GET + - HEAD + - apply: in + ip_categories: ACME_ALL + action: set_allow + methods: + - HEAD + - apply: in + ip_categories: ALL + action: set_deny + ''' + + test_ip_allow_optional_methods = Test_ip_category( + "IP Category: INTERNAL", + replay_file='replays/https_categories_internal.replay.yaml', + ip_allow_config=IP_ALLOW_CONTENT, + ip_category_config=localhost_is_internal_and_external, + acl_configuration='', + expected_responses=[200, 200, 400, 403]) + + test_ip_allow_optional_methods = Test_ip_category( + "IP Category: EXTERNAL", + replay_file='replays/https_categories_external.replay.yaml', + ip_allow_config=IP_ALLOW_CONTENT, + ip_category_config=localhost_is_external, + acl_configuration='', + expected_responses=[200, 403, 403]) + + # Because all requests are outright rejected for 127.0.0.1, ATS will + # reject all incoming transactions and not even give a 403 response. + test_ip_allow_optional_methods = Test_ip_category( + "IP Category: ALL", + replay_file='replays/https_categories_all.replay.yaml', + ip_allow_config=IP_ALLOW_CONTENT, + ip_category_config=localhost_is_neither, + acl_configuration='', + expected_responses=None) + + # Deny GET via remap.config ACL. + test_ip_allow_optional_methods = Test_ip_category( + "IP Category: EXTERNAL", + replay_file='replays/https_categories_external_remap.replay.yaml', + ip_allow_config=IP_ALLOW_CONTENT, + ip_category_config=localhost_is_external, + acl_configuration='@action=set_deny @src_ip_category=ACME_REMAP_EXTERNAL @method=GET', + expected_responses=[403, 200, 200]) + urtest.execute() diff --git a/tests/gold_tests/jsonrpc/json/admin_config_set_records_req.json b/tests/uranium_tests/jsonrpc/json/admin_config_set_records_req.json similarity index 100% rename from tests/gold_tests/jsonrpc/json/admin_config_set_records_req.json rename to tests/uranium_tests/jsonrpc/json/admin_config_set_records_req.json diff --git a/tests/gold_tests/jsonrpc/json/admin_detached_config_reload_req.json b/tests/uranium_tests/jsonrpc/json/admin_detached_config_reload_req.json similarity index 100% rename from tests/gold_tests/jsonrpc/json/admin_detached_config_reload_req.json rename to tests/uranium_tests/jsonrpc/json/admin_detached_config_reload_req.json diff --git a/tests/gold_tests/jsonrpc/json/admin_host_set_status_req.json b/tests/uranium_tests/jsonrpc/json/admin_host_set_status_req.json similarity index 100% rename from tests/gold_tests/jsonrpc/json/admin_host_set_status_req.json rename to tests/uranium_tests/jsonrpc/json/admin_host_set_status_req.json diff --git a/tests/gold_tests/jsonrpc/json/admin_lookup_records_req_1.json b/tests/uranium_tests/jsonrpc/json/admin_lookup_records_req_1.json similarity index 100% rename from tests/gold_tests/jsonrpc/json/admin_lookup_records_req_1.json rename to tests/uranium_tests/jsonrpc/json/admin_lookup_records_req_1.json diff --git a/tests/gold_tests/jsonrpc/json/admin_lookup_records_req_invalid_rec.json b/tests/uranium_tests/jsonrpc/json/admin_lookup_records_req_invalid_rec.json similarity index 100% rename from tests/gold_tests/jsonrpc/json/admin_lookup_records_req_invalid_rec.json rename to tests/uranium_tests/jsonrpc/json/admin_lookup_records_req_invalid_rec.json diff --git a/tests/gold_tests/jsonrpc/json/admin_lookup_records_req_metric.json b/tests/uranium_tests/jsonrpc/json/admin_lookup_records_req_metric.json similarity index 100% rename from tests/gold_tests/jsonrpc/json/admin_lookup_records_req_metric.json rename to tests/uranium_tests/jsonrpc/json/admin_lookup_records_req_metric.json diff --git a/tests/gold_tests/jsonrpc/json/admin_lookup_records_req_multiple.json b/tests/uranium_tests/jsonrpc/json/admin_lookup_records_req_multiple.json similarity index 100% rename from tests/gold_tests/jsonrpc/json/admin_lookup_records_req_multiple.json rename to tests/uranium_tests/jsonrpc/json/admin_lookup_records_req_multiple.json diff --git a/tests/gold_tests/jsonrpc/json/admin_plugin_send_basic_msg_req.json b/tests/uranium_tests/jsonrpc/json/admin_plugin_send_basic_msg_req.json similarity index 100% rename from tests/gold_tests/jsonrpc/json/admin_plugin_send_basic_msg_req.json rename to tests/uranium_tests/jsonrpc/json/admin_plugin_send_basic_msg_req.json diff --git a/tests/gold_tests/jsonrpc/json/admin_storage_x_device_status_req.json b/tests/uranium_tests/jsonrpc/json/admin_storage_x_device_status_req.json similarity index 100% rename from tests/gold_tests/jsonrpc/json/admin_storage_x_device_status_req.json rename to tests/uranium_tests/jsonrpc/json/admin_storage_x_device_status_req.json diff --git a/tests/gold_tests/jsonrpc/json/method_call_no_params.json b/tests/uranium_tests/jsonrpc/json/method_call_no_params.json similarity index 100% rename from tests/gold_tests/jsonrpc/json/method_call_no_params.json rename to tests/uranium_tests/jsonrpc/json/method_call_no_params.json diff --git a/tests/gold_tests/jsonrpc/plugins/CMakeLists.txt b/tests/uranium_tests/jsonrpc/plugins/CMakeLists.txt similarity index 93% rename from tests/gold_tests/jsonrpc/plugins/CMakeLists.txt rename to tests/uranium_tests/jsonrpc/plugins/CMakeLists.txt index b6f38c1417b..46d53250e05 100644 --- a/tests/gold_tests/jsonrpc/plugins/CMakeLists.txt +++ b/tests/uranium_tests/jsonrpc/plugins/CMakeLists.txt @@ -15,6 +15,6 @@ # ####################### -add_autest_plugin(jsonrpc_plugin_handler_test jsonrpc_plugin_handler_test.cc) +add_urtest_plugin(jsonrpc_plugin_handler_test jsonrpc_plugin_handler_test.cc) target_link_libraries(jsonrpc_plugin_handler_test PRIVATE jsonrpc_protocol libswoc::libswoc) diff --git a/tests/gold_tests/jsonrpc/plugins/jsonrpc_plugin_handler_test.cc b/tests/uranium_tests/jsonrpc/plugins/jsonrpc_plugin_handler_test.cc similarity index 100% rename from tests/gold_tests/jsonrpc/plugins/jsonrpc_plugin_handler_test.cc rename to tests/uranium_tests/jsonrpc/plugins/jsonrpc_plugin_handler_test.cc diff --git a/tests/uranium_tests/jsonrpc/test_basic_plugin_handler.py b/tests/uranium_tests/jsonrpc/test_basic_plugin_handler.py new file mode 100644 index 00000000000..3ec4b41d595 --- /dev/null +++ b/tests/uranium_tests/jsonrpc/test_basic_plugin_handler.py @@ -0,0 +1,196 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_basic_plugin_handler(urtest: UraniumTest) -> None: + ''' + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import os + import sys + from jsonrpc import Notification, Request, Response + + urtest.Summary = 'Test JSONRPC Handler inside a plugin' + urtest.ContinueOnFail = True + # Define default ATS, we will use the runroot file for traffic_ctl + ts = urtest.MakeATSProcess('ts', dump_runroot=True) + + urtest.testName = 'Basic plugin handler test' + + ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'rpc|jsonrpc_plugin_handler_test' + }) + + # Load plugin + urtest.PrepareTestPlugin( + os.path.join(urtest.Variables.AtsBuildUraniumTestsDir, 'jsonrpc', 'plugins', '.libs', 'jsonrpc_plugin_handler_test.so'), ts) + + tr = urtest.AddTestRun() + tr.Processes.Default.StartBefore(ts) + tr.Processes.Default.Command = "echo run jsonrpc_plugin_handler_test plugin" + tr.Processes.Default.ReturnCode = 0 + tr.Processes.StillRunningAfter = ts + ts.Ready = 0 + ts.Disk.traffic_out.Content = All( + Testers.IncludesExpression('Test Plugin Initialized.', 'plugin should be properly initialized'), + Testers.IncludesExpression( + 'test_join_hosts_method successfully registered', 'test_join_hosts_method should be properly registered'), + Testers.IncludesExpression( + 'test_join_hosts_notification successfully registered', 'test_join_hosts_notification should be properly registered')) + + # 1 - now let's try the registered api. + tr = urtest.AddTestRun("Test registered API") + tr.AddJsonRPCClientRequest(ts, Request.show_registered_handlers()) + tr.DelayStart = 2 + # Ok we can just check like this: + tr.Processes.Default.Streams.stdout = All( + Testers.IncludesExpression('test_join_hosts_method', 'Should be listed'), + Testers.IncludesExpression('test_join_hosts_notification', 'Should be listed')) + + # 2 - We perform the same as above but without implicit 'show_registered_handlers()' call. + tr = urtest.AddTestRun("Test registered API - using AddJsonRPCShowRegisterHandlerRequest") + tr.AddJsonRPCShowRegisterHandlerRequest(ts) + + # Ok we can just check like this: + tr.Processes.Default.Streams.stdout = All( + Testers.IncludesExpression('test_join_hosts_method', 'Should be listed'), + Testers.IncludesExpression('test_join_hosts_notification', 'Should be listed')) + + # 3 - Call the actual plugin method handler: + tr = urtest.AddTestRun("Test JSONRPC test_join_hosts_method") + tr.AddJsonRPCClientRequest(ts, Request.test_join_hosts_method(hosts=["yahoo.com", "aol.com", "vz.com"])) + + def validate_host_response(resp: Response): + ''' + Custom check for a particular response values. Also error validation. + ''' + if resp.is_error(): + return (False, resp.error_as_str()) + + join_str = resp.result['join'] + expected = "yahoo.comaol.comvz.com" + if join_str != expected: + return (False, f"Invalid response, expected yahoo.comaol.comvz.com, got {join_str}") + + return (True, "All good") + + tr.Processes.Default.Streams.stdout = Testers.CustomJSONRPCResponse(validate_host_response) + + # Custom response validator: + + def _validate_response_for_test_io_on_et_task(added, updated, resp: Response): + ''' + Custom check for a particular response values. Also error validation. + ''' + if resp.is_error(): + return (False, resp.error_as_str()) + + addedHosts = resp.result['addedHosts'] + updatedHosts = resp.result['updatedHosts'] + + if addedHosts == added and updatedHosts == updated: + return (True, "All good") + + return ( + False, + f"Invalid response, expected addedHosts == {added} and updatedHosts == {updated}, got {addedHosts} and {updatedHosts}") + + # 4 - Call plugin handler to perform a IO task on the ET_TASK thread. + tr = urtest.AddTestRun("Test test_io_on_et_task") + newHosts = [ + { + 'name': 'brbzull', + 'status': 'up' + }, { + 'name': 'brbzull1', + 'status': 'down' + }, { + 'name': 'brbzull3', + 'status': 'up' + }, { + 'name': 'brbzull4', + 'status': 'down' + }, { + 'name': 'yahoo', + 'status': 'down' + }, { + 'name': 'trafficserver', + 'status': 'down' + } + ] + + # the jsonrpc query + tr.AddJsonRPCClientRequest(ts, Request.test_io_on_et_task(hosts=newHosts)) + + def validate_response_for_test_io_on_et_task_1(resp: Response): + return _validate_response_for_test_io_on_et_task('6', '0', resp) + + tr.Processes.Default.Streams.stdout = Testers.CustomJSONRPCResponse(validate_response_for_test_io_on_et_task_1) + + # 5 + tr = urtest.AddTestRun("Test test_io_on_et_task - update, yahoo up") + updateYahoo = [{'name': 'yahoo', 'status': 'up'}] + + # the jsonrpc query + tr.AddJsonRPCClientRequest(ts, Request.test_io_on_et_task(hosts=updateYahoo)) + + def validate_response_for_test_io_on_et_task_2(resp: Response): + return _validate_response_for_test_io_on_et_task('0', '1', resp) + + tr.Processes.Default.Streams.stdout = Testers.CustomJSONRPCResponse(validate_response_for_test_io_on_et_task_2) + + # 6 + tr = urtest.AddTestRun("Test privileged field from plugin handlers") + tr.AddJsonRPCClientRequest(ts, Request.get_service_descriptor()) + + def validate_privileged_field_for_method(resp: Response, params): + for (name, val) in params: + if resp.is_error(): + return (False, resp.error_as_str()) + + data = resp.result['methods'] + e = list(filter(lambda x: x['name'] == name, data))[0] + # Method should be registered. + if e is None or e['privileged'] != val: + return (False, f"{name} privileged != {val}") + + return (True, "All good") + + def validate_privileged_field(resp: Response): + return validate_privileged_field_for_method( + resp, [('test_join_hosts_method', "1"), ('test_io_on_et_task', '1'), ('test_join_hosts_notification', "0")]) + + tr.Processes.Default.Streams.stdout = Testers.CustomJSONRPCResponse(validate_privileged_field) + urtest.execute() diff --git a/tests/uranium_tests/jsonrpc/test_config_reload_dedup.py b/tests/uranium_tests/jsonrpc/test_config_reload_dedup.py new file mode 100644 index 00000000000..024671677f0 --- /dev/null +++ b/tests/uranium_tests/jsonrpc/test_config_reload_dedup.py @@ -0,0 +1,181 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_config_reload_dedup(urtest: UraniumTest) -> None: + ''' + Test that a trigger-record value change in records.yaml is handled correctly + during a reload, including deduplication when the 3s config_update_cont timer + fires the same on_record_change callback again. + + Scenario: + 1. Change proxy.config.ssl.server.session_ticket.enable in records.yaml + via --cold. This is a trigger record for ssl_client_coordinator + (registered via register_record_config with 11 trigger records). + + 2. Touch sni.yaml — this also triggers ssl_client_coordinator via + add_file_and_node_dependency (proxy.config.ssl.servername.filename). + So ssl_client_coordinator gets hit from TWO paths: the value change + callback AND the file-mtime callback. + + 3. Touch other config files (ip_allow, logging) for broader coverage. + + 4. Trigger a reload and wait for completion. + + 5. Wait long enough (>6s) for the 3s config_update_cont timer to fire + at least once during or after the reload. When it fires: + a. If the reload is still in progress — create_config_context() + finds the existing subtask and logs "Duplicate reload …skipping". + b. If the reload already completed (SUCCESS) — create_config_context() + may create a new subtask, the handler runs again, and the task + re-settles to SUCCESS. + Either way, no crash, no stuck tasks, no state-transition conflicts. + + 6. Verify: + - "Reserved subtask" messages present (reserve_subtask worked) + - ssl_client_coordinator subtask completed successfully + - No "ignoring transition from" messages (no state corruption) + - Final status is success (even if briefly flickered) + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + urtest.Summary = 'Verify trigger-record value change + file-mtime dedup during reload' + urtest.ContinueOnFail = True + + # --- Setup --- + ts = urtest.MakeATSProcess("ts", enable_cache=True) + ts.StartupTimeout = 30 + + ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'rpc|config|config.reload|configproc', + }) + + # Provide valid content for config files whose handlers reject empty input. + ts.Disk.ip_allow_yaml.AddLines([ + 'ip_allow:', + '- apply: in', + ' ip_addrs: 0/0', + ' action: allow', + ' methods: ALL', + ]) + ts.Disk.logging_yaml.AddLines([ + 'logging:', + ' formats:', + ' - name: dedup_test', + ' format: "%"', + ]) + ts.Disk.sni_yaml.AddLines([ + 'sni:', + '- fqdn: "*.example.com"', + ' verify_client: NONE', + ]) + + # Files to touch — sni.yaml is key because it also triggers + # ssl_client_coordinator via add_file_and_node_dependency. + files_to_touch = [ + ts.Disk.ip_allow_yaml, + ts.Disk.logging_yaml, + ts.Disk.sni_yaml, + ] + touch_cmd = "touch " + " ".join([f.AbsRunTimePath for f in files_to_touch]) + + # ============================================================================ + # Test 1: Change a trigger-record VALUE in records.yaml via --cold + # + # proxy.config.ssl.server.session_ticket.enable is a trigger record for + # ssl_client_coordinator. Toggling it from 1→0 modifies records.yaml on + # disk. When the reload fires, rereadConfig → RecReadYamlConfigFile → + # RecSetRecord sets update_required on this record. RecFlushConfigUpdateCbs + # fires the on_record_change callback. Later the 3s timer may fire it again. + # ============================================================================ + tr = urtest.AddTestRun("Change trigger record value in records.yaml via --cold") + tr.Processes.Default.StartBefore(ts) + tr.Processes.Default.Command = ('sleep 3 && traffic_ctl config set proxy.config.ssl.server.session_ticket.enable 0 --cold') + tr.Processes.Default.Env = ts.Env + tr.Processes.Default.ReturnCode = 0 + tr.StillRunningAfter = ts + + # ============================================================================ + # Test 2: Touch config files — sni.yaml is the critical one + # + # sni.yaml's mtime change triggers RecSetSyncRequired on + # proxy.config.ssl.servername.filename, which is wired to + # ssl_client_coordinator. So ssl_client_coordinator gets hit from two paths: + # 1. proxy.config.ssl.server.session_ticket.enable value change (from records.yaml) + # 2. proxy.config.ssl.servername.filename sync-required (from sni.yaml mtime) + # This exercises the fan-in deduplication in on_record_change / reserve_subtask. + # ============================================================================ + tr = urtest.AddTestRun("Touch config files including sni.yaml") + tr.Processes.Default.Command = touch_cmd + tr.Processes.Default.ReturnCode = 0 + tr.StillRunningAfter = ts + + # ============================================================================ + # Test 3: Trigger reload + # ============================================================================ + tr = urtest.AddTestRun("Trigger reload with named token") + tr.Processes.Default.Command = "traffic_ctl config reload -t dedup_test" + tr.Processes.Default.Env = ts.Env + tr.Processes.Default.ReturnCode = 0 + tr.StillRunningAfter = ts + + # ============================================================================ + # Test 4: Wait long enough for the 3s timer to fire at least twice + # + # The config_update_cont timer fires every 3s. We wait 15s to ensure: + # - All subtasks complete (ip_allow, logging, remap, ssl, etc.) + # - The 3s timer fires at least 4 times during/after the reload + # - Any duplicate on_record_change callbacks have been processed + # ============================================================================ + tr = urtest.AddTestRun("Verify reload completed — no tasks stuck in progress") + tr.DelayStart = 15 + tr.Processes.Default.Command = "traffic_ctl config status -t dedup_test" + tr.Processes.Default.Env = ts.Env + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.stdout += Testers.ExcludesExpression( + "in_progress", "No task should remain in progress after 15s delay") + # ssl_client_coordinator must be present in the output (it was triggered) + tr.Processes.Default.Streams.stdout += Testers.ContainsExpression( + "ssl_client_coordinator", "ssl_client_coordinator subtask must appear in status") + tr.Processes.Default.Streams.stdout += Testers.ContainsExpression("success", "Final status must be success") + tr.StillRunningAfter = ts + + # ============================================================================ + # Test 5: No state-transition conflicts anywhere in the logs + # ============================================================================ + ts.Disk.traffic_out.Content += Testers.ExcludesExpression( + "ignoring transition from", "No state-transition conflicts should appear in traffic.out") + ts.Disk.traffic_out.Content += Testers.ContainsExpression( + "Reserved subtask", "reserve_subtask() must log pre-registration messages") + urtest.execute() diff --git a/tests/uranium_tests/jsonrpc/test_config_reload_failures.py b/tests/uranium_tests/jsonrpc/test_config_reload_failures.py new file mode 100644 index 00000000000..7836c8ef0eb --- /dev/null +++ b/tests/uranium_tests/jsonrpc/test_config_reload_failures.py @@ -0,0 +1,430 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_config_reload_failures(urtest: UraniumTest) -> None: + ''' + Test config reload failure scenarios. + + Tests: + 1. Failed tasks (invalid config content) + 2. Failed subtasks (invalid SSL certificates) + 3. Incomplete subtasks (timeout scenarios) + 4. Status propagation when subtasks fail + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + from jsonrpc import Request, Response + import os + + urtest.Summary = 'Test config reload failure scenarios and status propagation' + urtest.ContinueOnFail = True + + ts = urtest.MakeATSProcess('ts', dump_runroot=True, enable_tls=True) + + urtest.testName = 'config_reload_failures' + + # Enable debugging + ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'config|ssl|ip_allow', + 'proxy.config.ssl.server.cert.path': ts.Variables.SSLDir, + 'proxy.config.ssl.server.private_key.path': ts.Variables.SSLDir, + }) + + # Add valid SSL certs for baseline + ts.addDefaultSSLFiles() + + ts.Disk.ssl_multicert_yaml.AddLines( + [ + 'ssl_multicert:', + ' - dest_ip: "*"', + ' ssl_cert_name: server.pem', + ' ssl_key_name: server.key', + ]) + + # Override default diags check — this test intentionally triggers SSL errors + ts.Disk.diags_log.Content = Testers.ContainsExpression("ERROR", "Expected errors from invalid SSL cert injection") + + # ============================================================================ + # Test 1: Baseline - successful reload + # ============================================================================ + tr = urtest.AddTestRun("Baseline - successful reload") + tr.Processes.Default.StartBefore(ts) + tr.DelayStart = 2 + tr.AddJsonRPCClientRequest(ts, Request.admin_config_reload(force=True)) + + def validate_baseline(resp: Response): + '''Verify baseline reload succeeds''' + if resp.is_error(): + return (False, f"Baseline failed: {resp.error_as_str()}") + + result = resp.result + token = result.get('token', '') + return (True, f"Baseline succeeded: token={token}") + + tr.Processes.Default.Streams.stdout = Testers.CustomJSONRPCResponse(validate_baseline) + tr.StillRunningAfter = ts + + # ============================================================================ + # Test 2: Wait and check baseline completed successfully + # ============================================================================ + tr = urtest.AddTestRun("Verify baseline completed with success status") + tr.DelayStart = 3 + tr.AddJsonRPCClientRequest(ts, Request.admin_config_reload()) + + def validate_baseline_status(resp: Response): + '''Check baseline reload status''' + if resp.is_error(): + error = resp.error_as_str() + if 'in progress' in error.lower(): + return (True, f"Still in progress: {error}") + return (True, f"Query result: {error}") + + result = resp.result + tasks = result.get('tasks', []) + + # Check for any failed tasks + def find_failures(task_list): + failures = [] + for t in task_list: + if t.get('status') == 'fail': + failures.append(t.get('description', 'unknown')) + failures.extend(find_failures(t.get('sub_tasks', []))) + return failures + + failures = find_failures(tasks) + if failures: + return (True, f"Found failures (may be expected): {failures}") + + return (True, f"Baseline status OK, {len(tasks)} tasks") + + tr.Processes.Default.Streams.stdout = Testers.CustomJSONRPCResponse(validate_baseline_status) + tr.StillRunningAfter = ts + + # ============================================================================ + # Test 3: Inject invalid ip_allow config (should fail) + # ============================================================================ + tr = urtest.AddTestRun("Inject invalid ip_allow config") + tr.DelayStart = 2 + + # Invalid ip_allow YAML - missing required fields + invalid_ip_allow = """ip_allow: + - apply: invalid_value + action: not_a_valid_action + """ + + # This should trigger a validation error in IpAllow + # Note: The actual behavior depends on how strict the parser is + tr.AddJsonRPCClientRequest(ts, Request.admin_config_reload(force=True)) + + def validate_after_invalid_config(resp: Response): + '''Check reload after invalid config injection''' + if resp.is_error(): + return (True, f"Reload error (may be expected): {resp.error_as_str()}") + + result = resp.result + token = result.get('token', '') + errors = result.get('error', []) + + if errors: + return (True, f"Reload reported errors: {errors}") + + return (True, f"Reload started: {token}") + + tr.Processes.Default.Streams.stdout = Testers.CustomJSONRPCResponse(validate_after_invalid_config) + tr.StillRunningAfter = ts + + # ============================================================================ + # Test 4: Add invalid SSL cert reference (subtask failure) + # ============================================================================ + tr = urtest.AddTestRun("Configure invalid SSL cert path") + tr.DelayStart = 2 + + # Add a bad cert reference to ssl_multicert.yaml + # This should cause the SSL subtask to fail + sslcertpath = ts.Disk.ssl_multicert_yaml.AbsPath + tr.Disk.File(sslcertpath, id="ssl_multicert_yaml", typename="ats:config") + tr.Disk.ssl_multicert_yaml.AddLines( + [ + 'ssl_multicert:', + ' - dest_ip: "*"', + ' ssl_cert_name: server.pem', + ' ssl_key_name: server.key', + ' - dest_ip: 1.2.3.4', + ' ssl_cert_name: /nonexistent/bad.pem', + ' ssl_key_name: /nonexistent/bad.key', + ]) + + tr.AddJsonRPCClientRequest(ts, Request.admin_config_reload(force=True)) + + def validate_ssl_failure(resp: Response): + '''Check reload with bad SSL config''' + if resp.is_error(): + return (True, f"SSL reload error: {resp.error_as_str()}") + + result = resp.result + token = result.get('token', '') + return (True, f"SSL reload started: {token}") + + tr.Processes.Default.Streams.stdout = Testers.CustomJSONRPCResponse(validate_ssl_failure) + tr.StillRunningAfter = ts + + # ============================================================================ + # Test 5: Check for failed subtasks after SSL reload + # ============================================================================ + tr = urtest.AddTestRun("Check for failed SSL subtasks") + tr.DelayStart = 3 + tr.AddJsonRPCClientRequest(ts, Request.admin_config_reload()) + + def validate_failed_subtasks(resp: Response): + '''Check if SSL subtasks show failure''' + if resp.is_error(): + error = resp.error_as_str() + if 'in progress' in error.lower(): + return (True, f"Still in progress: {error}") + return (True, f"Query: {error}") + + result = resp.result + tasks = result.get('tasks', []) + + def analyze_tasks(task_list, depth=0): + analysis = [] + for t in task_list: + desc = t.get('description', 'unknown') + status = t.get('status', 'unknown') + logs = t.get('logs', []) + + info = f"{' '*depth}{desc}: {status}" + if status == 'fail' and logs: + info += f" - {logs[0][:50]}..." + + analysis.append(info) + + # Recurse into subtasks + analysis.extend(analyze_tasks(t.get('sub_tasks', []), depth + 1)) + + return analysis + + task_info = analyze_tasks(tasks) + return (True, f"Task analysis:\n" + "\n".join(task_info[:10])) + + tr.Processes.Default.Streams.stdout = Testers.CustomJSONRPCResponse(validate_failed_subtasks) + tr.StillRunningAfter = ts + + # ============================================================================ + # Test 6: Verify parent status reflects subtask failure + # ============================================================================ + tr = urtest.AddTestRun("Verify status propagation from subtask to parent") + tr.DelayStart = 1 + tr.AddJsonRPCClientRequest(ts, Request.admin_config_reload()) + + def validate_status_propagation(resp: Response): + '''Verify failed subtask propagates to parent''' + if resp.is_error(): + error = resp.error_as_str() + if 'in progress' in error.lower(): + return (True, f"In progress: {error}") + return (True, f"Query: {error}") + + result = resp.result + tasks = result.get('tasks', []) + + def check_propagation(task_list): + """ + For each task with subtasks, verify: + - If any subtask is 'fail', parent should be 'fail' + - If any subtask is 'in_progress', parent should be 'in_progress' + - If all subtasks are 'success', parent should be 'success' + """ + issues = [] + for t in task_list: + sub_tasks = t.get('sub_tasks', []) + if not sub_tasks: + continue + + parent_status = t.get('status', '') + sub_statuses = [st.get('status', '') for st in sub_tasks] + + if 'fail' in sub_statuses and parent_status != 'fail': + issues.append(f"{t.get('description')}: has failed subtask but parent is '{parent_status}'") + + if 'in_progress' in sub_statuses and parent_status != 'in_progress': + issues.append(f"{t.get('description')}: has in_progress subtask but parent is '{parent_status}'") + + # Recurse + issues.extend(check_propagation(sub_tasks)) + + return issues + + issues = check_propagation(tasks) + if issues: + return (True, f"Propagation issues found: {issues}") + + return (True, "Status propagation verified correctly") + + tr.Processes.Default.Streams.stdout = Testers.CustomJSONRPCResponse(validate_status_propagation) + tr.StillRunningAfter = ts + + # ============================================================================ + # Test 7: Check task logs contain error details + # ============================================================================ + tr = urtest.AddTestRun("Check failed tasks have error logs") + tr.DelayStart = 1 + tr.AddJsonRPCClientRequest(ts, Request.admin_config_reload()) + + def validate_error_logs(resp: Response): + '''Verify failed tasks have descriptive logs''' + if resp.is_error(): + return (True, f"Query: {resp.error_as_str()}") + + result = resp.result + tasks = result.get('tasks', []) + + def find_failed_with_logs(task_list): + results = [] + for t in task_list: + if t.get('status') == 'fail': + logs = t.get('logs', []) + desc = t.get('description', 'unknown') + if logs: + results.append(f"{desc}: {logs}") + else: + results.append(f"{desc}: NO LOGS (should have error details)") + + results.extend(find_failed_with_logs(t.get('sub_tasks', []))) + return results + + failed_info = find_failed_with_logs(tasks) + if failed_info: + return (True, f"Failed tasks with logs: {failed_info}") + + return (True, "No failed tasks found (baseline may have recovered)") + + tr.Processes.Default.Streams.stdout = Testers.CustomJSONRPCResponse(validate_error_logs) + tr.StillRunningAfter = ts + + # ============================================================================ + # Test 8: Force reload to reset state + # ============================================================================ + tr = urtest.AddTestRun("Force reload to reset") + tr.DelayStart = 2 + + # Reset ssl_multicert.yaml to valid state + sslcertpath = ts.Disk.ssl_multicert_yaml.AbsPath + tr.Disk.File(sslcertpath, id="ssl_multicert_yaml", typename="ats:config") + tr.Disk.ssl_multicert_yaml.AddLines( + [ + 'ssl_multicert:', + ' - dest_ip: "*"', + ' ssl_cert_name: server.pem', + ' ssl_key_name: server.key', + ]) + + tr.AddJsonRPCClientRequest(ts, Request.admin_config_reload(force=True)) + + def validate_reset(resp: Response): + '''Reset to clean state''' + if resp.is_error(): + return (True, f"Reset: {resp.error_as_str()}") + + result = resp.result + token = result.get('token', '') + return (True, f"Reset reload started: {token}") + + tr.Processes.Default.Streams.stdout = Testers.CustomJSONRPCResponse(validate_reset) + tr.StillRunningAfter = ts + + # ============================================================================ + # Test 9: Verify clean state after reset + # ============================================================================ + tr = urtest.AddTestRun("Verify clean state after reset") + tr.DelayStart = 3 + tr.AddJsonRPCClientRequest(ts, Request.admin_config_reload()) + + def validate_clean_state(resp: Response): + '''Verify we're back to clean state''' + if resp.is_error(): + error = resp.error_as_str() + if 'in progress' in error.lower(): + return (True, f"In progress: {error}") + return (True, f"Query: {error}") + + result = resp.result + tasks = result.get('tasks', []) + + # Count failures + def count_failures(task_list): + count = 0 + for t in task_list: + if t.get('status') == 'fail': + count += 1 + count += count_failures(t.get('sub_tasks', [])) + return count + + failures = count_failures(tasks) + if failures > 0: + return (True, f"Still have {failures} failed tasks (may need more time)") + + return (True, "Clean state - no failures") + + tr.Processes.Default.Streams.stdout = Testers.CustomJSONRPCResponse(validate_clean_state) + tr.StillRunningAfter = ts + + # ============================================================================ + # Test 10: Summary + # ============================================================================ + tr = urtest.AddTestRun("Final summary") + tr.DelayStart = 1 + tr.AddJsonRPCClientRequest(ts, Request.admin_config_reload()) + + def validate_summary(resp: Response): + '''Final summary of failure testing''' + if resp.is_error(): + return (True, f"Final: {resp.error_as_str()}") + + result = resp.result + + summary = """ + Config Reload Failure Testing Summary: + - Failed tasks: Detected when config validation fails + - Failed subtasks: SSL cert failures propagate to parent + - Status propagation: Parent status reflects worst subtask status + - Error logs: Failed tasks should include error details + """ + + return (True, f"Test complete. Token: {result.get('token', 'none')}") + + tr.Processes.Default.Streams.stdout = Testers.CustomJSONRPCResponse(validate_summary) + tr.StillRunningAfter = ts + urtest.execute() diff --git a/tests/uranium_tests/jsonrpc/test_config_reload_full_smoke.py b/tests/uranium_tests/jsonrpc/test_config_reload_full_smoke.py new file mode 100644 index 00000000000..c7e627f4e6b --- /dev/null +++ b/tests/uranium_tests/jsonrpc/test_config_reload_full_smoke.py @@ -0,0 +1,187 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_config_reload_full_smoke(urtest: UraniumTest) -> None: + ''' + Full reload smoke test. + + Verifies that ALL registered config handlers complete properly by: + Part A: Touching every registered config file, triggering a reload with a + named token, and verifying all deferred handlers reach a terminal + state (no in_progress after a delay). + Part B: Changing one record per module via traffic_ctl config set, waiting, + then verifying no terminal-state conflicts appear in diags.log. + + Registered configs at time of writing: + Files: ip_allow.yaml, parent.config, cache.config, hosting.config, + splitdns.config, logging.yaml, sni.yaml, ssl_multicert.yaml + Record-only: ssl_ticket_key (proxy.config.ssl.server.ticket_key.filename) + + The key assertion is that diags.log does NOT contain: + "ignoring transition from" — means two code paths disagree about task outcome + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + urtest.Summary = 'Full reload smoke test: all config files + record triggers' + urtest.ContinueOnFail = True + + # --- Setup --- + ts = urtest.MakeATSProcess("ts", enable_cache=True) + ts.Disk.records_config.update({ + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'rpc|config|reload', + }) + + # ============================================================================ + # Part A: File-based full reload + # + # ATS starts with valid config content (written via AddLines before start). + # We then touch every file to bump mtime and trigger a reload — this exercises + # the full parse path of each handler. + # ============================================================================ + + # Provide valid content for files whose handlers reject empty input. + ts.Disk.ip_allow_yaml.AddLines([ + 'ip_allow:', + '- apply: in', + ' ip_addrs: 0/0', + ' action: allow', + ' methods: ALL', + ]) + ts.Disk.logging_yaml.AddLines([ + 'logging:', + ' formats:', + ' - name: smoke', + ' format: "%"', + ]) + ts.Disk.sni_yaml.AddLines([ + 'sni:', + '- fqdn: "*.example.com"', + ' verify_client: NONE', + ]) + # parent.config, cache.config, hosting.config, splitdns.config, + # ssl_multicert.yaml are fine empty — handlers accept empty/comment-only files. + + # All registered config files whose mtime we'll bump to trigger reload. + files_to_touch = [ + ts.Disk.ip_allow_yaml, + ts.Disk.parent_config, + ts.Disk.cache_config, + ts.Disk.hosting_config, + ts.Disk.splitdns_config, + ts.Disk.logging_yaml, + ts.Disk.sni_yaml, + ts.Disk.ssl_multicert_yaml, + ] + touch_cmd = "touch " + " ".join([f.AbsRunTimePath for f in files_to_touch]) + # Modify records.yaml via traffic_ctl --cold to trigger a real records reload. + records_cmd = 'traffic_ctl config set proxy.config.diags.debug.tags "rpc|config|reload|upd" --cold' + + # Test 1: Start ATS, wait for it to settle, update records.yaml on disk + tr = urtest.AddTestRun("Update records.yaml via --cold") + tr.Processes.Default.StartBefore(ts) + tr.Processes.Default.Command = f"sleep 3 && {records_cmd}" + tr.Processes.Default.Env = ts.Env + tr.Processes.Default.ReturnCode = 0 + tr.StillRunningAfter = ts + + # Test 2: Touch all other config files to bump mtime + tr = urtest.AddTestRun("Touch all registered config files") + tr.Processes.Default.Command = touch_cmd + tr.Processes.Default.ReturnCode = 0 + tr.StillRunningAfter = ts + + # Test 3: Reload with token — all handlers re-read from disk + tr = urtest.AddTestRun("Reload with token - show details") + tr.Processes.Default.Command = "traffic_ctl config reload -t full_reload_smoke" + tr.Processes.Default.Env = ts.Env + tr.Processes.Default.ReturnCode = 0 + tr.StillRunningAfter = ts + + # Test 4: Query status after delay — all deferred handlers (e.g. logging) should have + # reached a terminal state by now. + tr = urtest.AddTestRun("Verify no tasks stuck in progress after delay") + tr.DelayStart = 15 + tr.Processes.Default.Command = "traffic_ctl config status -t full_reload_smoke" + tr.Processes.Default.Env = ts.Env + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.stdout += Testers.ExcludesExpression("in_progress", "No task should remain in progress after 15s") + tr.StillRunningAfter = ts + + # ============================================================================ + # Part B: Record-triggered reloads (tracing ENABLED — default) + # + # Change one record per module to exercise the RecordTriggeredReloadContinuation path. + # With proxy.config.admin.reload.trace_record_triggers=1 (default), each + # record-triggered reload creates a "rec-" parent task visible in status/history. + # ============================================================================ + + # One safe record per module: + records_to_change = [ + # (record_name, new_value) — pick values that won't break ATS + ("proxy.config.log.sampling_frequency", "2"), # logging + ("proxy.config.ssl.server.session_ticket.enable", "0"), # ssl_client_coordinator + ] + + for record_name, new_value in records_to_change: + tr = urtest.AddTestRun(f"Set {record_name}={new_value}") + tr.DelayStart = 2 + tr.Processes.Default.Command = f"traffic_ctl config set {record_name} {new_value}" + tr.Processes.Default.Env = ts.Env + tr.Processes.Default.ReturnCode = 0 + tr.StillRunningAfter = ts + + # Wait for record-triggered reloads to complete + tr = urtest.AddTestRun("Wait for record-triggered reloads") + tr.DelayStart = 10 + tr.Processes.Default.Command = "echo 'Waiting for record-triggered reloads to settle'" + tr.Processes.Default.ReturnCode = 0 + tr.StillRunningAfter = ts + + # Final dump of all reload history + tr = urtest.AddTestRun("Fetch all reload history") + tr.Processes.Default.Command = "traffic_ctl config status -c all" + tr.Processes.Default.Env = ts.Env + tr.Processes.Default.ReturnCode = 0 + tr.StillRunningAfter = ts + + # ============================================================================ + # Global assertions on diags.log + # ============================================================================ + + # No handler should have conflicting terminal state transitions. + # This catches bugs where e.g. evaluate_config() calls fail() and then + # change_configuration() calls complete() — the guard rejects the second call. + ts.Disk.diags_log.Content = Testers.ExcludesExpression( + "ignoring transition from", "No handler should fight over terminal states") + urtest.execute() diff --git a/tests/uranium_tests/jsonrpc/test_config_reload_reserve_subtask.py b/tests/uranium_tests/jsonrpc/test_config_reload_reserve_subtask.py new file mode 100644 index 00000000000..d6400750118 --- /dev/null +++ b/tests/uranium_tests/jsonrpc/test_config_reload_reserve_subtask.py @@ -0,0 +1,155 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_config_reload_reserve_subtask(urtest: UraniumTest) -> None: + ''' + Test that reserve_subtask correctly pre-registers subtasks during a reload. + + When records.yaml is changed alongside other config files, the "records" + subtask completes first (records.yaml is always processed first in + rereadConfig). This pushes the main task to SUCCESS before other + file-based or record-triggered handlers have a chance to register their + subtasks. reserve_subtask() must accept a SUCCESS parent and pull it back + to IN_PROGRESS by adding a CREATED child. + + This test: + 1. Modifies records.yaml on disk (--cold) to change a trigger record so + that a record-triggered handler fires during the next reload. + 2. Touches additional config files so file-based handlers also fire. + 3. Triggers a reload with a named token. + 4. Verifies that traffic.out contains "Reserved subtask" messages — proving + reserve_subtask() succeeded despite the parent being SUCCESS. + 5. Verifies no "ignoring transition" warnings (no state conflicts). + 6. Verifies the reload reaches a terminal state with all subtasks tracked. + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + urtest.Summary = 'Verify reserve_subtask pre-registers subtasks when records.yaml is first' + urtest.ContinueOnFail = True + + # --- Setup --- + ts = urtest.MakeATSProcess("ts", enable_cache=True) + # Allow extra startup time — cache clearing can take ~10s in some environments. + ts.StartupTimeout = 30 + ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'rpc|config|config.reload|filemanager', + }) + + # Provide valid content for files whose handlers reject empty input. + ts.Disk.ip_allow_yaml.AddLines([ + 'ip_allow:', + '- apply: in', + ' ip_addrs: 0/0', + ' action: allow', + ' methods: ALL', + ]) + ts.Disk.logging_yaml.AddLines([ + 'logging:', + ' formats:', + ' - name: reserve_test', + ' format: "%"', + ]) + ts.Disk.sni_yaml.AddLines([ + 'sni:', + '- fqdn: "*.example.com"', + ' verify_client: NONE', + ]) + + # Files to touch to trigger file-based handlers during reload. + files_to_touch = [ + ts.Disk.ip_allow_yaml, + ts.Disk.logging_yaml, + ts.Disk.sni_yaml, + ts.Disk.cache_config, + ] + touch_cmd = "touch " + " ".join([f.AbsRunTimePath for f in files_to_touch]) + + # ============================================================================ + # Test 1: Start ATS, let it settle, then modify records.yaml on disk + # ============================================================================ + tr = urtest.AddTestRun("Modify records.yaml via --cold to change a trigger record") + tr.Processes.Default.StartBefore(ts) + # Change debug tags — this touches records.yaml on disk so rereadConfig + # detects it as changed. The --cold flag modifies the file without + # notifying the running process (the reload will pick it up). + tr.Processes.Default.Command = ( + 'sleep 3 && traffic_ctl config set proxy.config.diags.debug.tags ' + '"rpc|config|config.reload|filemanager|upd" --cold') + tr.Processes.Default.Env = ts.Env + tr.Processes.Default.ReturnCode = 0 + tr.StillRunningAfter = ts + + # ============================================================================ + # Test 2: Touch additional config files to bump mtime + # ============================================================================ + tr = urtest.AddTestRun("Touch config files to trigger file-based handlers") + tr.Processes.Default.Command = touch_cmd + tr.Processes.Default.ReturnCode = 0 + tr.StillRunningAfter = ts + + # ============================================================================ + # Test 3: Trigger reload with a named token + # ============================================================================ + tr = urtest.AddTestRun("Trigger reload with named token") + tr.Processes.Default.Command = "traffic_ctl config reload -t reserve_subtask_test" + tr.Processes.Default.Env = ts.Env + tr.Processes.Default.ReturnCode = 0 + tr.StillRunningAfter = ts + + # ============================================================================ + # Test 4: Wait for all handlers to complete, then query status + # ============================================================================ + tr = urtest.AddTestRun("Verify reload completed — no tasks stuck in progress") + tr.DelayStart = 15 + tr.Processes.Default.Command = "traffic_ctl config status -t reserve_subtask_test" + tr.Processes.Default.Env = ts.Env + tr.Processes.Default.ReturnCode = 0 + # No subtask should still be in_progress after 15s. + tr.Processes.Default.Streams.stdout += Testers.ExcludesExpression( + "in_progress", "No task should remain in progress after 15s delay") + tr.Processes.Default.Streams.stdout += Testers.ContainsExpression("success", "Final reload status must be success") + tr.StillRunningAfter = ts + + # ============================================================================ + # Test 5: Verify no state-transition conflicts in traffic.out + # + # "ignoring transition from" means two code paths disagree about a task's + # outcome. This must never happen. + # ============================================================================ + ts.Disk.traffic_out.Content += Testers.ExcludesExpression( + "ignoring transition from", "No state-transition conflicts should appear in traffic.out") + ts.Disk.traffic_out.Content += Testers.ContainsExpression( + "Reserved subtask", "reserve_subtask() must log pre-registration messages") + urtest.execute() diff --git a/tests/uranium_tests/jsonrpc/test_config_reload_rpc.py b/tests/uranium_tests/jsonrpc/test_config_reload_rpc.py new file mode 100644 index 00000000000..aaa9b5af1f4 --- /dev/null +++ b/tests/uranium_tests/jsonrpc/test_config_reload_rpc.py @@ -0,0 +1,482 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_config_reload_rpc(urtest: UraniumTest) -> None: + ''' + Test inline config reload functionality via unified admin_config_reload RPC method. + + Inline mode is triggered by passing the "configs" parameter. + Tests the following features: + 1. Basic inline reload with single config + 2. Multiple configs in single request + 3. File-based vs inline mode detection + 4. Unknown config key error handling + 5. Invalid YAML content handling + 6. Reload while another is in progress + 7. Verify config is actually applied + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + from jsonrpc import Request, Response + + urtest.Summary = 'Test inline config reload via RPC' + urtest.ContinueOnFail = True + + ts = urtest.MakeATSProcess('ts', dump_runroot=True) + + urtest.testName = 'config_reload_rpc' + + # Initial configuration + ts.Disk.records_config.update({ + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'rpc|config', + }) + + # ============================================================================ + # Test 1: File-based reload (no configs parameter) + # ============================================================================ + tr = urtest.AddTestRun("File-based reload without configs parameter") + tr.Processes.Default.StartBefore(ts) + tr.AddJsonRPCClientRequest(ts, Request.admin_config_reload()) + + def validate_file_based(resp: Response): + '''Verify file-based reload works when configs is not provided''' + result = resp.result + token = result.get('token', '') + message = result.get('message', []) + + if token: + return (True, f"File-based reload started: token={token}") + + errors = result.get('errors', []) + if errors: + return (True, f"File-based reload response: {errors}") + + return (True, f"Response: {result}") + + tr.Processes.Default.Streams.stdout = Testers.CustomJSONRPCResponse(validate_file_based) + tr.StillRunningAfter = ts + + # ============================================================================ + # Test 2: Empty configs map (should trigger inline mode but process 0 configs) + # ============================================================================ + tr = urtest.AddTestRun("Empty configs map") + tr.DelayStart = 2 + tr.AddJsonRPCClientRequest(ts, Request.admin_config_reload(configs={})) + + def validate_empty_configs(resp: Response): + '''Verify behavior with empty configs''' + result = resp.result + + # Empty configs should succeed but with 0 changes + success = result.get('success', -1) + failed = result.get('failed', -1) + + if success == 0 and failed == 0: + return (True, f"Empty configs handled: success={success}, failed={failed}") + + # Or it might be an error + errors = result.get('errors', []) + if errors: + return (True, f"Empty configs rejected: {errors}") + + return (True, f"Result: {result}") + + tr.Processes.Default.Streams.stdout = Testers.CustomJSONRPCResponse(validate_empty_configs) + tr.StillRunningAfter = ts + + # ============================================================================ + # Test 3: Unknown config key (error code 6010) + # ============================================================================ + tr = urtest.AddTestRun("Unknown config key should error with code 6010") + tr.DelayStart = 2 + tr.AddJsonRPCClientRequest(ts, Request.admin_config_reload(configs={"unknown_config_key": {"some": "data"}})) + + def validate_unknown_key(resp: Response): + '''Verify error for unknown config key - should return error code 6010''' + result = resp.result + errors = result.get('errors', []) + + if not errors: + return (False, f"Expected error for unknown key, got: {result}") + + error_str = str(errors) + if '6010' in error_str or 'not registered' in error_str: + return (True, f"Unknown key rejected with code 6010: {errors}") + return (False, f"Expected error 6010, got: {errors}") + + tr.Processes.Default.Streams.stdout = Testers.CustomJSONRPCResponse(validate_unknown_key) + tr.StillRunningAfter = ts + + # ============================================================================ + # Test 3b: Unregistered config rejected (error code 6010) + # Note: remap.config is not registered in ConfigRegistry + # ============================================================================ + tr = urtest.AddTestRun("Unregistered config should error with code 6010") + tr.DelayStart = 1 + tr.AddJsonRPCClientRequest(ts, Request.admin_config_reload(configs={"remap.config": {"some": "data"}})) + + def validate_legacy_not_supported(resp: Response): + '''Verify unregistered config returns error code 6010''' + result = resp.result + errors = result.get('errors', []) + + if not errors: + return (False, f"Expected rejection for unregistered config, got: {result}") + + error_str = str(errors) + if '6010' in error_str or 'not registered' in error_str: + return (True, f"Unregistered config correctly rejected: {errors}") + return (False, f"Expected error 6010, got: {errors}") + + tr.Processes.Default.Streams.stdout = Testers.CustomJSONRPCResponse(validate_legacy_not_supported) + tr.StillRunningAfter = ts + + # ============================================================================ + # Test 4: RPC-injected content rejected for FileOnly config (ip_allow) + # ip_allow is registered with ConfigSource::FileOnly — RPC content must be rejected + # ============================================================================ + tr = urtest.AddTestRun("RPC-injected content rejected for FileOnly config (ip_allow)") + tr.DelayStart = 2 + tr.AddJsonRPCClientRequest( + ts, + Request.admin_config_reload( + configs={"ip_allow": [{ + "apply": "in", + "ip_addrs": "127.0.0.1", + "action": "allow", + "methods": ["GET", "HEAD"] + }]})) + + def validate_rpc_inject_rejected(resp: Response): + '''ip_allow is registered as FileOnly — RPC-injected content must be rejected with 6011''' + result = resp.result + errors = result.get('errors', []) + + if not errors: + return (False, f"Expected rejection for FileOnly config, got: {result}") + + error_str = str(errors) + if '6011' in error_str or 'does not support RPC' in error_str: + return (True, f"FileOnly config correctly rejected RPC injection: {errors}") + return (False, f"Expected error 6011, got: {errors}") + + tr.Processes.Default.Streams.stdout = Testers.CustomJSONRPCResponse(validate_rpc_inject_rejected) + tr.StillRunningAfter = ts + + # ============================================================================ + # Test 5: Multiple configs in single request + # ============================================================================ + tr = urtest.AddTestRun("Multiple configs in single request") + tr.DelayStart = 2 + tr.AddJsonRPCClientRequest( + ts, + Request.admin_config_reload( + configs={ + "ip_allow": [{ + "apply": "in", + "ip_addrs": "0.0.0.0/0", + "action": "allow" + }], + "sni": [{ + "fqdn": "*.test.com", + "verify_client": "NONE" + }], + "records": { + "diags": { + "debug": { + "enabled": 1 + } + } + } + })) + + def validate_multiple_configs(resp: Response): + '''All configs should be rejected — none support RPC content source at this stage''' + result = resp.result + errors = result.get('errors', []) + + if not errors: + return (False, f"Expected rejections for all configs, got: {result}") + + # Each config should produce an error (6010=not registered, 6011=RPC source not supported) + error_str = str(errors) + if '6010' in error_str or '6011' in error_str: + return (True, f"All configs rejected as expected: {len(errors)} errors") + return (False, f"Unexpected errors: {errors}") + + tr.Processes.Default.Streams.stdout = Testers.CustomJSONRPCResponse(validate_multiple_configs) + tr.StillRunningAfter = ts + + # ============================================================================ + # Test 6: Reload while another is in progress + # ============================================================================ + tr = urtest.AddTestRun("First reload request") + tr.DelayStart = 1 + tr.AddJsonRPCClientRequest(ts, Request.admin_config_reload()) + + def validate_first_reload(resp: Response): + '''Start a regular reload''' + result = resp.result + token = result.get('token', '') + return (True, f"First reload started: token={token}") + + tr.Processes.Default.Streams.stdout = Testers.CustomJSONRPCResponse(validate_first_reload) + tr.StillRunningAfter = ts + + # Immediately try inline reload + tr = urtest.AddTestRun("Inline reload while regular reload in progress") + tr.DelayStart = 0 # No delay - immediately after + tr.AddJsonRPCClientRequest(ts, Request.admin_config_reload(configs={"ip_allow": [{"apply": "in", "ip_addrs": "10.0.0.0/8"}]})) + + def validate_in_progress_rejection(resp: Response): + '''Should be rejected for RPC source not supported or reload in progress''' + result = resp.result + errors = result.get('errors', []) + + if not errors: + return (False, f"Expected rejection, got: {result}") + + error_str = str(errors) + # Either 6011 (RPC source not supported) or 6004 (reload in progress) + if '6011' in error_str or '6004' in error_str: + return (True, f"Correctly rejected: {errors}") + return (False, f"Unexpected error: {errors}") + + tr.Processes.Default.Streams.stdout = Testers.CustomJSONRPCResponse(validate_in_progress_rejection) + tr.StillRunningAfter = ts + + # ============================================================================ + # Test 7: Verify token is returned with inline- prefix + # ============================================================================ + tr = urtest.AddTestRun("Verify inline token prefix") + tr.DelayStart = 3 # Wait for previous reloads to complete + tr.AddJsonRPCClientRequest(ts, Request.admin_config_reload(configs={"unknown_for_token_test": {"data": "value"}})) + + def validate_inline_token(resp: Response): + '''Verify token has inline- prefix''' + result = resp.result + token = result.get('token', '') + + if token and token.startswith('inline-'): + return (True, f"Token has correct prefix: {token}") + + if not token: + # Check if there's an error (which is fine) + errors = result.get('errors', []) + if errors: + return (True, f"No token (error case): {errors}") + + return (True, f"Token result: {token}") + + tr.Processes.Default.Streams.stdout = Testers.CustomJSONRPCResponse(validate_inline_token) + tr.StillRunningAfter = ts + + # ============================================================================ + # Test 8: Nested YAML structure (records.diags.debug) + # ============================================================================ + tr = urtest.AddTestRun("Nested YAML structure") + tr.DelayStart = 2 + tr.AddJsonRPCClientRequest( + ts, + Request.admin_config_reload( + configs={"records": { + "diags": { + "debug": { + "enabled": 1, + "tags": "http|rpc|test" + } + }, + "http": { + "cache": { + "http": 1 + } + } + }})) + + def validate_nested_yaml(resp: Response): + '''Verify nested YAML handling''' + result = resp.result + success = result.get('success', 0) + failed = result.get('failed', 0) + errors = result.get('errors', []) + + return (True, f"Nested YAML: success={success}, failed={failed}, errors={errors}") + + tr.Processes.Default.Streams.stdout = Testers.CustomJSONRPCResponse(validate_nested_yaml) + tr.StillRunningAfter = ts + + # ============================================================================ + # Test 9: Query status after inline reload + # ============================================================================ + tr = urtest.AddTestRun("Query status after inline reload") + tr.DelayStart = 2 + tr.AddJsonRPCClientRequest(ts, Request.get_reload_config_status()) + + def validate_status_after_inline(resp: Response): + '''Check status includes inline reload info''' + if resp.is_error(): + return (True, f"Status query error (may be expected): {resp.error_as_str()}") + + result = resp.result + tasks = result.get('tasks', []) + + if tasks: + # Check if any task has inline- prefix + for task in tasks: + token = task.get('token', '') + if token.startswith('inline-'): + return (True, f"Found inline reload in status: {token}") + + return (True, f"Status result: {result}") + + tr.Processes.Default.Streams.stdout = Testers.CustomJSONRPCResponse(validate_status_after_inline) + tr.StillRunningAfter = ts + + # ============================================================================ + # Test 10: Large config content + # ============================================================================ + tr = urtest.AddTestRun("Large config content") + tr.DelayStart = 2 + + # Generate a larger config + large_ip_allow = [] + for i in range(50): + large_ip_allow.append({"apply": "in", "ip_addrs": f"10.{i}.0.0/16", "action": "allow"}) + + tr.AddJsonRPCClientRequest(ts, Request.admin_config_reload(configs={"ip_allow": large_ip_allow})) + + def validate_large_config(resp: Response): + '''Large ip_allow config should also be rejected (FileOnly)''' + result = resp.result + errors = result.get('errors', []) + + if not errors: + return (False, f"Expected rejection for FileOnly config, got: {result}") + + error_str = str(errors) + if '6011' in error_str: + return (True, f"Large config correctly rejected: {errors}") + return (False, f"Expected error 6011, got: {errors}") + + tr.Processes.Default.Streams.stdout = Testers.CustomJSONRPCResponse(validate_large_config) + tr.StillRunningAfter = ts + + # ============================================================================ + # Test 11: Reload directive for registered FileOnly config (sni) + # Directive-only request — sni is FileOnly, so the RPC handler rejects with 6011. + # Verifies the _reload structure is handled gracefully through the RPC stack. + # ============================================================================ + tr = urtest.AddTestRun("Reload directive for FileOnly config (sni)") + tr.DelayStart = 2 + tr.AddJsonRPCClientRequest(ts, Request.admin_config_reload(configs={"sni": {"_reload": {"fqdn": "*.example.com"}}})) + + def validate_directive_fileonly(resp: Response): + '''sni is FileOnly — directive-only request rejected with 6011''' + result = resp.result + errors = result.get('errors', []) + + if not errors: + return (False, f"Expected rejection for FileOnly config, got: {result}") + + error_str = str(errors) + if '6011' in error_str: + return (True, f"Directive-only correctly rejected for FileOnly config: {errors}") + return (False, f"Expected error 6011, got: {errors}") + + tr.Processes.Default.Streams.stdout = Testers.CustomJSONRPCResponse(validate_directive_fileonly) + tr.StillRunningAfter = ts + + # ============================================================================ + # Test 12: Reload directive for unregistered config (virtualhost) + # virtualhost is not registered yet — should get 6010. + # This is the intended use case once the virtualhost handler is registered. + # ============================================================================ + tr = urtest.AddTestRun("Reload directive for unregistered config (virtualhost)") + tr.DelayStart = 2 + tr.AddJsonRPCClientRequest(ts, Request.admin_config_reload(configs={"virtualhost": {"_reload": {"id": "myhost.example.com"}}})) + + def validate_directive_unregistered(resp: Response): + '''virtualhost is not registered — rejected with 6010''' + result = resp.result + errors = result.get('errors', []) + + if not errors: + return (False, f"Expected error for unregistered config, got: {result}") + + error_str = str(errors) + if '6010' in error_str or 'not registered' in error_str: + return (True, f"Directive for unregistered config rejected: {errors}") + return (False, f"Expected error 6010, got: {errors}") + + tr.Processes.Default.Streams.stdout = Testers.CustomJSONRPCResponse(validate_directive_unregistered) + tr.StillRunningAfter = ts + + # ============================================================================ + # Test 13: Directives mixed with content for FileOnly config (ip_allow) + # _reload directives alongside actual config content — still rejected with 6011. + # ============================================================================ + tr = urtest.AddTestRun("Directives mixed with content for FileOnly config") + tr.DelayStart = 2 + tr.AddJsonRPCClientRequest( + ts, + Request.admin_config_reload( + configs={ + "ip_allow": { + "_reload": { + "validate_only": "true" + }, + "rules": [{ + "apply": "in", + "ip_addrs": "0/0", + "action": "allow" + }] + } + })) + + def validate_directive_mixed(resp: Response): + '''ip_allow is FileOnly — mixed directive+content rejected with 6011''' + result = resp.result + errors = result.get('errors', []) + + if not errors: + return (False, f"Expected rejection, got: {result}") + + error_str = str(errors) + if '6011' in error_str: + return (True, f"Mixed directive+content correctly rejected: {errors}") + return (False, f"Expected error 6011, got: {errors}") + + tr.Processes.Default.Streams.stdout = Testers.CustomJSONRPCResponse(validate_directive_mixed) + tr.StillRunningAfter = ts + urtest.execute() diff --git a/tests/uranium_tests/jsonrpc/test_config_reload_tracking.py b/tests/uranium_tests/jsonrpc/test_config_reload_tracking.py new file mode 100644 index 00000000000..bbfe4e47adf --- /dev/null +++ b/tests/uranium_tests/jsonrpc/test_config_reload_tracking.py @@ -0,0 +1,303 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_config_reload_tracking(urtest: UraniumTest) -> None: + ''' + Test config reload tracking functionality. + + Tests the following features: + 1. Basic reload with token generation + 2. Querying reload status while in progress + 3. Reload history tracking + 4. Force reload while one is in progress + 5. Custom token names + 6. Duplicate token prevention + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + from jsonrpc import Request, Response + import time + + urtest.Summary = 'Test config reload tracking with tokens and status' + urtest.ContinueOnFail = True + + ts = urtest.MakeATSProcess('ts', dump_runroot=True) + + urtest.testName = 'config_reload_tracking' + + # Initial configuration + ts.Disk.records_config.update({ + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'rpc|config', + }) + + # Store tokens for later tests + stored_tokens = [] + + # ============================================================================ + # Test 1: Basic reload - verify token is returned + # ============================================================================ + tr = urtest.AddTestRun("Basic reload with auto-generated token") + tr.Processes.Default.StartBefore(ts) + tr.AddJsonRPCClientRequest(ts, Request.admin_config_reload()) + + def validate_basic_reload(resp: Response): + '''Verify reload returns a token''' + if resp.is_error(): + return (False, f"Error: {resp.error_as_str()}") + + result = resp.result + token = result.get('token', '') + created_time = result.get('created_time', '') + messages = result.get('message', []) + + if not token: + return (False, "No token returned") + + if not token.startswith('rldtk-'): + return (False, f"Token should start with 'rldtk-', got: {token}") + + # Store for later tests + stored_tokens.append(token) + + return (True, f"Reload started: token={token}, created={created_time}, messages={messages}") + + tr.Processes.Default.Streams.stdout = Testers.CustomJSONRPCResponse(validate_basic_reload) + tr.StillRunningAfter = ts + + # ============================================================================ + # Test 2: Query status of completed reload + # ============================================================================ + tr = urtest.AddTestRun("Query status of completed reload") + tr.DelayStart = 2 # Give time for reload to complete + tr.AddJsonRPCClientRequest(ts, Request.admin_config_reload_status()) + + def validate_status_query(resp: Response): + '''Check reload status after completion''' + if resp.is_error(): + # If method doesn't exist, that's OK - we're testing the main reload + return (True, f"Status query: {resp.error_as_str()}") + + result = resp.result + return (True, f"Reload status: {result}") + + tr.Processes.Default.Streams.stdout = Testers.CustomJSONRPCResponse(validate_status_query) + tr.StillRunningAfter = ts + + # ============================================================================ + # Test 3: Reload with custom token + # ============================================================================ + tr = urtest.AddTestRun("Reload with custom token") + tr.DelayStart = 1 + custom_token = f"my-custom-token-{int(time.time())}" + tr.AddJsonRPCClientRequest(ts, Request.admin_config_reload(token=custom_token)) + + def validate_custom_token(resp: Response): + '''Verify custom token is accepted''' + if resp.is_error(): + # Check if it's a "reload in progress" error + error_str = resp.error_as_str() + if 'in progress' in error_str.lower(): + return (True, f"Reload in progress (expected): {error_str}") + return (False, f"Error: {error_str}") + + result = resp.result + token = result.get('token', '') + + if token != custom_token: + return (False, f"Expected custom token '{custom_token}', got '{token}'") + + stored_tokens.append(token) + return (True, f"Custom token accepted: {token}") + + tr.Processes.Default.Streams.stdout = Testers.CustomJSONRPCResponse(validate_custom_token) + tr.StillRunningAfter = ts + + # ============================================================================ + # Test 4: Force reload while previous might still be processing + # ============================================================================ + tr = urtest.AddTestRun("Force reload") + tr.DelayStart = 1 + tr.AddJsonRPCClientRequest(ts, Request.admin_config_reload(force=True)) + + def validate_force_reload(resp: Response): + '''Verify force reload works''' + if resp.is_error(): + return (False, f"Force reload failed: {resp.error_as_str()}") + + result = resp.result + token = result.get('token', '') + if token: + stored_tokens.append(token) + + return (True, f"Force reload succeeded: token={token}") + + tr.Processes.Default.Streams.stdout = Testers.CustomJSONRPCResponse(validate_force_reload) + tr.StillRunningAfter = ts + + # ============================================================================ + # Test 5: Try duplicate token (should fail) + # ============================================================================ + tr = urtest.AddTestRun("Duplicate token rejection") + tr.DelayStart = 2 # Wait for previous reload to complete + + def make_duplicate_test(): + # Use the first token we stored + if stored_tokens: + return Request.admin_config_reload(token=stored_tokens[0]) + return Request.admin_config_reload(token="rldtk-duplicate-test") + + tr.AddJsonRPCClientRequest(ts, make_duplicate_test()) + + def validate_duplicate_rejection(resp: Response): + '''Verify duplicate tokens are rejected''' + if resp.is_error(): + return (True, f"Duplicate rejected (expected): {resp.error_as_str()}") + + result = resp.result + errors = result.get('error', []) + if errors: + return (True, f"Duplicate token rejected: {errors}") + + # If no error, check if token was actually reused + token = result.get('token', '') + return (True, f"Reload result: token={token}, errors={errors}") + + tr.Processes.Default.Streams.stdout = Testers.CustomJSONRPCResponse(validate_duplicate_rejection) + tr.StillRunningAfter = ts + + # ============================================================================ + # Test 6: Rapid succession reloads + # ============================================================================ + tr = urtest.AddTestRun("Rapid succession reloads - first") + tr.DelayStart = 1 + tr.AddJsonRPCClientRequest(ts, Request.admin_config_reload()) + + def validate_rapid_first(resp: Response): + '''First rapid reload''' + if resp.is_error(): + return (True, f"First rapid: {resp.error_as_str()}") + + result = resp.result + token = result.get('token', '') + return (True, f"First rapid reload: {token}") + + tr.Processes.Default.Streams.stdout = Testers.CustomJSONRPCResponse(validate_rapid_first) + tr.StillRunningAfter = ts + + # Second rapid reload (should see in-progress or succeed) + tr = urtest.AddTestRun("Rapid succession reloads - second") + tr.DelayStart = 0 # No delay - immediately after first + tr.AddJsonRPCClientRequest(ts, Request.admin_config_reload()) + + def validate_rapid_second(resp: Response): + '''Second rapid reload - may see in-progress''' + if resp.is_error(): + return (True, f"Second rapid (may be in progress): {resp.error_as_str()}") + + result = resp.result + token = result.get('token', '') + error = result.get('error', []) + + if error: + # In-progress is expected + return (True, f"Second rapid - in progress or error: {error}") + + return (True, f"Second rapid reload: {token}") + + tr.Processes.Default.Streams.stdout = Testers.CustomJSONRPCResponse(validate_rapid_second) + tr.StillRunningAfter = ts + + # ============================================================================ + # Test 7: Get reload history + # ============================================================================ + tr = urtest.AddTestRun("Get reload history") + tr.DelayStart = 2 + tr.AddJsonRPCClientRequest(ts, Request.admin_config_reload_history()) + + def validate_history(resp: Response): + '''Check reload history''' + if resp.is_error(): + # Method may not exist + return (True, f"History query: {resp.error_as_str()}") + + result = resp.result + history = result.get('history', []) + return (True, f"Reload history: {len(history)} entries") + + tr.Processes.Default.Streams.stdout = Testers.CustomJSONRPCResponse(validate_history) + tr.StillRunningAfter = ts + + # ============================================================================ + # Test 8: Trigger reload and verify new config is loaded + # ============================================================================ + tr = urtest.AddTestRun("Reload after config change") + tr.DelayStart = 1 + tr.AddJsonRPCClientRequest(ts, Request.admin_config_reload(force=True)) + + def validate_reload_after_change(resp: Response): + '''Verify reload after config change''' + if resp.is_error(): + return (False, f"Reload after change failed: {resp.error_as_str()}") + + result = resp.result + token = result.get('token', '') + return (True, f"Reload after config change: token={token}") + + tr.Processes.Default.Streams.stdout = Testers.CustomJSONRPCResponse(validate_reload_after_change) + tr.StillRunningAfter = ts + + # ============================================================================ + # Test 10: Final status check + # ============================================================================ + tr = urtest.AddTestRun("Final reload status") + tr.DelayStart = 2 + tr.AddJsonRPCClientRequest(ts, Request.admin_config_reload()) + + def validate_final_status(resp: Response): + '''Final status verification''' + if resp.is_error(): + error_str = resp.error_as_str() + if 'in progress' in error_str.lower(): + return (True, f"Reload still in progress: {error_str}") + return (True, f"Final status: {error_str}") + + result = resp.result + token = result.get('token', '') + created = result.get('created_time', '') + + return (True, f"Final reload: token={token}, created={created}") + + tr.Processes.Default.Streams.stdout = Testers.CustomJSONRPCResponse(validate_final_status) + tr.StillRunningAfter = ts + urtest.execute() diff --git a/tests/uranium_tests/jsonrpc/test_jsonrpc_api_schema.py b/tests/uranium_tests/jsonrpc/test_jsonrpc_api_schema.py new file mode 100644 index 00000000000..e863930b93a --- /dev/null +++ b/tests/uranium_tests/jsonrpc/test_jsonrpc_api_schema.py @@ -0,0 +1,239 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_jsonrpc_api_schema(urtest: UraniumTest) -> None: + ''' + JSONRPC Schema test. This test will run a basic request/response schema validation. + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import os + import sys + import tempfile + from string import Template + + urtest.Summary = 'Test jsonrpc admin API' + + # set the schema folder. + schema_folder = os.path.join(urtest.TestDirectory, '..', '..', '..', "src", "mgmt", "rpc", "schema") + + def substitute_context_in_file(process, file, context): + ''' + Perform substitution based on the passed context dict. This function will return a new path for the substituted file. + ''' + if os.path.isdir(file): + raise ValueError(f"Mapping substitution not supported for directories.") + + with open(os.path.join(process.TestDirectory, file), 'r') as req_file: + req_template = Template(req_file.read()) + req_content = req_template.substitute(context) + tf = tempfile.NamedTemporaryFile(delete=False, dir=process.RunDirectory, suffix=f"_{os.path.basename(file)}") + file = tf.name + with open(file, "w") as new_req_file: + new_req_file.write(req_content) + + return file + + def add_testrun_for_jsonrpc_request( + test_description, request_file_name, params_schema_file_name=None, result_schema_file_name=None, context=None): + ''' + Simple wrapper around the AddJsonRPCClientRequest method. + + context: + This can be used if a template substitution is needed in the request file. + + stdout_testers: + Testers to be run on the output stream. + + params_schema_file_name: + Schema file to validate the request 'params' field. + + result_schema_file_name: + Schema file to validate the response 'result' field. + ''' + tr = urtest.AddTestRun(test_description) + tr.Setup.Copy(request_file_name) + + if context: + request_file_name = substitute_context_in_file(tr, request_file_name, context) + + request_schema_file_name = os.path.join(schema_folder, "jsonrpc_request_schema.json") + tr.AddJsonRPCClientRequest( + ts, + file=os.path.join(ts.RunDirectory, os.path.basename(request_file_name)), + schema_file_name=request_schema_file_name, + params_field_schema_file_name=params_schema_file_name) + + tr.Processes.Default.ReturnCode = 0 + + response_schema_file_name = os.path.join(schema_folder, "jsonrpc_response_schema.json") + tr.Processes.Default.Streams.stdout = Testers.JSONRPCResponseSchemaValidator( + schema_file_name=response_schema_file_name, result_field_schema_file_name=result_schema_file_name) + + tr.StillRunningAfter = ts + return tr + + ts = urtest.MakeATSProcess('ts') + + urtest.testName = 'Basic JSONRPC API test' + + ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'rpc|filemanager|http|cache', + 'proxy.config.jsonrpc.filename': "jsonrpc.yaml", # We will be using this record to tests some RPC API. + }) + + # One of the API's will be checking the storage. Need this to get a response with content. + storage_path = os.path.join(urtest.RunDirectory, "ts", "storage") + ts.Disk.storage_yaml.AddLine( + f''' + cache: + spans: + - name: disk-1 + path: {storage_path} + size: 512M + ''') + + # The following tests will only validate the jsonrpc message, it will not run any validation on the content of the 'result' or 'params' + # of the jsonrpc message. This should be added once the schemas are available. + + # jsonrpc 2.0 schema file. This will not check the param fields. + + success_schema_file_name_name = os.path.join(schema_folder, "success_response_schema.json") + # admin_lookup_records + + params_schema_file_name = os.path.join(schema_folder, "admin_lookup_records_params_schema.json") + first = add_testrun_for_jsonrpc_request( + "Test admin_lookup_records", + request_file_name='json/admin_lookup_records_req_1.json', + params_schema_file_name=params_schema_file_name, + context={'record_name': 'proxy.config.jsonrpc.filename'}) + first.Processes.Default.StartBefore(ts) + + add_testrun_for_jsonrpc_request( + "Test admin_lookup_records w/error", request_file_name='json/admin_lookup_records_req_invalid_rec.json') + + add_testrun_for_jsonrpc_request( + "Test admin_lookup_records", + request_file_name='json/admin_lookup_records_req_1.json', + context={'record_name': 'proxy.config.jsonrpc.filename'}) + + add_testrun_for_jsonrpc_request( + "Test admin_lookup_records w/error", request_file_name='json/admin_lookup_records_req_invalid_rec.json') + + add_testrun_for_jsonrpc_request( + "Test admin_lookup_records w/error", + request_file_name='json/admin_lookup_records_req_multiple.json', + context={'record_name': 'proxy.config.jsonrpc.filename'}) + + add_testrun_for_jsonrpc_request( + "Test admin_lookup_records w/error", + request_file_name='json/admin_lookup_records_req_metric.json', + context={'record_name_regex': 'proxy.process.http.total_client_connections_ipv4*'}) + + # admin_config_set_records + add_testrun_for_jsonrpc_request( + "Test admin_lookup_records w/error", + request_file_name='json/admin_config_set_records_req.json', + context={ + 'record_name': 'proxy.config.jsonrpc.filename', + 'record_value': 'test_jsonrpc.yaml' + }) + + # admin_config_reload + # We will wait for this to have a stable schema. I think we may need to adjust the response a bit. + # add_testrun_for_jsonrpc_request( + # "Test admin_config_reload", + # request_file_name='json/admin_config_reload_req.json', + # result_schema_file_name=success_schema_file_name_name) + + # admin_host_set_status + add_testrun_for_jsonrpc_request( + "Test admin_host_set_status", + request_file_name='json/admin_host_set_status_req.json', + context={ + 'operation': 'up', + 'host': 'my.test.host.trafficserver.com' + }) + + # admin_host_set_status + add_testrun_for_jsonrpc_request( + "Test admin_host_set_status", + request_file_name='json/admin_host_set_status_req.json', + context={ + 'operation': 'down', + 'host': 'my.test.host.trafficserver.com' + }) + + # admin_server_start_drain + add_testrun_for_jsonrpc_request( + "Test admin_server_start_drain", + request_file_name='json/method_call_no_params.json', + context={'method': 'admin_server_start_drain'}) + + add_testrun_for_jsonrpc_request( + "Test admin_server_start_drain", + request_file_name='json/method_call_no_params.json', + context={'method': 'admin_server_start_drain'}) + + # admin_server_stop_drain + add_testrun_for_jsonrpc_request( + "Test admin_server_stop_drain", + request_file_name='json/method_call_no_params.json', + context={'method': 'admin_server_stop_drain'}) + + # admin_storage_get_device_status + add_testrun_for_jsonrpc_request( + "Test admin_storage_get_device_status", + request_file_name='json/admin_storage_x_device_status_req.json', + context={ + 'method': 'admin_storage_get_device_status', + 'device': f'{storage_path}/cache.db' + }) + + # admin_storage_set_device_offline + add_testrun_for_jsonrpc_request( + "Test admin_storage_set_device_offline", + request_file_name='json/admin_storage_x_device_status_req.json', + context={ + 'method': 'admin_storage_set_device_offline', + 'device': f'{storage_path}/cache.db' + }) + + # admin_plugin_send_basic_msg + add_testrun_for_jsonrpc_request( + "Test admin_plugin_send_basic_msg", + request_file_name='json/admin_plugin_send_basic_msg_req.json', + result_schema_file_name=success_schema_file_name_name) + urtest.execute() diff --git a/tests/gold_tests/lib/ats_autest.py b/tests/uranium_tests/lib/ats_urtest.py similarity index 100% rename from tests/gold_tests/lib/ats_autest.py rename to tests/uranium_tests/lib/ats_urtest.py diff --git a/tests/gold_tests/autest-site/jsonrpc.py b/tests/uranium_tests/lib/jsonrpc.py similarity index 100% rename from tests/gold_tests/autest-site/jsonrpc.py rename to tests/uranium_tests/lib/jsonrpc.py diff --git a/tests/uranium_tests/lib/ports.py b/tests/uranium_tests/lib/ports.py new file mode 100644 index 00000000000..648fbed5572 --- /dev/null +++ b/tests/uranium_tests/lib/ports.py @@ -0,0 +1,51 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. +"""Port helpers backed by the pytest Uranium runtime allocator.""" + +from __future__ import annotations + +from contextlib import contextmanager +from typing import Iterator +import socket + +from uranium_testkit.scenario import _scenario + + +def PortOpen(port: int, address: str | None = None, bound_ports: set[int] | None = None) -> bool: # noqa: N802 + """Return whether @a port is already bound or accepts a connection.""" + + if bound_ports and port in bound_ports: + return True + try: + with socket.create_connection((address or "localhost", port), timeout=0.2): + return True + except (OSError, socket.timeout): + return False + + +@contextmanager +def get_port_number() -> Iterator[int]: + """Yield a unique port allocated for the active pytest scenario.""" + + yield _scenario().runtime.allocate_port() + + +def get_port(obj: object, name: str) -> int: + """Allocate a port and assign it to ``obj.Variables[name]``.""" + + port = _scenario().runtime.allocate_port() + obj.Variables[name] = port + return port diff --git a/tests/gold_tests/autest-site/verifier_common.py b/tests/uranium_tests/lib/verifier_common.py similarity index 100% rename from tests/gold_tests/autest-site/verifier_common.py rename to tests/uranium_tests/lib/verifier_common.py diff --git a/tests/gold_tests/logging/all_headers_sanitizer.py b/tests/uranium_tests/logging/all_headers_sanitizer.py similarity index 100% rename from tests/gold_tests/logging/all_headers_sanitizer.py rename to tests/uranium_tests/logging/all_headers_sanitizer.py diff --git a/tests/gold_tests/logging/all_headers_sanitizer.sh b/tests/uranium_tests/logging/all_headers_sanitizer.sh similarity index 100% rename from tests/gold_tests/logging/all_headers_sanitizer.sh rename to tests/uranium_tests/logging/all_headers_sanitizer.sh diff --git a/tests/gold_tests/logging/gold/binary_log_v3_ascii.gold b/tests/uranium_tests/logging/gold/binary_log_v3_ascii.gold similarity index 100% rename from tests/gold_tests/logging/gold/binary_log_v3_ascii.gold rename to tests/uranium_tests/logging/gold/binary_log_v3_ascii.gold diff --git a/tests/gold_tests/logging/gold/binary_log_v3_json.gold b/tests/uranium_tests/logging/gold/binary_log_v3_json.gold similarity index 100% rename from tests/gold_tests/logging/gold/binary_log_v3_json.gold rename to tests/uranium_tests/logging/gold/binary_log_v3_json.gold diff --git a/tests/gold_tests/logging/gold/custom.gold b/tests/uranium_tests/logging/gold/custom.gold similarity index 100% rename from tests/gold_tests/logging/gold/custom.gold rename to tests/uranium_tests/logging/gold/custom.gold diff --git a/tests/gold_tests/logging/gold/custom_uds.gold b/tests/uranium_tests/logging/gold/custom_uds.gold similarity index 100% rename from tests/gold_tests/logging/gold/custom_uds.gold rename to tests/uranium_tests/logging/gold/custom_uds.gold diff --git a/tests/gold_tests/logging/gold/field-json-test.gold b/tests/uranium_tests/logging/gold/field-json-test.gold similarity index 100% rename from tests/gold_tests/logging/gold/field-json-test.gold rename to tests/uranium_tests/logging/gold/field-json-test.gold diff --git a/tests/gold_tests/logging/gold/field-test.gold b/tests/uranium_tests/logging/gold/field-test.gold similarity index 100% rename from tests/gold_tests/logging/gold/field-test.gold rename to tests/uranium_tests/logging/gold/field-test.gold diff --git a/tests/gold_tests/logging/gold/filter-test.gold b/tests/uranium_tests/logging/gold/filter-test.gold similarity index 100% rename from tests/gold_tests/logging/gold/filter-test.gold rename to tests/uranium_tests/logging/gold/filter-test.gold diff --git a/tests/gold_tests/logging/gold/pqsi-pqsp.gold b/tests/uranium_tests/logging/gold/pqsi-pqsp.gold similarity index 100% rename from tests/gold_tests/logging/gold/pqsi-pqsp.gold rename to tests/uranium_tests/logging/gold/pqsi-pqsp.gold diff --git a/tests/gold_tests/logging/gold/test_all_headers.gold b/tests/uranium_tests/logging/gold/test_all_headers.gold similarity index 100% rename from tests/gold_tests/logging/gold/test_all_headers.gold rename to tests/uranium_tests/logging/gold/test_all_headers.gold diff --git a/tests/gold_tests/logging/gold/test_all_headers_uds.gold b/tests/uranium_tests/logging/gold/test_all_headers_uds.gold similarity index 100% rename from tests/gold_tests/logging/gold/test_all_headers_uds.gold rename to tests/uranium_tests/logging/gold/test_all_headers_uds.gold diff --git a/tests/gold_tests/logging/log-filter.replays.yaml b/tests/uranium_tests/logging/log-filter.replays.yaml similarity index 100% rename from tests/gold_tests/logging/log-filter.replays.yaml rename to tests/uranium_tests/logging/log-filter.replays.yaml diff --git a/tests/gold_tests/logging/new_log_flds_observer.py b/tests/uranium_tests/logging/new_log_flds_observer.py similarity index 100% rename from tests/gold_tests/logging/new_log_flds_observer.py rename to tests/uranium_tests/logging/new_log_flds_observer.py diff --git a/tests/gold_tests/logging/pipe_buffer_is_larger_than.py b/tests/uranium_tests/logging/pipe_buffer_is_larger_than.py similarity index 100% rename from tests/gold_tests/logging/pipe_buffer_is_larger_than.py rename to tests/uranium_tests/logging/pipe_buffer_is_larger_than.py diff --git a/tests/gold_tests/logging/rename_qlog.sh b/tests/uranium_tests/logging/rename_qlog.sh similarity index 100% rename from tests/gold_tests/logging/rename_qlog.sh rename to tests/uranium_tests/logging/rename_qlog.sh diff --git a/tests/gold_tests/logging/replay/basic1.replay.yaml b/tests/uranium_tests/logging/replay/basic1.replay.yaml similarity index 100% rename from tests/gold_tests/logging/replay/basic1.replay.yaml rename to tests/uranium_tests/logging/replay/basic1.replay.yaml diff --git a/tests/gold_tests/logging/replay/log-debug-client-ip-http.test.yaml b/tests/uranium_tests/logging/replay/log-debug-client-ip-http.test.yaml similarity index 99% rename from tests/gold_tests/logging/replay/log-debug-client-ip-http.test.yaml rename to tests/uranium_tests/logging/replay/log-debug-client-ip-http.test.yaml index f92d3336d62..3b4ebefb1cb 100644 --- a/tests/gold_tests/logging/replay/log-debug-client-ip-http.test.yaml +++ b/tests/uranium_tests/logging/replay/log-debug-client-ip-http.test.yaml @@ -17,7 +17,7 @@ meta: version: "1.0" -autest: +urtest: description: 'Verify client-IP-filtered debug logging for HTTP/1.1' server: diff --git a/tests/gold_tests/logging/replay/log-debug-client-ip-http2.test.yaml b/tests/uranium_tests/logging/replay/log-debug-client-ip-http2.test.yaml similarity index 99% rename from tests/gold_tests/logging/replay/log-debug-client-ip-http2.test.yaml rename to tests/uranium_tests/logging/replay/log-debug-client-ip-http2.test.yaml index 5230a7ebeb9..95b1b205149 100644 --- a/tests/gold_tests/logging/replay/log-debug-client-ip-http2.test.yaml +++ b/tests/uranium_tests/logging/replay/log-debug-client-ip-http2.test.yaml @@ -17,7 +17,7 @@ meta: version: "1.0" -autest: +urtest: description: 'Verify client-IP-filtered debug logging for HTTP/2' server: diff --git a/tests/gold_tests/logging/replay/log-debug-client-ip-http3.test.yaml b/tests/uranium_tests/logging/replay/log-debug-client-ip-http3.test.yaml similarity index 99% rename from tests/gold_tests/logging/replay/log-debug-client-ip-http3.test.yaml rename to tests/uranium_tests/logging/replay/log-debug-client-ip-http3.test.yaml index 1a58e7f9ec0..fab8fb025c5 100644 --- a/tests/gold_tests/logging/replay/log-debug-client-ip-http3.test.yaml +++ b/tests/uranium_tests/logging/replay/log-debug-client-ip-http3.test.yaml @@ -17,7 +17,7 @@ meta: version: "1.0" -autest: +urtest: description: 'Verify client-IP-filtered debug logging for HTTP/3' requires: ats_features: [TS_HAS_QUICHE] diff --git a/tests/gold_tests/logging/replay/log-debug-client-ip-https.test.yaml b/tests/uranium_tests/logging/replay/log-debug-client-ip-https.test.yaml similarity index 99% rename from tests/gold_tests/logging/replay/log-debug-client-ip-https.test.yaml rename to tests/uranium_tests/logging/replay/log-debug-client-ip-https.test.yaml index 79e9b5aa4b7..8b565d012a6 100644 --- a/tests/gold_tests/logging/replay/log-debug-client-ip-https.test.yaml +++ b/tests/uranium_tests/logging/replay/log-debug-client-ip-https.test.yaml @@ -17,7 +17,7 @@ meta: version: "1.0" -autest: +urtest: description: 'Verify client-IP-filtered debug logging for HTTPS' server: diff --git a/tests/gold_tests/logging/sigusr2_rotate_custom_log.sh b/tests/uranium_tests/logging/sigusr2_rotate_custom_log.sh similarity index 100% rename from tests/gold_tests/logging/sigusr2_rotate_custom_log.sh rename to tests/uranium_tests/logging/sigusr2_rotate_custom_log.sh diff --git a/tests/gold_tests/logging/sigusr2_rotate_diags.sh b/tests/uranium_tests/logging/sigusr2_rotate_diags.sh similarity index 100% rename from tests/gold_tests/logging/sigusr2_rotate_diags.sh rename to tests/uranium_tests/logging/sigusr2_rotate_diags.sh diff --git a/tests/uranium_tests/logging/test_all_headers.py b/tests/uranium_tests/logging/test_all_headers.py new file mode 100644 index 00000000000..eb97d447cb1 --- /dev/null +++ b/tests/uranium_tests/logging/test_all_headers.py @@ -0,0 +1,136 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_all_headers(urtest: UraniumTest) -> None: + ''' + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import os + import sys + + urtest.Summary = ''' + Test new "all headers" log fields + ''' + + # Define ATS. + # + ts = urtest.MakeATSProcess("ts") + + # Define MicroServer. + # + server = urtest.MakeOriginServer("server") + + request_header = {"headers": "GET / HTTP/1.1\r\nHost: does.not.matter\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + response_header = { + "headers": "HTTP/1.1 200 OK\r\nConnection: close\r\nCache-control: max-age=85000\r\n\r\n", + "timestamp": "1469733493.993", + "body": "xxx" + } + server.addResponse("sessionlog.json", request_header, response_header) + + ts.Disk.records_config.update({ + 'proxy.config.diags.debug.enabled': 0, + 'proxy.config.diags.debug.tags': 'http|dns', + }) + + ts.Disk.remap_config.AddLine('map http://127.0.0.1:{0} http://127.0.0.1:{1}'.format(ts.Variables.port, server.Variables.Port)) + + # Mix in a numeric log field. Hopefull this will detect any binary alignment problems. + # + ts.Disk.logging_yaml.AddLines( + ''' + logging: + formats: + - name: custom + format: "% ___FS___ % ___FS___ % ___FS___ % ___FS___ % ___FS___ % ___FS___ END_TXN" + logs: + - filename: test_all_headers + format: custom + '''.split("\n")) + + # Configure comparison of "sanitized" log file with gold file at end of test. + # + sanitized_log_path = os.path.join(ts.Variables.LOGDIR, 'test_all_headers.log.san') + if Condition.CurlUsingUnixDomainSocket(): + urtest.Disk.File(sanitized_log_path, exists=True, content='gold/test_all_headers_uds.gold') + else: + urtest.Disk.File(sanitized_log_path, exists=True, content='gold/test_all_headers.gold') + + def reallyLong(): + value = 'abcdefghijklmnop' + value = value + value + value = value + value + value = value + value + retval = "" + for i in range(3): + retval += ' -H "x-header{}: {}"'.format(i, value) + return retval + + tr = urtest.AddTestRun() + tr.Processes.Default.StartBefore(server) + tr.Processes.Default.StartBefore(urtest.Processes.ts) + if Condition.CurlUsingUnixDomainSocket(): + tr.MakeCurlCommand( + '"http://127.0.0.1:{0}" --user-agent "007" -H "Host: 127.0.0.1:{0}" --verbose '.format(ts.Variables.port) + + reallyLong(), + ts=ts) + else: + tr.MakeCurlCommand('"http://127.0.0.1:{0}" --user-agent "007" --verbose '.format(ts.Variables.port) + reallyLong(), ts=ts) + tr.Processes.Default.ReturnCode = 0 + + # Repeat same curl, will be answered from the ATS cache. + # + tr = urtest.AddTestRun() + if Condition.CurlUsingUnixDomainSocket(): + tr.MakeCurlCommand( + '"http://127.0.0.1:{0}" --user-agent "007" -H "Host: 127.0.0.1:{0}" --verbose '.format(ts.Variables.port) + + reallyLong(), + ts=ts) + else: + tr.MakeCurlCommand('"http://127.0.0.1:{0}" --user-agent "007" --verbose '.format(ts.Variables.port) + reallyLong(), ts=ts) + tr.Processes.Default.ReturnCode = 0 + + # Delay to allow TS to flush report to disk, then "sanitize" generated log. + # + tr = urtest.AddTestRun() + + log_path = os.path.join(ts.Variables.LOGDIR, 'test_all_headers.log') + sanitizer_python_script = os.path.join(urtest.TestDirectory, 'all_headers_sanitizer.py') + sanitizer_shell_script = os.path.join(urtest.TestDirectory, 'all_headers_sanitizer.sh') + + tr.Processes.Default.Command = \ + (f'{sys.executable} {sanitizer_python_script} {log_path} {server.Variables.Port} | ' + f'sh {sanitizer_shell_script} > {sanitized_log_path}') + tr.Processes.Default.ReturnCode = 0 + urtest.execute() diff --git a/tests/uranium_tests/logging/test_binary_log_v3.py b/tests/uranium_tests/logging/test_binary_log_v3.py new file mode 100644 index 00000000000..696537311da --- /dev/null +++ b/tests/uranium_tests/logging/test_binary_log_v3.py @@ -0,0 +1,190 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_binary_log_v3(urtest: UraniumTest) -> None: + ''' + Uranium test of the self-describing v3 binary log format. + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import os + + urtest.Summary = ''' + Write a v3 binary log and read it back with traffic_logcat (ASCII + JSON), + and prove the per-LogObject binary_log_version:2 writer is still readable. + ''' + + # chi resolves to the client address; a Unix-domain-socket curl changes it. + urtest.SkipIf(Condition.CurlUsingUnixDomainSocket()) + + class BinaryLogV3Test: + ''' Drive traffic into a v3 binary log, then decode it with traffic_logcat + and compare the ASCII and JSON output against gold files. + ''' + + # A counter to give each ATS process a unique name. + __ts_counter = 1 + + # Number of transactions (and therefore log entries) to generate. + num_requests = 3 + + # One format exercising all four wire types: + # chi = client IP -> IP -> "127.0.0.1" + # cqu = request URL -> STRING -> "http://127.0.0.1:/get" + # pssc = response status -> sINT -> "200" + # sshv = resp HTTP version -> dINT -> "HTTP/1.1" (ASCII) / [1,1] (JSON) + # The dynamic listen port in cqu is masked with `` in the gold files. + log_format = '% % % %' + + def __init__(self): + # Origin returning a plain 200 over HTTP/1.1 (so sshv == HTTP/1.1). + self.httpbin = urtest.MakeHttpBinServer("httpbin") + self.ts = self.__configure_traffic_server() + self.__configure_traffic_test_run() + self.__configure_decode_test_runs() + + def __configure_traffic_server(self): + ''' Configure ATS with the ASCII, v2, and v3 binary log objects. + + Return: + The traffic_server process. + ''' + name = f"ts{BinaryLogV3Test.__ts_counter}" + BinaryLogV3Test.__ts_counter += 1 + ts = urtest.MakeATSProcess(name) + + ts.Disk.records_config.update( + { + # No caching: every request reaches the origin, so sshv is populated. + 'proxy.config.http.cache.http': 0, + # Flush quickly so the .blog is on disk shortly after the requests. + 'proxy.config.log.max_secs_per_buffer': 1, + 'proxy.config.log.periodic_tasks_interval': 1, + }) + + ts.Disk.remap_config.AddLine( + f'map http://127.0.0.1:{ts.Variables.port}/ http://127.0.0.1:{self.httpbin.Variables.Port}/') + + ts.Disk.logging_yaml.AddLines( + f''' + logging: + formats: + - name: custom_fmt + format: "{self.log_format}" + logs: + - filename: v2 + format: custom_fmt + mode: binary + binary_log_version: 2 + - filename: v3 + format: custom_fmt + mode: binary + binary_log_version: 3 + - filename: ascii + format: custom_fmt + mode: ascii + '''.split("\n")) + + logdir = ts.Variables.LOGDIR + self.v2_blog = os.path.join(logdir, 'v2.blog') + self.v3_blog = os.path.join(logdir, 'v3.blog') + self.ascii_log = os.path.join(logdir, 'ascii.log') + return ts + + def __configure_traffic_test_run(self): + ''' Generate num_requests identical origin-backed transactions. ''' + url = f'http://127.0.0.1:{self.ts.Variables.port}/get' + for i in range(BinaryLogV3Test.num_requests): + tr = urtest.AddTestRun(f'Generate binary log traffic #{i + 1}') + if i == 0: + tr.Processes.Default.StartBefore(self.httpbin) + tr.Processes.Default.StartBefore(self.ts) + tr.MakeCurlCommand(f'--http1.1 "{url}"', ts=self.ts) + tr.Processes.Default.ReturnCode = 0 + + def __configure_decode_test_runs(self): + ''' Decode the binary logs with traffic_logcat and compare to gold. ''' + logcat = os.path.join(self.ts.Variables.BINDIR, 'traffic_logcat') + + # Wait until the ASCII companion has every entry; the binary logs are + # written in the same periodic flush, so they are complete by then. + urtest.AddAwaitFileContainsTestRun('Await the logs to flush.', self.ascii_log, r'/get', BinaryLogV3Test.num_requests) + + # The binary_log_version:2 log decodes to the same ASCII as v3. + tr = urtest.AddTestRun('Decode the v2 binary log to ASCII') + tr.Processes.Default.Command = f'{logcat} {self.v2_blog}' + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.stdout = 'gold/binary_log_v3_ascii.gold' + + # traffic_logcat decodes the v3 .blog to the expected ASCII. + tr = urtest.AddTestRun('Decode the v3 binary log to ASCII') + tr.Processes.Default.Command = f'{logcat} {self.v3_blog}' + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.stdout = 'gold/binary_log_v3_ascii.gold' + + # traffic_logcat -j decodes the v3 .blog to the expected JSON. + tr = urtest.AddTestRun('Decode the v3 binary log to JSON') + tr.Processes.Default.Command = f'{logcat} -j {self.v3_blog}' + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.stdout = 'gold/binary_log_v3_json.gold' + + # traffic_logcat -H prints the v3 segment header, including the + # self-describing field-type schema, instead of decoding entries. + tr = urtest.AddTestRun('Print the v3 binary log header') + tr.Processes.Default.Command = f'{logcat} -H {self.v3_blog}' + tr.Processes.Default.ReturnCode = 0 + stdout = tr.Processes.Default.Streams.stdout + stdout += Testers.ContainsExpression(r'version:\s+3', 'The header reports segment version 3.') + stdout += Testers.ContainsExpression(r'format_type:\s+4 \(CUSTOM\)', 'The header reports the custom format type.') + stdout += Testers.ContainsExpression(r'fieldlist:\s+chi,cqu,pssc,sshv', 'The header lists the format field symbols.') + stdout += Testers.ContainsExpression(r'field_type_schema:\s+field_count=4', 'The v3 header has a 4-field type schema.') + stdout += Testers.ContainsExpression(r'chi\s+IP', 'chi is framed as an IP.') + stdout += Testers.ContainsExpression(r'cqu\s+STRING', 'cqu is framed as a STRING.') + stdout += Testers.ContainsExpression(r'pssc\s+sINT', 'pssc is framed as an sINT.') + stdout += Testers.ContainsExpression(r'sshv\s+dINT', 'sshv is framed as a dINT.') + + # traffic_logcat -H also prints the v2 header, but v2 segments carry no + # field-type schema. + tr = urtest.AddTestRun('Print the v2 binary log header') + tr.Processes.Default.Command = f'{logcat} -H {self.v2_blog}' + tr.Processes.Default.ReturnCode = 0 + stdout = tr.Processes.Default.Streams.stdout + stdout += Testers.ContainsExpression(r'version:\s+2', 'The header reports segment version 2.') + stdout += Testers.ContainsExpression(r'fieldlist:\s+chi,cqu,pssc,sshv', 'The header lists the format field symbols.') + stdout += Testers.ExcludesExpression(r'field_type_schema', 'A v2 segment has no field-type schema.') + + # + # Run the test. + # + BinaryLogV3Test() + urtest.execute() diff --git a/tests/uranium_tests/logging/test_custom_log.py b/tests/uranium_tests/logging/test_custom_log.py new file mode 100644 index 00000000000..3ff9cba882b --- /dev/null +++ b/tests/uranium_tests/logging/test_custom_log.py @@ -0,0 +1,113 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_custom_log(urtest: UraniumTest) -> None: + ''' + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import os + + urtest.Summary = ''' + Test custom log file format + ''' + + urtest.SkipIf(Condition.CurlUsingUnixDomainSocket()) + # this test depends on Linux specific behavior regarding loopback addresses + urtest.SkipUnless(Condition.IsPlatform("linux")) + + # Define default ATS + ts = urtest.MakeATSProcess("ts") + + # setup some config file for this server + ts.Disk.remap_config.AddLine('map / http://www.linkedin.com/ @action=deny') + + ts.Disk.logging_yaml.AddLines( + ''' + logging: + formats: + - name: custom + format: "% %" + logs: + - filename: test_log_field + format: custom + '''.split("\n")) + + # ######################################################################### + # at the end of the different test run a custom log file should exist + # Because of this we expect the testruns to pass the real test is if the + # customlog file exists and passes the format check + urtest.Disk.File(os.path.join(ts.Variables.LOGDIR, 'test_log_field.log'), exists=True, content='gold/custom.gold') + + # first test is a miss for default + tr = urtest.AddTestRun() + tr.MakeCurlCommand('"http://127.0.0.1:{0}" --verbose'.format(ts.Variables.port), ts=ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.StartBefore(urtest.Processes.ts) + + tr = urtest.AddTestRun() + tr.MakeCurlCommand('"http://127.1.1.1:{0}" --verbose'.format(ts.Variables.port), ts=ts) + tr.Processes.Default.ReturnCode = 0 + + tr = urtest.AddTestRun() + tr.MakeCurlCommand('"http://127.2.2.2:{0}" --verbose'.format(ts.Variables.port), ts=ts) + tr.Processes.Default.ReturnCode = 0 + + tr = urtest.AddTestRun() + tr.MakeCurlCommand('"http://127.3.3.3:{0}" --verbose'.format(ts.Variables.port), ts=ts) + tr.Processes.Default.ReturnCode = 0 + + tr = urtest.AddTestRun() + tr.MakeCurlCommand('"http://127.3.0.1:{0}" --verbose'.format(ts.Variables.port), ts=ts) + tr.Processes.Default.ReturnCode = 0 + + tr = urtest.AddTestRun() + tr.MakeCurlCommand('"http://127.43.2.1:{0}" --verbose'.format(ts.Variables.port), ts=ts) + tr.Processes.Default.ReturnCode = 0 + + tr = urtest.AddTestRun() + tr.MakeCurlCommand('"http://127.213.213.132:{0}" --verbose'.format(ts.Variables.port), ts=ts) + tr.Processes.Default.ReturnCode = 0 + + tr = urtest.AddTestRun() + tr.MakeCurlCommand('"http://127.123.32.243:{0}" --verbose'.format(ts.Variables.port), ts=ts) + tr.Processes.Default.ReturnCode = 0 + + # Wait for all expected log lines to be written. + urtest.AddAwaitFileContainsTestRun( + 'Await custom log lines.', + os.path.join(ts.Variables.LOGDIR, 'test_log_field.log'), + r'^127\.', + 8, + ) + urtest.execute() diff --git a/tests/uranium_tests/logging/test_log_field.py b/tests/uranium_tests/logging/test_log_field.py new file mode 100644 index 00000000000..bb463773558 --- /dev/null +++ b/tests/uranium_tests/logging/test_log_field.py @@ -0,0 +1,142 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_log_field(urtest: UraniumTest) -> None: + ''' + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import os + + urtest.Summary = ''' + Test log fields. + ''' + + ts = urtest.MakeATSProcess("ts", enable_cache=False) + server = urtest.MakeOriginServer("server") + + request_header = {'timestamp': 100, "headers": "GET /test-1 HTTP/1.1\r\nHost: test-1\r\n\r\n", "body": ""} + response_header = { + 'timestamp': 100, + "headers": + "HTTP/1.1 200 OK\r\nTest: 1\r\nContent-Type: application/json\r\nConnection: close\r\nContent-Type: application/json\r\nTransfer-Encoding: chunked\r\n\r\n", + "body": "Test 1" + } + server.addResponse("sessionlog.json", request_header, response_header) + server.addResponse( + "sessionlog.json", { + 'timestamp': 101, + "headers": "GET /test-2 HTTP/1.1\r\nHost: test-2\r\n\r\n", + "body": "" + }, { + 'timestamp': 101, + "headers": + "HTTP/1.1 200 OK\r\nTest: 2\r\nContent-Type: application/jason\r\nConnection: close\r\nContent-Type: application/json\r\n\r\n", + "body": "Test 2" + }) + server.addResponse( + "sessionlog.json", { + 'timestamp': 102, + "headers": "GET /test-3 HTTP/1.1\r\nHost: test-3\r\n\r\n", + "body": "" + }, { + 'timestamp': 102, + "headers": "HTTP/1.1 200 OK\r\nTest: 3\r\nConnection: close\r\nContent-Type: application/json\r\n\r\n", + "body": "Test 3" + }) + + nameserver = urtest.MakeDNServer("dns", default='127.0.0.1') + + ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'http|log', + 'proxy.config.net.connections_throttle': 100, + 'proxy.config.dns.nameservers': f"127.0.0.1:{nameserver.Variables.Port}", + 'proxy.config.dns.resolv_conf': 'NULL' + }) + # setup some config file for this server + ts.Disk.remap_config.AddLine('map / http://localhost:{}/'.format(server.Variables.Port)) + + ts.Disk.logging_yaml.AddLines( + ''' + logging: + formats: + - name: custom + format: 'Transfer-Encoding:%<{Transfer-Encoding}ssh> Content-Type:%<{Content-Type}essh> Request-ID:%<{X-Primary-Id}cqh??{X-Secondary-Id}cqh> Request-ID-Default:%<{X-Primary-Id}cqh??{X-Secondary-Id}cqh??"missing-id"> Request-ID-Or-IP:%<{X-Primary-Id}cqh??{X-Secondary-Id}cqh??chi> Remote-IP:%<{X-Remote-Ip}cqh??chi> Path-Fallback:%<{X-Path-Fallback}cqh??cqup[0:7]>' + logs: + - filename: field-test + format: custom + '''.split("\n")) + + # ######################################################################### + # at the end of the different test run a custom log file should exist + # Because of this we expect the testruns to pass the real test is if the + # customlog file exists and passes the format check + # Note that the gold file expects Chunked, not chunked, because microserver + # converts to Chunked. This is not an ATS issue, but a microserver issue. + urtest.Disk.File(os.path.join(ts.Variables.LOGDIR, 'field-test.log'), exists=True, content='gold/field-test.gold') + + # first test is a miss for default + tr = urtest.AddTestRun() + # Wait for the micro server + tr.Processes.Default.StartBefore(server) + tr.Processes.Default.StartBefore(nameserver) + # Delay on readiness of our ssl ports + tr.Processes.Default.StartBefore(urtest.Processes.ts) + + tr.MakeCurlCommand( + '--verbose --ipv4 --header "Host: test-1" --header "X-Primary-Id: primary-1" --header "X-Secondary-Id: secondary-1" --header "X-Remote-Ip: 203.0.113.10" http://127.0.0.1:{0}/test-1' + .format(ts.Variables.port), + ts=ts) + tr.Processes.Default.ReturnCode = 0 + + tr = urtest.AddTestRun() + tr.MakeCurlCommand( + '--verbose --ipv4 --header "Host: test-2" --header "X-Secondary-Id: secondary-2" http://127.0.0.1:{0}/test-2'.format( + ts.Variables.port), + ts=ts) + tr.Processes.Default.ReturnCode = 0 + + tr = urtest.AddTestRun() + tr.MakeCurlCommand('--verbose --ipv4 --header "Host: test-3" http://127.0.0.1:{0}/test-3'.format(ts.Variables.port), ts=ts) + tr.Processes.Default.ReturnCode = 0 + + # Wait for all expected log lines to be written. + urtest.AddAwaitFileContainsTestRun( + 'Await field-test log lines.', + os.path.join(ts.Variables.LOGDIR, 'field-test.log'), + r'^Transfer-Encoding:', + 3, + ) + urtest.execute() diff --git a/tests/uranium_tests/logging/test_log_field_json.py b/tests/uranium_tests/logging/test_log_field_json.py new file mode 100644 index 00000000000..7f8a1b93d65 --- /dev/null +++ b/tests/uranium_tests/logging/test_log_field_json.py @@ -0,0 +1,141 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_log_field_json(urtest: UraniumTest) -> None: + ''' + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import os + + urtest.Summary = ''' + Test log fields. + ''' + + ts = urtest.MakeATSProcess("ts", enable_cache=False) + server = urtest.MakeOriginServer("server") + + request_header = {'timestamp': 100, "headers": "GET /test-1 HTTP/1.1\r\nHost: test-1\r\n\r\n", "body": ""} + response_header = { + 'timestamp': 100, + "headers": + "HTTP/1.1 200 OK\r\nTest: 1\r\nContent-Type: application/json\r\nConnection: close\r\nContent-Type: application/json\r\n\r\n", + "body": "Test 1" + } + server.addResponse("sessionlog.json", request_header, response_header) + server.addResponse( + "sessionlog.json", { + 'timestamp': 101, + "headers": "GET /test-2 HTTP/1.1\r\nHost: test-2\r\n\r\n", + "body": "" + }, { + 'timestamp': 101, + "headers": + "HTTP/1.1 200 OK\r\nTest: 2\r\nContent-Type: application/jason\r\nConnection: close\r\nContent-Type: application/json\r\n\r\n", + "body": "Test 2" + }) + server.addResponse( + "sessionlog.json", { + 'timestamp': 102, + "headers": "GET /test-3 HTTP/1.1\r\nHost: test-3\r\n\r\n", + "body": "" + }, { + 'timestamp': 102, + "headers": "HTTP/1.1 200 OK\r\nTest: 3\r\nConnection: close\r\nContent-Type: application/json\r\n\r\n", + "body": "Test 3" + }) + + nameserver = urtest.MakeDNServer("dns", default='127.0.0.1') + + ts.Disk.records_config.update( + { + 'proxy.config.net.connections_throttle': 100, + 'proxy.config.dns.nameservers': f"127.0.0.1:{nameserver.Variables.Port}", + 'proxy.config.dns.resolv_conf': 'NULL' + }) + # setup some config file for this server + ts.Disk.remap_config.AddLine('map / http://localhost:{}/'.format(server.Variables.Port)) + + ts.Disk.logging_yaml.AddLines( + ''' + logging: + formats: + - name: custom + escape: json + format: '{"foo":"%<{Foo}cqh>","foo-slice":"%<{Foo}cqh[2:-3]>"}' + logs: + - filename: field-json-test + format: custom + '''.split("\n")) + + # ######################################################################### + # at the end of the different test run a custom log file should exist + # Because of this we expect the testruns to pass the real test is if the + # customlog file exists and passes the format check + urtest.Disk.File(os.path.join(ts.Variables.LOGDIR, 'field-json-test.log'), exists=True, content='gold/field-json-test.gold') + + # first test is a miss for default + tr = urtest.AddTestRun() + # Wait for the micro server + tr.Processes.Default.StartBefore(server) + tr.Processes.Default.StartBefore(nameserver) + # Delay on readiness of our ssl ports + tr.Processes.Default.StartBefore(urtest.Processes.ts) + + tr.MakeCurlCommand( + '--verbose --header "Host: test-1" --header "Foo: ab\td/ef" http://localhost:{0}/test-1'.format(ts.Variables.port), ts=ts) + tr.Processes.Default.ReturnCode = 0 + + tr = urtest.AddTestRun() + tr.MakeCurlCommand( + '--verbose --header "Host: test-2" --header "Foo: ab\x1fd/ef" http://localhost:{0}/test-2'.format(ts.Variables.port), ts=ts) + tr.Processes.Default.ReturnCode = 0 + + tr = urtest.AddTestRun() + tr.MakeCurlCommand( + '--verbose --header "Host: test-3" --header "Foo: abc\x7fde" http://localhost:{0}/test-3'.format(ts.Variables.port), ts=ts) + tr.Processes.Default.ReturnCode = 0 + + tr = urtest.AddTestRun() + tr.MakeCurlCommand( + '--verbose --header "Host: test-2" --header "Foo: ab\x80d/ef" http://localhost:{0}/test-4'.format(ts.Variables.port), ts=ts) + tr.Processes.Default.ReturnCode = 0 + + # Wait for all expected log lines to be written. + urtest.AddAwaitFileContainsTestRun( + 'Await field-json log lines.', + os.path.join(ts.Variables.LOGDIR, 'field-json-test.log'), + r'^\{"foo":', + 4, + ) + urtest.execute() diff --git a/tests/uranium_tests/logging/test_log_filenames.py b/tests/uranium_tests/logging/test_log_filenames.py new file mode 100644 index 00000000000..fb7596d8029 --- /dev/null +++ b/tests/uranium_tests/logging/test_log_filenames.py @@ -0,0 +1,277 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_log_filenames(urtest: UraniumTest) -> None: + ''' + Verify log file naming behavior. + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import os + import ports + + urtest.Summary = ''' + Verify log file naming behavior. + ''' + + class LogFilenamesTest: + """ Common test configuration logic across the filename tests. + """ + + # A counter for the ATS process to make each of them unique. + __ts_counter = 1 + + # The default log names for the various system logs. + default_log_data = {'diags': 'diags.log', 'error': 'error.log'} + + def __init__(self, description, log_data=default_log_data): + ''' Handle initialization tasks common across the tests. + + Args: + description (str): The description of the test. This is passed to + the TestRun. + + log_data (dict): The log name information passed to the + MakeATSProcess extension. + ''' + self.__description = description + self.ts = self.__configure_traffic_server(log_data) + self.tr = self.__configure_traffic_TestRun(description) + self.__configure_await_TestRun(self.sentinel_log_path) + + def __configure_traffic_server(self, log_data): + ''' Common ATS configuration logic. + + Args: + log_data (dict): The log name information passed to the + MakeATSProcess extension. + + Return: + The traffic_server process. + ''' + self._ts_name = f"ts{LogFilenamesTest.__ts_counter}" + LogFilenamesTest.__ts_counter += 1 + self.ts = urtest.MakeATSProcess(self._ts_name, use_traffic_out=False, log_data=log_data) + self.ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 0, + 'proxy.config.diags.debug.tags': 'log', + 'proxy.config.log.periodic_tasks_interval': 1, + }) + + # Intentionally retrieve a port that is closed, that is no server is + # listening on it. We will use this to attempt talking with a + # non-existent server, which will result in an error log entry. + ports.get_port(self.ts, 'closed_port') + self.ts.Disk.remap_config.AddLines( + [ + f'map /server/down http://127.0.0.1:{self.ts.Variables.closed_port}', + 'map / https://trafficserver.apache.org @action=deny', + ]) + + # The following log is configured so that we can wait upon it being + # written so we know that ATS is done writing logs. + self.sentinel_log_filename = "sentinel" + self.ts.Disk.logging_yaml.AddLine( + f''' + logging: + formats: + - name: url_and_return_code + format: "%: %" + logs: + - filename: {self.sentinel_log_filename} + format: url_and_return_code + ''') + + self.sentinel_log_path = os.path.join(self.ts.Variables.LOGDIR, f"{self.sentinel_log_filename}.log") + + return self.ts + + def __configure_await_TestRun(self, log_path): + ''' Configure a TestRun that awaits upon the provided log_path to + contain the sentinel log entry. + + Args: + log_path (str): The log file upon which we will wait. + ''' + description = self.__description + urtest.AddAwaitFileContainsTestRun( + f'Awaiting log files to be written for: {description}', + log_path, + r'^http://127\.0\.0\.1:\d+/: 502$', + ) + + def __configure_traffic_TestRun(self, description): + ''' Configure a TestRun to run the expected transactions. + + Args: + description (str): The description to use for the TestRun. + ''' + tr = urtest.AddTestRun(f'Run traffic for: {description}') + tr.MakeCurlCommand( + f'http://127.0.0.1:{self.ts.Variables.port}/some/path --verbose --next ' + f'http://127.0.0.1:{self.ts.Variables.port}/server/down --verbose', + ts=self.ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.StartBefore(self.ts) + + def configure_named_custom_log(self, custom_log_filename): + """ Configure ATS to log to the custom log file via logging.yaml. + + Args: + custom_log_filename (str): The name of the custom log file to + configure. + + Return: + The path to the configured custom log file. + """ + self.custom_log_filename = custom_log_filename + self.ts.Disk.logging_yaml.AddLine( + f''' + - filename: {custom_log_filename} + format: url_and_return_code + ''') + + if custom_log_filename in ('stdout', 'stderr'): + self.custom_log_path = custom_log_filename + if custom_log_filename == 'stdout': + self.ts.Disk.custom_log = self.ts.Streams.stdout + else: + self.ts.Disk.custom_log = self.ts.Streams.stderr + else: + self.custom_log_path = os.path.join(self.ts.Variables.LOGDIR, f"{custom_log_filename}.log") + self.ts.Disk.File(self.custom_log_path, id="custom_log") + return self.custom_log_path + + def set_log_expectations(self): + ''' Configure sanity checks for each of the log types (diags, error, + etc.) to verify they are emitting the expected content. + ''' + + diags_path = self.ts.Disk.diags_log.AbsPath + self.ts.Disk.diags_log.Content += Testers.ContainsExpression( + "logging.yaml finished loading", f"{diags_path} should contain traffic_server diag messages") + + error_log_path = self.ts.Disk.error_log.AbsPath + self.ts.Disk.error_log.Content += Testers.ContainsExpression( + "CONNECT: attempt fail", f"{error_log_path} should contain connection error messages") + + custom_log_path = self.ts.Disk.custom_log.AbsPath + self.ts.Disk.custom_log.Content += Testers.ContainsExpression( + "https://trafficserver.apache.org/some/path: 403", f"{custom_log_path} should contain the custom transaction logs") + + class DefaultNamedTest(LogFilenamesTest): + ''' Verify that if custom names are not configured, then the default + 'diags.log' and 'error.log' are written to. + ''' + + def __init__(self): + super().__init__('default log filename configuration') + + self.configure_named_custom_log('my_custom_log') + self.set_log_expectations() + + class CustomNamedTest(LogFilenamesTest): + ''' Verify that the user can assign custom filenames to diags.log, etc. + ''' + + def __init__(self): + log_data = {'diags': 'my_diags.log', 'error': 'my_error.log'} + super().__init__('specify log filename configuration', log_data) + + # Configure custom names for diags.log, etc. + self.ts.Disk.records_config.update( + { + 'proxy.config.diags.logfile.filename': 'my_diags.log', + 'proxy.config.error.logfile.filename': 'my_error.log', + }) + + self.configure_named_custom_log('my_custom_log') + self.set_log_expectations() + + class stdoutTest(LogFilenamesTest): + ''' Verify that we can configure the logs to go to stdout. + ''' + + def __init__(self): + + log_data = {'diags': 'stdout', 'error': 'stdout'} + super().__init__('specify logs to go to stdout', log_data) + + # Configure custom names for diags.log, etc. + self.ts.Disk.records_config.update( + { + 'proxy.config.diags.logfile.filename': 'stdout', + 'proxy.config.error.logfile.filename': 'stdout', + }) + + self.configure_named_custom_log('stdout') + + # The diags.log file will not be created since we are piping to stdout. + # Therefore, simply wait upon the port being open. + self.ts.Ready = When.PortOpen(self.ts.Variables.port) + self.set_log_expectations() + + class stderrTest(LogFilenamesTest): + ''' + Verify that we can configure the logs to go to stderr. + ''' + + def __init__(self): + + log_data = {'diags': 'stderr', 'error': 'stderr'} + super().__init__('specify logs to go to stderr', log_data) + + # Configure custom names for diags.log, etc. + self.ts.Disk.records_config.update( + { + 'proxy.config.diags.logfile.filename': 'stderr', + 'proxy.config.error.logfile.filename': 'stderr', + }) + + self.configure_named_custom_log('stderr') + + # The diags.log file will not be created since we are piping to stderr. + # Therefore, simply wait upon the port being open. + self.ts.Ready = When.PortOpen(self.ts.Variables.port) + self.set_log_expectations() + + # + # Run the tests. + # + DefaultNamedTest() + CustomNamedTest() + stdoutTest() + + stderrTest() + urtest.execute() diff --git a/tests/uranium_tests/logging/test_log_filter.py b/tests/uranium_tests/logging/test_log_filter.py new file mode 100644 index 00000000000..1b8f7f49e19 --- /dev/null +++ b/tests/uranium_tests/logging/test_log_filter.py @@ -0,0 +1,121 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_log_filter(urtest: UraniumTest) -> None: + ''' + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import os + + urtest.Summary = ''' + Test log filter. + ''' + + ts = urtest.MakeATSProcess("ts", enable_cache=False) + replay_file = "log-filter.replays.yaml" + server = urtest.MakeVerifierServerProcess("server", replay_file) + nameserver = urtest.MakeDNServer("dns", default='127.0.0.1') + + ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'log', + 'proxy.config.net.connections_throttle': 100, + 'proxy.config.dns.nameservers': f"127.0.0.1:{nameserver.Variables.Port}", + 'proxy.config.dns.resolv_conf': 'NULL' + }) + # setup some config file for this server + ts.Disk.remap_config.AddLine('map / http://localhost:{}/'.format(server.Variables.http_port)) + + ts.Disk.logging_yaml.AddLines( + ''' + logging: + filters: + - name: only_localhost + action: accept + condition: chi MATCH 127.0.0.1 + + - name: not_localhost + action: accept + condition: chi MATCH 3.3.3.3 + + - name: queryparamescaper_cquuc + action: WIPE_FIELD_VALUE + condition: cquuc CASE_INSENSITIVE_CONTAIN password,secret,access_token,session_redirect,cardNumber,code,query,search-query,prefix,keywords,email,handle + + formats: + - name: custom + format: '%' + + logs: + - filename: filter-test + format: custom + filters: + - queryparamescaper_cquuc + - only_localhost + + - filename: should-not-be-written + format: custom + filters: + - queryparamescaper_cquuc + - not_localhost + '''.split("\n")) + + # ######################################################################### + # at the end of the different test run a custom log file should exist + # Because of this we expect the testruns to pass the real test is if the + # customlog file exists and passes the format check + urtest.Disk.File(os.path.join(ts.Variables.LOGDIR, 'filter-test.log'), exists=True, content='gold/filter-test.gold') + + tr = urtest.AddTestRun() + tr.Processes.Default.StartBefore(server) + tr.Processes.Default.StartBefore(nameserver) + tr.Processes.Default.StartBefore(ts) + tr.AddVerifierClientProcess("client-1", replay_file, http_ports=[ts.Variables.port]) + + # Wait for all expected log lines to be written. + urtest.AddAwaitFileContainsTestRun( + 'Await filtered log lines.', + os.path.join(ts.Variables.LOGDIR, 'filter-test.log'), + r'^http://example\.com/test-', + 5, + ) + + # We already waited for the above, so we don't have to wait for this one. + test_run = urtest.AddTestRun() + test_run.Processes.Default.Command = ( + os.path.join(urtest.Variables.AtsTestToolsDir, 'condwait') + ' 1 1 -f ' + + os.path.join(ts.Variables.LOGDIR, 'should-not-be-written.log')) + test_run.Processes.Default.ReturnCode = 1 + urtest.execute() diff --git a/tests/uranium_tests/logging/test_log_milestone_fields.py b/tests/uranium_tests/logging/test_log_milestone_fields.py new file mode 100644 index 00000000000..bfd7129d7b5 --- /dev/null +++ b/tests/uranium_tests/logging/test_log_milestone_fields.py @@ -0,0 +1,195 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_log_milestone_fields(urtest: UraniumTest) -> None: + ''' + Verify that msdms milestone difference fields produce valid output for both + cache-miss and cache-hit transactions. This exercises the Phase 1 timing + fields proposed for the squid.log local_disk format. + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import os + + urtest.Summary = 'Verify msdms milestone logging fields for cache miss and cache hit paths' + urtest.ContinueOnFail = True + + class MilestoneFieldsTest: + """ + Sends two requests for the same cacheable URL: the first is a cache miss + that populates the cache, the second is a cache hit served from RAM cache. + A custom log format records all Phase 1 milestone timing fields plus the + cache result code. A validation script then parses the log and checks: + + - Every expected key=value pair is present on each line + - All values are integers, with "-" for unset milestones + - Cache miss line: ms > 0, origin-phase fields present + - Cache hit line: hit_proc >= 0 and hit_xfer >= 0 + - No epoch-length garbage values (> 1_000_000_000) + """ + + # All Phase 1 msdms fields plus ms, cache result code, and cache key hash. + LOG_FORMAT = ( + 'crc=% ckh=% ms=%' + ' c_ttfb=%<{TS_MILESTONE_UA_BEGIN_WRITE-TS_MILESTONE_SM_START}msdms>' + ' c_tls=%<{TS_MILESTONE_TLS_HANDSHAKE_END-TS_MILESTONE_TLS_HANDSHAKE_START}msdms>' + ' c_hdr=%<{TS_MILESTONE_UA_READ_HEADER_DONE-TS_MILESTONE_SM_START}msdms>' + ' c_proc=%<{TS_MILESTONE_CACHE_OPEN_READ_BEGIN-TS_MILESTONE_UA_READ_HEADER_DONE}msdms>' + ' cache=%<{TS_MILESTONE_CACHE_OPEN_READ_END-TS_MILESTONE_CACHE_OPEN_READ_BEGIN}msdms>' + ' dns=%<{TS_MILESTONE_SERVER_FIRST_CONNECT-TS_MILESTONE_CACHE_OPEN_READ_END}msdms>' + ' o_tcp=%<{TS_MILESTONE_SERVER_CONNECT_END-TS_MILESTONE_SERVER_FIRST_CONNECT}msdms>' + ' o_wait=%<{TS_MILESTONE_SERVER_FIRST_READ-TS_MILESTONE_SERVER_CONNECT_END}msdms>' + ' o_hdr=%<{TS_MILESTONE_SERVER_READ_HEADER_DONE-TS_MILESTONE_SERVER_FIRST_READ}msdms>' + ' o_proc=%<{TS_MILESTONE_UA_BEGIN_WRITE-TS_MILESTONE_SERVER_READ_HEADER_DONE}msdms>' + ' o_body=%<{TS_MILESTONE_SERVER_CLOSE-TS_MILESTONE_UA_BEGIN_WRITE}msdms>' + ' c_xfer=%<{TS_MILESTONE_SM_FINISH-TS_MILESTONE_SERVER_CLOSE}msdms>' + ' hit_proc=%<{TS_MILESTONE_UA_BEGIN_WRITE-TS_MILESTONE_CACHE_OPEN_READ_END}msdms>' + ' hit_xfer=%<{TS_MILESTONE_SM_FINISH-TS_MILESTONE_UA_BEGIN_WRITE}msdms>') + + def __init__(self): + self._server = urtest.MakeOriginServer("server") + self._nameserver = urtest.MakeDNServer("dns", default='127.0.0.1') + self._setupOriginServer() + self._setupTS() + + def _setupOriginServer(self): + self._server.addResponse( + "sessionlog.json", + { + 'timestamp': 100, + "headers": "GET /cacheable HTTP/1.1\r\nHost: example.com\r\n\r\n", + "body": "", + }, + { + 'timestamp': 100, + "headers": + ( + "HTTP/1.1 200 OK\r\n" + "Content-Type: text/plain\r\n" + "Cache-Control: max-age=300\r\n" + "Connection: close\r\n" + "\r\n"), + "body": "This is a cacheable response body for milestone testing.", + }, + ) + + def _setupTS(self): + self._ts = urtest.MakeATSProcess("ts", enable_cache=True) + + self._ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'http|log', + 'proxy.config.dns.nameservers': f'127.0.0.1:{self._nameserver.Variables.Port}', + 'proxy.config.dns.resolv_conf': 'NULL', + 'proxy.config.http.cache.http': 1, + 'proxy.config.log.max_secs_per_buffer': 1, + }) + + self._ts.Disk.remap_config.AddLine(f'map / http://127.0.0.1:{self._server.Variables.Port}/') + + self._ts.Disk.logging_yaml.AddLines( + f''' + logging: + formats: + - name: milestone_test + format: '{self.LOG_FORMAT}' + logs: + - filename: milestone_fields + format: milestone_test + mode: ascii + '''.split("\n")) + + @property + def _log_path(self) -> str: + return os.path.join(self._ts.Variables.LOGDIR, 'milestone_fields.log') + + @property + def _validate_script(self) -> str: + return os.path.join(urtest.TestDirectory, 'verify_milestone_fields.py') + + def run(self): + self._sendCacheMiss() + self._waitForCacheIO() + self._sendCacheHit() + self._waitForLog() + self._validateLog() + + def _sendCacheMiss(self): + tr = urtest.AddTestRun('Cache miss request') + tr.Processes.Default.StartBefore(self._server) + tr.Processes.Default.StartBefore(self._nameserver) + tr.Processes.Default.StartBefore(self._ts) + tr.MakeCurlCommand( + f'--verbose --header "Host: example.com" ' + f'http://127.0.0.1:{self._ts.Variables.port}/cacheable', ts=self._ts) + tr.Processes.Default.ReturnCode = 0 + tr.StillRunningAfter = self._server + tr.StillRunningAfter = self._ts + + def _waitForCacheIO(self): + tr = urtest.AddTestRun('Wait for cache write to complete') + tr.Processes.Default.Command = 'sleep 1' + tr.Processes.Default.ReturnCode = 0 + tr.StillRunningAfter = self._server + tr.StillRunningAfter = self._ts + + def _sendCacheHit(self): + tr = urtest.AddTestRun('Cache hit request') + tr.MakeCurlCommand( + f'--verbose --header "Host: example.com" ' + f'http://127.0.0.1:{self._ts.Variables.port}/cacheable', ts=self._ts) + tr.Processes.Default.ReturnCode = 0 + tr.StillRunningAfter = self._server + tr.StillRunningAfter = self._ts + + def _waitForLog(self): + tr = urtest.AddAwaitFileContainsTestRun( + 'Wait for milestone log lines to be written', + self._log_path, + r'^crc=.* hit_xfer=', + desired_count=2, + ) + tr.StillRunningAfter = self._server + tr.StillRunningAfter = self._ts + + def _validateLog(self): + tr = urtest.AddTestRun('Validate milestone fields in log') + tr.Processes.Default.Command = f'python3 {self._validate_script} {self._log_path}' + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.TimeOut = 10 + tr.Processes.Default.Streams.stdout += Testers.ContainsExpression('PASS', 'Validation script should report PASS') + tr.Processes.Default.Streams.stdout += Testers.ExcludesExpression('FAIL', 'Validation script should not report FAIL') + + MilestoneFieldsTest().run() + urtest.execute() diff --git a/tests/uranium_tests/logging/test_log_mstsms.py b/tests/uranium_tests/logging/test_log_mstsms.py new file mode 100644 index 00000000000..8de2e35c19c --- /dev/null +++ b/tests/uranium_tests/logging/test_log_mstsms.py @@ -0,0 +1,138 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_log_mstsms(urtest: UraniumTest) -> None: + ''' + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import os + + urtest.Summary = ''' + Test log fields. + ''' + + ts = urtest.MakeATSProcess("ts", enable_cache=True) + server = urtest.MakeOriginServer("server") + + request_header = {'timestamp': 100, "headers": "GET /test-1 HTTP/1.1\r\nHost: test-1\r\n\r\n", "body": ""} + response_header = { + 'timestamp': 100, + "headers": + "HTTP/1.1 200 OK\r\nTest: 1\r\nContent-Type: application/json\r\nConnection: close\r\nContent-Type: application/json\r\nTransfer-Encoding: chunked\r\n\r\n", + "body": "Test 1" + } + server.addResponse("sessionlog.json", request_header, response_header) + server.addResponse( + "sessionlog.json", { + 'timestamp': 101, + "headers": "GET /test-2 HTTP/1.1\r\nHost: test-2\r\n\r\n", + "body": "" + }, { + 'timestamp': 101, + "headers": + "HTTP/1.1 200 OK\r\nTest: 2\r\nContent-Type: application/jason\r\nConnection: close\r\nContent-Type: application/json\r\n\r\n", + "body": "Test 2" + }) + server.addResponse( + "sessionlog.json", { + 'timestamp': 102, + "headers": "GET /test-3 HTTP/1.1\r\nHost: test-3\r\n\r\n", + "body": "" + }, { + 'timestamp': 102, + "headers": "HTTP/1.1 200 OK\r\nTest: 3\r\nConnection: close\r\nContent-Type: application/json\r\n\r\n", + "body": "Test 3" + }) + + nameserver = urtest.MakeDNServer("dns", default='127.0.0.1') + + ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'http|log', + # 'proxy.config.net.connections_throttle': 100, + 'proxy.config.dns.nameservers': f"127.0.0.1:{nameserver.Variables.Port}", + 'proxy.config.dns.resolv_conf': 'NULL', + }) + # setup some config file for this server + ts.Disk.remap_config.AddLine('map / http://localhost:{}/'.format(server.Variables.Port)) + + ts.Disk.logging_yaml.AddLines( + ''' + logging: + formats: + - name: custom + format: 'mstsms:%' + logs: + - filename: field-mstsms + format: custom + '''.split("\n")) + + # first test is a miss for default + tr = urtest.AddTestRun() + # Wait for the micro server + tr.Processes.Default.StartBefore(server) + tr.Processes.Default.StartBefore(nameserver) + # Delay on readiness of our ssl ports + tr.Processes.Default.StartBefore(urtest.Processes.ts) + + tr.MakeCurlCommand('--verbose --header "Host: test-1" http://localhost:{0}/test-1'.format(ts.Variables.port), ts=ts) + tr.Processes.Default.ReturnCode = 0 + + tr = urtest.AddTestRun() + tr.MakeCurlCommand('--verbose --header "Host: test-2" http://localhost:{0}/test-2'.format(ts.Variables.port), ts=ts) + tr.Processes.Default.ReturnCode = 0 + + tr = urtest.AddTestRun() + tr.MakeCurlCommand('--verbose --header "Host: test-3" http://localhost:{0}/test-3'.format(ts.Variables.port), ts=ts) + tr.Processes.Default.ReturnCode = 0 + + # check comma count and ensure last character is a digit + def check_lines(path): + with open(path, 'r') as file: + for line_num, line in enumerate(file, 1): + line = line.rstrip('\n') + comma_count = line.count(',') + if comma_count != 19: + return False, "Check comma count", f"Expected 19 commas, got {comma_count}" + if not line[-1].isdigit(): + return False, "Check last char", f"Expected last character to be a digit got '{line[-1]}'" + return True, "", "" + + logpath = os.path.join(ts.Variables.LOGDIR, 'field-mstsms.log') + + # Wait for all expected log lines to be written. + tr = urtest.AddAwaitFileContainsTestRun('Await mstsms log lines.', logpath, r'^mstsms:', 3) + tr.Streams.All.Content = Testers.Lambda(lambda info, tester: check_lines(logpath)) + urtest.execute() diff --git a/tests/uranium_tests/logging/test_log_pipe.py b/tests/uranium_tests/logging/test_log_pipe.py new file mode 100644 index 00000000000..34e033bd03a --- /dev/null +++ b/tests/uranium_tests/logging/test_log_pipe.py @@ -0,0 +1,206 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_log_pipe(urtest: UraniumTest) -> None: + ''' + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import os + import sys + + urtest.Summary = ''' + Test custom log file format + ''' + urtest.SkipUnless(Condition.HasATSFeature('TS_HAS_PIPE_BUFFER_SIZE_CONFIG')) + + ts_counter = 1 + + def get_ts(logging_config, disable_log_checks=False): + """ + Create a Traffic Server process. + + :param disable_log_checks: Whether to disable the default diagnostics log + checks on systems where the kernel may reject increasing the pipe + buffer size for an unprivileged Traffic Server process. + """ + nonlocal ts_counter + ts = urtest.MakeATSProcess("ts{}".format(ts_counter), disable_log_checks=disable_log_checks) + ts_counter += 1 + + ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'log-file', + 'proxy.config.log.max_secs_per_buffer': 1, + }) + + # Since we're only verifying logs and not traffic, we don't need an origin + # server. The following will simply deny the requests and emit a log + # message. + ts.Disk.remap_config.AddLine('map / http://www.linkedin.com/ @action=deny') + + ts.Disk.logging_yaml.AddLines(logging_config) + + return ts + + # + # Test 1: Default configured log pipe size. + # + tr = urtest.AddTestRun() + pipe_name = "default_pipe_size.pipe" + ts = get_ts( + ''' + logging: + formats: + - name: custom + format: "% %" + logs: + - filename: '{}' + mode: ascii_pipe + format: custom + '''.format(pipe_name).split("\n")) + + pipe_path = os.path.join(ts.Variables.LOGDIR, pipe_name) + + ts.Disk.traffic_out.Content += Testers.ContainsExpression( + "Created named pipe .*{}".format(pipe_name), "Verify that the named pipe was created") + + ts.Disk.traffic_out.Content += Testers.ContainsExpression( + "no readers for pipe .*{}".format(pipe_name), "Verify that no readers for the pipe was detected.") + + ts.Disk.traffic_out.Content += Testers.ExcludesExpression( + "New buffer size for pipe".format(pipe_name), "Verify that the default pipe size was used.") + + curl = tr.Processes.Process("client_request", 'curl "http://127.0.0.1:{0}" --verbose'.format(ts.Variables.port)) + + reader_output = os.path.join(ts.Variables.LOGDIR, "reader_output") + pipe_reader = tr.Processes.Process("pipe_reader", 'cat {} | tee {}'.format(pipe_path, reader_output)) + + # Create an arbitrary process that just sleeps so that we can provide a wait + # condition upon the log being emitted. The test won't wait this entire sleep + # period, it will only poll until the FileContains Ready condition is + # fulfilled. + wait_for_log = tr.Processes.Process("wait_for_log", 'sleep 15') + wait_for_log.Ready = When.FileContains(reader_output, '127.0.0.1') + + # This is an arbitrary Default process that will simply provide for + # ordering of the Processes. + tr.Processes.Default.Command = "echo 'Default place holder for process ordering.'" + tr.Processes.Default.Return = 0 + + # Process ordering. + tr.Processes.Default.StartBefore(wait_for_log) + wait_for_log.StartBefore(curl) + curl.StartBefore(pipe_reader) + pipe_reader.StartBefore(ts) + + # + # Test 2: Change the log's buffer size. + # + tr = urtest.AddTestRun() + pipe_name = "change_pipe_size.pipe" + # 64 KB is the default, so set the size larger than that to verify we can + # increase the size. + pipe_size = 75000 + ts = get_ts( + ''' + logging: + formats: + - name: custom + format: "% %" + logs: + - filename: '{}' + mode: ascii_pipe + format: custom + pipe_buffer_size: {} + '''.format(pipe_name, pipe_size).split("\n"), + disable_log_checks=True) + + pipe_path = os.path.join(ts.Variables.LOGDIR, pipe_name) + + ts.Disk.diags_log.Content += Testers.ExcludesExpression( + r"ERROR:(?! Set pipe size failed for pipe .*: Operation not permitted)", + "The diagnostics should contain no unexpected errors.") + ts.Disk.diags_log.Content += Testers.ExcludesExpression("FATAL:", "The diagnostics should contain no fatal errors.") + + ts.Disk.traffic_out.Content += Testers.ContainsExpression( + "Created named pipe .*{}".format(pipe_name), "Verify that the named pipe was created") + + ts.Disk.traffic_out.Content += Testers.ContainsExpression( + "no readers for pipe .*{}".format(pipe_name), "Verify that no readers for the pipe was detected.") + + ts.Disk.traffic_out.Content += Testers.ContainsExpression( + "Previous buffer size for pipe .*{}".format(pipe_name), "Verify that the named pipe's size was adjusted") + + # See fcntl: + # "Attempts to set the pipe capacity below the page size + # are silently rounded up to the page size." + # + # As a result of this, we cannot check that the pipe size is the exact size we + # requested, but it should be at least that big. We use the + # pipe_buffer_is_larger_than.py helper script to verify that the pipe grew in + # size. + ts.Disk.traffic_out.Content += Testers.ContainsExpression( + "New buffer size for pipe.*{}".format(pipe_name), "Verify that the named pipe's size was adjusted") + buffer_verifier = "pipe_buffer_is_larger_than.py" + tr.Setup.Copy(buffer_verifier) + verify_buffer_size = tr.Processes.Process( + "verify_buffer_size", f"{sys.executable} {buffer_verifier} {pipe_path} {pipe_size} {ts.Disk.diags_log.AbsPath}") + verify_buffer_size.Return = 0 + verify_buffer_size.Streams.All += Testers.ContainsExpression("Success", "The buffer size verifier should report success.") + + curl = tr.Processes.Process("client_request", 'curl "http://127.0.0.1:{0}" --verbose'.format(ts.Variables.port)) + + reader_output = os.path.join(ts.Variables.LOGDIR, "reader_output") + pipe_reader = tr.Processes.Process("pipe_reader", 'cat {} | tee {}'.format(pipe_path, reader_output)) + + # Create an arbitrary process that just sleeps so that we can provide a wait + # condition upon the log being emitted. The test won't wait this entire sleep + # period, it will only poll until the FileContains Ready condition is + # fulfilled. + wait_for_log = tr.Processes.Process("wait_for_log", 'sleep 15') + wait_for_log.Ready = When.FileContains(reader_output, '127.0.0.1') + + # This is an arbitrary Default process that will simply provide for + # ordering of the Processes. + tr.Processes.Default.Command = "echo 'Default place holder for process ordering.'" + tr.Processes.Default.Return = 0 + + # Process ordering. + tr.Processes.Default.StartBefore(verify_buffer_size) + verify_buffer_size.StartBefore(wait_for_log) + wait_for_log.StartBefore(curl) + curl.StartBefore(pipe_reader) + pipe_reader.StartBefore(ts) + urtest.execute() diff --git a/tests/uranium_tests/logging/test_log_plugin_init.py b/tests/uranium_tests/logging/test_log_plugin_init.py new file mode 100644 index 00000000000..9bd715a603a --- /dev/null +++ b/tests/uranium_tests/logging/test_log_plugin_init.py @@ -0,0 +1,59 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_log_plugin_init(urtest: UraniumTest) -> None: + ''' + Verify text logging during plugin initialization. + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import os + + urtest.Summary = ''' + Verify plugins can fill a text log buffer before logging threads start. + ''' + + ts = urtest.MakeATSProcess('ts') + ts.Disk.records_config.update({'proxy.config.log.log_buffer_size': 9216}) + urtest.PrepareTestPlugin(os.path.join(urtest.Variables.AtsTestPluginsDir, 'test_log_interface.so'), ts, '--write-during-init') + + plugin_log = urtest.Disk.File(os.path.join(ts.Variables.LOGDIR, 'test_log_interface.log'), exists=True) + plugin_log.Content = Testers.ContainsExpression( + 'Writing during plugin initialization', 'The pre-initialization log buffer should be flushed') + + tr = urtest.AddTestRun('Start ATS with a plugin that fills a text log buffer during initialization') + tr.Processes.Default.Command = 'printf "traffic_server remained running"' + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.StartBefore(ts) + tr.StillRunningAfter = ts + urtest.execute() diff --git a/tests/uranium_tests/logging/test_log_retention.py b/tests/uranium_tests/logging/test_log_retention.py new file mode 100644 index 00000000000..921bd2e5c66 --- /dev/null +++ b/tests/uranium_tests/logging/test_log_retention.py @@ -0,0 +1,508 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_log_retention(urtest: UraniumTest) -> None: + ''' + Verify correct log retention behavior. + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import os + import socket + + urtest.Summary = ''' + Test the enforcement of proxy.config.log.max_space_mb_for_logs. + ''' + + # This test is sensitive to timing issues, especially in the OS CI for some + # reason. We'll leave the test here because it is helpful for when doing + # development on the log rotate code, but make it generally skipped when the + # suite of AuTests are run so it doesn't generate annoying false negatives. + urtest.SkipIf(Condition.true("This test is sensitive to timing issues which makes it flaky.")) + + class TestLogRetention: + __base_records_config = { + # Do not accept connections from clients until cache subsystem is operational. + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'logspace', + + # Enable log rotation and auto-deletion, the subjects of this test. + 'proxy.config.log.rolling_enabled': 3, + 'proxy.config.log.auto_delete_rolled_files': 1, + + # 10 MB is the minimum rolling size. + 'proxy.config.log.rolling_size_mb': 10, + 'proxy.config.log.periodic_tasks_interval': 1, + } + + __server = None + __ts_counter = 0 + __server_is_started = False + + def __init__(self, records_config, run_description, command="traffic_server"): + """ + Create a TestLogRetention instance. + """ + self.server = TestLogRetention.__create_server() + self.ts = self.__create_ts(records_config, command) + self.__initialize_processes() + self.tr = urtest.AddTestRun(run_description) + + def __initialize_processes(self): + """ + Create a run to initialize the server and traffic_server processes so + the caller doesn't have to. + """ + tr = urtest.AddTestRun("Initialize processes for ts{}".format(TestLogRetention.__ts_counter - 1)) + tr.MakeCurlCommand(self.get_curl_command(), ts=self.ts) + tr.Processes.Default.ReturnCode = 0 + if not TestLogRetention.__server_is_started: + self.server.StartBefore(self.ts) + tr.Processes.Default.StartBefore(self.server) + TestLogRetention.__server_is_started = True + else: + tr.Processes.Default.StartBefore(self.ts) + + tr.StillRunningAfter = self.ts + tr.StillRunningAfter = self.server + + @classmethod + def __create_server(cls): + """ + Create and return a server process. + + There is only one server process for all the tests. This function is + re-entrant, but subsequent calls to it will return the cached version + of the single server. + """ + if cls.__server: + return cls.__server + + server = urtest.MakeOriginServer("server") + request_header = { + "headers": "GET / HTTP/1.1\r\n" + "Host: does.not.matter\r\n\r\n", + "timestamp": "1469733493.993", + "body": "" + } + response_header = { + "headers": "HTTP/1.1 200 OK\r\n" + "Connection: close\r\n" + "Cache-control: max-age=85000\r\n\r\n", + "timestamp": "1469733493.993", + "body": "xxx" + } + server.addResponse("sessionlog.json", request_header, response_header) + cls.__server = server + return cls.__server + + def __create_ts(self, records_config, command="traffic_server"): + """ + Create an ATS process. + + records_config: records_config values for this test. + command: The ATS process to run for the test. + """ + ts_name = "ts{counter}".format(counter=TestLogRetention.__ts_counter) + TestLogRetention.__ts_counter += 1 + self.ts = urtest.MakeATSProcess(ts_name, command=command) + + combined_records_config = TestLogRetention.__base_records_config.copy() + combined_records_config.update(records_config) + self.ts.Disk.records_config.update(combined_records_config) + + self.ts.Disk.remap_config.AddLine( + 'map http://127.0.0.1:{0} http://127.0.0.1:{1}'.format(self.ts.Variables.port, self.server.Variables.Port)) + return self.ts + + def get_curl_command(self): + """ + Generate the appropriate single curl command. + """ + return '"http://127.0.0.1:{0}" --verbose'.format(self.ts.Variables.port) + + def get_command_to_rotate_once(self): + """ + Generate the set of curl commands to trigger a log rotate. + """ + return 'for i in {{{{1..2500}}}}; do {{curl}} "http://127.0.0.1:{0}" --verbose; done'.format(self.ts.Variables.port) + + def get_command_to_rotate_thrice(self): + """ + Generate the set of curl commands to trigger a log rotate. + """ + return 'for i in {{{{1..7500}}}}; do {{curl}} "http://127.0.0.1:{0}" --verbose; done'.format(self.ts.Variables.port) + + # + # Test 0: Verify that log deletion happens when no min_count is specified. + # + specified_hostname = 'my_hostname' + twelve_meg_log_space = { + # The following configures a 12 MB log cap with a required 2 MB head room. + # Thus the rotated log of just over 10 MB should be deleted because it + # will not leave enough head room. + 'proxy.config.log.max_space_mb_headroom': 2, + 'proxy.config.log.max_space_mb_for_logs': 12, + # Verify that setting a hostname changes the hostname used in rolled logs. + 'proxy.config.log.hostname': specified_hostname, + } + test = TestLogRetention(twelve_meg_log_space, "Verify log rotation and deletion of the configured log file with no min_count.") + + # Configure approximately 5 KB entries for a log with no specified min_count. + test.ts.Disk.logging_yaml.AddLines( + ''' + logging: + formats: + - name: long + format: "{prefix}: %" + logs: + - filename: test_deletion + format: long + '''.format(prefix="0123456789" * 500).split("\n")) + + # Verify that each log type was registered for auto-deletion. + test.ts.Disk.traffic_out.Content = Testers.ContainsExpression( + "Registering rotated log deletion for test_deletion.log with min roll count 0", + "Verify test_deletion.log auto-delete configuration") + test.ts.Disk.traffic_out.Content += Testers.ContainsExpression( + "Registering rotated log deletion for error.log with min roll count 0", "Verify error.log auto-delete configuration") + test.ts.Disk.traffic_out.Content += Testers.ContainsExpression( + "Registering rotated log deletion for traffic.out with min roll count 0", "Verify traffic.out auto-delete configuration") + test.ts.Disk.traffic_out.Content += Testers.ContainsExpression( + "Registering rotated log deletion for diags.log with min roll count 0", "Verify diags.log auto-delete configuration") + test.ts.Disk.traffic_out.Content += Testers.ContainsExpression( + "Registering rotated log deletion for manager.log with min roll count 0", "Verify manager.log auto-delete configuration") + # Verify test_deletion was rotated and deleted. + test.ts.Disk.traffic_out.Content += Testers.ContainsExpression( + f"The rolled logfile.*test_deletion.log_{specified_hostname}.*was auto-deleted.*bytes were reclaimed", + "Verify that space was reclaimed") + + test.tr.MakeCurlCommandMulti(test.get_command_to_rotate_once(), ts=test.ts) + test.tr.Processes.Default.ReturnCode = 0 + + test.tr.StillRunningAfter = test.ts + test.tr.StillRunningAfter = test.server + + # + # Test 1: Verify log deletion happens with a min_count of 1. + # + test = TestLogRetention( + twelve_meg_log_space, "Verify log rotation and deletion of the configured log file with a min_count of 1.") + + # Configure approximately 5 KB entries for a log with no specified min_count. + test.ts.Disk.logging_yaml.AddLines( + ''' + logging: + formats: + - name: long + format: "{prefix}: %" + logs: + - filename: test_deletion + rolling_min_count: 1 + format: long + '''.format(prefix="0123456789" * 500).split("\n")) + + # Verify that each log type was registered for auto-deletion. + test.ts.Disk.traffic_out.Content = Testers.ContainsExpression( + "Registering rotated log deletion for test_deletion.log with min roll count 1", + "Verify test_deletion.log auto-delete configuration") + # Only the test_deletion should have its min_count overridden. + test.ts.Disk.traffic_out.Content += Testers.ContainsExpression( + "Registering rotated log deletion for error.log with min roll count 0", "Verify error.log auto-delete configuration") + test.ts.Disk.traffic_out.Content += Testers.ContainsExpression( + "Registering rotated log deletion for traffic.out with min roll count 0", "Verify traffic.out auto-delete configuration") + test.ts.Disk.traffic_out.Content += Testers.ContainsExpression( + "Registering rotated log deletion for diags.log with min roll count 0", "Verify diags.log auto-delete configuration") + test.ts.Disk.traffic_out.Content += Testers.ContainsExpression( + "Registering rotated log deletion for manager.log with min roll count 0", "Verify manager.log auto-delete configuration") + # Verify test_deletion was rotated and deleted. + test.ts.Disk.traffic_out.Content += Testers.ContainsExpression( + f"The rolled logfile.*test_deletion.log_{specified_hostname}.*was auto-deleted.*bytes were reclaimed", + "Verify that space was reclaimed") + + test.tr.MakeCurlCommandMulti(test.get_command_to_rotate_once(), ts=test.ts) + test.tr.Processes.Default.ReturnCode = 0 + test.tr.StillRunningAfter = test.ts + test.tr.StillRunningAfter = test.server + + # + # Test 2: Verify log deletion happens for a plugin's logs. + # + test = TestLogRetention(twelve_meg_log_space, "Verify log rotation and deletion of plugin logs.") + urtest.PrepareTestPlugin(os.path.join(urtest.Variables.AtsTestPluginsDir, 'test_log_interface.so'), test.ts) + + # Verify that the plugin's logs and other core logs were registered for deletion. + test.ts.Disk.traffic_out.Content = Testers.ContainsExpression( + "Registering rotated log deletion for test_log_interface.log with min roll count 0", + "Verify test_log_interface.log auto-delete configuration") + test.ts.Disk.traffic_out.Content += Testers.ContainsExpression( + "Registering rotated log deletion for error.log with min roll count 0", "Verify error.log auto-delete configuration") + test.ts.Disk.traffic_out.Content += Testers.ContainsExpression( + "Registering rotated log deletion for traffic.out with min roll count 0", "Verify traffic.out auto-delete configuration") + test.ts.Disk.traffic_out.Content += Testers.ContainsExpression( + "Registering rotated log deletion for diags.log with min roll count 0", "Verify diags.log auto-delete configuration") + test.ts.Disk.traffic_out.Content += Testers.ContainsExpression( + "Registering rotated log deletion for manager.log with min roll count 0", "Verify manager.log auto-delete configuration") + # Verify test_deletion was rotated and deleted. + test.ts.Disk.traffic_out.Content += Testers.ContainsExpression( + "The rolled logfile.*test_log_interface.log_.*was auto-deleted.*bytes were reclaimed", "Verify that space was reclaimed") + + test.tr.MakeCurlCommandMulti(test.get_command_to_rotate_once(), ts=test.ts) + test.tr.Processes.Default.ReturnCode = 0 + test.tr.StillRunningAfter = test.ts + test.tr.StillRunningAfter = test.server + + # + # Test 3: Verify log deletion priority behavior. + # + twenty_two_meg_log_space = { + # The following configures a 22 MB log cap with a required 2 MB head room. + # This should allow enough room for two logs being rotated. + 'proxy.config.log.max_space_mb_headroom': 2, + 'proxy.config.log.max_space_mb_for_logs': 22, + } + test = TestLogRetention(twenty_two_meg_log_space, "Verify log deletion priority behavior.") + + # Configure approximately 5 KB entries for a log with no specified min_count. + test.ts.Disk.logging_yaml.AddLines( + ''' + logging: + formats: + - name: long + format: "{prefix}: %" + logs: + - filename: test_low_priority_deletion + rolling_min_count: 5 + format: long + + - filename: test_high_priority_deletion + rolling_min_count: 1 + format: long + '''.format(prefix="0123456789" * 500).split("\n")) + + # Verify that each log type was registered for auto-deletion. + test.ts.Disk.traffic_out.Content = Testers.ContainsExpression( + "Registering rotated log deletion for test_low_priority_deletion.log with min roll count 5", + "Verify test_low_priority_deletion.log auto-delete configuration") + test.ts.Disk.traffic_out.Content += Testers.ContainsExpression( + "Registering rotated log deletion for test_high_priority_deletion.log with min roll count 1", + "Verify test_high_priority_deletion.log auto-delete configuration") + # Only the test_deletion should have its min_count overridden. + test.ts.Disk.traffic_out.Content += Testers.ContainsExpression( + "Registering rotated log deletion for error.log with min roll count 0", "Verify error.log auto-delete configuration") + test.ts.Disk.traffic_out.Content += Testers.ContainsExpression( + "Registering rotated log deletion for traffic.out with min roll count 0", "Verify traffic.out auto-delete configuration") + test.ts.Disk.traffic_out.Content += Testers.ContainsExpression( + "Registering rotated log deletion for diags.log with min roll count 0", "Verify diags.log auto-delete configuration") + test.ts.Disk.traffic_out.Content += Testers.ContainsExpression( + "Registering rotated log deletion for manager.log with min roll count 0", "Verify manager.log auto-delete configuration") + # Verify test_deletion was rotated and deleted. + test.ts.Disk.traffic_out.Content += Testers.ExcludesExpression( + "The rolled logfile.*test_low_priority_deletion.log_.*was auto-deleted.*bytes were reclaimed", + "Verify that space was reclaimed from test_high_priority_deletion") + + # Verify that ATS derives the hostname correctly if the user does not specify a + # hostname via 'proxy.config.log.hostname'. + hostname = socket.gethostname() + test.ts.Disk.traffic_out.Content += Testers.ContainsExpression( + f"The rolled logfile.*test_high_priority_deletion.log_{hostname}.*was auto-deleted.*bytes were reclaimed", + "Verify that space was reclaimed from test_high_priority_deletion") + + test.tr.MakeCurlCommandMulti(test.get_command_to_rotate_once(), ts=test.ts) + test.tr.Processes.Default.ReturnCode = 0 + test.tr.StillRunningAfter = test.ts + test.tr.StillRunningAfter = test.server + + # + # Test 4: Verify min_count configuration overrides. + # + various_min_count_overrides = { + 'proxy.config.log.max_space_mb_for_logs': 22, + 'proxy.config.log.rolling_min_count': 3, + 'proxy.config.output.logfile.rolling_min_count': 4, + 'proxy.config.diags.logfile.rolling_min_count': 5, + } + test = TestLogRetention(various_min_count_overrides, "Verify that the various min_count configurations behave as expected") + + # Only the test_deletion should have its min_count overridden. + test.ts.Disk.traffic_out.Content = Testers.ContainsExpression( + "Registering rotated log deletion for error.log with min roll count 3", "Verify error.log auto-delete configuration") + test.ts.Disk.traffic_out.Content += Testers.ContainsExpression( + "Registering rotated log deletion for traffic.out with min roll count 4", "Verify traffic.out auto-delete configuration") + test.ts.Disk.traffic_out.Content += Testers.ContainsExpression( + "Registering rotated log deletion for diags.log with min roll count 5", "Verify diags.log auto-delete configuration") + test.ts.Disk.traffic_out.Content += Testers.ContainsExpression( + "Registering rotated log deletion for manager.log with min roll count 5", "Verify manager.log auto-delete configuration") + # In case a future log is added, make sure the developer doesn't forget to + # set the min count per configuration. + test.ts.Disk.traffic_out.Content += Testers.ExcludesExpression( + "Registering .* with min roll count 0", "Verify nothing has a default min roll count of 0 per configuration") + + # This test doesn't require a log rotation. We just verify that the logs communicate + # the appropriate min_count values above. + test.tr.MakeCurlCommand(test.get_curl_command(), ts=test.ts) + test.tr.Processes.Default.ReturnCode = 0 + test.tr.StillRunningAfter = test.ts + test.tr.StillRunningAfter = test.server + + # + # Test 5: Verify log deletion does not happen when it is disabled. + # + auto_delete_disabled = twelve_meg_log_space.copy() + auto_delete_disabled.update( + { + 'proxy.config.log.auto_delete_rolled_files': 0, + # Verify that setting a hostname changes the hostname used in rolled logs. + 'proxy.config.log.hostname': 'my_hostname', + }) + test = TestLogRetention(auto_delete_disabled, "Verify log deletion does not happen when auto-delet is disabled.") + + # Configure approximately 5 KB entries for a log with no specified min_count. + test.ts.Disk.logging_yaml.AddLines( + ''' + logging: + formats: + - name: long + format: "{prefix}: %" + logs: + - filename: test_deletion + rolling_min_count: 1 + format: long + '''.format(prefix="0123456789" * 500).split("\n")) + + # Verify that each log type was registered for auto-deletion. + test.ts.Disk.traffic_out.Content = Testers.ExcludesExpression( + "Registering rotated log deletion for test_deletion.log with min roll count 1", + "Verify test_deletion.log auto-delete configuration") + # Only the test_deletion should have its min_count overridden. + test.ts.Disk.traffic_out.Content += Testers.ExcludesExpression( + "Registering rotated log deletion for error.log with min roll count 0", "Verify error.log auto-delete configuration") + test.ts.Disk.traffic_out.Content += Testers.ExcludesExpression( + "Registering rotated log deletion for traffic.out with min roll count 0", "Verify traffic.out auto-delete configuration") + test.ts.Disk.traffic_out.Content += Testers.ExcludesExpression( + "Registering rotated log deletion for diags.log with min roll count 0", "Verify diags.log auto-delete configuration") + test.ts.Disk.traffic_out.Content += Testers.ExcludesExpression( + "Registering rotated log deletion for manager.log with min roll count 0", "Verify manager.log auto-delete configuration") + # Verify test_deletion was not deleted. + test.ts.Disk.traffic_out.Content += Testers.ExcludesExpression( + "The rolled logfile.*test_deletion.log_.*was auto-deleted.*bytes were reclaimed", "Verify that space was reclaimed") + + test.tr.MakeCurlCommandMulti(test.get_command_to_rotate_once(), ts=test.ts) + test.tr.Processes.Default.ReturnCode = 0 + test.tr.StillRunningAfter = test.ts + test.tr.StillRunningAfter = test.server + + # + # Test 6: Verify that max_roll_count is respected. + # + max_roll_count_of_2 = { + 'proxy.config.diags.debug.tags': 'log-file', + + # Provide plenty of max_space: we want auto-deletion to happen because of + # rolling_max_count, not max_space_mb_for_logs. + 'proxy.config.log.max_space_mb_headroom': 2, + 'proxy.config.log.max_space_mb_for_logs': 100, + + # This is the configuration under test. + 'proxy.config.log.rolling_max_count': 2, + } + test = TestLogRetention(max_roll_count_of_2, "Verify max_roll_count is respected.") + + # Configure approximately 5 KB entries for a log with no specified min_count. + test.ts.Disk.logging_yaml.AddLines( + ''' + logging: + formats: + - name: long + format: "{prefix}: %" + logs: + - filename: test_deletion + format: long + '''.format(prefix="0123456789" * 500).split("\n")) + + # Verify that trim happened for the rolled file. + test.ts.Disk.traffic_out.Content = Testers.ContainsExpression( + "rolled logfile.*test_deletion.log.*old.* was auto-deleted", "Verify test_deletion.log was trimmed") + + test.tr.MakeCurlCommandMulti(test.get_command_to_rotate_thrice(), ts=test.ts) + test.tr.Processes.Default.ReturnCode = 0 + test.tr.StillRunningAfter = test.ts + test.tr.StillRunningAfter = test.server + + # + # Test 7: Verify log deletion happens after a config reload. + # + test = TestLogRetention(twelve_meg_log_space, "Verify log rotation and deletion after a config reload.") + + test.ts.Disk.logging_yaml.AddLines( + ''' + logging: + formats: + - name: long + format: "{prefix}: %" + logs: + - filename: test_deletion + format: long + '''.format(prefix="0123456789" * 500).split("\n")) + + # Verify that the plugin's logs and other core logs were registered for deletion. + test.ts.Disk.traffic_out.Content = Testers.ContainsExpression( + "Registering rotated log deletion for test_deletion.log with min roll count 0", + "Verify test_deletion.log auto-delete configuration") + test.ts.Disk.traffic_out.Content += Testers.ContainsExpression( + "Registering rotated log deletion for error.log with min roll count 0", "Verify error.log auto-delete configuration") + test.ts.Disk.traffic_out.Content += Testers.ContainsExpression( + "Registering rotated log deletion for traffic.out with min roll count 0", "Verify traffic.out auto-delete configuration") + test.ts.Disk.traffic_out.Content += Testers.ContainsExpression( + "Registering rotated log deletion for diags.log with min roll count 0", "Verify diags.log auto-delete configuration") + test.ts.Disk.traffic_out.Content += Testers.ContainsExpression( + "Registering rotated log deletion for manager.log with min roll count 0", "Verify manager.log auto-delete configuration") + # Verify test_deletion was rotated and deleted. + test.ts.Disk.traffic_out.Content += Testers.ContainsExpression( + "The rolled logfile.*test_deletion.log_.*was auto-deleted.*bytes were reclaimed", "Verify that space was reclaimed") + + # Touch logging.yaml so the config reload applies to logging objects. + test.tr.Processes.Default.Command = "touch " + test.ts.Disk.logging_yaml.AbsRunTimePath + test.tr.Processes.Default.ReturnCode = 0 + test.tr.StillRunningAfter = test.ts + test.tr.StillRunningAfter = test.server + + tr = urtest.AddConfigReload(test.ts, description="Perform a config reload") + tr.StillRunningAfter = test.server + + tr = urtest.AddTestRun("Get the log to rotate.") + tr.MakeCurlCommandMulti(test.get_command_to_rotate_once(), ts=test.ts) + tr.Processes.Default.ReturnCode = 0 + tr.StillRunningAfter = test.ts + tr.StillRunningAfter = test.server + urtest.execute() diff --git a/tests/uranium_tests/logging/test_new_log_flds.py b/tests/uranium_tests/logging/test_new_log_flds.py new file mode 100644 index 00000000000..9a8e556b427 --- /dev/null +++ b/tests/uranium_tests/logging/test_new_log_flds.py @@ -0,0 +1,142 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_new_log_flds(urtest: UraniumTest) -> None: + ''' + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import os + import sys + + urtest.Summary = ''' + Test new log fields + ''' + + urtest.SkipUnless(Condition.HasCurlFeature('http2')) + + # ---- + # Setup httpbin Origin Server + # ---- + httpbin = urtest.MakeHttpBinServer("httpbin") + + # ---- + # Setup ATS + # ---- + ts = urtest.MakeATSProcess("ts", enable_tls=True) + + ts.addDefaultSSLFiles() + + ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'snowflake|http', + 'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir), + 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts.Variables.SSLDir), + }) + + ts.Disk.remap_config.AddLine( + 'map http://127.0.0.1:{0} http://127.0.0.1:{1}/ip'.format(ts.Variables.port, httpbin.Variables.Port)) + + ts.Disk.remap_config.AddLine( + 'map https://127.0.0.1:{0} http://127.0.0.1:{1}/ip'.format(ts.Variables.ssl_port, httpbin.Variables.Port)) + + ts.Disk.remap_config.AddLine( + 'map https://reallyreallyreallyreallylong.com http://127.0.0.1:{1}/ip'.format( + ts.Variables.ssl_port, httpbin.Variables.Port)) + + ts.Disk.ssl_multicert_yaml.AddLines( + """ + ssl_multicert: + - dest_ip: "*" + ssl_cert_name: server.pem + ssl_key_name: server.key + """.split("\n")) + + ts.Disk.logging_yaml.AddLines( + ''' + logging: + formats: + - name: custom + format: "% % % %" + logs: + - filename: test_new_log_flds + format: custom + '''.split("\n")) + + tr = urtest.AddTestRun() + # Delay on readiness of ssl port + tr.Processes.Default.StartBefore(urtest.Processes.ts) + tr.Processes.Default.StartBefore(httpbin) + # + tr.MakeCurlCommand('"http://127.0.0.1:{0}" --verbose'.format(ts.Variables.port), ts=ts) + tr.Processes.Default.ReturnCode = 0 + + tr = urtest.AddTestRun() + tr.MakeCurlCommand('"http://127.0.0.1:{0}" --verbose'.format(ts.Variables.port), ts=ts) + tr.Processes.Default.ReturnCode = 0 + + tr = urtest.AddTestRun() + tr.MakeCurlCommand('"http://127.0.0.1:{0}" "http://127.0.0.1:{0}" --http1.1 --verbose'.format(ts.Variables.port), ts=ts) + tr.Processes.Default.ReturnCode = 0 + + if not Condition.CurlUsingUnixDomainSocket(): + tr = urtest.AddTestRun() + tr.MakeCurlCommand( + '"https://127.0.0.1:{0}" "https://127.0.0.1:{0}" --http2 --insecure --verbose'.format(ts.Variables.ssl_port), ts=ts) + tr.Processes.Default.ReturnCode = 0 + + tr = urtest.AddTestRun() + tr.MakeCurlCommand( + ( + '"https://reallyreallyreallyreallylong.com:{0}" --http2 --insecure --verbose' + + ' --resolve reallyreallyreallyreallylong.com:{0}:127.0.0.1').format(ts.Variables.ssl_port), + ts=ts) + tr.Processes.Default.ReturnCode = 0 + + # Wait for the final log line to be written. + # + urtest.AddAwaitFileContainsTestRun( + 'Await new log field output.', + os.path.join(ts.Variables.LOGDIR, 'test_new_log_flds.log'), + r'reallyreallyreallyreallylong\.com$', + ) + + # Validate generated log. + # + tr = urtest.AddTestRun() + observer_script = os.path.join(urtest.TestDirectory, 'new_log_flds_observer.py') + log_path = os.path.join(ts.Variables.LOGDIR, 'test_new_log_flds.log') + tr.Processes.Default.Command = f'{sys.executable} {observer_script} < {log_path}' + tr.Processes.Default.ReturnCode = 0 + urtest.execute() diff --git a/tests/uranium_tests/logging/test_pqsi_pqsp.py b/tests/uranium_tests/logging/test_pqsi_pqsp.py new file mode 100644 index 00000000000..f1f938e607a --- /dev/null +++ b/tests/uranium_tests/logging/test_pqsi_pqsp.py @@ -0,0 +1,110 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_pqsi_pqsp(urtest: UraniumTest) -> None: + ''' + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import os + + urtest.Summary = ''' + Test pqsi and pqsp log fields. + ''' + + ts = urtest.MakeATSProcess("ts", enable_cache=False) + server = urtest.MakeOriginServer("server") + + request_header = { + "headers": "GET /test HTTP/1.1\r\n" + "Host: whatever\r\n" + "\r\n", + "body": "", + 'timestamp': "1469733493.993", + } + response_header = { + "headers": "HTTP/1.1 200 OK\r\n" + "Connection: close\r\n" + "\r\n", + "body": "body\n", + 'timestamp': "1469733493.993", + } + server.addResponse("sessionlog.json", request_header, response_header) + + nameserver = urtest.MakeDNServer("dns", default='127.0.0.1') + + ts.Disk.records_config.update( + { + 'proxy.config.dns.nameservers': f"127.0.0.1:{nameserver.Variables.Port}", + 'proxy.config.dns.resolv_conf': 'NULL', + 'proxy.config.http.cache.http': 1, + 'proxy.config.http.cache.required_headers': 0, + }) + ts.Disk.remap_config.AddLine('map / http://localhost:{}/'.format(server.Variables.Port)) + + ts.Disk.logging_yaml.AddLines( + ''' + logging: + formats: + - name: custom + format: '% %' + logs: + - filename: field-test + format: custom + '''.split("\n")) + + tr = urtest.AddTestRun() + tr.Processes.Default.StartBefore(server) + tr.Processes.Default.StartBefore(nameserver) + # Delay on readiness of our ssl ports + tr.Processes.Default.StartBefore(urtest.Processes.ts) + tr.MakeCurlCommand(f'--verbose http://localhost:{ts.Variables.port}/test', ts=ts) + tr.Processes.Default.ReturnCode = 0 + + # Response for this duplicate request should come from cache. + tr = urtest.AddTestRun() + tr.MakeCurlCommand(f'--verbose http://localhost:{ts.Variables.port}/test', ts=ts) + tr.Processes.Default.ReturnCode = 0 + + log_filespec = os.path.join(ts.Variables.LOGDIR, 'field-test.log') + + # Wait for both transaction log lines to be written. Their write order is not + # deterministic. + urtest.AddAwaitFileContainsTestRun('Await pqsi/pqsp cache-miss line.', log_filespec, r'^127\.0\.0\.1 [1-6][0-9]*$') + urtest.AddAwaitFileContainsTestRun('Await pqsi/pqsp cache-hit line.', log_filespec, r'^0 0$') + + tr = urtest.AddTestRun() + tr.Processes.Default.Command = "sed 's/^127\\.0\\.0\\.1 [1-6][0-9]*$$/abc/' < " + log_filespec + " | LC_ALL=C sort" + tr.Processes.Default.Streams.stdout = "gold/pqsi-pqsp.gold" + tr.Processes.Default.ReturnCode = 0 + urtest.execute() diff --git a/tests/uranium_tests/logging/test_qlog_quiche.py b/tests/uranium_tests/logging/test_qlog_quiche.py new file mode 100644 index 00000000000..fe5f3a5baa3 --- /dev/null +++ b/tests/uranium_tests/logging/test_qlog_quiche.py @@ -0,0 +1,147 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_qlog_quiche(urtest: UraniumTest) -> None: + ''' + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import os + + urtest.Summary = ''' + Basic test for qlog using quiche library. + ''' + + urtest.SkipUnless(Condition.HasATSFeature('TS_HAS_QUICHE')) + + Generate_Qlog = True + No_Qlog = False + + class quiche_qlog_Test: + replay_file = "replay/basic1.replay.yaml" + client_counter: int = 0 + ts_counter: int = 0 + server_counter: int = 0 + + def __init__(self, name: str): + """Initialize the test. + :param name: The name of the test. + """ + self._name = name + self._generate_qlog = False + + def with_qlogs(self): + self._generate_qlog = True + return self + + def without_qlogs(self): + self._generate_qlog = False + return self + + def _configure_server(self, tr: 'TestRun'): + """Configure the server. + + :param tr: The TestRun object to associate the server process with. + """ + server = tr.AddVerifierServerProcess(f"server_{quiche_qlog_Test.server_counter}", self.replay_file) + quiche_qlog_Test.server_counter += 1 + self._server = server + + def _configure_traffic_server(self, tr: 'TestRun'): + """Configure Traffic Server. + + :param tr: The TestRun object to associate the ts process with. + """ + ts = urtest.MakeATSProcess(f"ts-{quiche_qlog_Test.ts_counter}", enable_quic=True, enable_tls=True) + self._ts = ts + quiche_qlog_Test.ts_counter += 1 + self._ts.addDefaultSSLFiles() + self._ts.Disk.records_config.update( + ''' + diags: + debug: + enabled: 1 + tags: vv_quic|v_quic + quic: + no_activity_timeout_in: 3000 + ''') + if self._generate_qlog: + self._ts.Disk.records_config.update( + ''' + quic: + qlog: + file_base: log/test_qlog # we expect to have log/test_qlog-.sqlog + ''') + self._ts.Disk.ssl_multicert_yaml.AddLines( + f""" + ssl_multicert: + - dest_ip: "*" + ssl_cert_name: {ts.Variables.SSLDir}/server.pem + ssl_key_name: {ts.Variables.SSLDir}/server.key + """.split("\n")) + + self._ts.Disk.remap_config.AddLine(f'map / http://127.0.0.1:{self._server.Variables.http_port}') + + def test(self): + """Run the test.""" + tr = urtest.AddTestRun(self._name) + self._configure_server(tr) + self._configure_traffic_server(tr) + + tr.Processes.Default.StartBefore(self._server) + tr.Processes.Default.StartBefore(self._ts) + tr.AddVerifierClientProcess( + f"client-{quiche_qlog_Test.client_counter}", self.replay_file, http3_ports=[self._ts.Variables.ssl_port]) + quiche_qlog_Test.client_counter += 1 + tr.Processes.Default.ReturnCode = 0 + + # If requested we will copy the file to a known name as the runtime name is not know + # by the test. So we will rename them all and grab the first one for validation. + test_run = urtest.AddTestRun(f"{self._name} : check qlog files") + qlog_base_name = "test_qlog" + rename_script = os.path.join(urtest.TestDirectory, 'rename_qlog.sh') + test_run.Processes.Default.Command = f'sleep 5; bash {rename_script} {self._ts.Variables.LOGDIR} {qlog_base_name}' + test_run.Processes.Default.ReturnCode = 0 if self._generate_qlog else 2 # exit 2 is what we want if no qlog was generated. + + # Basic valdation + if self._generate_qlog: + tr = urtest.AddTestRun("Check qlog content") + file = os.path.join(self._ts.Variables.LOGDIR, "1.sqlog") + f = tr.Disk.File(file) + tr.Processes.Default.Command = "echo 0" + tr.Processes.Default.ReturnCode = 0 + f.Content = Testers.IncludesExpression('"title":"Apache Traffic Server"', 'Should include this basic text') + + quiche_qlog_Test("Generate qlog test").with_qlogs().test() + quiche_qlog_Test("Do not generate qlog").without_qlogs().test() + urtest.execute() diff --git a/tests/uranium_tests/logging/test_sigusr2.py b/tests/uranium_tests/logging/test_sigusr2.py new file mode 100644 index 00000000000..c201c8a8459 --- /dev/null +++ b/tests/uranium_tests/logging/test_sigusr2.py @@ -0,0 +1,183 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_sigusr2(urtest: UraniumTest) -> None: + ''' + Verify support of external log rotation via SIGUSR2. + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import os + import shlex + import sys + + TS_PID_SCRIPT = 'ts_process_handler.py' + ROTATE_DIAGS_SCRIPT = 'sigusr2_rotate_diags.sh' + ROTATE_CUSTOM_LOG_SCRIPT = 'sigusr2_rotate_custom_log.sh' + + class Sigusr2Test: + """ + Handle this test-specific Traffic Server configuration. + """ + + __ts_counter = 1 + __server_counter = 1 + + @classmethod + def _next_ts_name(cls): + ts_name = f"sigusr2_ts{cls.__ts_counter}" + cls.__ts_counter += 1 + return ts_name + + @classmethod + def _next_server_name(cls): + server_name = f"sigusr2_server{cls.__server_counter}" + cls.__server_counter += 1 + return server_name + + @staticmethod + def _make_script_command(script_name, *args): + quoted_args = ' '.join(shlex.quote(str(arg)) for arg in args) + return f"bash ./{script_name} {quoted_args}" + + def _configure_traffic_server(self, tr): + ts_name = self._next_ts_name() + ts = tr.MakeATSProcess(ts_name) + ts.Disk.records_config.update( + { + 'proxy.config.http.wait_for_cache': 1, + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'log', + 'proxy.config.log.periodic_tasks_interval': 1, + + # All log rotation should be handled externally. + 'proxy.config.log.rolling_enabled': 0, + 'proxy.config.log.auto_delete_rolled_files': 0, + }) + return ts, ts_name + + def _configure_server(self): + server = urtest.MakeOriginServer(self._next_server_name()) + + for path in ['/first', '/second', '/third']: + request_header = { + 'headers': f'GET {path} HTTP/1.1\r\nHost: does.not.matter\r\n\r\n', + 'timestamp': '1469733493.993', + 'body': '' + } + response_header = { + 'headers': 'HTTP/1.1 200 OK\r\n' + 'Connection: close\r\n' + 'Cache-control: max-age=85000\r\n\r\n', + 'timestamp': '1469733493.993', + 'body': 'xxx' + } + server.addResponse('sessionlog.json', request_header, response_header) + + return server + + def add_system_log_test(self): + tr = urtest.AddTestRun('Verify system logs can be rotated') + ts, ts_name = self._configure_traffic_server(tr) + + rotated_diags_log = f'{ts.Disk.diags_log.AbsPath}_old' + ts.Disk.File(rotated_diags_log, id='diags_log_old') + + tr.Processes.Default.Command = self._make_script_command( + ROTATE_DIAGS_SCRIPT, sys.executable, f'./{TS_PID_SCRIPT}', ts_name, ts.Disk.diags_log.AbsPath, rotated_diags_log) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.StartBefore(ts) + + ts.Disk.diags_log.Content += Testers.ExcludesExpression( + 'traffic server running', 'The new diags.log should not reference the running traffic server') + ts.Disk.diags_log.Content += Testers.ContainsExpression( + 'Reseated diags.log', 'The new diags.log should indicate the newly opened diags.log') + ts.Disk.diags_log_old.Content += Testers.ContainsExpression( + 'traffic server running', 'The rotated diags.log should keep the original startup message') + + def add_configured_log_test(self): + tr = urtest.AddTestRun('Verify yaml.log logs can be rotated') + ts, ts_name = self._configure_traffic_server(tr) + server = self._configure_server() + + ts.Disk.remap_config.AddLine(f'map http://127.0.0.1:{ts.Variables.port} http://127.0.0.1:{server.Variables.Port}') + ts.Disk.logging_yaml.AddLine( + ''' + logging: + formats: + - name: has_path + format: "%: %" + logs: + - filename: test_rotation + format: has_path + ''') + + configured_log = os.path.join(ts.Variables.LOGDIR, 'test_rotation.log') + ts.Disk.File(configured_log, id='configured_log') + + rotated_configured_log = f'{configured_log}_old' + ts.Disk.File(rotated_configured_log, id='configured_log_old') + + ts.StartBefore(server) + tr.Processes.Default.Command = self._make_script_command( + ROTATE_CUSTOM_LOG_SCRIPT, sys.executable, f'./{TS_PID_SCRIPT}', ts_name, ts.Variables.port, configured_log, + rotated_configured_log) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.StartBefore(ts) + + ts.Disk.configured_log.Content += Testers.ExcludesExpression( + '/first', 'The new test_rotation.log should not have the first GET retrieval in it.') + ts.Disk.configured_log.Content += Testers.ExcludesExpression( + '/second', 'The new test_rotation.log should not have the second GET retrieval in it.') + ts.Disk.configured_log.Content += Testers.ContainsExpression( + '/third', 'The new test_rotation.log should have the third GET retrieval in it.') + + ts.Disk.configured_log_old.Content += Testers.ContainsExpression( + '/first', 'test_rotation.log_old should have the first GET retrieval in it.') + ts.Disk.configured_log_old.Content += Testers.ContainsExpression( + '/second', 'test_rotation.log_old should have the second GET retrieval in it.') + ts.Disk.configured_log_old.Content += Testers.ExcludesExpression( + '/third', 'test_rotation.log_old should not have the third GET retrieval in it.') + + urtest.Summary = ''' + Verify support of external log rotation via SIGUSR2. + ''' + + urtest.Setup.Copy(TS_PID_SCRIPT) + urtest.Setup.Copy(ROTATE_DIAGS_SCRIPT) + urtest.Setup.Copy(ROTATE_CUSTOM_LOG_SCRIPT) + + sigusr2_test = Sigusr2Test() + sigusr2_test.add_system_log_test() + sigusr2_test.add_configured_log_test() + urtest.execute() diff --git a/tests/gold_tests/logging/ts_process_handler.py b/tests/uranium_tests/logging/ts_process_handler.py similarity index 100% rename from tests/gold_tests/logging/ts_process_handler.py rename to tests/uranium_tests/logging/ts_process_handler.py diff --git a/tests/gold_tests/logging/verify_milestone_fields.py b/tests/uranium_tests/logging/verify_milestone_fields.py similarity index 100% rename from tests/gold_tests/logging/verify_milestone_fields.py rename to tests/uranium_tests/logging/verify_milestone_fields.py diff --git a/tests/uranium_tests/next_hop/parent_config/test_parent_config_dest_ip.py b/tests/uranium_tests/next_hop/parent_config/test_parent_config_dest_ip.py new file mode 100644 index 00000000000..71837b757b7 --- /dev/null +++ b/tests/uranium_tests/next_hop/parent_config/test_parent_config_dest_ip.py @@ -0,0 +1,146 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_parent_config_dest_ip(urtest: UraniumTest) -> None: + ''' + Verify ATS parent.config dest_ip dns bug fix. + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + urtest.Summary = ''' + Verify ATS parent_config with dest_ip=... + ''' + + # Create origin + origin = urtest.MakeOriginServer("origin") + + # default root + request_header_chk = { + "headers": "GET / HTTP/1.1\r\n" + "Host: ats\r\n" + "\r\n", + "timestamp": "1469733493.993", + "body": "", + } + + response_header_chk = { + "headers": "HTTP/1.1 200 OK\r\n" + "Connection: close\r\n" + "\r\n", + "timestamp": "1469733493.993", + "body": "", + } + + origin.addResponse("sessionlog.json", request_header_chk, response_header_chk) + + request_header = { + "headers": + f"GET /foo.txt HTTP/1.1\r\n" + "Host: does.not.matter\r\n" # But cant be omitted + "\r\n", + "timestamp": "1469733493.993", + "body": "" + } + + response_header = { + "headers": "HTTP/1.1 200 OK\r\n" + "Connection: close\r\n" + "Cache-control: max-age=60\r\n" + "\r\n", + "timestamp": "1469733493.993", + "body": f"This is the body for foo.txt\n" + } + origin.addResponse("sessionlog.json", request_header, response_header) + + # Configure DNS for cache layering + dns = urtest.MakeDNServer("dns") + dns.addRecords(records={f"origin": ["127.0.0.1"]}) + dns.addRecords(records={f"ts1": ["127.0.0.1"]}) + dns.addRecords(records={f"ts0": ["127.0.0.1"]}) + dns.addRecords(records={f"foo.bar": ["142.250.72.14"]}) # google.com + + # Configure Traffic Server Mid + ts1 = urtest.MakeATSProcess("ts1") + ts1.Disk.remap_config.AddLine(f"map / http://origin:{origin.Variables.Port}") + + ts1.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'http|dns|hostdb|parent', + 'proxy.config.dns.nameservers': f"127.0.0.1:{dns.Variables.Port}", + 'proxy.config.dns.resolv_conf': "NULL", + 'proxy.config.hostdb.lookup_timeout': 2, + 'proxy.config.http.connect_attempts_timeout': 1, + 'proxy.config.http.parent_proxy.self_detect': 0, + 'proxy.config.http.insert_response_via_str': 1, + 'proxy.config.proxy_name': 'ts1', + }) + + # Configure Traffic Server Edge + ts0 = urtest.MakeATSProcess("ts0") + ts0.Disk.remap_config.AddLine("map http://foo.bar http://foo.bar") + + ts0.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'http|dns|hostdb|parent', + 'proxy.config.dns.nameservers': f"127.0.0.1:{dns.Variables.Port}", + 'proxy.config.dns.resolv_conf': "NULL", + 'proxy.config.hostdb.lookup_timeout': 2, + 'proxy.config.http.connect_attempts_timeout': 1, + 'proxy.config.http.parent_proxy.self_detect': 0, + 'proxy.config.http.insert_response_via_str': 1, + 'proxy.config.proxy_name': 'ts0', + }) + + ts0.Disk.parent_config.AddLines( + [ + f"dest_ip=93.184.216.34 port=80 go_direct=true", # example.com + f'dest_host=foo.bar port=80 parent="ts1:{ts1.Variables.port}|1;" go_direct="false" parent_is_proxy="true"', + ]) + + # Start everything up + tr = urtest.AddTestRun("init") + tr.Processes.Default.StartBefore(origin) + tr.Processes.Default.StartBefore(dns) + tr.Processes.Default.StartBefore(ts0) + tr.Processes.Default.StartBefore(ts1) + tr.Processes.Default.Command = 'echo start TS, TSH_N, HTTP origin and DNS.' + tr.Processes.Default.ReturnCode = 0 + + curl_and_args = f"-s -D /dev/stdout -o /dev/stderr -x http://127.0.0.1:{ts0.Variables.port}" + + # Request asset that goes through the layers + tr = urtest.AddTestRun("request") + ps = tr.Processes.Default + tr.MakeCurlCommand(curl_and_args + ' http://foo.bar/foo.txt', ts=ts0) + ps.ReturnCode = 0 + ps.Streams.stdout.Content = Testers.ContainsExpression("Via:.* ts1 .* ts0 ", "expected via header") + tr.StillRunningAfter = ts0 + urtest.execute() diff --git a/tests/gold_tests/next_hop/strategies_ch/body.gold b/tests/uranium_tests/next_hop/strategies_ch/body.gold similarity index 100% rename from tests/gold_tests/next_hop/strategies_ch/body.gold rename to tests/uranium_tests/next_hop/strategies_ch/body.gold diff --git a/tests/uranium_tests/next_hop/strategies_ch/test_strategies_ch.py b/tests/uranium_tests/next_hop/strategies_ch/test_strategies_ch.py new file mode 100644 index 00000000000..b30120dff32 --- /dev/null +++ b/tests/uranium_tests/next_hop/strategies_ch/test_strategies_ch.py @@ -0,0 +1,160 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_strategies_ch(urtest: UraniumTest) -> None: + ''' + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + urtest.Summary = ''' + Test next hop selection using strategies.yaml with consistent hashing. + ''' + + # Define and populate MicroServer. + # + server = urtest.MakeOriginServer("server") + response_header = { + "headers": "HTTP/1.1 200 OK\r\n" + "Connection: close\r\n" + "Cache-control: max-age=85000\r\n" + "\r\n", + "timestamp": "1469733493.993", + "body": "This is the body.\n" + } + num_objects = 32 + for i in range(num_objects): + request_header = { + "headers": + f"GET /obj{i} HTTP/1.1\r\n" + "Host: does.not.matter\r\n" # But cannot be omitted. + "\r\n", + "timestamp": "1469733493.993", + "body": "" + } + server.addResponse("sessionlog.json", request_header, response_header) + + dns = urtest.MakeDNServer("dns") + + # Define next hop trafficserver instances. + # + num_nh = 8 + ts_nh = [] + for i in range(num_nh): + ts = urtest.MakeATSProcess(f"ts_nh{i}", use_traffic_out=False, command=f"traffic_server 2>nh_trace{i}.log") + ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'http|dns', + 'proxy.config.dns.nameservers': f"127.0.0.1:{dns.Variables.Port}", + 'proxy.config.dns.resolv_conf': "NULL", + }) + ts.Disk.remap_config.AddLine(f"map / http://127.0.0.1:{server.Variables.Port}") + ts.ReturnCode = Any(0, -2) + ts_nh.append(ts) + + ts = urtest.MakeATSProcess("ts") + + ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'http|dns|parent|next_hop|host_statuses|hostdb', + 'proxy.config.dns.nameservers': f"127.0.0.1:{dns.Variables.Port}", # Only nameservers if resolv_conf NULL. + 'proxy.config.dns.resolv_conf': "NULL", # This defaults to /etc/resvolv.conf (OS namesevers) if not NULL. + 'proxy.config.http.cache.http': 0, + 'proxy.config.http.uncacheable_requests_bypass_parent': 0, + 'proxy.config.http.no_dns_just_forward_to_parent': 1, + 'proxy.config.http.parent_proxy.mark_down_hostdb': 0, + 'proxy.config.http.parent_proxy.self_detect': 0, + }) + + ts.Disk.File(ts.Variables.CONFIGDIR + "/strategies.yaml", id="strategies", typename="ats:config") + s = ts.Disk.strategies + s.AddLine("groups:") + s.AddLine(" - &g1") + for i in range(num_nh): + dns.addRecords(records={f"next_hop{i}": ["127.0.0.1"]}) + s.AddLine(f" - host: next_hop{i}") + s.AddLine(f" protocol:") + s.AddLine(f" - scheme: http") + s.AddLine(f" port: {ts_nh[i].Variables.port}") + # The health check URL does not seem to be used currently. + # s.AddLine(f" health_check_url: http://next_hop{i}:{ts_nh[i].Variables.port}") + s.AddLine(f" weight: 1.0") + s.AddLines( + [ + "strategies:", + " - strategy: the-strategy", + " policy: consistent_hash", + " hash_key: path", + " go_direct: false", + " parent_is_proxy: true", + " ignore_self_detect: true", + " groups:", + " - *g1", + " scheme: http", + ]) + + # Fallover not currently tested. + # + # s.AddLines([ + # " fallover:", + # " max_simple_retries: 2", + # " ring_mode: exhaust_ring", + # " response_codes:", + # " - 404", + # " health_check:", + # " - passive",]) + + ts.Disk.remap_config.AddLine("map http://dummy.com http://not_used @strategy=the-strategy") + + tr = urtest.AddTestRun() + tr.Processes.Default.StartBefore(server) + tr.Processes.Default.StartBefore(dns) + for i in range(num_nh): + tr.Processes.Default.StartBefore(ts_nh[i]) + tr.Processes.Default.StartBefore(urtest.Processes.ts) + tr.Processes.Default.Command = 'echo start TS, HTTP server, DNS server and next hop TSes' + tr.Processes.Default.ReturnCode = 0 + + for i in range(num_objects): + tr = urtest.AddTestRun() + tr.MakeCurlCommand(f'--verbose --proxy 127.0.0.1:{ts.Variables.port} http://dummy.com/obj{i}', ts=ts) + tr.Processes.Default.Streams.stdout = "body.gold" + tr.Processes.Default.ReturnCode = 0 + + tr = urtest.AddTestRun() + # For some reason, the * won't be expanded when the command is executed, if stdout is not piped through "cat". + tr.Processes.Default.Command = "grep -F '200 OK' nh_trace*.log | cat" + tr.Processes.Default.Streams.stdout = "trace.gold" + tr.Processes.Default.ReturnCode = 0 + urtest.execute() diff --git a/tests/gold_tests/next_hop/strategies_ch/trace.gold b/tests/uranium_tests/next_hop/strategies_ch/trace.gold similarity index 100% rename from tests/gold_tests/next_hop/strategies_ch/trace.gold rename to tests/uranium_tests/next_hop/strategies_ch/trace.gold diff --git a/tests/gold_tests/next_hop/strategies_ch2/body.gold b/tests/uranium_tests/next_hop/strategies_ch2/body.gold similarity index 100% rename from tests/gold_tests/next_hop/strategies_ch2/body.gold rename to tests/uranium_tests/next_hop/strategies_ch2/body.gold diff --git a/tests/uranium_tests/next_hop/strategies_ch2/test_strategies_ch2.py b/tests/uranium_tests/next_hop/strategies_ch2/test_strategies_ch2.py new file mode 100644 index 00000000000..55908a2dbc7 --- /dev/null +++ b/tests/uranium_tests/next_hop/strategies_ch2/test_strategies_ch2.py @@ -0,0 +1,176 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_strategies_ch2(urtest: UraniumTest) -> None: + ''' + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + urtest.Summary = ''' + Test next hop selection using strategies.yaml with consistent hashing, with fallover + ''' + + # Define and populate MicroServer. + # + server = urtest.MakeOriginServer("server") + response_header = { + "headers": "HTTP/1.1 200 OK\r\n" + "Connection: close\r\n" + "Cache-control: max-age=85000\r\n" + "\r\n", + "timestamp": "1469733493.993", + "body": "This is the body.\n" + } + num_objects = 32 + for i in range(num_objects): + request_header = { + "headers": + f"GET /obj{i} HTTP/1.1\r\n" + "Host: does.not.matter\r\n" # But cannot be omitted. + "\r\n", + "timestamp": "1469733493.993", + "body": "" + } + server.addResponse("sessionlog.json", request_header, response_header) + + dns = urtest.MakeDNServer("dns") + + # Define next hop trafficserver instances. + # + num_nh = 8 + ts_nh = [] + for i in range(num_nh): + ts = urtest.MakeATSProcess(f"ts_nh{i}") + ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'http|dns', + 'proxy.config.dns.nameservers': f"127.0.0.1:{dns.Variables.Port}", + 'proxy.config.dns.resolv_conf': "NULL", + }) + ts.Disk.remap_config.AddLine(f"map / http://127.0.0.1:{server.Variables.Port}") + ts_nh.append(ts) + + ts = urtest.MakeATSProcess("ts", use_traffic_out=False, command="traffic_server 2> trace.log") + ts.ReturnCode = Any(0, -2) + + ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'http|dns|parent|next_hop|host_statuses|hostdb', + 'proxy.config.dns.nameservers': f"127.0.0.1:{dns.Variables.Port}", # Only nameservers if resolv_conf NULL. + 'proxy.config.dns.resolv_conf': "NULL", # This defaults to /etc/resvolv.conf (OS namesevers) if not NULL. + 'proxy.config.http.cache.http': 0, + 'proxy.config.http.uncacheable_requests_bypass_parent': 0, + 'proxy.config.http.no_dns_just_forward_to_parent': 1, + 'proxy.config.http.parent_proxy.mark_down_hostdb': 0, + 'proxy.config.http.down_server.cache_time': 1, + 'proxy.config.http.parent_proxy.self_detect': 0, + }) + + ts.Disk.File(ts.Variables.CONFIGDIR + "/strategies.yaml", id="strategies", typename="ats:config") + s = ts.Disk.strategies + s.AddLine("groups:") + s.AddLine(" - &g1") + for i in range(num_nh): + dns.addRecords(records={f"next_hop{i}": ["127.0.0.1"]}) + s.AddLine(f" - host: next_hop{i}") + s.AddLine(f" protocol:") + s.AddLine(f" - scheme: http") + s.AddLine(f" port: {ts_nh[i].Variables.port}") + # The health check URL does not seem to be used currently. + # s.AddLine(f" health_check_url: http://next_hop{i}:{ts_nh[i].Variables.port}") + s.AddLine(f" weight: 1.0") + + s.AddLines( + [ + "strategies:", + " - strategy: the-strategy", + " policy: consistent_hash", + " hash_key: path", + " go_direct: false", + " parent_is_proxy: true", + " ignore_self_detect: true", + " groups:", + " - *g1", + " scheme: http", + ]) + + # Use default fallover config. + # + # s.AddLines([ + # " fallover:", + # " max_simple_retries: 2", + # " ring_mode: exhaust_ring", + # " response_codes:", + # " - 404", + # " health_check:", + # " - passive", + # ]) + + ts.Disk.remap_config.AddLine("map http://dummy.com http://not_used @strategy=the-strategy") + + tr = urtest.AddTestRun() + tr.Processes.Default.StartBefore(server) + tr.Processes.Default.StartBefore(dns) + for i in range(num_nh): + if (i != 3) and (i != 6): + tr.Processes.Default.StartBefore(ts_nh[i]) + tr.Processes.Default.StartBefore(urtest.Processes.ts) + tr.Processes.Default.Command = 'echo start TS, HTTP server, DNS server and all but 2 next hop TSes' + tr.Processes.Default.ReturnCode = 0 + + for i in range(num_objects): + tr = urtest.AddTestRun() + tr.MakeCurlCommand(f'--verbose --proxy 127.0.0.1:{ts.Variables.port} http://dummy.com/obj{i}', ts=ts) + tr.Processes.Default.Streams.stdout = "body.gold" + tr.Processes.Default.ReturnCode = 0 + + tr = urtest.AddTestRun() + tr.Processes.Default.StartBefore(ts_nh[3]) + tr.Processes.Default.StartBefore(ts_nh[6]) + tr.Processes.Default.Command = 'echo start the 2 unstarted next hop TSes' + tr.Processes.Default.ReturnCode = 0 + + for i in range(num_objects): + tr = urtest.AddTestRun() + tr.MakeCurlCommand(f'--verbose --proxy 127.0.0.1:{ts.Variables.port} http://dummy.com/obj{i}', ts=ts) + tr.Processes.Default.Streams.stdout = "body.gold" + tr.Processes.Default.ReturnCode = 0 + + tr = urtest.AddTestRun() + tr.Processes.Default.Command = ( + "grep -F ParentResultType::SPECIFIED trace.log | sed 's/^.*(next_hop) [^ ]* //' | sed 's/[.][0-9]*$$//'") + tr.Processes.Default.Streams.stdout = "trace.gold" + tr.Processes.Default.ReturnCode = 0 + urtest.execute() diff --git a/tests/gold_tests/next_hop/strategies_ch2/trace.gold b/tests/uranium_tests/next_hop/strategies_ch2/trace.gold similarity index 100% rename from tests/gold_tests/next_hop/strategies_ch2/trace.gold rename to tests/uranium_tests/next_hop/strategies_ch2/trace.gold diff --git a/tests/uranium_tests/next_hop/strategies_ch5/test_strategies_ch5.py b/tests/uranium_tests/next_hop/strategies_ch5/test_strategies_ch5.py new file mode 100644 index 00000000000..c858d9840c7 --- /dev/null +++ b/tests/uranium_tests/next_hop/strategies_ch5/test_strategies_ch5.py @@ -0,0 +1,174 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_strategies_ch5(urtest: UraniumTest) -> None: + ''' + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + urtest.Summary = ''' + Test next hop consistent hashing with 5 rings (MAX_GROUP_RINGS). + Validates mapWrapped and chashIter work at full ring capacity. + ''' + + # Define and populate MicroServer. + # + server = urtest.MakeOriginServer("server") + response_header = { + "headers": "HTTP/1.1 200 OK\r\n" + "Connection: close\r\n" + "Cache-control: max-age=85000\r\n" + "\r\n", + "timestamp": "1469733493.993", + "body": "This is the body.\n" + } + num_objects = 32 + for i in range(num_objects): + request_header = { + "headers": f"GET /obj{i} HTTP/1.1\r\n" + "Host: does.not.matter\r\n" + "\r\n", + "timestamp": "1469733493.993", + "body": "" + } + server.addResponse("sessionlog.json", request_header, response_header) + + dns = urtest.MakeDNServer("dns") + + # 5 rings, 2 hosts each = 10 next hops. + # only the last ring will be started + # + num_rings = 5 + hosts_per_ring = 2 + num_hosts = num_rings * hosts_per_ring + ts_nh = [] + for ii in range(num_hosts): + ts = urtest.MakeATSProcess(f"ts_nh{ii}") + ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'http|dns', + 'proxy.config.dns.nameservers': f"127.0.0.1:{dns.Variables.Port}", + 'proxy.config.dns.resolv_conf': "NULL", + }) + line = f"map / http://127.0.0.1:{server.Variables.Port}" + if ii < (num_hosts - hosts_per_ring): + line += " @plugin=header_rewrite.so @pparam=hdr_rw.conf" + ts.Disk.remap_config.AddLine(line) + ts.Disk.MakeConfigFile("hdr_rw.conf").AddLine("set-status 502") + ts_nh.append(ts) + + ts = urtest.MakeATSProcess("ts", use_traffic_out=False, command="traffic_server 2> trace.log") + ts.ReturnCode = Any(0, -2) + + ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'http|dns|parent|next_hop|host_statuses|hostdb', + 'proxy.config.dns.nameservers': f"127.0.0.1:{dns.Variables.Port}", + 'proxy.config.dns.resolv_conf': "NULL", + 'proxy.config.http.cache.http': 0, + 'proxy.config.http.parent_proxy.per_parent_connect_attempts': 1, + 'proxy.config.http.uncacheable_requests_bypass_parent': 0, + 'proxy.config.http.no_dns_just_forward_to_parent': 1, + 'proxy.config.http.parent_proxy.mark_down_hostdb': 0, + 'proxy.config.http.down_server.cache_time': 1, + 'proxy.config.http.parent_proxy.self_detect': 0, + }) + + ts.Disk.File(ts.Variables.CONFIGDIR + "/strategies.yaml", id="strategies", typename="ats:config") + s = ts.Disk.strategies + s.AddLine("groups:") + + # Build 5 groups. + idx = 0 + for ring in range(num_rings): + s.AddLine(f" - &g{ring}") + for h in range(hosts_per_ring): + dns.addRecords(records={f"next_hop_{idx}": ["127.0.0.1"]}) + s.AddLine(f" - host: next_hop_{idx}") + s.AddLine(f" protocol:") + s.AddLine(f" - scheme: http") + s.AddLine(f" port: {ts_nh[idx].Variables.port}") + s.AddLine(f" weight: 1.0") + idx = idx + 1 + + strategy_lines = [ + "strategies:", + " - strategy: the-strategy", + " policy: consistent_hash", + " hash_key: path", + " go_direct: false", + " parent_is_proxy: true", + " ignore_self_detect: true", + " scheme: http", + " failover:", + " ring_mode: alternate_ring", + " max_simple_retries: 5", + " response_codes: [404]", + " max_unavailable_retries: 5", + " markdown_codes: [502]", + " groups:", + ] + for ring in range(num_rings): + strategy_lines.append(f" - *g{ring}") + s.AddLines(strategy_lines) + + ts.Disk.remap_config.AddLine("map http://dummy.com http://not_used @strategy=the-strategy") + + # Only start ring 5 (last ring, indices 8-9). Forces fallover through all 5 rings. + tr = urtest.AddTestRun() + ps = tr.Processes.Default + ps.StartBefore(server) + ps.StartBefore(dns) + #for idx in range((num_rings - 1) * hosts_per_ring, len(ts_nh)): + for idx in range(len(ts_nh)): + ps.StartBefore(ts_nh[idx]) + ps.StartBefore(urtest.Processes.ts) + ps.Command = 'echo start TS, origin, DNS, and only last-ring next hops' + ps.ReturnCode = 0 + + # Send requests - must fall through rings 1-4 (down) to ring 5. + for i in range(num_objects): + tr = urtest.AddTestRun() + tr.MakeCurlCommand(f'--verbose --proxy 127.0.0.1:{ts.Variables.port} http://dummy.com/obj{i}', ts=ts) + ps = tr.Processes.Default + ps.Streams.stdout.Content = Testers.ContainsExpression("This is the body.", "expected body") + ps.ReturnCode = 0 + + tr = urtest.AddTestRun() + ps = tr.Processes.Default + ps.Command = ("grep -F ParentResultType::SPECIFIED trace.log | sed 's/^.*(next_hop) [^ ]* //' | sed 's/[.][0-9]*$$//'") + ps.Streams.stdout = "trace.gold" + ps.ReturnCode = 0 + urtest.execute() diff --git a/tests/gold_tests/next_hop/strategies_ch5/trace.gold b/tests/uranium_tests/next_hop/strategies_ch5/trace.gold similarity index 100% rename from tests/gold_tests/next_hop/strategies_ch5/trace.gold rename to tests/uranium_tests/next_hop/strategies_ch5/trace.gold diff --git a/tests/gold_tests/next_hop/strategies_stale/body.gold b/tests/uranium_tests/next_hop/strategies_stale/body.gold similarity index 100% rename from tests/gold_tests/next_hop/strategies_stale/body.gold rename to tests/uranium_tests/next_hop/strategies_stale/body.gold diff --git a/tests/uranium_tests/next_hop/strategies_stale/test_strategies_stale.py b/tests/uranium_tests/next_hop/strategies_stale/test_strategies_stale.py new file mode 100644 index 00000000000..322f869a15d --- /dev/null +++ b/tests/uranium_tests/next_hop/strategies_stale/test_strategies_stale.py @@ -0,0 +1,163 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_strategies_stale(urtest: UraniumTest) -> None: + ''' + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + urtest.Summary = ''' + Test next hop selection using strategies.yaml with consistent hashing. + ''' + + # Define and populate MicroServer. + # + server = urtest.MakeOriginServer("server") + response_header = { + "headers": "HTTP/1.1 200 OK\r\n" + "Connection: close\r\n" + "Cache-control: max-age=2\r\n" + "\r\n", + "timestamp": "1469733493.993", + "body": "This is the body.\n" + } + + request_header = { + "headers": + f"GET /obj0 HTTP/1.1\r\n" + "Host: does.not.matter\r\n" # But cannot be omitted. + "\r\n", + "timestamp": "1469733493.993", + "body": "" + } + server.addResponse("sessionlog.json", request_header, response_header) + + dns = urtest.MakeDNServer("dns") + + # Define next hop trafficserver instances. + # + ts_nh = urtest.MakeATSProcess(f"ts_nh0", use_traffic_out=False, command=f"traffic_server 2>nh_trace.log") + ts_nh.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'http|dns', + 'proxy.config.dns.nameservers': f"127.0.0.1:{dns.Variables.Port}", + 'proxy.config.dns.resolv_conf': "NULL", + }) + ts_nh.Disk.remap_config.AddLine(f"map / http://127.0.0.1:{server.Variables.Port}") + ts_nh.ReturnCode = Any(0, -2) + + ts = urtest.MakeATSProcess("ts") + + ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'http|dns|parent|next_hop|host_statuses|hostdb', + 'proxy.config.dns.nameservers': f"127.0.0.1:{dns.Variables.Port}", # Only nameservers if resolv_conf NULL. + 'proxy.config.dns.resolv_conf': "NULL", # This defaults to /etc/resvolv.conf (OS namesevers) if not NULL. + 'proxy.config.http.cache.http': 1, + 'proxy.config.http.uncacheable_requests_bypass_parent': 0, + 'proxy.config.http.no_dns_just_forward_to_parent': 1, + 'proxy.config.http.parent_proxy.mark_down_hostdb': 0, + 'proxy.config.http.parent_proxy.self_detect': 0, + }) + + ts.Disk.File(ts.Variables.CONFIGDIR + "/strategies.yaml", id="strategies", typename="ats:config") + s = ts.Disk.strategies + s.AddLine("groups:") + s.AddLine(" - &g1") + dns.addRecords(records={"next_hop0": ["127.0.0.1"]}) + s.AddLine(f" - host: next_hop0") + s.AddLine(f" protocol:") + s.AddLine(f" - scheme: http") + s.AddLine(f" port: {ts_nh.Variables.port}") + # The health check URL does not seem to be used currently. + # s.AddLine(f" health_check_url: http://next_hop0:{ts_nh.Variables.port}") + s.AddLine(f" weight: 1.0") + s.AddLines( + [ + "strategies:", + " - strategy: the-strategy", + " policy: consistent_hash", + " hash_key: path", + " go_direct: false", + " parent_is_proxy: true", + " ignore_self_detect: true", + " groups:", + " - *g1", + " scheme: http", + ]) + + # Fallover not currently tested. + # + # s.AddLines([ + # " fallover:", + # " max_simple_retries: 2", + # " ring_mode: exhaust_ring", + # " response_codes:", + # " - 404", + # " health_check:", + # " - passive",]) + + ts.Disk.remap_config.AddLine("map http://dummy.com http://not_used @strategy=the-strategy") + + tr = urtest.AddTestRun() + tr.Processes.Default.StartBefore(server) + tr.Processes.Default.StartBefore(dns) + tr.Processes.Default.StartBefore(ts_nh) + tr.Processes.Default.StartBefore(urtest.Processes.ts) + tr.Processes.Default.Command = 'echo start TS, HTTP server, DNS server and next hop TSes' + tr.Processes.Default.ReturnCode = 0 + + tr = urtest.AddTestRun() + tr.MakeCurlCommand(f'--verbose --proxy 127.0.0.1:{ts.Variables.port} http://dummy.com/obj0', ts=ts) + tr.Processes.Default.Streams.stdout = "body.gold" + tr.Processes.Default.ReturnCode = 0 + + # Wait enough time that the item should be aged out of the cache. + tr = urtest.AddTestRun("Wait for cached object to be stale.") + tr.Processes.Default.Command = "sleep 4" + tr.StillRunningAfter = ts + + # Request should come back as 200 + tr = urtest.AddTestRun() + tr.MakeCurlCommand(f'--verbose --proxy 127.0.0.1:{ts.Variables.port} http://dummy.com/obj0', ts=ts) + tr.Processes.Default.Streams.stdout = "body.gold" + tr.Processes.Default.ReturnCode = 0 + + tr = urtest.AddTestRun() + tr.Processes.Default.Command = "grep 'Stale in cache' nh_trace*.log | cat" + tr.Processes.Default.Streams.stdout.Content = Testers.ContainsExpression( + "Stale in cache", "Expected stale content to be evaluated") + tr.Processes.Default.ReturnCode = 0 + urtest.execute() diff --git a/tests/gold_tests/next_hop/zzz_strategies_peer/body.gold b/tests/uranium_tests/next_hop/zzz_strategies_peer/body.gold similarity index 100% rename from tests/gold_tests/next_hop/zzz_strategies_peer/body.gold rename to tests/uranium_tests/next_hop/zzz_strategies_peer/body.gold diff --git a/tests/uranium_tests/next_hop/zzz_strategies_peer/test_zzz_strategies_peer.py b/tests/uranium_tests/next_hop/zzz_strategies_peer/test_zzz_strategies_peer.py new file mode 100644 index 00000000000..f4a39b1c9d5 --- /dev/null +++ b/tests/uranium_tests/next_hop/zzz_strategies_peer/test_zzz_strategies_peer.py @@ -0,0 +1,198 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_zzz_strategies_peer(urtest: UraniumTest) -> None: + ''' + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + urtest.Summary = ''' + Test next hop selection using strategies.yaml with consistent hashing, with peering. + ''' + + # This test must run after tls_conn_timeout and is listed in tests/serial_tests.txt + # to preserve that ordering. + + # Define and populate MicroServer. + # + server = urtest.MakeOriginServer("server") + response_header = { + "headers": "HTTP/1.1 200 OK\r\n" + "Connection: close\r\n" + "Cache-control: max-age=85000\r\n" + "\r\n", + "timestamp": "1469733493.993", + "body": "This is the body.\n" + } + num_object = 16 + for i in range(num_object): + request_header = { + "headers": + f"GET /obj{i} HTTP/1.1\r\n" + "Host: does.not.matter\r\n" # But cannot be omitted. + "\r\n", + "timestamp": "1469733493.993", + "body": "" + } + server.addResponse("sessionlog.json", request_header, response_header) + + dns = urtest.MakeDNServer("dns") + + # Define upstream trafficserver instances. + # + num_upstream = 6 + ts_upstream = [] + for i in range(num_upstream): + ts = urtest.MakeATSProcess(f"ts_upstream{i}") + dns.addRecords(records={f"ts_upstream{i}": ["127.0.0.1"]}) + ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'http|dns', + 'proxy.config.dns.nameservers': f"127.0.0.1:{dns.Variables.Port}", + 'proxy.config.dns.resolv_conf': "NULL", + }) + ts.Disk.remap_config.AddLine(f"map / http://127.0.0.1:{server.Variables.Port}") + ts_upstream.append(ts) + + # Define peer trafficserver instances. + # + num_peer = 8 + ts_peer = [] + for i in range(num_peer): + ts = urtest.MakeATSProcess(f"ts_peer{i}", use_traffic_out=False, command=f"traffic_server 2> trace_peer{i}.log") + ts.ReturnCode = Any(0, -2) + ts_peer.append(ts) + for i in range(num_peer): + ts = ts_peer[i] + dns.addRecords(records={f"ts_peer{i}": ["127.0.0.1"]}) + + ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'http|dns|parent|next_hop|host_statuses|hostdb|cachekey', + 'proxy.config.dns.nameservers': f"127.0.0.1:{dns.Variables.Port}", # Only nameservers if resolv_conf NULL. + 'proxy.config.dns.resolv_conf': "NULL", # This defaults to /etc/resvolv.conf (OS namesevers) if not NULL. + 'proxy.config.http.cache.http': 1, + 'proxy.config.http.cache.required_headers': 0, + 'proxy.config.http.uncacheable_requests_bypass_parent': 0, + 'proxy.config.http.no_dns_just_forward_to_parent': 1, + 'proxy.config.http.parent_proxy.mark_down_hostdb': 0, + 'proxy.config.http.parent_proxy.self_detect': 1, + }) + + ts.Disk.File(ts.Variables.CONFIGDIR + "/strategies.yaml", id="strategies", typename="ats:config") + s = ts.Disk.strategies + s.AddLine("groups:") + s.AddLine(" - &peer_group") + for j in range(num_peer): + s.AddLine(f" - host: ts_peer{j}") + s.AddLine(f" protocol:") + s.AddLine(f" - scheme: http") + s.AddLine(f" port: {ts_peer[j].Variables.port}") + # The health check URL does not seem to be used currently. + # s.AddLine(f" health_check_url: http://ts_peer{j}:{ts_peer[j].Variables.port}") + s.AddLine(f" weight: 1.0") + s.AddLine(" - &peer_upstream") + for j in range(num_upstream): + s.AddLine(f" - host: ts_upstream{j}") + s.AddLine(f" protocol:") + s.AddLine(f" - scheme: http") + s.AddLine(f" port: {ts_upstream[j].Variables.port}") + # The health check URL does not seem to be used currently. + # s.AddLine(f" health_check_url: http://ts_upstream{j}:{ts_upstream[j].Variables.port}") + s.AddLine(f" weight: 1.0") + s.AddLines( + [ + "strategies:", + " - strategy: the-strategy", + " policy: consistent_hash", + " hash_key: cache_key", + " go_direct: false", + " parent_is_proxy: true", + " cache_peer_result: false", + " ignore_self_detect: false", + " groups:", + " - *peer_group", + " - *peer_upstream", + " scheme: http", + " failover:", + " ring_mode: peering_ring", + f" self: ts_peer{i}", + #" max_simple_retries: 2", + #" response_codes:", + #" - 404", + #" health_check:", + #" - passive", + ]) + + suffix = " @strategy=the-strategy @plugin=cachekey.so @pparam=--uri-type=remap @pparam=--capture-prefix=/(.*):(.*)/$1/" + ts.Disk.remap_config.AddLines( + [ + "map http://dummy.com http://not_used" + suffix, + "map http://not_used http://also_not_used" + suffix, + ]) + + tr = urtest.AddTestRun() + tr.Processes.Default.StartBefore(server) + tr.Processes.Default.StartBefore(dns) + for i in range(num_peer): + tr.Processes.Default.StartBefore(ts_peer[i]) + for i in range(num_upstream): + tr.Processes.Default.StartBefore(ts_upstream[i]) + tr.Processes.Default.Command = 'echo start peer and upstream TSes, HTTP server and DNS server' + tr.Processes.Default.ReturnCode = 0 + + for i in range(num_object): + tr = urtest.AddTestRun() + tr.MakeCurlCommand( + f'--verbose --proxy 127.0.0.1:{ts_peer[i % num_peer].Variables.port} http://dummy.com/obj{i}', ts=ts_peer[i % num_peer]) + tr.Processes.Default.Streams.stdout = "body.gold" + tr.Processes.Default.ReturnCode = 0 + + for i in range(num_object): + tr = urtest.AddTestRun() + # num_peer must not be a multiple of 3 + tr.MakeCurlCommand( + f'--verbose --proxy 127.0.0.1:{ts_peer[(i * 3) % num_peer].Variables.port} http://dummy.com/obj{i}', + ts=ts_peer[(i * 3) % num_peer]) + tr.Processes.Default.Streams.stdout = "body.gold" + tr.Processes.Default.ReturnCode = 0 + + tr = urtest.AddTestRun() + tr.Processes.Default.Command = ( + "grep -e '^+++' -e '^[A-Z].*TTP/' -e '^.alts. --' -e 'ParentResultType::SPECIFIED' trace_peer*.log" + " | sed 's/^.*(next_hop) [^ ]* //' | sed 's/[.][0-9]*$$//'") + tr.Processes.Default.Streams.stdout = "trace.gold" + tr.Processes.Default.ReturnCode = 0 + urtest.execute() diff --git a/tests/gold_tests/next_hop/zzz_strategies_peer/trace.gold b/tests/uranium_tests/next_hop/zzz_strategies_peer/trace.gold similarity index 100% rename from tests/gold_tests/next_hop/zzz_strategies_peer/trace.gold rename to tests/uranium_tests/next_hop/zzz_strategies_peer/trace.gold diff --git a/tests/gold_tests/next_hop/zzz_strategies_peer2/body.gold b/tests/uranium_tests/next_hop/zzz_strategies_peer2/body.gold similarity index 100% rename from tests/gold_tests/next_hop/zzz_strategies_peer2/body.gold rename to tests/uranium_tests/next_hop/zzz_strategies_peer2/body.gold diff --git a/tests/uranium_tests/next_hop/zzz_strategies_peer2/test_zzz_strategies_peer2.py b/tests/uranium_tests/next_hop/zzz_strategies_peer2/test_zzz_strategies_peer2.py new file mode 100644 index 00000000000..32ce449abb7 --- /dev/null +++ b/tests/uranium_tests/next_hop/zzz_strategies_peer2/test_zzz_strategies_peer2.py @@ -0,0 +1,190 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_zzz_strategies_peer2(urtest: UraniumTest) -> None: + ''' + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + urtest.Summary = ''' + Test next hop using strategies.yaml with consistent hashing, with peering, and no upstream group" + ''' + + # This test must run after tls_conn_timeout and is listed in tests/serial_tests.txt + # to preserve that ordering. + + # Define and populate MicroServer. + # + server = urtest.MakeOriginServer("server") + response_header = { + "headers": "HTTP/1.1 200 OK\r\n" + "Connection: close\r\n" + "Cache-control: max-age=85000\r\n" + "\r\n", + "timestamp": "1469733493.993", + "body": "This is the body.\n" + } + num_object = 16 + for i in range(num_object): + request_header = { + "headers": + f"GET /obj{i} HTTP/1.1\r\n" + "Host: does.not.matter\r\n" # But cannot be omitted. + "\r\n", + "timestamp": "1469733493.993", + "body": "" + } + server.addResponse("sessionlog.json", request_header, response_header) + + dns = urtest.MakeDNServer("dns") + + # Define upstream trafficserver instances. + # + num_upstream = 6 + ts_upstream = [] + for i in range(num_upstream): + ts = urtest.MakeATSProcess(f"ts_upstream{i}") + dns.addRecords(records={f"ts_upstream{i}": ["127.0.0.1"]}) + ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'http|dns', + 'proxy.config.dns.nameservers': f"127.0.0.1:{dns.Variables.Port}", + 'proxy.config.dns.resolv_conf': "NULL", + }) + ts.Disk.remap_config.AddLine(f"map / http://127.0.0.1:{server.Variables.Port}") + ts_upstream.append(ts) + + # Define peer trafficserver instances. + # + num_peer = 8 + ts_peer = [] + for i in range(num_peer): + ts = urtest.MakeATSProcess(f"ts_peer{i}", use_traffic_out=False, command=f"traffic_server 2> trace_peer{i}.log") + ts.ReturnCode = Any(0, -2) + ts_peer.append(ts) + for i in range(num_peer): + ts = ts_peer[i] + dns.addRecords(records={f"ts_peer{i}": ["127.0.0.1"]}) + + ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'http|dns|parent|next_hop|host_statuses|hostdb', + 'proxy.config.dns.nameservers': f"127.0.0.1:{dns.Variables.Port}", # Only nameservers if resolv_conf NULL. + 'proxy.config.dns.resolv_conf': "NULL", # This defaults to /etc/resvolv.conf (OS namesevers) if not NULL. + 'proxy.config.http.cache.http': 1, + 'proxy.config.http.cache.required_headers': 0, + 'proxy.config.http.uncacheable_requests_bypass_parent': 0, + 'proxy.config.http.no_dns_just_forward_to_parent': 0, + 'proxy.config.http.parent_proxy.mark_down_hostdb': 0, + 'proxy.config.http.parent_proxy.self_detect': 1, + }) + + ts.Disk.File(ts.Variables.CONFIGDIR + "/strategies.yaml", id="strategies", typename="ats:config") + s = ts.Disk.strategies + s.AddLine("groups:") + s.AddLine(" - &peer_group") + for j in range(num_peer): + s.AddLine(f" - host: ts_peer{j}") + s.AddLine(f" protocol:") + s.AddLine(f" - scheme: http") + s.AddLine(f" port: {ts_peer[j].Variables.port}") + # The health check URL does not seem to be used currently. + # s.AddLine(f" health_check_url: http://ts_peer{j}:{ts_peer[j].Variables.port}") + s.AddLine(f" weight: 1.0") + s.AddLines( + [ + "strategies:", + " - strategy: the-strategy", + " policy: consistent_hash", + " hash_key: path", + " go_direct: true", + " parent_is_proxy: true", + " cache_peer_result: false", + " ignore_self_detect: false", + " groups:", + " - *peer_group", + " scheme: http", + " failover:", + " ring_mode: peering_ring", + f" self: ts_peer{i}", + #" max_simple_retries: 2", + #" response_codes:", + #" - 404", + #" health_check:", + #" - passive", + ]) + + for i in range(num_upstream): + prefix = f"http://ts_upstream{i}:{ts_upstream[i].Variables.port}/" + ts.Disk.remap_config.AddLine(f"map {prefix} {prefix} @strategy=the-strategy") + + tr = urtest.AddTestRun() + tr.Processes.Default.StartBefore(server) + tr.Processes.Default.StartBefore(dns) + for i in range(num_peer): + tr.Processes.Default.StartBefore(ts_peer[i]) + for i in range(num_upstream): + tr.Processes.Default.StartBefore(ts_upstream[i]) + tr.Processes.Default.Command = 'echo start peer and upstream TSes, HTTP server and DNS server' + tr.Processes.Default.ReturnCode = 0 + + for i in range(num_object): + tr = urtest.AddTestRun() + tr.MakeCurlCommand( + f'--verbose --proxy 127.0.0.1:{ts_peer[i % num_peer].Variables.port} http://ts_upstream0:{ts_upstream[0].Variables.port}/obj{i}', + ts=ts_peer[i % num_peer]) + tr.Processes.Default.Streams.stdout = "body.gold" + tr.Processes.Default.ReturnCode = 0 + + for i in range(num_object): + tr = urtest.AddTestRun() + # num_peer must not be a multiple of 3 + tr.MakeCurlCommand( + f'--verbose --proxy 127.0.0.1:{ts_peer[(i * 3) % num_peer].Variables.port} http://ts_upstream0:{ts_upstream[0].Variables.port}/obj{i}', + ts=ts_peer[(i * 3) % num_peer]) + tr.Processes.Default.Streams.stdout = "body.gold" + tr.Processes.Default.ReturnCode = 0 + + normalize_ports = "" + for i in range(num_upstream): + normalize_ports += f" | sed 's/:{ts_upstream[i].Variables.port}/:UP_PORT{i}/'" + + tr = urtest.AddTestRun() + tr.Processes.Default.Command = ( + "grep -e '^+++' -e '^[A-Z].*TTP/' -e '^.alts. --' -e 'ParentResultType::SPECIFIED' trace_peer*.log" + " | sed 's/^.*(next_hop) [^ ]* //' | sed 's/[.][0-9]*$$//' " + normalize_ports) + tr.Processes.Default.Streams.stdout = "trace.gold" + tr.Processes.Default.ReturnCode = 0 + urtest.execute() diff --git a/tests/gold_tests/next_hop/zzz_strategies_peer2/trace.gold b/tests/uranium_tests/next_hop/zzz_strategies_peer2/trace.gold similarity index 100% rename from tests/gold_tests/next_hop/zzz_strategies_peer2/trace.gold rename to tests/uranium_tests/next_hop/zzz_strategies_peer2/trace.gold diff --git a/tests/gold_tests/null_transform/gold/null_transform-200.gold b/tests/uranium_tests/null_transform/gold/null_transform-200.gold similarity index 100% rename from tests/gold_tests/null_transform/gold/null_transform-200.gold rename to tests/uranium_tests/null_transform/gold/null_transform-200.gold diff --git a/tests/gold_tests/null_transform/gold/null_transform-tag.gold b/tests/uranium_tests/null_transform/gold/null_transform-tag.gold similarity index 100% rename from tests/gold_tests/null_transform/gold/null_transform-tag.gold rename to tests/uranium_tests/null_transform/gold/null_transform-tag.gold diff --git a/tests/uranium_tests/null_transform/test_null_transform.py b/tests/uranium_tests/null_transform/test_null_transform.py new file mode 100644 index 00000000000..302b6076588 --- /dev/null +++ b/tests/uranium_tests/null_transform/test_null_transform.py @@ -0,0 +1,86 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_null_transform(urtest: UraniumTest) -> None: + ''' + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + urtest.Summary = ''' + Test a basic null transform plugin + ''' + + urtest.SkipUnless(Condition.PluginExists('null_transform.so')) + + urtest.ContinueOnFail = True + + # Define default ATS + ts = urtest.MakeATSProcess("ts") + server = urtest.MakeOriginServer("server") + + urtest.testName = "" + request_header = {"headers": "GET / HTTP/1.1\r\nHost: www.example.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + # Expected response from origin server + response_header = { + "headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", + "timestamp": "1469733493.993", + "body": "This is expected response." + } + + # Add response the server dictionary + server.addResponse("sessionfile.log", request_header, response_header) + ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.show_location': 0, + 'proxy.config.diags.debug.tags': 'null_transform' + }) + ts.Disk.remap_config.AddLine('map http://www.example.com http://127.0.0.1:{0}'.format(server.Variables.Port)) + + # Load plugin + urtest.PrepareInstalledPlugin('null_transform.so', ts) + + # www.example.com Host + tr = urtest.AddTestRun() + tr.MakeCurlCommand( + '--proxy 127.0.0.1:{0} "http://www.example.com" --verbose -H "Proxy-Connection: keep-alive"'.format(ts.Variables.port), + ts=ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.StartBefore(server) + tr.Processes.Default.StartBefore(urtest.Processes.ts) + tr.Processes.Default.Streams.stderr = "gold/null_transform-200.gold" + tr.StillRunningAfter = server + + # Check Plugin Loading Information + ts.Disk.traffic_out.Content = "gold/null_transform-tag.gold" + urtest.execute() diff --git a/tests/gold_tests/origin_connection/gold/two_200_ok.gold b/tests/uranium_tests/origin_connection/gold/two_200_ok.gold similarity index 100% rename from tests/gold_tests/origin_connection/gold/two_200_ok.gold rename to tests/uranium_tests/origin_connection/gold/two_200_ok.gold diff --git a/tests/gold_tests/origin_connection/gold/two_503_congested.gold b/tests/uranium_tests/origin_connection/gold/two_503_congested.gold similarity index 100% rename from tests/gold_tests/origin_connection/gold/two_503_congested.gold rename to tests/uranium_tests/origin_connection/gold/two_503_congested.gold diff --git a/tests/gold_tests/origin_connection/minimum_keep_alive.test.yaml b/tests/uranium_tests/origin_connection/minimum_keep_alive.test.yaml similarity index 99% rename from tests/gold_tests/origin_connection/minimum_keep_alive.test.yaml rename to tests/uranium_tests/origin_connection/minimum_keep_alive.test.yaml index 5ce491c5e04..a5ad215c487 100644 --- a/tests/gold_tests/origin_connection/minimum_keep_alive.test.yaml +++ b/tests/uranium_tests/origin_connection/minimum_keep_alive.test.yaml @@ -17,7 +17,7 @@ meta: version: "1.0" -autest: +urtest: description: 'Retain the per-server minimum without a maximum or metrics' server: diff --git a/tests/gold_tests/origin_connection/per_server_metric_enabled.replay.yaml b/tests/uranium_tests/origin_connection/per_server_metric_enabled.replay.yaml similarity index 100% rename from tests/gold_tests/origin_connection/per_server_metric_enabled.replay.yaml rename to tests/uranium_tests/origin_connection/per_server_metric_enabled.replay.yaml diff --git a/tests/gold_tests/origin_connection/slow_servers.replay.yaml b/tests/uranium_tests/origin_connection/slow_servers.replay.yaml similarity index 100% rename from tests/gold_tests/origin_connection/slow_servers.replay.yaml rename to tests/uranium_tests/origin_connection/slow_servers.replay.yaml diff --git a/tests/uranium_tests/origin_connection/test_per_server_connection_max.py b/tests/uranium_tests/origin_connection/test_per_server_connection_max.py new file mode 100644 index 00000000000..429f1bc4508 --- /dev/null +++ b/tests/uranium_tests/origin_connection/test_per_server_connection_max.py @@ -0,0 +1,203 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_per_server_connection_max(urtest: UraniumTest) -> None: + ''' + Verify the behavior of proxy.config.http.per_server.connection.max. + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + urtest.Summary = __doc__ + import os + + urtest.SkipIf(Condition.CurlUsingUnixDomainSocket()) + + class PerServerConnectionMaxTest: + """Define an object to test our max origin connection behavior.""" + + _replay_file: str = 'slow_servers.replay.yaml' + _origin_max_connections: int = 3 + + def __init__(self) -> None: + """Configure the test processes in preparation for the TestRun.""" + self._configure_dns() + self._configure_server() + self._configure_trafficserver() + + def _configure_dns(self) -> None: + """Configure a nameserver for the test.""" + self._dns = urtest.MakeDNServer("dns", default='127.0.0.1') + + def _configure_server(self) -> None: + """Configure the server to be used in the test.""" + self._server = urtest.MakeVerifierServerProcess('server', self._replay_file) + + def _configure_trafficserver(self) -> None: + """Configure Traffic Server to be used in the test.""" + self._ts = urtest.MakeATSProcess("ts1") + self._ts.Disk.remap_config.AddLine(f'map / http://127.0.0.1:{self._server.Variables.http_port}') + self._ts.Disk.records_config.update( + { + 'proxy.config.dns.nameservers': f"127.0.0.1:{self._dns.Variables.Port}", + 'proxy.config.dns.resolv_conf': 'NULL', + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'http|conn_track', + 'proxy.config.http.per_server.connection.max': self._origin_max_connections, + 'proxy.config.http.per_server.connection.metric_enabled': 1, + 'proxy.config.http.per_server.connection.metric_prefix': 'foo', + 'proxy.config.http.per_server.connection.match': 'port', + }) + self._ts.Disk.diags_log.Content += Testers.ContainsExpression( + f'WARNING:.*too many connections:.*limit={self._origin_max_connections}', + 'Verify the user is warned about the connection limit being hit.') + + def _test_metrics(self) -> None: + """Use traffic_ctl to test metrics.""" + tr = urtest.AddTestRun("Check connection metrics") + tr.Processes.Default.Command = 'traffic_ctl metric match per_server' + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Env = self._ts.Env + tr.Processes.Default.Streams.All = Testers.ContainsExpression( + f'per_server.total_connection.foo.127.0.0.1:{self._server.Variables.http_port} 4', + 'incorrect statistic return, or possible error.') + tr.Processes.Default.Streams.All = Testers.ContainsExpression( + f'per_server.blocked_connection.foo.127.0.0.1:{self._server.Variables.http_port} 1', + 'incorrect statistic return, or possible error.') + + def run(self) -> None: + """Configure the TestRun.""" + tr = urtest.AddTestRun('Verify we enforce proxy.config.http.per_server.connection.max') + tr.Processes.Default.StartBefore(self._dns) + tr.Processes.Default.StartBefore(self._server) + tr.Processes.Default.StartBefore(self._ts) + + tr.AddVerifierClientProcess('client', self._replay_file, http_ports=[self._ts.Variables.port]) + + self._test_metrics() + + class ConnectMethodTest: + """Test our max origin connection behavior with CONNECT traffic.""" + + _process_counter: int = 0 + _client_counter: int = 0 + + def __init__(self, max_conn) -> None: + """Configure the server processes in preparation for the TestRun.""" + self._configure_dns() + self._configure_origin_server() + self._configure_trafficserver(max_conn) + ConnectMethodTest._process_counter += 1 + + def _configure_dns(self) -> None: + """Configure a nameserver for the test.""" + self._dns = urtest.MakeDNServer(f"dns_{ConnectMethodTest._process_counter}", default='127.0.0.1') + + def _configure_origin_server(self) -> None: + """Configure the httpbin origin server.""" + self._server = urtest.MakeHttpBinServer(f"server_{ConnectMethodTest._process_counter}") + + def _configure_trafficserver(self, max_conn) -> None: + self._ts = urtest.MakeATSProcess("ts2_" + str(max_conn)) + + self._ts.Disk.records_config.update( + { + 'proxy.config.dns.nameservers': f"127.0.0.1:{self._dns.Variables.Port}", + 'proxy.config.dns.resolv_conf': 'NULL', + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'http|dns|hostdb|conn_track', + 'proxy.config.http.server_ports': f"{self._ts.Variables.port} {self._ts.Variables.uds_path}", + 'proxy.config.http.connect_ports': f"{self._server.Variables.Port}", + 'proxy.config.http.per_server.connection.metric_enabled': 1, + 'proxy.config.http.per_server.connection.max': max_conn, + }) + + self._ts.Disk.remap_config.AddLines( + [ + f"map http://foo.com/ http://www.this.origin.com:{self._server.Variables.Port}/", + ]) + self._ts.addPrivateConnectAllowYaml() + + def _configure_client_with_slow_response(self, tr) -> 'Test.Process': + """Configure a client to perform a CONNECT request with a slow response from the server.""" + p = tr.Processes.Process(f'slow_client_{ConnectMethodTest._client_counter}') + ConnectMethodTest._client_counter += 1 + tr.MakeCurlCommand(f"-v --fail -s -p -x 127.0.0.1:{self._ts.Variables.port} 'http://foo.com/delay/2'", p=p, ts=self._ts) + return p + + def _test_metrics(self, blocked) -> None: + """Use traffic_ctl to test metrics.""" + tr = urtest.AddTestRun("Check connection metrics") + tr.Processes.Default.Command = 'traffic_ctl metric match per_server; sleep 2' + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Env = self._ts.Env + tr.Processes.Default.Streams.All = Testers.ContainsExpression( + f'per_server.total_connection.www.this.origin.com.127.0.0.1:{self._server.Variables.Port} 5', + 'incorrect statistic return, or possible error.') + tr.Processes.Default.Streams.All = Testers.ContainsExpression( + f'per_server.blocked_connection.www.this.origin.com.127.0.0.1:{self._server.Variables.Port} {blocked}', + 'incorrect statistic return, or possible error.') + + def run(self, blocked, gold_file) -> None: + """Verify per_server.connection.max with CONNECT traffic.""" + tr = urtest.AddTestRun() + tr.Processes.Default.StartBefore(self._dns) + tr.Processes.Default.StartBefore(self._server) + tr.Processes.Default.StartBefore(self._ts) + + slow0 = self._configure_client_with_slow_response(tr) + slow1 = self._configure_client_with_slow_response(tr) + slow2 = self._configure_client_with_slow_response(tr) + + tr.Processes.Default.StartBefore(slow0) + tr.Processes.Default.StartBefore(slow1) + tr.Processes.Default.StartBefore(slow2) + + # With those three slow transactions going on in the background, do a + # couple quick transactions and make sure they both reply with a 503 + # response. + tr.MakeCurlCommandMulti( + f"sleep 1; {{curl}} -v --fail -s -p -x 127.0.0.1:{self._ts.Variables.port} 'http://foo.com/get'" + f"--next -v --fail -s -p -x 127.0.0.1:{self._ts.Variables.port} 'http://foo.com/get'", + ts=self._ts) + # Curl will have a 22 exit code if it receives a 5XX response (and we + # expect a 503). + tr.Processes.Default.ReturnCode = 22 if blocked else 0 + tr.Processes.Default.Streams.stderr = gold_file + tr.Processes.Default.TimeOut = 3 + + self._test_metrics(blocked) + + PerServerConnectionMaxTest().run() + ConnectMethodTest(3).run(blocked=2, gold_file="gold/two_503_congested.gold") + ConnectMethodTest(0).run(blocked=0, gold_file="gold/two_200_ok.gold") + urtest.execute() diff --git a/tests/uranium_tests/origin_connection/test_per_server_metric_enabled.py b/tests/uranium_tests/origin_connection/test_per_server_metric_enabled.py new file mode 100644 index 00000000000..4749fc20b0c --- /dev/null +++ b/tests/uranium_tests/origin_connection/test_per_server_metric_enabled.py @@ -0,0 +1,132 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_per_server_metric_enabled(urtest: UraniumTest) -> None: + ''' + Verify per_server connection tracking stays accurate when + proxy.config.http.per_server.connection.metric_enabled is set. + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + urtest.Summary = __doc__ + + urtest.SkipIf(Condition.CurlUsingUnixDomainSocket()) + + class PerServerMetricEnabledTest: + """Verify that enabling the per_server metrics does not break the group connection count. + + Enabling proxy.config.http.per_server.connection.metric_enabled used to + make the metric the authoritative connection count, leaving the group's + internal counter at zero. That made every pooled origin session look like + it was at or below proxy.config.http.per_server.connection.min, so + keep-alive origin connections were never reaped on inactivity timeout. + """ + + _replay_file: str = 'per_server_metric_enabled.replay.yaml' + _keep_alive_timeout: int = 2 + + def __init__(self) -> None: + """Configure the test processes in preparation for the TestRun.""" + self._configure_server() + self._configure_trafficserver() + + def _configure_server(self) -> None: + """Configure the origin server to be used in the test.""" + self._server = urtest.MakeVerifierServerProcess('metric_enabled_server', self._replay_file) + + def _configure_trafficserver(self) -> None: + """Configure Traffic Server to be used in the test.""" + self._ts = urtest.MakeATSProcess("ts_metric_enabled") + self._ts.Disk.remap_config.AddLine(f'map / http://127.0.0.1:{self._server.Variables.http_port}') + self._ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'http_ss|conn_track', + 'proxy.config.http.per_server.connection.metric_enabled': 1, + 'proxy.config.http.per_server.connection.metric_prefix': 'bar', + 'proxy.config.http.per_server.connection.match': 'port', + # No minimum number of keep alive origin connections: the pooled + # connection should be closed once it times out. + 'proxy.config.http.per_server.connection.min': 0, + 'proxy.config.http.keep_alive_no_activity_timeout_out': self._keep_alive_timeout, + 'proxy.config.http.server_session_sharing.pool': 'global', + }) + # The connection count should never be decremented below zero. + self._ts.Disk.diags_log.Content += Testers.ExcludesExpression( + 'Number of tracked connections should be greater than or equal to zero', + 'Verify the group connection count is not double decremented.') + + def _test_connection_is_reaped(self) -> None: + """Verify the idle origin connection is closed once it times out.""" + tr = urtest.AddTestRun("Verify the idle keep-alive origin connection is reaped") + tr.Processes.Default.Command = ( + f'sleep {self._keep_alive_timeout * 3}; ' + 'traffic_ctl metric get proxy.process.http.current_server_connections; ' + 'traffic_ctl metric match per_server') + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Env = self._ts.Env + tr.Processes.Default.Streams.All += Testers.ContainsExpression( + 'proxy.process.http.current_server_connections 0', + 'The idle origin connection should have been closed by the keep-alive timeout.') + tr.Processes.Default.Streams.All += Testers.ContainsExpression( + f'per_server.current_connection.bar.127.0.0.1:{self._server.Variables.http_port} 0', + 'The per_server connection gauge should have been decremented back to zero.') + tr.Processes.Default.Streams.All += Testers.ContainsExpression( + f'per_server.total_connection.bar.127.0.0.1:{self._server.Variables.http_port} 1', + 'A single origin connection should have been tracked.') + + def _test_tracker_info(self) -> None: + """Verify the JSONRPC connection tracker report agrees with the metrics.""" + tr = urtest.AddTestRun("Verify the connection tracker report") + tr.Processes.Default.Command = "traffic_ctl rpc invoke get_connection_tracker_info -p 'table: outbound' -f json" + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Env = self._ts.Env + # Once the connection is released the group count drops to zero and the + # group is removed from the table, so either the table is empty or the + # remaining group reports no current connections. + tr.Processes.Default.Streams.All += Testers.ContainsExpression( + r'"(count|current)":\s*"?0"?', 'The tracker should report no current outbound connections.') + + def run(self) -> None: + """Configure the TestRuns.""" + tr = urtest.AddTestRun('Perform a transaction that leaves a pooled origin connection') + tr.Processes.Default.StartBefore(self._server) + tr.Processes.Default.StartBefore(self._ts) + + tr.AddVerifierClientProcess('metric_enabled_client', self._replay_file, http_ports=[self._ts.Variables.port]) + + self._test_connection_is_reaped() + self._test_tracker_info() + + PerServerMetricEnabledTest().run() + urtest.execute() diff --git a/tests/uranium_tests/parent_config/test_parent_config_reload.py b/tests/uranium_tests/parent_config/test_parent_config_reload.py new file mode 100644 index 00000000000..90dfaf33857 --- /dev/null +++ b/tests/uranium_tests/parent_config/test_parent_config_reload.py @@ -0,0 +1,98 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_parent_config_reload(urtest: UraniumTest) -> None: + ''' + Test parent.config reload via ConfigRegistry. + + Verifies that: + 1. parent.config reload works after file touch + 2. Record value change (retry_time) triggers parent reload + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import os + + urtest.Summary = ''' + Test parent.config reload via ConfigRegistry. + ''' + + urtest.ContinueOnFail = True + + ts = urtest.MakeATSProcess("ts") + ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'parent_select|config', + }) + + # Initial parent.config with a simple rule + ts.Disk.parent_config.AddLine('dest_domain=example.com parent="origin.example.com:80"') + + config_dir = ts.Variables.CONFIGDIR + + # ================================================================ + # Test 1: Touch parent.config → reload → handler fires + # ================================================================ + + tr = urtest.AddTestRun("Touch parent.config") + tr.Processes.Default.StartBefore(ts) + tr.Processes.Default.Command = f"sleep 3 && touch {os.path.join(config_dir, 'parent.config')} && sleep 1" + tr.Processes.Default.ReturnCode = 0 + tr.StillRunningAfter = ts + + tr = urtest.AddConfigReload(ts, expect_tasks=["parent.config"], description="Reload after parent.config touch") + tr.StillRunningAfter = ts + + # ================================================================ + # Test 2: Change retry_time record value → triggers parent reload + # No file touch, no explicit config reload — the + # RecRegisterConfigUpdateCb fires automatically. + # ================================================================ + + tr = urtest.AddTestRun("Change parent retry_time record value") + p = tr.Processes.Process("reload-2") + p.Command = ("traffic_ctl config set proxy.config.http.parent_proxy.retry_time 60; " + "sleep 30") + p.Env = ts.Env + p.ReturnCode = Any(0, -2) + # Wait for the 3rd "finished loading" + p.Ready = When.FileContains(ts.Disk.diags_log.Name, "parent.config finished loading", 3) + p.Timeout = 20 + tr.Processes.Default.StartBefore(p) + ## TODO: we should have an extension like When.ReloadCompleted(token, success) to validate this instead of parsing + ## diags. + tr.Processes.Default.Command = 'echo "waiting for parent.config reload after record change"' + tr.TimeOut = 25 + tr.StillRunningAfter = ts + urtest.execute() diff --git a/tests/gold_tests/parent_proxy/replays/parent_retry_failure_stays_down.test.yaml b/tests/uranium_tests/parent_proxy/replays/parent_retry_failure_stays_down.test.yaml similarity index 99% rename from tests/gold_tests/parent_proxy/replays/parent_retry_failure_stays_down.test.yaml rename to tests/uranium_tests/parent_proxy/replays/parent_retry_failure_stays_down.test.yaml index d2c5fc5bd88..eabee3181a0 100644 --- a/tests/gold_tests/parent_proxy/replays/parent_retry_failure_stays_down.test.yaml +++ b/tests/uranium_tests/parent_proxy/replays/parent_retry_failure_stays_down.test.yaml @@ -36,7 +36,7 @@ meta: version: "1.0" -autest: +urtest: description: 'A failed retry probe must not restore a degraded parent' server: diff --git a/tests/gold_tests/parent_proxy/replays/parent_retry_non_retryable_post.test.yaml b/tests/uranium_tests/parent_proxy/replays/parent_retry_non_retryable_post.test.yaml similarity index 99% rename from tests/gold_tests/parent_proxy/replays/parent_retry_non_retryable_post.test.yaml rename to tests/uranium_tests/parent_proxy/replays/parent_retry_non_retryable_post.test.yaml index ab1a8de0b46..789ebd391b1 100644 --- a/tests/gold_tests/parent_proxy/replays/parent_retry_non_retryable_post.test.yaml +++ b/tests/uranium_tests/parent_proxy/replays/parent_retry_non_retryable_post.test.yaml @@ -22,7 +22,7 @@ meta: version: "1.0" -autest: +urtest: description: 'Parent must not be marked down for a non-retryable POST' server: diff --git a/tests/gold_tests/parent_proxy/replays/parent_retry_success_restores.test.yaml b/tests/uranium_tests/parent_proxy/replays/parent_retry_success_restores.test.yaml similarity index 99% rename from tests/gold_tests/parent_proxy/replays/parent_retry_success_restores.test.yaml rename to tests/uranium_tests/parent_proxy/replays/parent_retry_success_restores.test.yaml index 91a8942d53e..140b66d3fe4 100644 --- a/tests/gold_tests/parent_proxy/replays/parent_retry_success_restores.test.yaml +++ b/tests/uranium_tests/parent_proxy/replays/parent_retry_success_restores.test.yaml @@ -30,7 +30,7 @@ meta: version: "1.0" -autest: +urtest: description: 'A successful retry restores a parent that had been marked down' server: diff --git a/tests/uranium_tests/parent_proxy/test_consistent_hash_ring_walk.py b/tests/uranium_tests/parent_proxy/test_consistent_hash_ring_walk.py new file mode 100644 index 00000000000..877b65e7c9f --- /dev/null +++ b/tests/uranium_tests/parent_proxy/test_consistent_hash_ring_walk.py @@ -0,0 +1,162 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_consistent_hash_ring_walk(urtest: UraniumTest) -> None: + """ + Verify ParentConsistentHash::selectParent() does not waste work walking an + all-down consistent_hash pool. + + A consistent-hash ring has 1024 replica nodes per parent (default + ATSConsistentHash replicas, DEFAULT_PARENT_WEIGHT=1.0), so num_parents*1024 ring + nodes total. When every parent is down, selectParent must walk the ring to + conclude so -- but it must read each parent's HostStatus (the global + host_status_rwlock) at most ONCE, not once per replica node. selectParent tracks + the distinct parents examined and stops as soon as all are rejected, so an + all-down selection over N parents costs exactly N getHostStatus calls -- not the + ~2*N*1024 (two ring passes) the naive walk cost, which put ~8-13 ms of inline + ET_NET CPU per request and wedged the vipd health probe in inc-p1s2-260703. + + Proven by the per-selection "getHostStatus calls: " debug line + (ParentConsistentHash.cc): it must equal the parent count, not a five-figure ring + walk. + + Pool size > MAX_PARENTS (64) on purpose: num_parents is NOT capped at + MAX_PARENTS by the config parser, so the per-selection "seen parent" tracking + must be sized to the actual pool (a fixed [MAX_PARENTS] array would overflow). + Running a >64-parent pool to completion (no crash, correct 502, bounded count) + guards that sizing. + + no_dns_just_forward_to_parent=1 lets ATS skip origin resolution and go straight + to parent selection, so this test needs no DNS server and no origin -- every + parent is HostStatus-DOWN => PARENT_FAIL => 502 before any connect. + """ + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import os + + from ports import get_port + + urtest.Summary = ''' + An all-down consistent_hash parent pool (marked down via HostStatus) drives + selectParent to read each distinct parent's HostStatus exactly once + (O(num_parents)), not once per ring replica node (~2 * num_parents * 1024). + ''' + urtest.ContinueOnFail = True + + # > MAX_PARENTS (64): exercises the pool-sized "seen parent" tracking and would + # overflow a fixed [MAX_PARENTS] array. Distinct names matter -- consistent_hash + # keys the ring on hostname, so identical names would collapse to one ring node. + NUM_PARENTS = 100 + + class ParentDownRingWalkTest: + """All parents HostStatus-DOWN => selectParent reads each parent once => 502.""" + + parent_hostnames = [f'deadparent{i:03d}' for i in range(1, NUM_PARENTS + 1)] + + def __init__(self): + self._setupTS() + + def _setupTS(self): + self.ts = urtest.MakeATSProcess('ts', enable_cache=False) + + # A dead (reserved-but-unbound) port per parent. They are never actually + # connected -- every parent is HostStatus-DOWN so selectParent returns + # PARENT_FAIL before any connect -- but parent.config needs a port. + self._parent_ports = [] + for i in range(len(self.parent_hostnames)): + name = f'dead_parent_port_{i}' + get_port(self.ts, name) + self._parent_ports.append(getattr(self.ts.Variables, name)) + + self.ts.Disk.records_config.update( + { + # Enable only the parent_select debug ctl so the per-selection + # "getHostStatus calls: " line is emitted. + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'parent_select', + # Skip origin DNS: forward straight to parent selection. No DNS + # server / origin needed for this isolation. + 'proxy.config.http.no_dns_just_forward_to_parent': 1, + # self_detect off: this test populates the HostStatus map via + # `traffic_ctl host down` below, not via self_detect (the + # deadparents never resolve to this box anyway). + 'proxy.config.http.parent_proxy.fail_threshold': 10, + 'proxy.config.http.parent_proxy.retry_time': 300, + 'proxy.config.http.parent_proxy.self_detect': 0, + 'proxy.config.url_remap.remap_required': 0, + }) + + # Consistent-hash pool of distinct parent hostnames, go_direct=false so an + # all-down pool yields 502 (not a direct-to-origin fallback). + self._parents = list(zip(self.parent_hostnames, self._parent_ports)) + parent_list = ', '.join(f'{host}:{port}|1' for host, port in self._parents) + self.ts.Disk.parent_config.AddLine( + f'dest_domain=. parent="{parent_list}" round_robin=consistent_hash ' + 'go_direct=false parent_is_proxy=true') + + # The all-down selection is a single findParent call. selectParent reads + # each distinct parent's HostStatus at most once and stops once all are + # rejected, so the per-selection "getHostStatus calls" count equals the + # parent count -- not the ~2*N*1024 five-figure ring walk it cost before. + self.ts.Disk.traffic_out.Content += Testers.ContainsExpression( + r'getHostStatus calls: %d\b' % NUM_PARENTS, + 'selectParent read each distinct parent once (O(num_parents)), not the full ring.') + # It must NOT take the HostStatus lock once per ring replica node (the old + # bug reported a five-figure count). + self.ts.Disk.traffic_out.Content += Testers.ExcludesExpression( + r'getHostStatus calls: [0-9]{5}', 'selectParent must not read HostStatus once per ring replica node.') + + def run(self): + traffic_ctl = os.path.join(self.ts.Variables.BINDIR, 'traffic_ctl') + + # 1. Bring the whole parent pool DOWN via HostStatus (one RPC call). + # Populates hosts_statuses (=> every getHostStatus takes the rwlock) + # and forces host_stat==DOWN (=> the full ring walk, no early exit). + down = urtest.AddTestRun('Mark the entire parent pool down via HostStatus') + down.Processes.Default.StartBefore(self.ts) + down.Processes.Default.Command = f'{traffic_ctl} host down ' + ' '.join(self.parent_hostnames) + down.Processes.Default.Env = self.ts.Env + down.Processes.Default.ReturnCode = 0 + + # 2. One request through the all-down pool -> bounded walk -> 502. A crash + # here (e.g. seen-parent tracking overflowing on a >64 pool) fails the test. + load = urtest.AddTestRun('Request through the all-down pool -> bounded selection -> 502') + load.MakeCurlCommand( + f'-s -o /dev/null -w "%{{http_code}}" --proxy 127.0.0.1:{self.ts.Variables.port} http://example.com/ring-walk-probe', + ts=self.ts) + load.Processes.Default.Streams.stdout = Testers.ContainsExpression('502', 'All parents down => 502.') + load.Processes.Default.ReturnCode = 0 + load.StillRunningAfter = self.ts + + ParentDownRingWalkTest().run() + urtest.execute() diff --git a/tests/uranium_tests/parent_proxy/test_parent_retry.py b/tests/uranium_tests/parent_proxy/test_parent_retry.py new file mode 100644 index 00000000000..4ce4df5afbe --- /dev/null +++ b/tests/uranium_tests/parent_proxy/test_parent_retry.py @@ -0,0 +1,66 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_parent_retry(urtest: UraniumTest) -> None: + """ + Test parent_retry config settings + """ + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + urtest.testName = "Test parent_retry settings" + urtest.ContinueOnFail = True + + class ParentRetryTest: + """ + Test loading parent.config with parent_retry setting enabled + """ + ts_parent_hostname = "localhost:8081" + + def __init__(self): + """Initialize the test.""" + self._configure_ts_child() + + def _configure_ts_child(self): + self.ts_child = urtest.MakeATSProcess("ts_child") + self.ts_child.Disk.parent_config.AddLine( + f'dest_domain=. method=get parent="{self.ts_parent_hostname}" parent_retry=unavailable_server_retry unavailable_server_retry_responses="502,503"' + ) + + def run(self): + tr = urtest.AddTestRun() + tr.Processes.Default.StartBefore(self.ts_child) + tr.MakeCurlCommand(f'"{self.ts_child.Variables.port}" --verbose', ts=self.ts_child) + tr.StillRunningAfter = self.ts_child + + ParentRetryTest().run() + urtest.execute() diff --git a/tests/gold_tests/pipeline/desync_client.py b/tests/uranium_tests/pipeline/desync_client.py similarity index 100% rename from tests/gold_tests/pipeline/desync_client.py rename to tests/uranium_tests/pipeline/desync_client.py diff --git a/tests/gold_tests/pipeline/desync_client_miss.py b/tests/uranium_tests/pipeline/desync_client_miss.py similarity index 100% rename from tests/gold_tests/pipeline/desync_client_miss.py rename to tests/uranium_tests/pipeline/desync_client_miss.py diff --git a/tests/gold_tests/pipeline/desync_server.py b/tests/uranium_tests/pipeline/desync_server.py similarity index 100% rename from tests/gold_tests/pipeline/desync_server.py rename to tests/uranium_tests/pipeline/desync_server.py diff --git a/tests/gold_tests/pipeline/noop_keepalive_client.py b/tests/uranium_tests/pipeline/noop_keepalive_client.py similarity index 100% rename from tests/gold_tests/pipeline/noop_keepalive_client.py rename to tests/uranium_tests/pipeline/noop_keepalive_client.py diff --git a/tests/gold_tests/pipeline/pipeline_client.py b/tests/uranium_tests/pipeline/pipeline_client.py similarity index 100% rename from tests/gold_tests/pipeline/pipeline_client.py rename to tests/uranium_tests/pipeline/pipeline_client.py diff --git a/tests/gold_tests/pipeline/pipeline_server.py b/tests/uranium_tests/pipeline/pipeline_server.py similarity index 100% rename from tests/gold_tests/pipeline/pipeline_server.py rename to tests/uranium_tests/pipeline/pipeline_server.py diff --git a/tests/gold_tests/pipeline/request_framing_client.py b/tests/uranium_tests/pipeline/request_framing_client.py similarity index 100% rename from tests/gold_tests/pipeline/request_framing_client.py rename to tests/uranium_tests/pipeline/request_framing_client.py diff --git a/tests/gold_tests/pipeline/request_framing_server.py b/tests/uranium_tests/pipeline/request_framing_server.py similarity index 100% rename from tests/gold_tests/pipeline/request_framing_server.py rename to tests/uranium_tests/pipeline/request_framing_server.py diff --git a/tests/uranium_tests/pipeline/test_delete_maxforwards_body_desync.py b/tests/uranium_tests/pipeline/test_delete_maxforwards_body_desync.py new file mode 100644 index 00000000000..24bacc38bb0 --- /dev/null +++ b/tests/uranium_tests/pipeline/test_delete_maxforwards_body_desync.py @@ -0,0 +1,109 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_delete_maxforwards_body_desync(urtest: UraniumTest) -> None: + '''Verify a DELETE self-answered from cache drains its request body.''' + + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + from ports import get_port + import sys + + urtest.Summary = ''' + Verify that a DELETE (Max-Forwards: 0) answered from the cache drains its request + body instead of leaving it to be parsed as the next request on the keep-alive + connection. + ''' + + urtest.ContinueOnFail = False + + class TestDeleteBodyDrain: + """A DELETE with Max-Forwards: 0 is answered directly from cache. If the + request body is not drained, its bytes are parsed as the next request on the + keep-alive connection (a CL.0 desync). This drives that path and asserts the + smuggled request never reaches the origin nor produces a second response.""" + + _server_script: str = 'desync_server.py' + _client_script: str = 'desync_client.py' + _hostname: str = 'www.example.com' + + def __init__(self) -> None: + tr = urtest.AddTestRun('DELETE self-response must drain the request body.') + tr.TimeOut = 60 + self._configure_server(tr) + self._configure_traffic_server(tr) + self._configure_client(tr) + + def _configure_server(self, tr: 'TestRun') -> 'Process': + server = tr.Processes.Process('server') + tr.Setup.Copy(self._server_script) + port = get_port(server, 'http_port') + server.Command = f'{sys.executable} {self._server_script} 127.0.0.1 {port}' + server.Ready = When.PortOpenv4(port) + # The origin must serve the warmed path, and must never see the smuggled one. + server.Streams.All += Testers.ContainsExpression(r'ORIGIN_RECV path=\[/\]', 'origin should serve the warmed GET /') + server.Streams.All += Testers.ExcludesExpression('poisoned', 'the smuggled request must never reach the origin') + self._server = server + return server + + def _configure_traffic_server(self, tr: 'TestRun') -> 'Process': + ts = tr.MakeATSProcess('ts', enable_cache=True) + self._ts = ts + ts.Disk.remap_config.AddLine(f'map http://{self._hostname}/ http://127.0.0.1:{self._server.Variables.http_port}/') + ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 0, + 'proxy.config.diags.debug.tags': 'http', + 'proxy.config.http.cache.http': 1, + 'proxy.config.http.insert_age_in_response': 1, + }) + return ts + + def _configure_client(self, tr: 'TestRun') -> 'Process': + client = tr.Processes.Default + tr.Setup.Copy(self._client_script) + client.Command = f'{sys.executable} {self._client_script} 127.0.0.1 {self._ts.Variables.port} {self._hostname}' + client.ReturnCode = 0 + # The DELETE must self-answer 200 from a warm cache, or the hit path was + # never exercised and the test would pass vacuously. + client.Streams.All += Testers.ContainsExpression( + 'DELETE_STATUS=200', 'the DELETE must self-answer 200 from a warm cache') + # The desync signatures: a second (smuggled) response, or smuggled bytes. + client.Streams.All += Testers.ExcludesExpression( + 'SECOND_RESPONSE_RECEIVED=True', 'the client must not receive a smuggled second response') + client.Streams.All += Testers.ExcludesExpression('poisoned', 'no smuggled response bytes should reach the client') + client.StartBefore(self._server) + client.StartBefore(self._ts) + + TestDeleteBodyDrain() + urtest.execute() diff --git a/tests/uranium_tests/pipeline/test_delete_maxfwd_noop_drain.py b/tests/uranium_tests/pipeline/test_delete_maxfwd_noop_drain.py new file mode 100644 index 00000000000..f39c28ec363 --- /dev/null +++ b/tests/uranium_tests/pipeline/test_delete_maxfwd_noop_drain.py @@ -0,0 +1,108 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_delete_maxfwd_noop_drain(urtest: UraniumTest) -> None: + '''Verify the cache-miss (INTERNAL_CACHE_NOOP) DELETE self-response drains its body.''' + + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + from ports import get_port + import sys + + urtest.Summary = 'A DELETE to an uncached path self-answers 404 (NOOP) and must drain its body.' + urtest.ContinueOnFail = False + + class TestDeleteMaxfwdNoopDrain: + """A DELETE to an uncached path is self-answered 404 via INTERNAL_CACHE_NOOP. + + Verify the accompanying request body is drained so its bytes are not framed as + the next request on the connection. + """ + + _server_script: str = 'desync_server.py' + _client_script: str = 'desync_client_miss.py' + _hostname: str = 'www.example.com' + + def __init__(self) -> None: + tr = urtest.AddTestRun('A NOOP-path DELETE self-response must drain its request body.') + tr.TimeOut = 40 + self._configure_server(tr) + self._configure_traffic_server(tr) + self._configure_client(tr) + + def _configure_server(self, tr: 'TestRun') -> 'Process': + """Configure the origin server. + + :param tr: The test run to associate the origin server with. + :return: The origin server process. + """ + server = tr.Processes.Process('server') + tr.Setup.Copy(self._server_script) + port = get_port(server, 'http_port') + server.Command = f'{sys.executable} {self._server_script} 127.0.0.1 {port}' + server.Ready = When.PortOpenv4(port) + server.Streams.All += Testers.ExcludesExpression('misspoison', 'the smuggled request must not reach the origin') + self._server = server + return server + + def _configure_traffic_server(self, tr: 'TestRun') -> 'Process': + """Configure ATS. + + :param tr: The test run to associate the ATS process with. + :return: The ATS process. + """ + ts = tr.MakeATSProcess('ts', enable_cache=True) + self._ts = ts + ts.Disk.remap_config.AddLine(f'map http://{self._hostname}/ http://127.0.0.1:{self._server.Variables.http_port}/') + ts.Disk.records_config.update({'proxy.config.http.cache.http': 1}) + return ts + + def _configure_client(self, tr: 'TestRun') -> 'Process': + """Configure the client. + + :param tr: The test run to associate the client process with. + :return: The client process. + """ + client = tr.Processes.Default + tr.Setup.Copy(self._client_script) + client.Command = f'{sys.executable} {self._client_script} 127.0.0.1 {self._ts.Variables.port} {self._hostname}' + client.ReturnCode = 0 + client.Streams.All += Testers.ContainsExpression( + 'DELETE_STATUS=404', 'the DELETE must hit the NOOP (miss) path, not a cache hit') + client.Streams.All += Testers.ExcludesExpression('SECOND_RESPONSE_RECEIVED=True', 'no smuggled second response') + client.Streams.All += Testers.ExcludesExpression('misspoison', 'no smuggled bytes should reach the client') + client.StartBefore(self._server) + client.StartBefore(self._ts) + + TestDeleteMaxfwdNoopDrain() + urtest.execute() diff --git a/tests/uranium_tests/pipeline/test_noop_keepalive.py b/tests/uranium_tests/pipeline/test_noop_keepalive.py new file mode 100644 index 00000000000..c94c96b314e --- /dev/null +++ b/tests/uranium_tests/pipeline/test_noop_keepalive.py @@ -0,0 +1,111 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_noop_keepalive(urtest: UraniumTest) -> None: + '''Non-idempotency audit: a NOOP self-response drains its body exactly once and keeps the connection alive.''' + + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + from ports import get_port + import sys + + urtest.Summary = 'A cache-miss/NOOP DELETE self-response drains its body once and preserves keep-alive for the next request.' + urtest.ContinueOnFail = False + + class TestNoopKeepAlive: + """Audit that the NOOP self-response drain runs exactly once. + + On one keep-alive connection, a NOOP self-response (a DELETE to an uncached + path) with a benign body must drain that body exactly once; a following GET + must then still be served. A double-drain would consume into the next request + and close the connection. + """ + + _server_script: str = 'desync_server.py' + _client_script: str = 'noop_keepalive_client.py' + _hostname: str = 'www.example.com' + + def __init__(self) -> None: + tr = urtest.AddTestRun('A NOOP self-response must drain the body exactly once (keep-alive preserved).') + tr.TimeOut = 40 + self._configure_server(tr) + self._configure_traffic_server(tr) + self._configure_client(tr) + + def _configure_server(self, tr: 'TestRun') -> 'Process': + """Configure the origin server. + + :param tr: The test run to associate the origin server with. + :return: The origin server process. + """ + server = tr.Processes.Process('server') + tr.Setup.Copy(self._server_script) + port = get_port(server, 'http_port') + server.Command = f'{sys.executable} {self._server_script} 127.0.0.1 {port}' + server.Ready = When.PortOpenv4(port) + self._server = server + return server + + def _configure_traffic_server(self, tr: 'TestRun') -> 'Process': + """Configure ATS. + + :param tr: The test run to associate the ATS process with. + :return: The ATS process. + """ + ts = tr.MakeATSProcess('ts', enable_cache=True) + self._ts = ts + ts.Disk.remap_config.AddLine(f'map http://{self._hostname}/ http://127.0.0.1:{self._server.Variables.http_port}/') + ts.Disk.records_config.update({'proxy.config.http.cache.http': 1}) + return ts + + def _configure_client(self, tr: 'TestRun') -> 'Process': + """Configure the client. + + :param tr: The test run to associate the client process with. + :return: The client process. + """ + client = tr.Processes.Default + tr.Setup.Copy(self._client_script) + client.Command = f'{sys.executable} {self._client_script} 127.0.0.1 {self._ts.Variables.port} {self._hostname}' + client.ReturnCode = 0 + client.Streams.All += Testers.ContainsExpression( + 'DELETE_STATUS=404', 'the DELETE to an uncached path must self-answer 404 (NOOP path)') + client.Streams.All += Testers.ContainsExpression( + 'SECOND_REQUEST_STATUS=200', 'the following GET / must be served (keep-alive preserved, drained exactly once)') + client.Streams.All += Testers.ContainsExpression( + 'KEEPALIVE_PRESERVED=yes', 'the connection must not be closed by a spurious double-drain') + client.StartBefore(self._server) + client.StartBefore(self._ts) + + TestNoopKeepAlive() + urtest.execute() diff --git a/tests/uranium_tests/pipeline/test_pipeline.py b/tests/uranium_tests/pipeline/test_pipeline.py new file mode 100644 index 00000000000..854a397fb6c --- /dev/null +++ b/tests/uranium_tests/pipeline/test_pipeline.py @@ -0,0 +1,272 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_pipeline(urtest: UraniumTest) -> None: + '''Test a pipelined requests.''' + + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + from ports import get_port + import sys + + urtest.Summary = '''Test pipelined requests and HTTP/1.1 request framing.''' + + IP_ALLOW_CONTENT = ''' + ip_allow: + - apply: in + ip_addrs: 0/0 + action: deny + methods: + - DELETE + ''' + + class TestPipelining: + """Verify that a set of pipelined requests is handled correctly.""" + + _client_script: str = 'pipeline_client.py' + _server_script: str = 'pipeline_server.py' + + _dns_counter: int = 0 + _server_counter: int = 0 + _ts_counter: int = 0 + + def __init__(self, buffer_requests: bool) -> None: + """Configure the TestRun. + + :param buffer_requests: Whether to configure ATS to buffer client requests. + """ + tr = urtest.AddTestRun('Test a pipelined chunked encoded request.') + tr.TimeOut = 10 + self._configure_dns(tr) + self._configure_server(tr) + self._configure_traffic_server(tr, buffer_requests) + self._configure_client(tr) + + def _configure_dns(self, tr: 'TestRun') -> 'Process': + """Configure the DNS. + + :param tr: The test run to associate with the DNS process with. + :return: The DNS process. + """ + name = f'dns_{self._dns_counter}' + TestPipelining._dns_counter += 1 + dns = tr.MakeDNServer(name, default='127.0.0.1') + self._dns = dns + return dns + + def _configure_server(self, tr: 'TestRun') -> 'Process': + """Configure the origin server. + + :param tr: The test run to associate with the origin server with. + :return: The origin server process. + """ + name = f'server_{self._server_counter}' + TestPipelining._server_counter += 1 + server = tr.Processes.Process(name) + tr.Setup.Copy(self._server_script) + port = get_port(server, "http_port") + server.Command = f'{sys.executable} {self._server_script} 127.0.0.1 {port} ' + server.ReturnCode = 0 + server.Ready = When.PortOpenv4(port) + server.Streams.All += Testers.ContainsExpression('/first', 'Should receive the first request') + server.Streams.All += Testers.ContainsExpression('/second', 'Should receive the second request') + + # The third request should be denied due to the ip_allow.yaml rule. + server.Streams.All += Testers.ExcludesExpression('/third', 'Should not receive the third request') + self._server = server + return server + + def _configure_traffic_server(self, tr: 'TestRun', buffer_requests: bool) -> 'Process': + """Configure ATS. + + :param tr: The test run to associate with the ATS process with. + :param buffer_requests: Whether to enable request_buffer_enabled. + :return: The ATS process. + """ + name = f'ts_{self._ts_counter}' + TestPipelining._ts_counter += 1 + ts = tr.MakeATSProcess(name, enable_cache=False) + self._ts = ts + ts.Disk.remap_config.AddLine(f'map / http://backend.server.com:{self._server.Variables.http_port}') + ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'http|ip_allow', + 'proxy.config.dns.nameservers': f'127.0.0.1:{self._dns.Variables.Port}', + 'proxy.config.dns.resolv_conf': 'NULL', + }) + if buffer_requests: + ts.Disk.records_config.update({ + 'proxy.config.http.request_buffer_enabled': 1, + }) + ts.Disk.ip_allow_yaml.AddLines(IP_ALLOW_CONTENT.split("\n")) + return ts + + def _configure_client(self, tr: 'TestRun') -> 'Process': + """Configure the client. + + :param tr: The test run to associate with the client process with. + :return: The client process. + """ + client = tr.Processes.Default + tr.Setup.Copy(self._client_script) + client.Command = ( + f'{sys.executable} {self._client_script} 127.0.0.1 {self._ts.Variables.port} ' + 'server.com server.com') + client.ReturnCode = 0 + client.Streams.All += Testers.ContainsExpression('X-Response: first', "Should receive the origin's first response.") + client.Streams.All += Testers.ContainsExpression('X-Response: second', "Should receive the origin's second response.") + client.Streams.All += Testers.ExcludesExpression('X-Response: third', "Should not receive the origin's third response.") + client.Streams.All += Testers.ContainsExpression( + '403', 'Should receive the ATS-generated rejection of the DELETE request.') + client.StartBefore(self._dns) + client.StartBefore(self._server) + client.StartBefore(self._ts) + + class TestRequestFraming: + """Verify Traffic Server preserves HTTP/1.1 request framing. + + A body-less POST (Content-Length: 0) immediately followed by a second + pipelined request must be delivered to the origin as two independent + requests, so the second request cannot be folded into the first. A request + with conflicting Content-Length header fields must be rejected rather than + forwarded. + """ + + _client_script: str = 'request_framing_client.py' + _server_script: str = 'request_framing_server.py' + _counter: int = 0 + + def __init__(self, mode: str) -> None: + """Configure a test run for the given request mode. + + :param mode: 'pipeline' for a body-less POST followed by a pipelined + request, or 'conflicting_cl' for a request with conflicting + Content-Length header fields. + """ + self._mode = mode + self._name = f'framing_{mode}_{TestRequestFraming._counter}' + TestRequestFraming._counter += 1 + + description = { + 'pipeline': 'Test a body-less POST followed by a pipelined request.', + 'conflicting_cl': 'Test a request with conflicting Content-Length headers.', + }[mode] + tr = urtest.AddTestRun(description) + tr.TimeOut = 20 + self._configure_server(tr) + self._configure_traffic_server(tr) + self._configure_client(tr) + + def _configure_server(self, tr: 'TestRun') -> 'Process': + """Configure the recording origin server.""" + server = tr.Processes.Process(f'origin_{self._name}') + tr.Setup.Copy(self._server_script) + http_port = get_port(server, "http_port") + server.Command = f'{sys.executable} {self._server_script} 127.0.0.1 {http_port} ' + server.ReturnCode = 0 + server.Ready = When.PortOpenv4(http_port) + + if self._mode == 'pipeline': + # The origin must see exactly the two requests the client sent, with + # their boundaries intact. If ATS folded them together, the origin + # would see a single request or the second request's bytes inside + # the POST body. + server.Streams.All += Testers.ContainsExpression( + r'REQUEST_LINE: POST / HTTP/1.1', 'Origin should receive the POST as its own request.') + server.Streams.All += Testers.ContainsExpression( + r'REQUEST_LINE: GET /second HTTP/1.1', 'Origin should receive the GET as its own request.') + server.Streams.All += Testers.ContainsExpression( + r'ORIGIN_REQUEST_COUNT: 2', 'Origin should receive the second request as a distinct request.') + server.Streams.All += Testers.ExcludesExpression( + r'ORIGIN_REQUEST_COUNT: 3', 'Origin should receive exactly two requests, no more.') + # The second request's bytes must never appear inside the first + # (POST) request's body. + server.Streams.All += Testers.ExcludesExpression( + r"BODY:.*GET /second", 'The GET request must not appear inside the POST body.') + server.Streams.All += Testers.ExcludesExpression( + r"BODY:.*X-Marker", 'The second request header must not appear in the POST body.') + else: + # An ambiguously-framed request must be rejected by ATS before it + # ever reaches the origin. + server.Streams.All += Testers.ExcludesExpression( + r'REQUEST_LINE:', 'Origin must not receive an ambiguously-framed request.') + self._server = server + return server + + def _configure_traffic_server(self, tr: 'TestRun') -> 'Process': + """Configure ATS as a reverse proxy in front of the origin.""" + ts = tr.MakeATSProcess(f'ts_{self._name}', enable_cache=False) + self._ts = ts + ts.Disk.remap_config.AddLine(f'map / http://127.0.0.1:{self._server.Variables.http_port}/') + ts.Disk.records_config.update({ + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'http', + }) + return ts + + def _configure_client(self, tr: 'TestRun') -> 'Process': + """Configure the client that sends the framed request.""" + client = tr.Processes.Default + tr.Setup.Copy(self._client_script) + client.Command = ( + f'{sys.executable} {self._client_script} 127.0.0.1 {self._ts.Variables.port} ' + f'www.example.com {self._mode}') + client.ReturnCode = 0 + if self._mode == 'pipeline': + # Two independent responses must come back, one per request. + client.Streams.All += Testers.ContainsExpression( + r'STATUS_LINE_COUNT: 2', 'Client should receive two independent responses.') + client.Streams.All += Testers.ContainsExpression( + r'X-Origin-Response: first', 'Client should receive the response to the POST.') + client.Streams.All += Testers.ContainsExpression( + r'X-Origin-Response: second', 'Client should receive the response to the GET.') + else: + # The conflicting Content-Length request must be rejected with a + # 400, exactly one response must come back, and the second request + # must never be answered. + client.Streams.All += Testers.ContainsExpression( + r'HTTP/1.1 400', 'Client should receive a 400 for the ambiguous request.') + client.Streams.All += Testers.ContainsExpression( + r'STATUS_LINE_COUNT: 1', 'Client should receive exactly one response.') + client.Streams.All += Testers.ExcludesExpression( + r'X-Origin-Response: second', 'The second request must not be answered.') + client.StartBefore(self._server) + client.StartBefore(self._ts) + + TestPipelining(buffer_requests=False) + TestPipelining(buffer_requests=True) + + TestRequestFraming(mode='pipeline') + TestRequestFraming(mode='conflicting_cl') + urtest.execute() diff --git a/tests/gold_tests/pluginTest/TSVConnFd/CMakeLists.txt b/tests/uranium_tests/pluginTest/TSVConnFd/CMakeLists.txt similarity index 95% rename from tests/gold_tests/pluginTest/TSVConnFd/CMakeLists.txt rename to tests/uranium_tests/pluginTest/TSVConnFd/CMakeLists.txt index 5afc14b10e4..ef9c897cf3e 100644 --- a/tests/gold_tests/pluginTest/TSVConnFd/CMakeLists.txt +++ b/tests/uranium_tests/pluginTest/TSVConnFd/CMakeLists.txt @@ -15,4 +15,4 @@ # ####################### -add_autest_plugin(TSVConnFd TSVConnFd.cc) +add_urtest_plugin(TSVConnFd TSVConnFd.cc) diff --git a/tests/gold_tests/pluginTest/TSVConnFd/TSVConnFd.cc b/tests/uranium_tests/pluginTest/TSVConnFd/TSVConnFd.cc similarity index 100% rename from tests/gold_tests/pluginTest/TSVConnFd/TSVConnFd.cc rename to tests/uranium_tests/pluginTest/TSVConnFd/TSVConnFd.cc diff --git a/tests/uranium_tests/pluginTest/TSVConnFd/test_TSVConnFd.py b/tests/uranium_tests/pluginTest/TSVConnFd/test_TSVConnFd.py new file mode 100644 index 00000000000..f2c204dcf61 --- /dev/null +++ b/tests/uranium_tests/pluginTest/TSVConnFd/test_TSVConnFd.py @@ -0,0 +1,94 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_TSVConnFd(urtest: UraniumTest) -> None: + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import os + + urtest.Summary = ''' + Test TSVConnFdCreate() TS API call. + ''' + + plugin_name = "TSVConnFd" + + server = urtest.MakeOriginServer("server") + + request_header = {"headers": "GET / HTTP/1.1\r\nHost: doesnotmatter\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": "112233"} + server.addResponse("sessionlog.json", request_header, response_header) + + # File to be deleted when tests are fully completed. + # + InProgressFilePathspec = os.path.join(urtest.RunDirectory, "in_progress") + + ts = urtest.MakeATSProcess("ts", block_for_debug=False) + + ts.Disk.records_config.update( + { + 'proxy.config.proxy_name': 'Poxy_Proxy', # This will be the server name. + 'proxy.config.url_remap.remap_required': 1, + 'proxy.config.diags.debug.enabled': 3, + 'proxy.config.diags.debug.tags': f'{plugin_name}', + }) + + rp = os.path.join(urtest.Variables.AtsBuildUraniumTestsDir, 'pluginTest', 'TSVConnFd', '.libs', f'{plugin_name}.so') + ts.Setup.Copy(rp, ts.Env['PROXY_CONFIG_PLUGIN_PLUGIN_DIR']) + + urtest.GetTcpPort("tcp_port") + + ts.Disk.plugin_config.AddLine(f"{plugin_name}.so {InProgressFilePathspec} {ts.Variables.tcp_port}") + + ts.Disk.remap_config.AddLine("map http://myhost.test http://127.0.0.1:{0}".format(server.Variables.Port)) + + ipv4flag = "" + if not Condition.CurlUsingUnixDomainSocket(): + ipv4flag = "--ipv4" + # Dummy transaction to trigger plugin. + # + tr = urtest.AddTestRun() + tr.Processes.Default.StartBefore(server) + tr.Processes.Default.StartBefore(ts) + tr.MakeCurlCommandMulti( + f'touch {InProgressFilePathspec} ; ' + + f'{{curl}} --verbose {ipv4flag} --header "Host:myhost.test" http://localhost:{ts.Variables.port}/', + ts=ts) + tr.Processes.Default.ReturnCode = 0 + + # Give tests up to 10 seconds to complete. + # + tr = urtest.AddTestRun() + tr.Processes.Default.Command = ( + os.path.join(urtest.Variables.AtsTestToolsDir, 'condwait') + ' 15 1 -f ' + InProgressFilePathspec) + tr.Processes.Default.ReturnCode = 0 + urtest.execute() diff --git a/tests/gold_tests/pluginTest/access_control/etc/hmac_keys.txt b/tests/uranium_tests/pluginTest/access_control/etc/hmac_keys.txt similarity index 100% rename from tests/gold_tests/pluginTest/access_control/etc/hmac_keys.txt rename to tests/uranium_tests/pluginTest/access_control/etc/hmac_keys.txt diff --git a/tests/gold_tests/pluginTest/access_control/replays/access_control.replay.yaml b/tests/uranium_tests/pluginTest/access_control/replays/access_control.replay.yaml similarity index 100% rename from tests/gold_tests/pluginTest/access_control/replays/access_control.replay.yaml rename to tests/uranium_tests/pluginTest/access_control/replays/access_control.replay.yaml diff --git a/tests/uranium_tests/pluginTest/access_control/test_access_control.py b/tests/uranium_tests/pluginTest/access_control/test_access_control.py new file mode 100644 index 00000000000..e5abfc155fe --- /dev/null +++ b/tests/uranium_tests/pluginTest/access_control/test_access_control.py @@ -0,0 +1,102 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_access_control(urtest: UraniumTest) -> None: + ''' + Test access control plugin behaviors + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + import hashlib + import os + import re + + urtest.Summary = ''' + Test access control plugin behaviors + ''' + + urtest.SkipUnless(Condition.PluginExists('access_control.so')) + + class AccessControlTest: + replayFile = "replays/access_control.replay.yaml" + + def __init__(self): + self.setupOriginServer() + self.setupTS() + + def setupOriginServer(self): + self.server = urtest.MakeVerifierServerProcess("verifier-server", self.replayFile) + + def setupTS(self): + self.ts = urtest.MakeATSProcess("ts", enable_tls=True) + self.ts.addDefaultSSLFiles() + self.ts.Disk.ssl_multicert_yaml.AddLines( + """ + ssl_multicert: + - dest_ip: "*" + ssl_cert_name: server.pem + ssl_key_name: server.key + """.split("\n")) + self.ts.Disk.records_config.update( + { + "proxy.config.diags.debug.enabled": 1, + "proxy.config.diags.debug.tags": "http|access_control", + "proxy.config.http.insert_response_via_str": 2, + 'proxy.config.ssl.server.cert.path': f"{self.ts.Variables.SSLDir}", + 'proxy.config.ssl.server.private_key.path': f"{self.ts.Variables.SSLDir}", + 'proxy.config.ssl.client.alpn_protocols': 'http/1.1', + 'proxy.config.ssl.client.verify.server.policy': 'PERMISSIVE', + }) + + self.ts.Setup.Copy("etc/hmac_keys.txt") + self.ts.Disk.remap_config.AddLines( + { + f'''map / https://127.0.0.1:{self.server.Variables.https_port}/ \ + @plugin=access_control.so \ + @pparam=--symmetric-keys-map={urtest.RunDirectory}/hmac_keys.txt \ + @pparam=--check-cookie=TokenCookie \ + @pparam=--extract-subject-to-header=@TokenSubject \ + @pparam=--extract-tokenid-to-header=@TokenId \ + @pparam=--extract-status-to-header=@TokenStatus \ + @pparam=--token-response-header=TokenRespHdr''' + }) + + def run(self): + tr = urtest.AddTestRun("Session Cookie") + tr.AddVerifierClientProcess( + "verifier-client", self.replayFile, http_ports=[self.ts.Variables.port], https_ports=[self.ts.Variables.ssl_port]) + tr.Processes.Default.StartBefore(self.ts) + tr.Processes.Default.StartBefore(self.server) + tr.StillRunningAfter = self.ts + tr.StillRunningAfter = self.server + + AccessControlTest().run() + urtest.execute() diff --git a/tests/uranium_tests/pluginTest/block_errors/test_block_errors.py b/tests/uranium_tests/pluginTest/block_errors/test_block_errors.py new file mode 100644 index 00000000000..44a25fa5e45 --- /dev/null +++ b/tests/uranium_tests/pluginTest/block_errors/test_block_errors.py @@ -0,0 +1,197 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_block_errors(urtest: UraniumTest) -> None: + """ + Verify block_errors plugin message handling. + """ + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + urtest.Summary = ''' + Verify block_errors plugin message handling via traffic_ctl. + ''' + + urtest.SkipUnless(Condition.PluginExists('block_errors.so'),) + + # Define ATS and configure it. + ts = urtest.MakeATSProcess("ts") + + ts.Disk.records_config.update({ + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'block_errors', + }) + + # Configure block_errors plugin with initial values. + ts.Disk.plugin_config.AddLine('block_errors.so') + + # Verify the plugin loads. + ts.Disk.diags_log.Content = Testers.ContainsExpression( + "loading plugin.*block_errors.so", "Verify the block_errors plugin got loaded.") + + # + # Test 1: Verify the plugin starts with default values. + # + tr = urtest.AddTestRun("Verify plugin starts with default values.") + tr.Processes.Default.Command = "echo verifying plugin starts with default values" + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.StartBefore(ts) + tr.StillRunningAfter = ts + + # Verify the default values are logged at startup. + ts.Disk.traffic_out.Content = Testers.ContainsExpression( + "reset limit: 1000 per minute, timeout limit: 4 minutes, shutdown connection: 0 enabled: 1", + "Verify block_errors starts with default values.") + + # + # Test 2: Verify changing the 'enabled' setting via traffic_ctl. + # + tr = urtest.AddTestRun("Verify changing 'enabled' via traffic_ctl.") + tr.Processes.Default.Command = "traffic_ctl plugin msg block_errors.enabled 0" + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Env = ts.Env + tr.StillRunningAfter = ts + + tr = urtest.AddTestRun("Await the enabled change.") + tr.Processes.Default.Command = "echo awaiting enabled change" + tr.Processes.Default.ReturnCode = 0 + await_enabled = tr.Processes.Process('await_enabled', 'sleep 30') + await_enabled.Ready = When.FileContains(ts.Disk.traffic_out.Name, "msg_hook: command=enabled data=0") + tr.Processes.Default.StartBefore(await_enabled) + + ts.Disk.traffic_out.Content += Testers.ContainsExpression( + "msg_hook: command=enabled data=0", "Verify block_errors received the enabled command.") + ts.Disk.traffic_out.Content += Testers.ContainsExpression( + "reset limit: 1000 per minute, timeout limit: 4 minutes, shutdown connection: 0 enabled: 0", + "Verify block_errors applied the enabled=0 setting.") + + # + # Test 3: Verify changing the 'limit' setting via traffic_ctl. + # + tr = urtest.AddTestRun("Verify changing 'limit' via traffic_ctl.") + tr.Processes.Default.Command = "traffic_ctl plugin msg block_errors.limit 500" + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Env = ts.Env + tr.StillRunningAfter = ts + + tr = urtest.AddTestRun("Await the limit change.") + tr.Processes.Default.Command = "echo awaiting limit change" + tr.Processes.Default.ReturnCode = 0 + await_limit = tr.Processes.Process('await_limit', 'sleep 30') + await_limit.Ready = When.FileContains(ts.Disk.traffic_out.Name, "msg_hook: command=limit data=500") + tr.Processes.Default.StartBefore(await_limit) + + ts.Disk.traffic_out.Content += Testers.ContainsExpression( + "msg_hook: command=limit data=500", "Verify block_errors received the limit command.") + ts.Disk.traffic_out.Content += Testers.ContainsExpression( + "reset limit: 500 per minute", "Verify block_errors applied the limit=500 setting.") + + # + # Test 4: Verify changing the 'cycles' setting via traffic_ctl. + # + tr = urtest.AddTestRun("Verify changing 'cycles' via traffic_ctl.") + tr.Processes.Default.Command = "traffic_ctl plugin msg block_errors.cycles 8" + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Env = ts.Env + tr.StillRunningAfter = ts + + tr = urtest.AddTestRun("Await the cycles change.") + tr.Processes.Default.Command = "echo awaiting cycles change" + tr.Processes.Default.ReturnCode = 0 + await_cycles = tr.Processes.Process('await_cycles', 'sleep 30') + await_cycles.Ready = When.FileContains(ts.Disk.traffic_out.Name, "msg_hook: command=cycles data=8") + tr.Processes.Default.StartBefore(await_cycles) + + ts.Disk.traffic_out.Content += Testers.ContainsExpression( + "msg_hook: command=cycles data=8", "Verify block_errors received the cycles command.") + ts.Disk.traffic_out.Content += Testers.ContainsExpression( + "timeout limit: 8 minutes", "Verify block_errors applied the cycles=8 setting.") + + # + # Test 5: Verify changing the 'shutdown' setting via traffic_ctl. + # + tr = urtest.AddTestRun("Verify changing 'shutdown' via traffic_ctl.") + tr.Processes.Default.Command = "traffic_ctl plugin msg block_errors.shutdown 1" + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Env = ts.Env + tr.StillRunningAfter = ts + + tr = urtest.AddTestRun("Await the shutdown change.") + tr.Processes.Default.Command = "echo awaiting shutdown change" + tr.Processes.Default.ReturnCode = 0 + await_shutdown = tr.Processes.Process('await_shutdown', 'sleep 30') + await_shutdown.Ready = When.FileContains(ts.Disk.traffic_out.Name, "msg_hook: command=shutdown data=1") + tr.Processes.Default.StartBefore(await_shutdown) + + ts.Disk.traffic_out.Content += Testers.ContainsExpression( + "msg_hook: command=shutdown data=1", "Verify block_errors received the shutdown command.") + ts.Disk.traffic_out.Content += Testers.ContainsExpression( + "shutdown connection: 1", "Verify block_errors applied the shutdown=1 setting.") + + # + # Test 6: Verify an unknown command is handled gracefully. + # + tr = urtest.AddTestRun("Verify unknown command is handled gracefully.") + tr.Processes.Default.Command = "traffic_ctl plugin msg block_errors.unknown_command test" + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Env = ts.Env + tr.StillRunningAfter = ts + + tr = urtest.AddTestRun("Await the unknown command response.") + tr.Processes.Default.Command = "echo awaiting unknown command response" + tr.Processes.Default.ReturnCode = 0 + await_unknown = tr.Processes.Process('await_unknown', 'sleep 30') + await_unknown.Ready = When.FileContains(ts.Disk.traffic_out.Name, "msg_hook: unknown command 'unknown_command'") + tr.Processes.Default.StartBefore(await_unknown) + + ts.Disk.traffic_out.Content += Testers.ContainsExpression( + "msg_hook: unknown command 'unknown_command'", "Verify block_errors logs unknown commands.") + + # + # Test 7: Verify messages for other plugins are ignored. + # + tr = urtest.AddTestRun("Verify messages for other plugins are ignored.") + tr.Processes.Default.Command = "traffic_ctl plugin msg other_plugin.command test" + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Env = ts.Env + tr.StillRunningAfter = ts + + tr = urtest.AddTestRun("Await the other plugin message response.") + tr.Processes.Default.Command = "echo awaiting other plugin message response" + tr.Processes.Default.ReturnCode = 0 + await_other = tr.Processes.Process('await_other', 'sleep 30') + await_other.Ready = When.FileContains(ts.Disk.traffic_out.Name, "msg_hook: message for a different plugin: other_plugin") + tr.Processes.Default.StartBefore(await_other) + + ts.Disk.traffic_out.Content += Testers.ContainsExpression( + "msg_hook: message for a different plugin: other_plugin", "Verify block_errors ignores messages for other plugins.") + urtest.execute() diff --git a/tests/uranium_tests/pluginTest/cache_fill/test_cache_fill.py b/tests/uranium_tests/pluginTest/cache_fill/test_cache_fill.py new file mode 100644 index 00000000000..6a902099afc --- /dev/null +++ b/tests/uranium_tests/pluginTest/cache_fill/test_cache_fill.py @@ -0,0 +1,261 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_cache_fill(urtest: UraniumTest) -> None: + ''' + Test cache_fill plugin + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + urtest.Summary = ''' + Basic cache_fill plugin test + ''' + + urtest.SkipUnless( + Condition.PluginExists('cache_fill.so'), + Condition.PluginExists('xdebug.so'), + ) + # Skip until cache_fill supports UDS + urtest.SkipIf(Condition.CurlUsingUnixDomainSocket()) + urtest.ContinueOnFail = True + urtest.testName = "cache_fill" + + class CacheFillTest: + + def __init__(self): + self.setUpOriginServer() + self.setUpTS() + self.curl_and_args = '-s -D /dev/stdout -v -x localhost:{} -H "x-debug: x-cache,x-cache-key"'.format( + self.ts.Variables.port) + + def setUpOriginServer(self): + # Define and configure origin server + self.server = urtest.MakeOriginServer("server") + + req = { + "headers": + "GET /nostore HTTP/1.1\r\n" + "Host: www.example.com\r\n" + "Accept: */*" + "Range: bytes=0-4\r\n" + "\r\n", + "timestamp": "1469733493.993", + "body": "" + } + + res = { + "headers": + "HTTP/1.1 200 OK\r\n" + "Cache-Control: nostore\r\n" + "Connection: close\r\n" + + 'Etag: 994324f6-78f6bc3e8d639\r\n', + "timestamp": "1469733493.993", + "body": "hello hello" + } + + self.server.addResponse("sessionlog.json", req, res) + + req = { + "headers": "GET /200 HTTP/1.1\r\n" + "Host: www.example.com\r\n" + "\r\n", + "timestamp": "1469733493.993", + "body": "" + } + + res = { + "headers": + "HTTP/1.1 200 OK\r\n" + "Cache-Control: max-age=1\r\n" + "Connection: close\r\n" + + 'Etag: 772102f4-56f4bc1e6d417\r\n', + "timestamp": "1469733493.993", + "body": "hello hello" + } + + self.server.addResponse("sessionlog.json", req, res) + + req = { + "headers": "GET /range HTTP/1.1\r\n" + "Host: www.example.com\r\n" + "Accept: */*" + "Range: bytes=0-4\r\n" + + "\r\n", + "timestamp": "1469733493.993", + "body": "" + } + res = { + "headers": + "HTTP/1.1 200 OK\r\n" + "Cache-Control: max-age=1\r\n" + "Connection: close\r\n" + + 'Etag: 883213f5-67f5bc2e7d528\r\n', + "timestamp": "1469733493.993", + "body": "hello hello" + } + + self.server.addResponse("sessionlog.json", req, res) + + def setUpTS(self): + # Define and configure ATS + self.ts = urtest.MakeATSProcess("ts") + + self.ts.Disk.remap_config.AddLines( + [ + 'map http://www.example.com/200 http://127.0.0.1:{}/200 @plugin=cache_fill.so'.format( + self.server.Variables.Port), + 'map http://www.example.com/range http://127.0.0.1:{}/range @plugin=cache_fill.so'.format( + self.server.Variables.Port), + 'map http://www.example.com/nostore http://127.0.0.1:{}/nostore @plugin=cache_fill.so'.format( + self.server.Variables.Port), + 'map http://www.example.com/304 http://127.0.0.1:{}/range @plugin=cache_fill.so'.format( + self.server.Variables.Port), + ]) + + self.ts.Disk.plugin_config.AddLine('xdebug.so --enable=x-cache,x-cache-key') + + self.ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'cache_fill|.*cache.*', + }) + + def test_cacheMiss(self): + # Cache miss; background fetch should fill cache + tr = urtest.AddTestRun("Cache miss") + ps = tr.Processes.Default + ps.StartBefore(self.server, ready=When.PortOpen(self.server.Variables.Port)) + ps.StartBefore(urtest.Processes.ts) + tr.MakeCurlCommand(self.curl_and_args + ' http://www.example.com/200', ts=self.ts) + ps.ReturnCode = 0 + ps.Streams.stdout.Content = Testers.ContainsExpression("X-Cache: miss", "expected cache miss") + ps.Streams.stdout.Content += Testers.ContainsExpression("200 OK", "Expected 200 OK status") + tr.StillRunningAfter = self.ts + + def test_cacheHit(self): + # Cache hit-fresh from background fill + tr = urtest.AddTestRun("Cache hit-fresh") + ps = tr.Processes.Default + tr.MakeCurlCommand(self.curl_and_args + ' http://www.example.com/200', ts=self.ts) + ps.ReturnCode = 0 + ps.Streams.stdout.Content = Testers.ContainsExpression("X-Cache: hit-fresh", "expected cache hit") + ps.Streams.stdout.Content += Testers.ContainsExpression("200 OK", "Expected 200 OK status") + tr.StillRunningAfter = self.ts + + def test_rangeReq_CacheMiss(self): + # Cache miss; background fetch should fill cache + tr = urtest.AddTestRun("Range cache miss") + ps = tr.Processes.Default + tr.MakeCurlCommand(self.curl_and_args + ' http://www.example.com/range -r 0-4', ts=self.ts) + ps.ReturnCode = 0 + ps.Streams.stdout.Content = Testers.ContainsExpression("X-Cache: miss", "expected cache miss") + ps.Streams.stdout.Content += Testers.ContainsExpression("200 OK", "Expected 200 status") + tr.StillRunningAfter = self.ts + + def test_rangeReq_CacheHit(self): + # Cache hit-fresh from background fill + tr = urtest.AddTestRun("Range cache hit-fresh") + ps = tr.Processes.Default + tr.MakeCurlCommand(self.curl_and_args + ' http://www.example.com/range -r 0-4', ts=self.ts) + ps.ReturnCode = 0 + ps.Streams.stdout.Content = Testers.ContainsExpression("X-Cache: hit-fresh", "expected cache hit") + ps.Streams.stdout.Content += Testers.ContainsExpression("206 Partial Content", "Expected 206 status") + ps.Streams.stdout.Content += Testers.ContainsExpression( + "Content-Range: bytes 0-4/11", "Expected Content-Range: bytes 0-4/11") + tr.StillRunningAfter = self.ts + + def test_noStore_noFill(self): + # Background fetch should NOT fill cache + tr = urtest.AddTestRun("nostore cache miss") + ps = tr.Processes.Default + tr.MakeCurlCommand(self.curl_and_args + ' http://www.example.com/nostore -r 0-4', ts=self.ts) + ps.ReturnCode = 0 + ps.Streams.stdout.Content = Testers.ContainsExpression("X-Cache: miss", "expected cache miss") + ps.Streams.stdout.Content += Testers.ContainsExpression("200 OK", "Expected 200 status") + tr.StillRunningAfter = self.ts + + def test_nostore_cacheMiss(self): + # Cache hit miss because background fill was not triggered + tr = urtest.AddTestRun("nostore cache hit-fresh") + ps = tr.Processes.Default + tr.MakeCurlCommand(self.curl_and_args + ' http://www.example.com/nostore -r 0-4', ts=self.ts) + ps.ReturnCode = 0 + ps.Streams.stdout.Content = Testers.ContainsExpression("X-Cache: miss", "expected cache miss") + ps.Streams.stdout.Content += Testers.ContainsExpression("200 OK", "Expected 200 status") + tr.StillRunningAfter = self.ts + + def setUpGlobalPlugin(self): + ## Check global plugin ## + self.ts.Disk.plugin_config.AddLine('cache_fill.so') + + self.ts.Disk.remap_config.AddLines( + ['map http://www.example.com/global http://127.0.0.1:{}/global'.format(self.server.Variables.Port)]) + + req = { + "headers": "GET /global HTTP/1.1\r\n" + "Host: www.example.com\r\n" + "\r\n", + "timestamp": "1469733493.993", + "body": "" + } + res = { + "headers": + "HTTP/1.1 200 OK\r\n" + "Cache-Control: max-age=1\r\n" + "Connection: close\r\n" + + 'Etag: 661091f3-45f3bc0e5d306\r\n', + "timestamp": "1469733493.993", + "body": "hello hello" + } + + self.server.addResponse("sessionlog.json", req, res) + + def test_global_cacheMiss(self): + # Global implementation: Cache miss; background fetch should fill cache + tr = urtest.AddTestRun("Cache miss - global implementation") + + ps = tr.Processes.Default + tr.MakeCurlCommand(self.curl_and_args + ' http://www.example.com/global', ts=self.ts) + ps.ReturnCode = 0 + ps.Streams.stdout.Content = Testers.ContainsExpression("X-Cache: miss", "expected cache miss") + + ps.Streams.stdout.Content += Testers.ContainsExpression("200 OK", "Expected 200 status") + tr.StillRunningAfter = self.ts + + def test_global_cacheHit(self): + # Global implementation: Cache hit-fresh from background fill + tr = urtest.AddTestRun("Cache hit-fresh - global implementation") + ps = tr.Processes.Default + tr.MakeCurlCommand(self.curl_and_args + ' http://www.example.com/global', ts=self.ts) + ps.ReturnCode = 0 + ps.Streams.stdout.Content = Testers.ContainsExpression("X-Cache: hit-fresh", "expected cache hit") + ps.Streams.stdout.Content += Testers.ContainsExpression("200 OK", "Expected 200 status") + tr.StillRunningAfter = self.ts + + def runTraffic(self): + self.test_cacheMiss() + self.test_cacheHit() + self.test_rangeReq_CacheMiss() + self.test_rangeReq_CacheHit() + self.test_noStore_noFill() + self.test_nostore_cacheMiss() + self.setUpGlobalPlugin() + self.test_global_cacheMiss() + self.test_global_cacheHit() + + def run(self): + self.runTraffic() + + CacheFillTest().run() + urtest.execute() diff --git a/tests/gold_tests/pluginTest/cache_promote/replay/cache_promote.replay.yaml.tmpl b/tests/uranium_tests/pluginTest/cache_promote/replay/cache_promote.replay.yaml.tmpl similarity index 100% rename from tests/gold_tests/pluginTest/cache_promote/replay/cache_promote.replay.yaml.tmpl rename to tests/uranium_tests/pluginTest/cache_promote/replay/cache_promote.replay.yaml.tmpl diff --git a/tests/uranium_tests/pluginTest/cache_promote/test_cache_promote.py b/tests/uranium_tests/pluginTest/cache_promote/test_cache_promote.py new file mode 100644 index 00000000000..1164725e708 --- /dev/null +++ b/tests/uranium_tests/pluginTest/cache_promote/test_cache_promote.py @@ -0,0 +1,96 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_cache_promote(urtest: UraniumTest) -> None: + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import os + + urtest.Summary = ''' + Test cache_promote plugin + ''' + + urtest.SkipUnless(Condition.PluginExists('cache_promote.so')) + + class CachePromotePluginTest: + + def __init__(self): + self._tr = urtest.AddTestRun("cache_promote") + self.__setupOriginServer() + self.__setupTS() + self.__setupClient() + + def __setupClient(self): + # Eval template file to set origin server port to redirect + template_file_path = os.path.join(self._tr.TestDirectory, "replay/cache_promote.replay.yaml.tmpl") + with open(template_file_path, 'r') as f: + template = f.read() + + replay_yaml_path = os.path.join(self._tr.RunDirectory, "cache_promote.replay.yaml") + with open(replay_yaml_path, 'w') as f: + f.write(template.format(httpbin_port=self._httpbin.Variables.Port)) + + self._tr.AddVerifierClientProcess("verifier-client", replay_yaml_path, http_ports=[self._ts.Variables.port]) + + def __setupOriginServer(self): + self._httpbin = urtest.MakeHttpBinServer("httpbin") + + def __setupTS(self): + self._ts = urtest.MakeATSProcess("ts", enable_cache=True) + self._ts.Disk.records_config.update( + { + "proxy.config.diags.debug.enabled": 1, + "proxy.config.diags.debug.tags": "http|cache_promote", + "proxy.config.http.number_of_redirections": 1, # follow redirect + "proxy.config.http.redirect.actions": "self:follow", # redirects to self are not followed by default + }) + self._ts.Disk.plugin_config.AddLine("xdebug.so --enable=x-cache,x-cache-key") + self._ts.Disk.remap_config.AddLines( + { + f""" + map /test_0/ http://127.0.0.1:{self._httpbin.Variables.Port}/ \ + @plugin=cache_promote.so @pparam=--policy=lru @pparam=--hits=2 @pparam=--buckets=15000000 + + map /test_1/ http://127.0.0.1:{self._httpbin.Variables.Port}/ \ + @plugin=cache_promote.so @pparam=--policy=lru @pparam=--hits=2 @pparam=--buckets=15000000 @pparam=--disable-on-redirect \ + @plugin=cachekey.so @pparam=--static-prefix=trafficserver.apache.org/443 + """ + }) + + def run(self): + self._tr.Processes.Default.StartBefore(self._ts) + self._tr.Processes.Default.StartBefore(self._httpbin) + self._tr.StillRunningAfter = self._ts + self._tr.StillRunningAfter = self._httpbin + + CachePromotePluginTest().run() + urtest.execute() diff --git a/tests/gold_tests/pluginTest/cache_range_requests/gold/full.stderr.gold b/tests/uranium_tests/pluginTest/cache_range_requests/gold/full.stderr.gold similarity index 100% rename from tests/gold_tests/pluginTest/cache_range_requests/gold/full.stderr.gold rename to tests/uranium_tests/pluginTest/cache_range_requests/gold/full.stderr.gold diff --git a/tests/gold_tests/pluginTest/cache_range_requests/gold/inner.stderr.gold b/tests/uranium_tests/pluginTest/cache_range_requests/gold/inner.stderr.gold similarity index 100% rename from tests/gold_tests/pluginTest/cache_range_requests/gold/inner.stderr.gold rename to tests/uranium_tests/pluginTest/cache_range_requests/gold/inner.stderr.gold diff --git a/tests/gold_tests/pluginTest/cache_range_requests/gold/last.stderr.gold b/tests/uranium_tests/pluginTest/cache_range_requests/gold/last.stderr.gold similarity index 100% rename from tests/gold_tests/pluginTest/cache_range_requests/gold/last.stderr.gold rename to tests/uranium_tests/pluginTest/cache_range_requests/gold/last.stderr.gold diff --git a/tests/gold_tests/pluginTest/cache_range_requests/reason.conf b/tests/uranium_tests/pluginTest/cache_range_requests/reason.conf similarity index 100% rename from tests/gold_tests/pluginTest/cache_range_requests/reason.conf rename to tests/uranium_tests/pluginTest/cache_range_requests/reason.conf diff --git a/tests/uranium_tests/pluginTest/cache_range_requests/test_cache_range_requests.py b/tests/uranium_tests/pluginTest/cache_range_requests/test_cache_range_requests.py new file mode 100644 index 00000000000..8e995992cde --- /dev/null +++ b/tests/uranium_tests/pluginTest/cache_range_requests/test_cache_range_requests.py @@ -0,0 +1,390 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_cache_range_requests(urtest: UraniumTest) -> None: + ''' + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + urtest.Summary = ''' + Basic cache_range_requests plugin test + ''' + + # Test description: + # Preload the cache with the entire asset to be range requested. + # Reload remap rule with cache_range_requests plugin + # Request content through the cache_range_requests plugin + + urtest.SkipUnless( + Condition.PluginExists('cache_range_requests.so'), + Condition.PluginExists('header_rewrite.so'), + Condition.PluginExists('xdebug.so'), + ) + urtest.ContinueOnFail = False + urtest.testName = "cache_range_requests" + + # Define and configure ATS + ts = urtest.MakeATSProcess("ts") + + # Define and configure origin server + server = urtest.MakeOriginServer("server", lookup_key="{%uuid}") + + # default root + req_chk = { + "headers": "GET / HTTP/1.1\r\n" + "Host: www.example.com\r\n" + "uuid: none\r\n" + "\r\n", + "timestamp": "1469733493.993", + "body": "" + } + + res_chk = {"headers": "HTTP/1.1 200 OK\r\n" + "Connection: close\r\n" + "\r\n", "timestamp": "1469733493.993", "body": ""} + + server.addResponse("sessionlog.json", req_chk, res_chk) + + body = "lets go surfin now" + + req_full = { + "headers": "GET /path HTTP/1.1\r\n" + "Host: www.example.com\r\n" + "Accept: */*\r\n" + "uuid: full\r\n" + "\r\n", + "timestamp": "1469733493.993", + "body": "" + } + + res_full = { + "headers": "HTTP/1.1 200 OK\r\n" + "Cache-Control: max-age=500\r\n" + "Connection: close\r\n" + 'Etag: "path"\r\n' + "\r\n", + "timestamp": "1469733493.993", + "body": body + } + + server.addResponse("sessionlog.json", req_full, res_full) + + block_bytes = 7 + bodylen = len(body) + + inner_str = "7-15" + + req_inner = { + "headers": + "GET /path HTTP/1.1\r\n" + "Host: www.example.com\r\n" + "Accept: */*\r\n" + "Range: bytes={}\r\n".format(inner_str) + + "uuid: inner\r\n" + "\r\n", + "timestamp": "1469733493.993", + "body": "" + } + + res_inner = { + "headers": + "HTTP/1.1 206 Partial Content\r\n" + "Accept-Ranges: bytes\r\n" + "Cache-Control: max-age=500\r\n" + + "Content-Range: bytes {0}/{1}\r\n".format(inner_str, bodylen) + "Connection: close\r\n" + 'Etag: "path"\r\n' + "\r\n", + "timestamp": "1469733493.993", + "body": body[7:15] + } + + server.addResponse("sessionlog.json", req_inner, res_inner) + + frange_str = "0-" + + req_frange = { + "headers": + "GET /path HTTP/1.1\r\n" + "Host: www.example.com\r\n" + "Accept: */*\r\n" + "Range: bytes={}\r\n".format(frange_str) + + "uuid: frange\r\n" + "\r\n", + "timestamp": "1469733493.993", + "body": "" + } + + res_frange = { + "headers": + "HTTP/1.1 206 Partial Content\r\n" + "Accept-Ranges: bytes\r\n" + "Cache-Control: max-age=500\r\n" + + "Content-Range: bytes 0-{0}/{0}\r\n".format(bodylen) + "Connection: close\r\n" + 'Etag: "path"\r\n' + "\r\n", + "timestamp": "1469733493.993", + "body": body + } + + server.addResponse("sessionlog.json", req_frange, res_frange) + + last_str = "-5" + + req_last = { + "headers": + "GET /path HTTP/1.1\r\n" + "Host: www.example.com\r\n" + "Accept: */*\r\n" + "Range: bytes={}\r\n".format(last_str) + + "uuid: last\r\n" + "\r\n", + "timestamp": "1469733493.993", + "body": "" + } + + res_last = { + "headers": + "HTTP/1.1 206 Partial Content\r\n" + "Accept-Ranges: bytes\r\n" + "Cache-Control: max-age=200\r\n" + + "Content-Range: bytes {0}-{1}/{1}\r\n".format(bodylen - 5, bodylen) + "Connection: close\r\n" + 'Etag: "path"\r\n' + + "\r\n", + "timestamp": "1469733493.993", + "body": body[-5:] + } + + server.addResponse("sessionlog.json", req_last, res_last) + + pselect_str = "1-10" + + req_pselect = { + "headers": + "GET /path HTTP/1.1\r\n" + "Host: parentselect\r\n" + "Accept: */*\r\n" + "Range: bytes={}\r\n".format(pselect_str) + + "uuid: pselect\r\n" + "\r\n", + "timestamp": "1469733493.993", + "body": "" + } + + res_pselect = { + "headers": + "HTTP/1.1 206 Partial Content\r\n" + "Accept-Ranges: bytes\r\n" + "Cache-Control: max-age=200\r\n" + + "Content-Range: bytes {}/19\r\n".format(pselect_str) + "Connection: close\r\n" + 'Etag: "path"\r\n' + "\r\n", + "timestamp": "1469733493.993", + "body": body[1:10] + } + + server.addResponse("sessionlog.json", req_pselect, res_pselect) + + req_psd = { + "headers": + "GET /path HTTP/1.1\r\n" + "Host: psd\r\n" + "Accept: */*\r\n" + "Range: bytes={}\r\n".format(pselect_str) + + "uuid: pselect\r\n" + "\r\n", + "timestamp": "1469733493.993", + "body": "" + } + + server.addResponse("sessionlog.json", req_psd, res_pselect) + + # long cache key test: URL path long enough to push cache key over 16384 bytes + long_path = 'A' * 16400 + req_long_key = { + "headers": + "GET /{} HTTP/1.1\r\n".format(long_path) + "Host: www.longkey.com\r\n" + "Accept: */*\r\n" + "Range: bytes=0-17\r\n" + + "uuid: long_key\r\n" + "\r\n", + "timestamp": "1469733493.993", + "body": "" + } + + res_long_key = { + "headers": + "HTTP/1.1 206 Partial Content\r\n" + "Accept-Ranges: bytes\r\n" + "Cache-Control: max-age=500\r\n" + + "Content-Range: bytes 0-17/{}\r\n".format(len(body)) + "Connection: close\r\n" + 'Etag: "longkey"\r\n' + "\r\n", + "timestamp": "1469733493.993", + "body": body + } + + server.addResponse("sessionlog.json", req_long_key, res_long_key) + + # cache range requests plugin remap + ts.Setup.CopyAs('reason.conf', urtest.RunDirectory) + ts.Disk.remap_config.AddLines( + [ + 'map http://www.example.com http://127.0.0.1:{}'.format(server.Variables.Port) + + ' @plugin=header_rewrite.so @pparam={}/reason.conf @plugin=cache_range_requests.so'.format(urtest.RunDirectory), + + # long cache key: URL alone exceeds the 16384-byte stack buffer + 'map http://www.longkey.com http://127.0.0.1:{}'.format(server.Variables.Port) + ' @plugin=cache_range_requests.so', + + # parent select cache key option + 'map http://parentselect http://127.0.0.1:{}'.format(server.Variables.Port) + + ' @plugin=cache_range_requests.so @pparam=--ps-cachekey', + + # deprecated + 'map http://psd http://127.0.0.1:{}'.format(server.Variables.Port) + + ' @plugin=cache_range_requests.so @pparam=ps_mode:cache_key_url', + ]) + + # cache debug + ts.Disk.plugin_config.AddLine('xdebug.so --enable=x-cache,x-parentselection-key') + + # minimal configuration + ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'cache_range_requests|http', + }) + + curl_and_args = '-s -D /dev/stdout -o /dev/stderr -x localhost:{} -H "x-debug: x-cache"'.format(ts.Variables.port) + + # 0 Test - Fetch whole asset into cache + tr = urtest.AddTestRun("full asset cache miss bypass") + ps = tr.Processes.Default + ps.StartBefore(server, ready=When.PortOpen(server.Variables.Port)) + ps.StartBefore(urtest.Processes.ts) + tr.MakeCurlCommand(curl_and_args + ' http://www.example.com/path -H "uuid: full"', ts=ts) + ps.ReturnCode = 0 + ps.Streams.stderr = "gold/full.stderr.gold" + tr.StillRunningAfter = ts + + # test inner range + # 1 Test - Fetch range into cache + tr = urtest.AddTestRun("inner range cache miss") + ps = tr.Processes.Default + tr.MakeCurlCommand(curl_and_args + ' http://www.example.com/path -r {} -H "uuid: inner"'.format(inner_str), ts=ts) + ps.ReturnCode = 0 + ps.Streams.stderr = "gold/inner.stderr.gold" + ps.Streams.stdout.Content = Testers.ContainsExpression("X-Cache: miss", "expected cache miss") + ps.Streams.stdout.Content += Testers.ContainsExpression("Content-Range: bytes 7-15/18", "expected content-range header") + ps.Streams.stdout.Content += Testers.ContainsExpression("206 Foo Bar", "Expected 206 Foo Bar status") + tr.StillRunningAfter = ts + + # 2 Test - Fetch from cache + tr = urtest.AddTestRun("inner range cache hit") + ps = tr.Processes.Default + tr.MakeCurlCommand(curl_and_args + ' http://www.example.com/path -r {}'.format(inner_str), ts=ts) + ps.ReturnCode = 0 + ps.Streams.stderr = "gold/inner.stderr.gold" + ps.Streams.stdout.Content = Testers.ContainsExpression("X-Cache: hit", "expected cache hit") + ps.Streams.stdout.Content += Testers.ContainsExpression("Content-Range: bytes 7-15/18", "expected content-range header") + ps.Streams.stdout.Content += Testers.ContainsExpression("206 Foo Bar", "Expected 206 Foo Bar status") + tr.StillRunningAfter = ts + + # full range + + # 3 Test - 0- request + tr = urtest.AddTestRun("0- request miss") + ps = tr.Processes.Default + tr.MakeCurlCommand(curl_and_args + ' http://www.example.com/path -r {} -H "uuid: frange"'.format(frange_str), ts=ts) + ps.ReturnCode = 0 + ps.Streams.stderr = "gold/full.stderr.gold" + ps.Streams.stdout.Content = Testers.ContainsExpression("X-Cache: miss", "expected cache miss") + ps.Streams.stdout.Content += Testers.ContainsExpression("Content-Range: bytes 0-18/18", "expected content-range header") + ps.Streams.stdout.Content += Testers.ContainsExpression("206 Foo Bar", "Expected 206 Foo Bar status") + tr.StillRunningAfter = ts + + # 4 Test - 0- request + tr = urtest.AddTestRun("0- request hit") + ps = tr.Processes.Default + tr.MakeCurlCommand(curl_and_args + ' http://www.example.com/path -r {}'.format(frange_str), ts=ts) + ps.ReturnCode = 0 + ps.Streams.stderr = "gold/full.stderr.gold" + ps.Streams.stdout.Content = Testers.ContainsExpression("X-Cache: hit", "expected cache hit") + ps.Streams.stdout.Content += Testers.ContainsExpression("Content-Range: bytes 0-18/18", "expected Content-Range header") + ps.Streams.stdout.Content += Testers.ContainsExpression("206 Foo Bar", "Expected 206 Foo Bar header") + tr.StillRunningAfter = ts + + # end range + + # 5 Test - -5 request miss + tr = urtest.AddTestRun("-5 request miss") + ps = tr.Processes.Default + tr.MakeCurlCommand(curl_and_args + ' http://www.example.com/path -r {} -H "uuid: last"'.format(last_str), ts=ts) + ps.ReturnCode = 0 + ps.Streams.stderr = "gold/last.stderr.gold" + ps.Streams.stdout.Content = Testers.ContainsExpression("X-Cache: miss", "expected cache miss") + ps.Streams.stdout.Content += Testers.ContainsExpression("Content-Range: bytes 13-18/18", "expected content-range header") + ps.Streams.stdout.Content += Testers.ContainsExpression("206 Foo Bar", "Expected 206 Foo Bar status") + tr.StillRunningAfter = ts + + # 6 Test - -5 request hit + tr = urtest.AddTestRun("-5 request hit") + ps = tr.Processes.Default + tr.MakeCurlCommand(curl_and_args + ' http://www.example.com/path -r {}'.format(last_str), ts=ts) + ps.ReturnCode = 0 + ps.Streams.stderr = "gold/last.stderr.gold" + ps.Streams.stdout.Content = Testers.ContainsExpression("X-Cache: hit", "expected cache hit") + ps.Streams.stdout.Content += Testers.ContainsExpression("Content-Range: bytes 13-18/18", "expected content-range header") + ps.Streams.stdout.Content += Testers.ContainsExpression("206 Foo Bar", "Expected 206 Bar Bar status") + tr.StillRunningAfter = ts + + # Ensure 404's aren't getting cached + + # 7 Test - 404 + tr = urtest.AddTestRun("404 request 1st") + ps = tr.Processes.Default + tr.MakeCurlCommand(curl_and_args + ' http://www.example.com/404 -r 0-', ts=ts) + ps.Streams.stdout = "gold/404.stdout.gold" + ps.Streams.stdout.Content = Testers.ContainsExpression("X-Cache: miss", "expected cache miss") + tr.StillRunningAfter = ts + + # 8 Test - 404 + tr = urtest.AddTestRun("404 request 2nd") + ps = tr.Processes.Default + tr.MakeCurlCommand(curl_and_args + ' http://www.example.com/404 -r 0-', ts=ts) + ps.Streams.stdout.Content = Testers.ContainsExpression("X-Cache: miss", "expected cache miss") + ps.Streams.stdout.Content += Testers.ContainsExpression("404 Not Found", "expected 404 response") + tr.StillRunningAfter = ts + + # 9 Test - origin returns 200 response to range request + tr = urtest.AddTestRun("origin returns 200") + ps = tr.Processes.Default + tr.MakeCurlCommand(curl_and_args + ' http://www.example.com/path -r {} -H "uuid: full"', ts=ts) + ps.ReturnCode = 3 # <--- note the return code + ps.Streams.stdout.Content = Testers.ContainsExpression("X-Cache: miss", "expected cache miss") + ps.Streams.stdout.Content += Testers.ContainsExpression("200 OK", "expected full 200 response") + ps.Streams.stdout.Content += Testers.ExcludesExpression("Content-Range:", "didn't expect Content-Range header") + tr.StillRunningAfter = ts + + curl_and_args = '-s -D /dev/stdout -o /dev/stderr -x localhost:{} -H "x-debug: x-parentselection-key"'.format(ts.Variables.port) + + # 10 Test - cache_key_url request + tr = urtest.AddTestRun("cache_key_url request") + ps = tr.Processes.Default + tr.MakeCurlCommand(curl_and_args + ' http://parentselect/path -r {} -H "uuid: pselect"'.format(pselect_str), ts=ts) + ps.ReturnCode = 0 + ps.Streams.stdout.Content = Testers.ContainsExpression( + "X-ParentSelection-Key: .*-bytes=", + "expected bytes in parent selection key", + ) + tr.StillRunningAfter = ts + tr.StillRunningAfter = server + + # 11 Test - non cache_key_url request ... no X-ParentSelection-Key + tr = urtest.AddTestRun("non cache_key_url request") + ps = tr.Processes.Default + tr.MakeCurlCommand(curl_and_args + ' http://www.example.com/path -r {} -H "uuid: inner"'.format(inner_str), ts=ts) + ps.ReturnCode = 0 + ps.Streams.stdout.Content = Testers.ExcludesExpression("X-ParentSelection-Key", "parent select key shouldn't show up") + tr.StillRunningAfter = ts + tr.StillRunningAfter = server + + # 12 Test - cache_key_url request -- deprecated + tr = urtest.AddTestRun("cache_key_url request - deprecated") + ps = tr.Processes.Default + tr.MakeCurlCommand(curl_and_args + ' http://psd/path -r {} -H "uuid: pselect"'.format(pselect_str), ts=ts) + ps.ReturnCode = 0 + ps.Streams.stdout.Content = Testers.ContainsExpression( + "X-ParentSelection-Key: .*-bytes=", + "expected bytes in parent selection key", + ) + tr.StillRunningAfter = ts + tr.StillRunningAfter = server + + # 13 Test - range request where URL+range_value exceeds 16384 bytes (spill path) + tr = urtest.AddTestRun("long cache key spill") + ps = tr.Processes.Default + tr.MakeCurlCommand(curl_and_args + ' http://www.longkey.com/{} -r 0-17 -H "uuid: long_key"'.format(long_path), ts=ts) + ps.ReturnCode = 0 + ps.Streams.stdout.Content = Testers.ContainsExpression("206", "expected 206 for long-key range request") + ts.Disk.diags_log.Content = Testers.ExcludesExpression( + "disabling cache for this transaction", + "spill path must not disable cache", + ) + tr.StillRunningAfter = ts + tr.StillRunningAfter = server + urtest.execute() diff --git a/tests/uranium_tests/pluginTest/cache_range_requests/test_cache_range_requests_cache_complete_responses.py b/tests/uranium_tests/pluginTest/cache_range_requests/test_cache_range_requests_cache_complete_responses.py new file mode 100644 index 00000000000..8efefbd745d --- /dev/null +++ b/tests/uranium_tests/pluginTest/cache_range_requests/test_cache_range_requests_cache_complete_responses.py @@ -0,0 +1,420 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_cache_range_requests_cache_complete_responses(urtest: UraniumTest) -> None: + ''' + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + urtest.Summary = ''' + cache_range_requests cache-complete-responses test + ''' + + # Test description: + # Three rounds of testing: + # Round 1: + # - seed the cache with an object that is smaller than the slice block size + # - issue requests with various ranges and validate responses are 200s + # Round 2: + # - seed the cache with an object that is larger than the slice block size + # - issue requests with various ranges and validate responses are 206s + # The first two rounds test cache miss, cache hit, and refresh hit scenarios + # - uses the cachekey plugin to add the `Range` request header to the cache key + # - requests content through the slice and cache_range_requests plugin with a 4MB slice block size + # - demonstrates how one might normalize the `Range` header to avoid cache pollution + # The third round tests cache miss, hit, and refresh hit scenarios without any other plugins + # - tests cache misses, then hits, using the same object but two different ranges + # - demonstrates why normalization of the `Range` header is required to prevent pollution + + urtest.SkipUnless( + Condition.PluginExists('cachekey.so'), + Condition.PluginExists('cache_range_requests.so'), + Condition.PluginExists('slice.so'), + Condition.PluginExists('xdebug.so'), + ) + urtest.ContinueOnFail = False + urtest.testName = "cache_range_requests_cache_200s" + + # Generate bodies for our responses + small_body_len = 10000 + small_body = 'x' * small_body_len + + slice_body_len = 4 * 1024 * 1024 + slice_body = 'x' * slice_body_len + + # Define and configure ATS + ts = urtest.MakeATSProcess("ts") + + # Define and configure origin server + server = urtest.MakeOriginServer("server", lookup_key="{%UID}") + + # default root + req_chk = { + "headers": "GET / HTTP/1.1\r\n" + "Host: www.example.com\r\n" + "uuid: none\r\n" + "\r\n", + "timestamp": "1469733493.993", + "body": "" + } + + res_chk = {"headers": "HTTP/1.1 200 OK\r\n" + "Connection: close\r\n" + "\r\n", "timestamp": "1469733493.993", "body": ""} + + server.addResponse("sessionlog.json", req_chk, res_chk) + + small_req = { + "headers": "GET /obj HTTP/1.1\r\n" + "Host: www.example.com\r\n" + "Accept: */*\r\n" + "UID: SMALL\r\n" + "\r\n", + "timestamp": "1469733493.993", + "body": "" + } + + small_resp = { + "headers": + "HTTP/1.1 200 OK\r\n" + "Cache-Control: max-age=1\r\n" + "Connection: close\r\n" + + 'Etag: "772102f4-56f4bc1e6d417"\r\n' + "\r\n", + "timestamp": "1469733493.993", + "body": small_body + } + + small_reval_req = { + "headers": "GET /obj HTTP/1.1\r\n" + "Host: www.example.com\r\n" + "Accept: */*\r\n" + "UID: SMALL-INM\r\n" + "\r\n", + "timestamp": "1469733493.993", + "body": "" + } + + small_reval_resp = { + "headers": + "HTTP/1.1 304 Not Modified\r\n" + "Cache-Control: max-age=10\r\n" + "Connection: close\r\n" + + 'Etag: "772102f4-56f4bc1e6d417"\r\n' + "\r\n", + "timestamp": "1469733493.993" + } + + slice_req = { + "headers": + "GET /slice HTTP/1.1\r\n" + "Host: www.example.com\r\n" + "Range: bytes=0-4194303\r\n" + "Accept: */*\r\n" + + "UID: SLICE\r\n" + "\r\n", + "timestamp": "1469733493.993", + } + + slice_resp = { + "headers": + "HTTP/1.1 206 Partial Content\r\n" + "Cache-Control: max-age=1\r\n" + + "Content-Range: bytes 0-{}/{}\r\n".format(slice_body_len - 1, slice_body_len * 2) + "\r\n" + + "Content-Length: {}\r\n".format(slice_body_len) + "\r\n" + "Connection: close\r\n" + + 'Etag: "872104f4-d6bcaa1e6f979"\r\n' + "\r\n", + "timestamp": "1469733493.993", + "body": slice_body + } + + slice_reval_req = { + "headers": "GET /slice HTTP/1.1\r\n" + "Host: www.example.com\r\n" + "Accept: */*\r\n" + "UID: SLICE-INM\r\n" + "\r\n", + "timestamp": "1469733493.993", + "body": "" + } + + slice_reval_resp = { + "headers": + "HTTP/1.1 304 Not Modified\r\n" + "Cache-Control: max-age=10\r\n" + "Connection: close\r\n" + + 'Etag: "872104f4-d6bcaa1e6f979"\r\n' + "\r\n", + "timestamp": "1469733493.993" + } + + naieve_req = { + "headers": "GET /naieve/obj HTTP/1.1\r\n" + "Host: www.example.com\r\n" + "Accept: */*\r\n" + "UID: NAIEVE\r\n" + "\r\n", + "timestamp": "1469733493.993", + "body": "" + } + + naieve_resp = { + "headers": + "HTTP/1.1 200 OK\r\n" + "Cache-Control: max-age=1\r\n" + "Connection: close\r\n" + + 'Etag: "cad04ff4-56f4bc197ceda"\r\n' + "\r\n", + "timestamp": "1469733493.993", + "body": small_body + } + + naieve_reval_req = { + "headers": "GET /naieve/obj HTTP/1.1\r\n" + "Host: www.example.com\r\n" + "Accept: */*\r\n" + "UID: NAIEVE-INM\r\n" + "\r\n", + "timestamp": "1469733493.993", + "body": "" + } + + naieve_reval_resp = { + "headers": + "HTTP/1.1 304 Not Modified\r\n" + "Cache-Control: max-age=10\r\n" + "Connection: close\r\n" + + 'Etag: "cad04ff4-56f4bc197ceda"\r\n' + "\r\n", + "timestamp": "1469733493.993" + } + + server.addResponse("sessionlog.json", small_req, small_resp) + server.addResponse("sessionlog.json", small_reval_req, small_reval_resp) + server.addResponse("sessionlog.json", slice_req, slice_resp) + server.addResponse("sessionlog.json", slice_reval_req, slice_reval_resp) + server.addResponse("sessionlog.json", naieve_req, naieve_resp) + server.addResponse("sessionlog.json", naieve_reval_req, naieve_reval_resp) + + # remap with the cache range requests plugin only + # this is a "naieve" configuration due to the lack of range normalization performed at remap time by slice + # this config should only be used if ranges have been reliably normalized by the requestor (either the client itself or a cache) + ts.Disk.remap_config.AddLines( + [ + f'map http://example.com/naieve http://127.0.0.1:{server.Variables.Port}/naieve \\' + + ' @plugin=cache_range_requests.so @pparam=--cache-complete-responses', + ]) + + # remap with slice, cachekey, and the cache range requests plugin to ensure range normalization and cache keys are correct + ts.Disk.remap_config.AddLines( + [ + f'map http://example.com http://127.0.0.1:{server.Variables.Port} \\' + ' @plugin=slice.so @pparam=--blockbytes=4m \\', + ' @plugin=cachekey.so @pparam=--key-type=cache_key @pparam=--include-headers=Range @pparam=--remove-all-params=true \\', + ' @plugin=cache_range_requests.so @pparam=--no-modify-cachekey @pparam=--cache-complete-responses', + ]) + + # cache debug + ts.Disk.plugin_config.AddLine('xdebug.so --enable=x-cache,x-cache-key') + + # enable debug + ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'cachekey|cache_range_requests|slice', + }) + + # base cURL command + curl_and_args = '-s -D /dev/stdout -o /dev/stderr -x localhost:{} -H "x-debug: x-cache, x-cache-key"'.format(ts.Variables.port) + + # Test round 1: ensure we fetch and cache objects that are returned with a + # 200 OK and no Content-Range when the object is smaller than the slice + # block size + + # 0 Test - Fetch /obj with a Range header but less than 4MB + tr = urtest.AddTestRun("cache miss on /obj") + ps = tr.Processes.Default + ps.StartBefore(server, ready=When.PortOpen(server.Variables.Port)) + ps.StartBefore(urtest.Processes.ts) + tr.MakeCurlCommand(curl_and_args + ' -H "UID: SMALL" http://example.com/obj -r 0-5000', ts=ts) + ps.ReturnCode = 0 + ps.Streams.stdout.Content = Testers.ContainsExpression("200 OK", "expected 200 OK") + ps.Streams.stdout.Content = Testers.ExcludesExpression("Content-Range:", "expected no Content-Range header") + ps.Streams.stdout.Content = Testers.ContainsExpression("X-Cache: miss, none", "expected cache miss") + ps.Streams.stdout.Content = Testers.ContainsExpression( + "X-Cache-Key: /.*?/Range:bytes=0-4194303/obj", "expected cache key with bytes 0-4194303") + tr.StillRunningAfter = ts + + # 1 Test - Fetch /obj with a different range but less than 4MB + tr = urtest.AddTestRun("cache hit-fresh on /obj") + ps = tr.Processes.Default + tr.MakeCurlCommand(curl_and_args + ' -H "UID: SMALL" http://example.com/obj -r 5001-5999', ts=ts) + ps.ReturnCode = 0 + ps.Streams.stdout.Content = Testers.ContainsExpression("200 OK", "expected 200 OK") + ps.Streams.stdout.Content = Testers.ExcludesExpression("Content-Range:", "expected no Content-Range header") + ps.Streams.stdout.Content = Testers.ContainsExpression("X-Cache: hit-fresh, none", "expected cache hit") + ps.Streams.stdout.Content = Testers.ContainsExpression( + "X-Cache-Key: /.*?/Range:bytes=0-4194303/obj", "expected cache key with bytes 0-4194303") + tr.StillRunningAfter = ts + + # 2 Test - Revalidate /obj with a different range but less than 4MB + tr = urtest.AddTestRun("cache hit-stale on /obj") + tr.DelayStart = 2 + ps = tr.Processes.Default + tr.MakeCurlCommand(curl_and_args + ' -H "UID: SMALL-INM" http://example.com/obj -r 0-403', ts=ts) + ps.ReturnCode = 0 + ps.Streams.stdout.Content = Testers.ContainsExpression("200 OK", "expected 200 OK") + ps.Streams.stdout.Content = Testers.ExcludesExpression("Content-Range:", "expected no Content-Range header") + ps.Streams.stdout.Content = Testers.ContainsExpression("X-Cache: hit-stale, none", "expected cache hit stale") + ps.Streams.stdout.Content = Testers.ContainsExpression( + "X-Cache-Key: /.*?/Range:bytes=0-4194303/obj", "expected cache key with bytes 0-4194303") + tr.StillRunningAfter = ts + + # 3 Test - Fetch /obj with a different range but less than 4MB + tr = urtest.AddTestRun("cache hit on /obj post revalidation") + ps = tr.Processes.Default + tr.MakeCurlCommand(curl_and_args + ' -H "UID: SMALL" http://example.com/obj -r 0-3999', ts=ts) + ps.ReturnCode = 0 + ps.Streams.stdout.Content = Testers.ContainsExpression("200 OK", "expected 200 OK") + ps.Streams.stdout.Content = Testers.ExcludesExpression("Content-Range:", "expected no Content-Range header") + ps.Streams.stdout.Content = Testers.ContainsExpression("X-Cache: hit-fresh, none", "expected cache hit-fresh") + ps.Streams.stdout.Content = Testers.ContainsExpression( + "X-Cache-Key: /.*?/Range:bytes=0-4194303/obj", "expected cache key with bytes 0-4194303") + tr.StillRunningAfter = ts + + # Test round 2: repeat, but ensure we have 206s and matching Content-Range + # headers due to a base object that exceeds the slice block size + + # 4 Test - Fetch /slice with a Range header but less than 4MB + tr = urtest.AddTestRun("cache miss on /slice") + ps = tr.Processes.Default + tr.MakeCurlCommand(curl_and_args + ' -H "UID: SLICE" http://example.com/slice -r 0-5000', ts=ts) + ps.ReturnCode = 0 + ps.Streams.stdout.Content = Testers.ContainsExpression("206 Partial Content", "expected 206 Partial Content") + ps.Streams.stdout.Content = Testers.ContainsExpression( + "Content-Range: bytes 0-5000/8388608", "expected Content-Range: bytes 0-5000/8388608") + ps.Streams.stdout.Content = Testers.ContainsExpression("X-Cache: miss, none", "expected cache miss") + ps.Streams.stdout.Content = Testers.ContainsExpression( + "X-Cache-Key: /.*?/Range:bytes=0-4194303/slice", "expected cache key with bytes 0-4194303") + tr.StillRunningAfter = ts + + # 5 Test - Fetch /slice with a different range but less than 4MB + tr = urtest.AddTestRun("cache hit-fresh on /slice") + ps = tr.Processes.Default + tr.MakeCurlCommand(curl_and_args + ' -H "UID: SLICE" http://example.com/slice -r 5001-5999', ts=ts) + ps.ReturnCode = 0 + ps.Streams.stdout.Content = Testers.ContainsExpression("206 Partial Content", "expected 206 Partial Content") + ps.Streams.stdout.Content = Testers.ContainsExpression( + "Content-Range: bytes 5001-5999/8388608", "expected Content-Range: bytes 5001-5999/8388608") + ps.Streams.stdout.Content = Testers.ContainsExpression("X-Cache: hit-fresh, none", "expected cache hit") + ps.Streams.stdout.Content = Testers.ContainsExpression( + "X-Cache-Key: /.*?/Range:bytes=0-4194303/slice", "expected cache key with bytes 0-4194303") + tr.StillRunningAfter = ts + + # 6 Test - Revalidate /slice with a different range but less than 4MB + tr = urtest.AddTestRun("cache hit-stale on /slice") + tr.DelayStart = 2 + ps = tr.Processes.Default + tr.MakeCurlCommand(curl_and_args + ' -H "UID: SLICE-INM" http://example.com/slice -r 0-403', ts=ts) + ps.ReturnCode = 0 + ps.Streams.stdout.Content = Testers.ContainsExpression("206 Partial Content", "expected 206 Partial Content") + ps.Streams.stdout.Content = Testers.ContainsExpression( + "Content-Range: bytes 0-403/8388608", "expected Content-Range: bytes 0-403/8388608") + ps.Streams.stdout.Content = Testers.ContainsExpression("X-Cache: hit-stale, none", "expected cache hit stale") + ps.Streams.stdout.Content = Testers.ContainsExpression( + "X-Cache-Key: /.*?/Range:bytes=0-4194303/slice", "expected cache key with bytes 0-4194303") + tr.StillRunningAfter = ts + + # 7 Test - Fetch /slice with a different range but less than 4MB + tr = urtest.AddTestRun("cache hit on /slice post revalidation") + ps = tr.Processes.Default + tr.MakeCurlCommand(curl_and_args + ' -H "UID: SLICE" http://example.com/slice -r 0-3999', ts=ts) + ps.ReturnCode = 0 + ps.Streams.stdout.Content = Testers.ContainsExpression("206 Partial Content", "expected 206 Partial Content") + ps.Streams.stdout.Content = Testers.ContainsExpression( + "Content-Range: bytes 0-3999/8388608", "expected Content-Range: bytes 0-3999/8388608") + ps.Streams.stdout.Content = Testers.ContainsExpression("X-Cache: hit-fresh, none", "expected cache hit-fresh") + ps.Streams.stdout.Content = Testers.ContainsExpression( + "X-Cache-Key: /.*?/Range:bytes=0-4194303/slice", "expected cache key with bytes 0-4194303") + tr.StillRunningAfter = ts + + # Test round 3: test behavior of the cache range requests plugin when caching complete ranges *without* the slice and cachekey plugins + # this tests the "naieve" case that requires range normalization to be + # performed by the requestor and demonstrates how the cache can be + # polluted without normalization + + # 8 Test - Fetch /naieve/obj with a Range header + tr = urtest.AddTestRun("cache miss on /naieve/obj") + ps = tr.Processes.Default + tr.MakeCurlCommand(curl_and_args + ' -H "UID: NAIEVE" http://example.com/naieve/obj -r 0-5000', ts=ts) + ps.ReturnCode = 0 + ps.Streams.stdout.Content = Testers.ContainsExpression("200 OK", "expected 200 OK") + ps.Streams.stdout.Content = Testers.ExcludesExpression("Content-Range:", "expected no Content-Range header") + ps.Streams.stdout.Content = Testers.ContainsExpression("X-Cache: miss", "expected cache miss") + ps.Streams.stdout.Content = Testers.ContainsExpression( + "X-Cache-Key: http://.*?/naieve/obj-bytes=0-5000", "expected cache key with bytes 0-5000") + tr.StillRunningAfter = ts + + # 9 Test - Fetch /naieve/obj with the same Range header + tr = urtest.AddTestRun("cache hit-fresh on /naieve/obj") + ps = tr.Processes.Default + tr.MakeCurlCommand(curl_and_args + ' -H "UID: NAIEVE" http://example.com/naieve/obj -r 0-5000', ts=ts) + ps.ReturnCode = 0 + ps.Streams.stdout.Content = Testers.ContainsExpression("200 OK", "expected 200 OK") + ps.Streams.stdout.Content = Testers.ExcludesExpression("Content-Range:", "expected no Content-Range header") + ps.Streams.stdout.Content = Testers.ContainsExpression("X-Cache: hit-fresh", "expected cache hit") + ps.Streams.stdout.Content = Testers.ContainsExpression( + "X-Cache-Key: http://.*?/naieve/obj-bytes=0-5000", "expected cache key with bytes 0-5000") + tr.StillRunningAfter = ts + + # 10 Test - Revalidate /naieve/obj with the same Range header + tr = urtest.AddTestRun("cache hit-stale on /naieve/obj") + tr.DelayStart = 2 + ps = tr.Processes.Default + tr.MakeCurlCommand(curl_and_args + ' -H "UID: NAIEVE-INM" http://example.com/naieve/obj -r 0-5000', ts=ts) + ps.ReturnCode = 0 + ps.Streams.stdout.Content = Testers.ContainsExpression("200 OK", "expected 200 OK") + ps.Streams.stdout.Content = Testers.ExcludesExpression("Content-Range:", "expected no Content-Range header") + ps.Streams.stdout.Content = Testers.ContainsExpression("X-Cache: hit-stale", "expected cache hit stale") + ps.Streams.stdout.Content = Testers.ContainsExpression( + "X-Cache-Key: http://.*?/naieve/obj-bytes=0-5000", "expected cache key with bytes 0-5000") + tr.StillRunningAfter = ts + + # 11 Test - Fetch /naieve/obj with the same Range header + tr = urtest.AddTestRun("cache hit on /naieve/obj post revalidation") + ps = tr.Processes.Default + tr.MakeCurlCommand(curl_and_args + ' -H "UID: NAIEVE" http://example.com/naieve/obj -r 0-5000', ts=ts) + ps.ReturnCode = 0 + ps.Streams.stdout.Content = Testers.ContainsExpression("200 OK", "expected 200 OK") + ps.Streams.stdout.Content = Testers.ExcludesExpression("Content-Range:", "expected no Content-Range header") + ps.Streams.stdout.Content = Testers.ContainsExpression("X-Cache: hit-fresh", "expected cache hit-fresh") + ps.Streams.stdout.Content = Testers.ContainsExpression( + "X-Cache-Key: http://.*?/naieve/obj-bytes=0-5000", "expected cache key with bytes 0-5000") + tr.StillRunningAfter = ts + + # 12 Test - Fetch /naieve/obj with a *different* Range header; note the cache key changes and is a miss for the same object + tr = urtest.AddTestRun("cache miss on /naieve/obj") + ps = tr.Processes.Default + tr.MakeCurlCommand(curl_and_args + ' -H "UID: NAIEVE" http://example.com/naieve/obj -r 444-777', ts=ts) + ps.ReturnCode = 0 + ps.Streams.stdout.Content = Testers.ContainsExpression("200 OK", "expected 200 OK") + ps.Streams.stdout.Content = Testers.ExcludesExpression("Content-Range:", "expected no Content-Range header") + ps.Streams.stdout.Content = Testers.ContainsExpression("X-Cache: miss", "expected cache miss") + ps.Streams.stdout.Content = Testers.ContainsExpression( + "X-Cache-Key: http://.*?/naieve/obj-bytes=444-777", "expected cache key with bytes 444-777") + tr.StillRunningAfter = ts + + # 13 Test - Fetch /naieve/obj with the prior Range header; now a cache hit but we've effectively cached /naieve/obj twice + # this is why a Range normalization strategy should _always_ be employed when using `--cache-complete-responses` + tr = urtest.AddTestRun("cache hit on /naieve/obj") + ps = tr.Processes.Default + tr.MakeCurlCommand(curl_and_args + ' -H "UID: NAIEVE" http://example.com/naieve/obj -r 444-777', ts=ts) + ps.ReturnCode = 0 + ps.Streams.stdout.Content = Testers.ContainsExpression("200 OK", "expected 200 OK") + ps.Streams.stdout.Content = Testers.ExcludesExpression("Content-Range:", "expected no Content-Range header") + ps.Streams.stdout.Content = Testers.ContainsExpression("X-Cache: hit", "expected cache hit-fresh") + ps.Streams.stdout.Content = Testers.ContainsExpression( + "X-Cache-Key: http://.*?/naieve/obj-bytes=444-777", "expected cache key with bytes 444-777") + tr.StillRunningAfter = ts + + # 14 Test - Fetch /naieve/obj with the original Range header (0-5000); still a cache hit + tr = urtest.AddTestRun("cache hit on /naieve/obj after requesting with a different Range") + ps = tr.Processes.Default + tr.MakeCurlCommand(curl_and_args + ' -H "UID: NAIEVE" http://example.com/naieve/obj -r 0-5000', ts=ts) + ps.ReturnCode = 0 + ps.Streams.stdout.Content = Testers.ContainsExpression("200 OK", "expected 200 OK") + ps.Streams.stdout.Content = Testers.ExcludesExpression("Content-Range:", "expected no Content-Range header") + ps.Streams.stdout.Content = Testers.ContainsExpression("X-Cache: hit-fresh", "expected cache hit-fresh") + ps.Streams.stdout.Content = Testers.ContainsExpression( + "X-Cache-Key: http://.*?/naieve/obj-bytes=0-5000", "expected cache key with bytes 0-5000") + tr.StillRunningAfter = ts + urtest.execute() diff --git a/tests/uranium_tests/pluginTest/cache_range_requests/test_cache_range_requests_cachekey.py b/tests/uranium_tests/pluginTest/cache_range_requests/test_cache_range_requests_cachekey.py new file mode 100644 index 00000000000..af39fa7ad34 --- /dev/null +++ b/tests/uranium_tests/pluginTest/cache_range_requests/test_cache_range_requests_cachekey.py @@ -0,0 +1,185 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_cache_range_requests_cachekey(urtest: UraniumTest) -> None: + ''' + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + urtest.Summary = ''' + cache_range_requests with cachekey + ''' + + # Test description: + # Preload the cache with the entire asset to be range requested. + # Reload remap rule with cache_range_requests plugin + # Request content through the cache_range_requests plugin + + urtest.SkipUnless( + Condition.PluginExists('cache_range_requests.so'), + Condition.PluginExists('cachekey.so'), + Condition.PluginExists('xdebug.so'), + ) + urtest.ContinueOnFail = False + urtest.testName = "cache_range_requests_cachekey" + + # Define and configure ATS, enable traffic_ctl config reload + ts = urtest.MakeATSProcess("ts") + + # Define and configure origin server + server = urtest.MakeOriginServer("server", lookup_key="{%uuid}") + + # default root + req_chk = { + "headers": "GET / HTTP/1.1\r\n" + "Host: www.example.com\r\n" + "uuid: none\r\n" + "\r\n", + "timestamp": "1469733493.993", + "body": "" + } + + res_chk = {"headers": "HTTP/1.1 200 OK\r\n" + "Connection: close\r\n" + "\r\n", "timestamp": "1469733493.993", "body": ""} + + server.addResponse("sessionlog.json", req_chk, res_chk) + + body = "lets go surfin now" + bodylen = len(body) + + # this request should work + req_full = { + "headers": "GET /path HTTP/1.1\r\n" + "Host: www.example.com\r\n" + "Accept: */*\r\n" + "uuid: full\r\n" + "\r\n", + "timestamp": "1469733493.993", + "body": "" + } + + res_full = { + "headers": + "HTTP/1.1 206 Partial Content\r\n" + "Accept-Ranges: bytes\r\n" + 'Etag: "foo"\r\n' + + "Cache-Control: public, max-age=500\r\n" + "Connection: close\r\n" + "\r\n", + "timestamp": "1469733493.993", + "body": body + } + + server.addResponse("sessionlog.json", req_full, res_full) + + # this request should work + req_good = { + "headers": + "GET /path HTTP/1.1\r\n" + "Host: www.example.com\r\n" + "Accept: */*\r\n" + "Range: bytes=0-\r\n" + + "uuid: range_full\r\n" + "\r\n", + "timestamp": "1469733493.993", + "body": "" + } + + res_good = { + "headers": + "HTTP/1.1 206 Partial Content\r\n" + "Accept-Ranges: bytes\r\n" + 'Etag: "foo"\r\n' + + "Cache-Control: public, max-age=500\r\n" + "Content-Range: bytes 0-{0}/{0}\r\n".format(bodylen) + + "Connection: close\r\n" + "\r\n", + "timestamp": "1469733493.993", + "body": body + } + + server.addResponse("sessionlog.json", req_good, res_good) + + # this request should fail with a cache_range_requests asset + req_fail = { + "headers": + "GET /path HTTP/1.1\r\n" + "Host: www.fail.com\r\n" + "Accept: */*\r\n" + "Range: bytes=0-\r\n" + + "uuid: range_fail\r\n" + "\r\n", + "timestamp": "1469733493.993", + "body": "" + } + + res_fail = { + "headers": + "HTTP/1.1 206 Partial Content\r\n" + "Accept-Ranges: bytes\r\n" + 'Etag: "foo"\r\n' + + "Cache-Control: public, max-age=500\r\n" + "Content-Range: bytes 0-{0}/{0}\r\n".format(bodylen) + + "Connection: close\r\n" + "\r\n", + "timestamp": "1469733493.993", + "body": body + } + + server.addResponse("sessionlog.json", req_fail, res_fail) + + # cache range requests plugin remap, working config + ts.Disk.remap_config.AddLine( + 'map http://www.example.com http://127.0.0.1:{}'.format(server.Variables.Port) + + ' @plugin=cachekey.so @pparam=--include-headers=Range' + ' @plugin=cache_range_requests.so @pparam=--no-modify-cachekey',) + + # improperly configured cache_range_requests with cachekey + ts.Disk.remap_config.AddLine( + 'map http://www.fail.com http://127.0.0.1:{}'.format(server.Variables.Port) + + ' @plugin=cachekey.so @pparam=--static-prefix=foo' + ' @plugin=cache_range_requests.so',) + + # cache debug + ts.Disk.plugin_config.AddLine('xdebug.so --enable=x-cache') + + # minimal configuration + ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'cache_range_requests', + }) + + curl_and_args = '-s -D /dev/stdout -o /dev/stderr -x localhost:{} -H "x-debug: x-cache"'.format(ts.Variables.port) + + # 0 Test - Fetch full asset into cache (ensure cold) + tr = urtest.AddTestRun("full asset fetch") + ps = tr.Processes.Default + ps.StartBefore(server, ready=When.PortOpen(server.Variables.Port)) + ps.StartBefore(urtest.Processes.ts) + tr.MakeCurlCommand(curl_and_args + ' http://www.example.com/path -H "uuid: full"', ts=ts) + ps.ReturnCode = 0 + ps.Streams.stdout.Content = Testers.ContainsExpression("X-Cache: miss", "expected cache miss for load") + tr.StillRunningAfter = ts + + # 1 Test - Fetch whole asset into cache via range request (ensure cold) + tr = urtest.AddTestRun("0- asset fetch") + ps = tr.Processes.Default + tr.MakeCurlCommand(curl_and_args + ' http://www.example.com/path -r 0- -H "uuid: range_full"', ts=ts) + ps.ReturnCode = 0 + ps.Streams.stdout.Content = Testers.ContainsExpression("X-Cache: miss", "expected cache miss for load") + tr.StillRunningAfter = ts + + # 2 Test - Ensure assert happens instead of possible cache poisoning. + tr = urtest.AddTestRun("Attempt poisoning") + ps = tr.Processes.Default + tr.MakeCurlCommand(curl_and_args + ' http://www.fail.com/path -r 0- -H "uuid: range_fail"', ts=ts) + ps.ReturnCode = 0 + tr.StillRunningAfter = ts + + ts.Disk.diags_log.Content = Testers.ContainsExpression("ERROR", "error condition hit") + ts.Disk.diags_log.Content = Testers.ContainsExpression( + "Failed to change the cache url, disabling cache for this transaction to avoid cache poisoning.", + "ensure failure for misconfiguration") + urtest.execute() diff --git a/tests/uranium_tests/pluginTest/cache_range_requests/test_cache_range_requests_cachekey_global.py b/tests/uranium_tests/pluginTest/cache_range_requests/test_cache_range_requests_cachekey_global.py new file mode 100644 index 00000000000..2102609a3e4 --- /dev/null +++ b/tests/uranium_tests/pluginTest/cache_range_requests/test_cache_range_requests_cachekey_global.py @@ -0,0 +1,166 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_cache_range_requests_cachekey_global(urtest: UraniumTest) -> None: + ''' + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + urtest.Summary = ''' + cache_range_requests with cachekey as a global plugin + ''' + + # Test description: + # Preload the cache with the entire asset to be range requested. + # Reload remap rule with cache_range_requests plugin + # Request content through the cache_range_requests plugin + + urtest.SkipUnless( + Condition.PluginExists('cache_range_requests.so'), + Condition.PluginExists('cachekey.so'), + Condition.PluginExists('xdebug.so'), + ) + urtest.ContinueOnFail = False + urtest.testName = "cache_range_requests_cachekey_global" + + # Define and configure ATS, enable traffic_ctl config reload + ts = urtest.MakeATSProcess("ts") + + # Define and configure origin server + server = urtest.MakeOriginServer("server", lookup_key="{%uuid}") + + # default root + req_chk = { + "headers": "GET / HTTP/1.1\r\n" + "Host: www.example.com\r\n" + "uuid: none\r\n" + "\r\n", + "timestamp": "1469733493.993", + "body": "" + } + + res_chk = {"headers": "HTTP/1.1 200 OK\r\n" + "Connection: close\r\n" + "\r\n", "timestamp": "1469733493.993", "body": ""} + + server.addResponse("sessionlog.json", req_chk, res_chk) + + body = "lets go surfin now" + bodylen = len(body) + + # this request should work + req_full = { + "headers": "GET /path HTTP/1.1\r\n" + "Host: www.example.com\r\n" + "Accept: */*\r\n" + "uuid: full\r\n" + "\r\n", + "timestamp": "1469733493.993", + "body": "" + } + + res_full = { + "headers": + "HTTP/1.1 206 Partial Content\r\n" + "Accept-Ranges: bytes\r\n" + 'Etag: "foo"\r\n' + + "Cache-Control: public, max-age=500\r\n" + "Connection: close\r\n" + "\r\n", + "timestamp": "1469733493.993", + "body": body + } + + server.addResponse("sessionlog.json", req_full, res_full) + + # this request should work + req_good = { + "headers": + "GET /path HTTP/1.1\r\n" + "Host: www.example.com\r\n" + "Accept: */*\r\n" + "Range: bytes=0-\r\n" + + "uuid: range_full\r\n" + "\r\n", + "timestamp": "1469733493.993", + "body": "" + } + + res_good = { + "headers": + "HTTP/1.1 206 Partial Content\r\n" + "Accept-Ranges: bytes\r\n" + 'Etag: "foo"\r\n' + + "Cache-Control: public, max-age=500\r\n" + "Content-Range: bytes 0-{0}/{0}\r\n".format(bodylen) + + "Connection: close\r\n" + "\r\n", + "timestamp": "1469733493.993", + "body": body + } + + server.addResponse("sessionlog.json", req_good, res_good) + + # this request should fail with a cache_range_requests asset + req_fail = { + "headers": + "GET /path HTTP/1.1\r\n" + "Host: www.fail.com\r\n" + "Accept: */*\r\n" + "Range: bytes=0-\r\n" + + "uuid: range_fail\r\n" + "\r\n", + "timestamp": "1469733493.993", + "body": "" + } + + res_fail = { + "headers": + "HTTP/1.1 206 Partial Content\r\n" + "Accept-Ranges: bytes\r\n" + 'Etag: "foo"\r\n' + + "Cache-Control: public, max-age=500\r\n" + "Content-Range: bytes 0-{0}/{0}\r\n".format(bodylen) + + "Connection: close\r\n" + "\r\n", + "timestamp": "1469733493.993", + "body": body + } + + server.addResponse("sessionlog.json", req_fail, res_fail) + + # cache range requests plugin remap, working config + ts.Disk.remap_config.AddLine('map http://www.example.com http://127.0.0.1:{}'.format(server.Variables.Port)) + + # improperly configured cache_range_requests with cachekey + ts.Disk.remap_config.AddLine('map http://www.fail.com http://127.0.0.1:{}'.format(server.Variables.Port)) + + # cache debug + ts.Disk.plugin_config.AddLines( + [ + 'cachekey.so --include-headers=Range --static-prefix=foo', + 'cache_range_requests.so --no-modify-cachekey', + 'xdebug.so --enable=x-cache,x-cache-key,x-parentselection-key', + ]) + + # minimal configuration + ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'cachekey|cache_range_requests', + }) + + curl_and_args = '-s -D /dev/stdout -o /dev/stderr -x localhost:{} -H "x-debug: x-cache-key"'.format(ts.Variables.port) + + # 0 Test - Fetch full asset via range + tr = urtest.AddTestRun("asset fetch via range") + ps = tr.Processes.Default + ps.StartBefore(server, ready=When.PortOpen(server.Variables.Port)) + ps.StartBefore(urtest.Processes.ts) + tr.MakeCurlCommand(curl_and_args + ' http://www.example.com/path -r0- -H "uuid: full"', ts=ts) + ps.ReturnCode = 0 + ps.Streams.stdout.Content = Testers.ContainsExpression( + "X-Cache-Key: /foo/Range:bytes=0-/path", "expected cachekey style range request in cachekey") + tr.StillRunningAfter = ts + urtest.execute() diff --git a/tests/uranium_tests/pluginTest/cache_range_requests/test_cache_range_requests_ident.py b/tests/uranium_tests/pluginTest/cache_range_requests/test_cache_range_requests_ident.py new file mode 100644 index 00000000000..cdf8c12751a --- /dev/null +++ b/tests/uranium_tests/pluginTest/cache_range_requests/test_cache_range_requests_ident.py @@ -0,0 +1,371 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_cache_range_requests_ident(urtest: UraniumTest) -> None: + ''' + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import datetime + import os + import time + + urtest.Summary = ''' + cache_range_requests X-Crr-Ident plugin test + ''' + + # Test description: + # Preload the cache with the entire asset to be range requested. + # Ensure asset is stale and request with properly formed header. + + urtest.SkipUnless( + Condition.PluginExists('cache_range_requests.so'), + Condition.PluginExists('xdebug.so'), + ) + urtest.ContinueOnFail = False + urtest.testName = "cache_range_requests_ident" + + # Define and configure origin server + server = urtest.MakeOriginServer("server") + + # default root + req_chk = { + "headers": "GET / HTTP/1.1\r\n" + "Host: www.example.com\r\n" + "uuid: none\r\n" + "\r\n", + "timestamp": "1469733493.993", + "body": "" + } + + res_chk = {"headers": "HTTP/1.1 200 OK\r\n" + "Connection: close\r\n" + "\r\n", "timestamp": "1469733493.993", "body": ""} + + server.addResponse("sessionlog.json", req_chk, res_chk) + + body = "lets go surfin now" + bodylen = len(body) + + # baseline for testing + last_modified = "Fri, 07 Mar 2025 18:06:58 GMT" + etag = '"772102f4-56f4bc1e6d417"' + + req_both = { + "headers": "GET /both HTTP/1.1\r\n" + "Host: www.example.com\r\n" + "Accept: */*\r\n" + "Range: bytes=0-\r\n" + "\r\n", + "timestamp": "1469733493.993", + "body": "" + } + + res_both = { + "headers": + "HTTP/1.1 206 Partial Content\r\n" + "Accept-Ranges: bytes\r\n" + "Cache-Control: max-age=1\r\n" + + "Content-Range: bytes 0-{0}/{0}\r\n".format(bodylen) + "Connection: close\r\n" + 'Etag: ' + etag + '\r\n' + + 'Last-Modified: ' + last_modified + '\r\n' + "\r\n", + "timestamp": "1469733493.993", + "body": body + } + + server.addResponse("sessionlog.json", req_both, res_both) + + req_etag = { + "headers": "GET /etag HTTP/1.1\r\n" + "Host: www.example.com\r\n" + "Accept: */*\r\n" + "Range: bytes=0-\r\n" + "\r\n", + "timestamp": "1469733493.993", + "body": "" + } + + res_etag = { + "headers": + "HTTP/1.1 206 Partial Content\r\n" + "Accept-Ranges: bytes\r\n" + "Cache-Control: max-age=1\r\n" + + "Content-Range: bytes 0-{0}/{0}\r\n".format(bodylen) + "Connection: close\r\n" + 'Etag: ' + etag + '\r\n' + "\r\n", + "timestamp": "1469733493.993", + "body": body + } + + server.addResponse("sessionlog.json", req_etag, res_etag) + + req_lm = { + "headers": "GET /lm HTTP/1.1\r\n" + "Host: www.example.com\r\n" + "Accept: */*\r\n" + "Range: bytes=0-\r\n" + "\r\n", + "timestamp": "1469733493.993", + "body": "" + } + + res_lm = { + "headers": + "HTTP/1.1 206 Partial Content\r\n" + "Accept-Ranges: bytes\r\n" + "Cache-Control: max-age=1\r\n" + + "Content-Range: bytes 0-{0}/{0}\r\n".format(bodylen) + "Connection: close\r\n" + 'Last-Modified: ' + last_modified + + '\r\n' + "\r\n", + "timestamp": "1469733493.993", + "body": body + } + + server.addResponse("sessionlog.json", req_lm, res_lm) + + # test for custom Ident header + req_custom = { + "headers": "GET /custom HTTP/1.1\r\n" + "Host: www.example.com\r\n" + "Accept: */*\r\n" + "Range: bytes=0-\r\n" + "\r\n", + "timestamp": "1469733493.993", + "body": "" + } + + etag_custom = 'foo' + + res_custom = { + "headers": + "HTTP/1.1 206 Partial Content\r\n" + "Accept-Ranges: bytes\r\n" + "Cache-Control: max-age=1\r\n" + + "Content-Range: bytes 0-{0}/{0}\r\n".format(bodylen) + "Connection: close\r\n" + 'Etag: ' + etag_custom + '\r\n' + + '\r\n', + "timestamp": "1469733493.993", + "body": body + } + + server.addResponse("sessionlog.json", req_custom, res_custom) + + # Long lived asset for FRESH to STALE testing + req_fresh = { + "headers": "GET /fresh HTTP/1.1\r\n" + "Host: www.example.com\r\n" + "Accept: */*\r\n" + "Range: bytes=0-\r\n" + "\r\n", + "timestamp": "1469733493.993", + "body": "" + } + + etag_fresh = 'fresh' + + res_fresh = { + "headers": + "HTTP/1.1 206 Partial Content\r\n" + "Accept-Ranges: bytes\r\n" + "Cache-Control: max-age=3600\r\n" + + "Content-Range: bytes 0-{0}/{0}\r\n".format(bodylen) + "Connection: close\r\n" + 'Etag: ' + etag_fresh + '\r\n' + + '\r\n' + 'Last-Modified: ' + last_modified + '\r\n' + "\r\n", + "timestamp": "1469733493.993", + "body": body + } + + server.addResponse("sessionlog.json", req_fresh, res_fresh) + + # Define and configure ATS + ts = urtest.MakeATSProcess("ts") + + # cache range requests plugin remap + ts.Disk.remap_config.AddLines( + [ + f'map http://ident http://127.0.0.1:{server.Variables.Port}' + + ' @plugin=cache_range_requests.so @pparam=--consider-ident', + f'map http://identheader http://127.0.0.1:{server.Variables.Port}' + + ' @plugin=cache_range_requests.so @pparam=--consider-ident' + ' @pparam=--ident-header=CrrIdent', + ]) + + # cache debug + ts.Disk.plugin_config.AddLine('xdebug.so --enable=x-cache') + + # minimal configuration + ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'cache_range_requests', + }) + + curl_and_args = '-s -D /dev/stdout -o /dev/stderr -x localhost:{} -H "x-debug: x-cache"'.format(ts.Variables.port) + + ## + ## Stale to Fresh testing + ## + + ## Fetch short lived assets into cache + + # 0 Test - Fetch both asset into cache + tr = urtest.AddTestRun("0- range cache load") + ps = tr.Processes.Default + ps.StartBefore(server, ready=When.PortOpen(server.Variables.Port)) + ps.StartBefore(urtest.Processes.ts) + tr.MakeCurlCommand(curl_and_args + ' http://ident/both -r 0-', ts=ts) + ps.ReturnCode = 0 + ps.Streams.stdout.Content = Testers.ContainsExpression("X-Cache: miss", "expected cache miss for load") + tr.StillRunningAfter = ts + + # 1 Test - Fetch etag asset into cache + tr = urtest.AddTestRun("0- etag cache load") + ps = tr.Processes.Default + tr.MakeCurlCommand(curl_and_args + ' http://ident/etag -r 0-', ts=ts) + ps.ReturnCode = 0 + ps.Streams.stdout.Content = Testers.ContainsExpression("X-Cache: miss", "expected cache miss for load") + tr.StillRunningAfter = ts + + # 2 Test - Fetch lm asset into cache + tr = urtest.AddTestRun("0- lm cache load") + ps = tr.Processes.Default + tr.MakeCurlCommand(curl_and_args + ' http://ident/lm -r 0-', ts=ts) + ps.ReturnCode = 0 + ps.Streams.stdout.Content = Testers.ContainsExpression("X-Cache: miss", "expected cache miss for load") + tr.StillRunningAfter = ts + + ## both tests + + # 3 Test - Ensure Etag header match results in hit-fresh + tr = urtest.AddTestRun("0- both Etag check") + tr.DelayStart = 2 # Time to ensure stale + ps = tr.Processes.Default + tr.MakeCurlCommand(curl_and_args + f" http://ident/both -r 0- -H 'X-Crr-Ident: Etag {etag}'", ts=ts) + ps.ReturnCode = 0 + ps.Streams.stdout.Content = Testers.ContainsExpression("X-Cache: hit-fresh", "expected cache hit-fresh") + tr.StillRunningAfter = ts + + # 4 Test - Plugin expects Etag even if Last-Modified matches - hit-stale + tr = urtest.AddTestRun("0- both Last-Modified check") + ps = tr.Processes.Default + tr.MakeCurlCommand(curl_and_args + f' http://ident/both -r 0- -H "X-Crr-Ident: Last-Modified {last_modified}"', ts=ts) + ps.ReturnCode = 0 + ps.Streams.stdout.Content = Testers.ContainsExpression("X-Cache: hit-stale", "expected cache hit-stale") + tr.StillRunningAfter = ts + + # 5 Test - Bad etag stays stale + tr = urtest.AddTestRun("0- both bad Etag check") + ps = tr.Processes.Default + tr.MakeCurlCommand(curl_and_args + f" http://ident/both -r 0- -H 'X-Crr-Ident: Etag no_match'", ts=ts) + ps.ReturnCode = 0 + ps.Streams.stdout.Content = Testers.ContainsExpression("X-Cache: hit-stale", "expected cache hit-stale") + tr.StillRunningAfter = ts + + ## etag only supplied + + # 6 Test - Ensure Etag header match results in hit-fresh + tr = urtest.AddTestRun("0- etag Etag check") + ps = tr.Processes.Default + tr.MakeCurlCommand(curl_and_args + f" http://ident/etag -r 0- -H 'X-Crr-Ident: Etag {etag}'", ts=ts) + ps.ReturnCode = 0 + ps.Streams.stdout.Content = Testers.ContainsExpression("X-Cache: hit-fresh", "expected cache hit-fresh") + tr.StillRunningAfter = ts + + # 7 Test - Last modified will result in stale + tr = urtest.AddTestRun("0- etag lm cache stale check") + ps = tr.Processes.Default + tr.MakeCurlCommand(curl_and_args + f' http://ident/etag -r 0- -H "X-Crr-Ident: Last-Modified {last_modified}"', ts=ts) + ps.ReturnCode = 0 + ps.Streams.stdout.Content = Testers.ContainsExpression("X-Cache: hit-stale", "expected cache stale") + tr.StillRunningAfter = ts + + # 8 Test - Bad etag stays stale + tr = urtest.AddTestRun("0- etag bad Etag check") + ps = tr.Processes.Default + tr.MakeCurlCommand(curl_and_args + f" http://ident/etag -r 0- -H 'X-Crr-Ident: Etag no_match'", ts=ts) + ps.ReturnCode = 0 + ps.Streams.stdout.Content = Testers.ContainsExpression("X-Cache: hit-stale", "expected cache hit-stale") + tr.StillRunningAfter = ts + + ## last modified + + # 9 Test - Last modified will result in fresh + tr = urtest.AddTestRun("0- lm lm fresh check") + ps = tr.Processes.Default + tr.MakeCurlCommand(curl_and_args + f' http://ident/lm -r 0- -H "X-Crr-Ident: Last-Modified {last_modified}"', ts=ts) + ps.ReturnCode = 0 + ps.Streams.stdout.Content = Testers.ContainsExpression("X-Cache: hit-fresh", "expected cache fresh") + tr.StillRunningAfter = ts + + # 10 Test - Ensure Etag header match results in hit-fresh + tr = urtest.AddTestRun("0- lm Etag check") + ps = tr.Processes.Default + tr.MakeCurlCommand(curl_and_args + f" http://ident/lm -r 0- -H 'X-Crr-Ident: Etag {etag}'", ts=ts) + ps.ReturnCode = 0 + ps.Streams.stdout.Content = Testers.ContainsExpression("X-Cache: hit-stale", "expected cache hit-stale") + tr.StillRunningAfter = ts + + ## Fresh to stale testing + + # 11 Test - Fetch "fresh" into cache + tr = urtest.AddTestRun("0- fresh range cache load") + ps = tr.Processes.Default + tr.MakeCurlCommand(curl_and_args + ' http://ident/fresh -r 0-', ts=ts) + ps.ReturnCode = 0 + ps.Streams.stdout.Content = Testers.ContainsExpression("X-Cache: miss", "expected cache miss for load") + tr.StillRunningAfter = ts + + # 12 Test - Ensure "fresh" is in cache + tr = urtest.AddTestRun("0- fresh range cache check") + ps = tr.Processes.Default + tr.MakeCurlCommand(curl_and_args + ' http://ident/fresh -r 0-', ts=ts) + ps.ReturnCode = 0 + ps.Streams.stdout.Content = Testers.ContainsExpression("X-Cache: hit-fresh", "expected cache fresh") + tr.StillRunningAfter = ts + + # 13 request with different etag and ensure it goes stale + tr = urtest.AddTestRun("0- fresh range to stale") + ps = tr.Processes.Default + tr.MakeCurlCommand(curl_and_args + " http://ident/fresh -r 0- -H 'X-Crr-Ident: Etag not_the_same'", ts=ts) + ps.ReturnCode = 0 + ps.Streams.stdout.Content = Testers.ContainsExpression("X-Cache: hit-stale", "expected cache hit-stale") + tr.StillRunningAfter = ts + + # 14 request with Last-Modified ensure it goes stale (expected etag) + tr = urtest.AddTestRun("0- etag fresh range to stale") + ps = tr.Processes.Default + tr.MakeCurlCommand(curl_and_args + f' http://ident/fresh -r 0- -H "X-Crr-Ident: Last-Modified {last_modified}"', ts=ts) + ps.ReturnCode = 0 + ps.Streams.stdout.Content = Testers.ContainsExpression("X-Cache: hit-stale", "expected cache hit-stale") + tr.StillRunningAfter = ts + + # 15 Test - Ensure Etag header match results in hit-fresh + tr = urtest.AddTestRun("0- fresh ensure Etag check") + ps = tr.Processes.Default + tr.MakeCurlCommand(curl_and_args + f" http://ident/fresh -r 0- -H 'X-Crr-Ident: Etag {etag_fresh}'", ts=ts) + ps.ReturnCode = 0 + ps.Streams.stdout.Content = Testers.ContainsExpression("X-Cache: hit-fresh", "expected cache hit-fresh") + tr.StillRunningAfter = ts + + # 16 hit test asset to ensure its still fresh + tr = urtest.AddTestRun("0- plain request path again") + ps = tr.Processes.Default + tr.MakeCurlCommand(curl_and_args + ' http://ident/fresh -r 0-', ts=ts) + ps.ReturnCode = 0 + ps.Streams.stdout.Content = Testers.ContainsExpression("X-Cache: hit-fresh", "expected cache hit-fresh") + tr.StillRunningAfter = ts + + # 17 request with force stale + tr = urtest.AddTestRun("0- force stale") + ps = tr.Processes.Default + tr.MakeCurlCommand(curl_and_args + " http://ident/fresh -r 0- -H 'X-Crr-Ident: Stale'", ts=ts) + ps.ReturnCode = 0 + ps.Streams.stdout.Content = Testers.ContainsExpression("X-Cache: hit-stale", "expected cache hit-stale") + tr.StillRunningAfter = ts + + ## custom header + + # 18 Test - Fetch custom asset into cache + tr = urtest.AddTestRun("0- custom range cache load") + ps = tr.Processes.Default + tr.MakeCurlCommand(curl_and_args + ' http://identheader/custom -r 0-', ts=ts) + ps.ReturnCode = 0 + ps.Streams.stdout.Content = Testers.ContainsExpression("X-Cache: miss", "expected cache miss for load") + tr.StillRunningAfter = ts + + # 19 Test - Ensure CrrIdent Etag header results in hit-fresh, custom header + tr = urtest.AddTestRun("0- fresh CrrIdent Etag check") + ps = tr.Processes.Default + tr.MakeCurlCommand(curl_and_args + f" http://identheader/custom -r 0- -H 'CrrIdent: Etag {etag_custom}'", ts=ts) + ps.ReturnCode = 0 + ps.Streams.stdout.Content = Testers.ContainsExpression("X-Cache: hit-fresh", "expected cache hit-fresh") + tr.StillRunningAfter = ts + urtest.execute() diff --git a/tests/gold_tests/pluginTest/cert_update/gold/client-cert-after.gold b/tests/uranium_tests/pluginTest/cert_update/gold/client-cert-after.gold similarity index 100% rename from tests/gold_tests/pluginTest/cert_update/gold/client-cert-after.gold rename to tests/uranium_tests/pluginTest/cert_update/gold/client-cert-after.gold diff --git a/tests/gold_tests/pluginTest/cert_update/gold/client-cert-pre.gold b/tests/uranium_tests/pluginTest/cert_update/gold/client-cert-pre.gold similarity index 100% rename from tests/gold_tests/pluginTest/cert_update/gold/client-cert-pre.gold rename to tests/uranium_tests/pluginTest/cert_update/gold/client-cert-pre.gold diff --git a/tests/gold_tests/pluginTest/cert_update/gold/server-cert-after.gold b/tests/uranium_tests/pluginTest/cert_update/gold/server-cert-after.gold similarity index 100% rename from tests/gold_tests/pluginTest/cert_update/gold/server-cert-after.gold rename to tests/uranium_tests/pluginTest/cert_update/gold/server-cert-after.gold diff --git a/tests/gold_tests/pluginTest/cert_update/gold/server-cert-pre.gold b/tests/uranium_tests/pluginTest/cert_update/gold/server-cert-pre.gold similarity index 100% rename from tests/gold_tests/pluginTest/cert_update/gold/server-cert-pre.gold rename to tests/uranium_tests/pluginTest/cert_update/gold/server-cert-pre.gold diff --git a/tests/gold_tests/pluginTest/cert_update/gold/update.gold b/tests/uranium_tests/pluginTest/cert_update/gold/update.gold similarity index 100% rename from tests/gold_tests/pluginTest/cert_update/gold/update.gold rename to tests/uranium_tests/pluginTest/cert_update/gold/update.gold diff --git a/tests/gold_tests/pluginTest/cert_update/ssl/client1.pem b/tests/uranium_tests/pluginTest/cert_update/ssl/client1.pem similarity index 100% rename from tests/gold_tests/pluginTest/cert_update/ssl/client1.pem rename to tests/uranium_tests/pluginTest/cert_update/ssl/client1.pem diff --git a/tests/gold_tests/pluginTest/cert_update/ssl/client2.pem b/tests/uranium_tests/pluginTest/cert_update/ssl/client2.pem similarity index 100% rename from tests/gold_tests/pluginTest/cert_update/ssl/client2.pem rename to tests/uranium_tests/pluginTest/cert_update/ssl/client2.pem diff --git a/tests/gold_tests/pluginTest/cert_update/ssl/server1.pem b/tests/uranium_tests/pluginTest/cert_update/ssl/server1.pem similarity index 100% rename from tests/gold_tests/pluginTest/cert_update/ssl/server1.pem rename to tests/uranium_tests/pluginTest/cert_update/ssl/server1.pem diff --git a/tests/gold_tests/pluginTest/cert_update/ssl/server2.pem b/tests/uranium_tests/pluginTest/cert_update/ssl/server2.pem similarity index 100% rename from tests/gold_tests/pluginTest/cert_update/ssl/server2.pem rename to tests/uranium_tests/pluginTest/cert_update/ssl/server2.pem diff --git a/tests/uranium_tests/pluginTest/cert_update/test_cert_update.py b/tests/uranium_tests/pluginTest/cert_update/test_cert_update.py new file mode 100644 index 00000000000..ddf6ed0fbbc --- /dev/null +++ b/tests/uranium_tests/pluginTest/cert_update/test_cert_update.py @@ -0,0 +1,170 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_cert_update(urtest: UraniumTest) -> None: + ''' + Test the cert_update plugin. + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import ports + + urtest.Summary = ''' + Test cert_update plugin. + ''' + + urtest.SkipIf(Condition.CurlUsingUnixDomainSocket()) + urtest.SkipUnless( + Condition.HasProgram("openssl", "Openssl need to be installed on system for this test to work"), + Condition.PluginExists('cert_update.so')) + + # Set up origin server + server = urtest.MakeOriginServer("server") + request_header = {"headers": "GET / HTTP/1.1\r\nHost: doesnotmatter\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + server.addResponse("sessionlog.json", request_header, response_header) + + # Set up ATS + ts = urtest.MakeATSProcess("ts", enable_tls=True) + + # Set up ssl files + ts.addSSLfile("ssl/server1.pem") + ts.addSSLfile("ssl/server2.pem") + ts.addSSLfile("ssl/client1.pem") + ts.addSSLfile("ssl/client2.pem") + + # reserve port, attach it to 'ts' so it is released later + ports.get_port(ts, 's_server_port') + + ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'cert_update', + 'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir), + 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts.Variables.SSLDir), + 'proxy.config.ssl.client.cert.path': '{0}'.format(ts.Variables.SSLDir), + 'proxy.config.ssl.client.private_key.path': '{0}'.format(ts.Variables.SSLDir), + 'proxy.config.url_remap.pristine_host_hdr': 1 + }) + + ts.Disk.ssl_multicert_yaml.AddLines( + """ + ssl_multicert: + - dest_ip: "*" + ssl_cert_name: server1.pem + ssl_key_name: server1.pem + """.split("\n")) + + ts.Disk.remap_config.AddLines( + [ + 'map https://bar.com http://127.0.0.1:{0}'.format(server.Variables.Port), + 'map https://foo.com https://127.0.0.1:{0}'.format(ts.Variables.s_server_port), + ]) + + ts.Disk.sni_yaml.AddLines([ + 'sni:', + '- fqdn: "*foo.com"', + ' client_cert: "client1.pem"', + ]) + + # Set up plugin + urtest.PrepareInstalledPlugin('cert_update.so', ts) + + # Server-Cert-Pre + # curl should see that Traffic Server presents bar.com cert from alice + tr = urtest.AddTestRun("Server-Cert-Pre") + tr.Processes.Default.StartBefore(server) + tr.Processes.Default.StartBefore(urtest.Processes.ts) + tr.MakeCurlCommand( + '--verbose --insecure --ipv4 --resolve bar.com:{0}:127.0.0.1 https://bar.com:{0}'.format(ts.Variables.ssl_port), ts=ts) + tr.Processes.Default.Streams.stderr = "gold/server-cert-pre.gold" + tr.Processes.Default.ReturnCode = 0 + tr.StillRunningAfter = server + + # Server-Cert-Update + tr = urtest.AddTestRun("Server-Cert-Update") + tr.Processes.Default.Env = ts.Env + tr.Processes.Default.Command = ( + '{0}/traffic_ctl plugin msg cert_update.server {1}/server2.pem'.format(ts.Variables.BINDIR, ts.Variables.SSLDir)) + ts.Disk.traffic_out.Content = "gold/update.gold" + ts.StillRunningAfter = server + + # Server-Cert-After + # after use traffic_ctl to update server cert, curl should see bar.com cert from bob + tr = urtest.AddTestRun("Server-Cert-After") + tr.Processes.Default.Env = ts.Env + tr.MakeCurlCommand( + '--verbose --insecure --ipv4 --resolve bar.com:{0}:127.0.0.1 https://bar.com:{0}'.format(ts.Variables.ssl_port), ts=ts) + tr.Processes.Default.Streams.stderr = "gold/server-cert-after.gold" + tr.Processes.Default.ReturnCode = 0 + ts.StillRunningAfter = server + + # Client-Cert-Pre + # s_server should see client (Traffic Server) as alice.com + tr = urtest.AddTestRun("Client-Cert-Pre") + s_server = tr.Processes.Process( + "s_server", "openssl s_server -www -key {0}/server1.pem -cert {0}/server1.pem -accept {1} -Verify 1 -msg".format( + ts.Variables.SSLDir, ts.Variables.s_server_port)) + s_server.Ready = When.PortReady(ts.Variables.s_server_port) + tr.MakeCurlCommand( + '--verbose --insecure --ipv4 --header "Host: foo.com" https://localhost:{}'.format(ts.Variables.ssl_port), ts=ts) + tr.Processes.Default.StartBefore(s_server) + s_server.Streams.all = "gold/client-cert-pre.gold" + tr.Processes.Default.ReturnCode = 0 + ts.StillRunningAfter = server + + # Client-Cert-Update + tr = urtest.AddTestRun("Client-Cert-Update") + tr.Processes.Default.Env = ts.Env + tr.Processes.Default.Command = ( + 'mv {0}/client2.pem {0}/client1.pem && {1}/traffic_ctl plugin msg cert_update.client {0}/client1.pem'.format( + ts.Variables.SSLDir, ts.Variables.BINDIR)) + ts.Disk.traffic_out.Content = "gold/update.gold" + ts.StillRunningAfter = server + + # Client-Cert-After + # after use traffic_ctl to update client cert, s_server should see client (Traffic Server) as bob.com + tr = urtest.AddTestRun("Client-Cert-After") + s_server = tr.Processes.Process( + "s_server", "openssl s_server -www -key {0}/server1.pem -cert {0}/server1.pem -accept {1} -Verify 1 -msg".format( + ts.Variables.SSLDir, ts.Variables.s_server_port)) + s_server.Ready = When.PortReady(ts.Variables.s_server_port) + tr.Processes.Default.Env = ts.Env + # Move client2.pem to replace client1.pem since cert path matters in client context mapping + tr.MakeCurlCommand( + '--verbose --insecure --ipv4 --header "Host: foo.com" https://localhost:{0}'.format(ts.Variables.ssl_port), ts=ts) + tr.Processes.Default.StartBefore(s_server) + s_server.Streams.all = "gold/client-cert-after.gold" + tr.Processes.Default.ReturnCode = 0 + ts.StillRunningAfter = server + urtest.execute() diff --git a/tests/gold_tests/pluginTest/certifier/certs/ca-serial.txt b/tests/uranium_tests/pluginTest/certifier/certs/ca-serial.txt similarity index 100% rename from tests/gold_tests/pluginTest/certifier/certs/ca-serial.txt rename to tests/uranium_tests/pluginTest/certifier/certs/ca-serial.txt diff --git a/tests/gold_tests/pluginTest/certifier/certs/ca.cert b/tests/uranium_tests/pluginTest/certifier/certs/ca.cert similarity index 100% rename from tests/gold_tests/pluginTest/certifier/certs/ca.cert rename to tests/uranium_tests/pluginTest/certifier/certs/ca.cert diff --git a/tests/gold_tests/pluginTest/certifier/certs/ca.key b/tests/uranium_tests/pluginTest/certifier/certs/ca.key similarity index 100% rename from tests/gold_tests/pluginTest/certifier/certs/ca.key rename to tests/uranium_tests/pluginTest/certifier/certs/ca.key diff --git a/tests/gold_tests/pluginTest/certifier/replays/https-no-sni.replay.yaml b/tests/uranium_tests/pluginTest/certifier/replays/https-no-sni.replay.yaml similarity index 100% rename from tests/gold_tests/pluginTest/certifier/replays/https-no-sni.replay.yaml rename to tests/uranium_tests/pluginTest/certifier/replays/https-no-sni.replay.yaml diff --git a/tests/gold_tests/pluginTest/certifier/replays/https-path-traversal.replay.yaml b/tests/uranium_tests/pluginTest/certifier/replays/https-path-traversal.replay.yaml similarity index 100% rename from tests/gold_tests/pluginTest/certifier/replays/https-path-traversal.replay.yaml rename to tests/uranium_tests/pluginTest/certifier/replays/https-path-traversal.replay.yaml diff --git a/tests/gold_tests/pluginTest/certifier/replays/https-two-sessions.replay.yaml b/tests/uranium_tests/pluginTest/certifier/replays/https-two-sessions.replay.yaml similarity index 100% rename from tests/gold_tests/pluginTest/certifier/replays/https-two-sessions.replay.yaml rename to tests/uranium_tests/pluginTest/certifier/replays/https-two-sessions.replay.yaml diff --git a/tests/gold_tests/pluginTest/certifier/replays/https.replay.yaml b/tests/uranium_tests/pluginTest/certifier/replays/https.replay.yaml similarity index 100% rename from tests/gold_tests/pluginTest/certifier/replays/https.replay.yaml rename to tests/uranium_tests/pluginTest/certifier/replays/https.replay.yaml diff --git a/tests/uranium_tests/pluginTest/certifier/test_certifier.py b/tests/uranium_tests/pluginTest/certifier/test_certifier.py new file mode 100644 index 00000000000..e5a1b58c99a --- /dev/null +++ b/tests/uranium_tests/pluginTest/certifier/test_certifier.py @@ -0,0 +1,307 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_certifier(urtest: UraniumTest) -> None: + ''' + Test certifier plugin behaviors + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + import hashlib + import os + import re + + urtest.Summary = ''' + Test certifier plugin behaviors + ''' + + urtest.SkipUnless(Condition.PluginExists('certifier.so')) + + def prepare_certifier_storage(source_path: str, destination_path: str) -> None: + """Copy the certifier files and make its mutable state writable by ATS.""" + store_path = os.path.join(destination_path, 'store') + serial_path = os.path.join(destination_path, 'ca-serial.txt') + + def set_permissions() -> None: + os.chmod(serial_path, 0o666) + os.chmod(store_path, 0o777) + + Setup.Copy(source_path, destination_path) + Setup.MakeDir(store_path) + Setup.Lambda(func_setup=set_permissions, description="Make certifier state writable by ATS") + + class DynamicCertTest: + httpsReplayFile = "replays/https.replay.yaml" + certPathSrc = os.path.join(urtest.TestDirectory, "certs") + host = "www.tls.com" + certPathDest = "" + + def __init__(self): + self.setupOriginServer() + self.setupTS() + + def setupOriginServer(self): + self.server = urtest.MakeVerifierServerProcess("verifier-server1", self.httpsReplayFile) + + def setupTS(self): + self.ts = urtest.MakeATSProcess("ts1", enable_tls=True) + self.ts.addDefaultSSLFiles() + # copy over the cert store in which the certs will be generated/stored + self.certPathDest = os.path.join(self.ts.Variables.CONFIGDIR, "certifier-certs") + prepare_certifier_storage(self.certPathSrc, self.certPathDest) + self.ts.Disk.records_config.update( + { + "proxy.config.diags.debug.enabled": 1, + "proxy.config.diags.debug.tags": "http|certifier|ssl", + "proxy.config.ssl.server.cert.path": f'{self.ts.Variables.SSLDir}', + "proxy.config.ssl.server.private_key.path": f'{self.ts.Variables.SSLDir}', + }) + self.ts.Disk.ssl_multicert_yaml.AddLines( + """ + ssl_multicert: + - dest_ip: "*" + ssl_cert_name: server.pem + ssl_key_name: server.key + """.split("\n")) + self.ts.Disk.remap_config.AddLine(f"map / http://127.0.0.1:{self.server.Variables.http_port}/",) + self.ts.Disk.plugin_config.AddLine( + f'certifier.so -s {os.path.join(self.certPathDest, "store")} -m 1000 -c {os.path.join(self.certPathDest, "ca.cert")} -k {os.path.join(self.certPathDest, "ca.key")} -r {os.path.join(self.certPathDest, "ca-serial.txt")}' + ) + # Verify logs for dynamic generation of certs + self.ts.Disk.traffic_out.Content += Testers.ContainsExpression( + "creating shadow certs", "Verify the certifier plugin generates the certificate dynamically.") + + def runHTTPSTraffic(self): + tr = urtest.AddTestRun("Test dynamic generation of certs") + tr.AddVerifierClientProcess( + "client1", self.httpsReplayFile, http_ports=[self.ts.Variables.port], https_ports=[self.ts.Variables.ssl_port]) + tr.Processes.Default.StartBefore(self.server) + tr.Processes.Default.StartBefore(self.ts) + tr.StillRunningAfter = self.server + tr.StillRunningAfter = self.ts + + def verifyCert(self, certPath): + tr = urtest.AddTestRun("Verify the content of the generated cert") + tr.Processes.Default.Command = f'openssl x509 -in {certPath} -text -noout' + tr.Processes.Default.ReturnCode = 0 + # Verify certificate content + tr.Processes.Default.Streams.All += Testers.ContainsExpression( + "Subject: CN ?= ?www.tls.com", "Subject should match the host in the request") + tr.Processes.Default.Streams.All += Testers.ContainsExpression( + r"X509v3 extensions:\n.*X509v3 Subject Alternative Name:.*\n.*DNS:www.tls.com", + "Should contain the SAN extension", + reflags=re.MULTILINE) + + def verifyCertNotExist(self, certPath): + tr = urtest.AddTestRun("Verify the cert doesn't exist in the store") + tr.Processes.Default.Command = "echo verify" + certFile = tr.Disk.File(certPath, exists=False) + + def run(self): + # the certifier plugin generates the cert and store it in a directory + # named with the first three character of the md5 hash of the hostname + genCertPath = os.path.join( + self.certPathDest, 'store', str(hashlib.md5(self.host.encode('utf-8')).hexdigest()[:3]), self.host + ".crt") + self.verifyCertNotExist(genCertPath) + self.runHTTPSTraffic() + self.verifyCert(genCertPath) + + DynamicCertTest().run() + + class ReuseExistingCertTest: + httpsReplayFile = "replays/https-two-sessions.replay.yaml" + certPathSrc = os.path.join(urtest.TestDirectory, "certs") + host = "www.tls.com" + certPathDest = "" + + def __init__(self): + self.setupOriginServer() + self.setupTS() + + def setupOriginServer(self): + self.server = urtest.MakeVerifierServerProcess("verifier-server2", self.httpsReplayFile) + + def setupTS(self): + self.ts = urtest.MakeATSProcess("ts2", enable_tls=True) + self.ts.addDefaultSSLFiles() + # copy over the cert store in which the certs will be generated/stored + self.certPathDest = os.path.join(self.ts.Variables.CONFIGDIR, "certifier-certs") + prepare_certifier_storage(self.certPathSrc, self.certPathDest) + self.ts.Disk.records_config.update( + { + "proxy.config.diags.debug.enabled": 1, + "proxy.config.diags.debug.tags": "http|certifier|ssl", + "proxy.config.ssl.server.cert.path": f'{self.ts.Variables.SSLDir}', + "proxy.config.ssl.server.private_key.path": f'{self.ts.Variables.SSLDir}', + }) + self.ts.Disk.ssl_multicert_yaml.AddLines( + """ + ssl_multicert: + - dest_ip: "*" + ssl_cert_name: server.pem + ssl_key_name: server.key + """.split("\n")) + self.ts.Disk.remap_config.AddLine(f"map / http://127.0.0.1:{self.server.Variables.http_port}/",) + self.ts.Disk.plugin_config.AddLine( + f'certifier.so -s {os.path.join(self.certPathDest, "store")} -m 1000 -c {os.path.join(self.certPathDest, "ca.cert")} -k {os.path.join(self.certPathDest, "ca.key")} -r {os.path.join(self.certPathDest, "ca-serial.txt")}' + ) + # Verify logs for reusing existing cert + self.ts.Disk.traffic_out.Content += Testers.ContainsExpression( + "reusing existing cert and context for www.tls.com", "Should reuse the existing certificate") + + def runHTTPSTraffic(self): + tr = urtest.AddTestRun("Test dynamic generation of certs") + tr.AddVerifierClientProcess( + "client2", self.httpsReplayFile, http_ports=[self.ts.Variables.port], https_ports=[self.ts.Variables.ssl_port]) + tr.Processes.Default.StartBefore(self.server) + tr.Processes.Default.StartBefore(self.ts) + tr.StillRunningAfter = self.server + tr.StillRunningAfter = self.ts + + def run(self): + self.runHTTPSTraffic() + + ReuseExistingCertTest().run() + + class UnsafeSniTest: + httpsReplayFile = "replays/https-path-traversal.replay.yaml" + certPathSrc = os.path.join(urtest.TestDirectory, "certs") + escapedCert = "certifier-escaped.crt" + certPathDest = "" + + def __init__(self): + self.setupOriginServer() + self.setupTS() + + def setupOriginServer(self): + self.server = urtest.MakeVerifierServerProcess("verifier-server3", self.httpsReplayFile) + + def setupTS(self): + self.ts = urtest.MakeATSProcess("ts3", enable_tls=True) + self.ts.addDefaultSSLFiles() + self.certPathDest = os.path.join(self.ts.Variables.CONFIGDIR, "certifier-certs") + prepare_certifier_storage(self.certPathSrc, self.certPathDest) + self.ts.Disk.records_config.update( + { + "proxy.config.diags.debug.enabled": 1, + "proxy.config.diags.debug.tags": "http|certifier|ssl", + "proxy.config.ssl.server.cert.path": f'{self.ts.Variables.SSLDir}', + "proxy.config.ssl.server.private_key.path": f'{self.ts.Variables.SSLDir}', + }) + self.ts.Disk.ssl_multicert_yaml.AddLines( + """ + ssl_multicert: + - dest_ip: "*" + ssl_cert_name: server.pem + ssl_key_name: server.key + """.split("\n")) + self.ts.Disk.remap_config.AddLine(f"map / http://127.0.0.1:{self.server.Variables.http_port}/",) + self.ts.Disk.plugin_config.AddLine( + f'certifier.so -s {os.path.join(self.certPathDest, "store")} -m 1000 -c {os.path.join(self.certPathDest, "ca.cert")} -k {os.path.join(self.certPathDest, "ca.key")} -r {os.path.join(self.certPathDest, "ca-serial.txt")}' + ) + self.ts.Disk.traffic_out.Content += Testers.ContainsExpression( + "rejecting unsafe SNI for certificate storage", + "Should reject SNI values that cannot be used safely as file names.") + + def runHTTPSTraffic(self): + tr = urtest.AddTestRun("Test unsafe SNI is rejected") + tr.AddVerifierClientProcess( + "client3", self.httpsReplayFile, http_ports=[self.ts.Variables.port], https_ports=[self.ts.Variables.ssl_port]) + tr.Processes.Default.StartBefore(self.server) + tr.Processes.Default.StartBefore(self.ts) + tr.StillRunningAfter = self.server + tr.StillRunningAfter = self.ts + + def verifyCertNotExist(self, certPath): + tr = urtest.AddTestRun("Verify unsafe SNI did not escape the store") + tr.Processes.Default.Command = "echo verify" + tr.Disk.File(certPath, exists=False) + + def run(self): + escapedCertPath = os.path.join(self.certPathDest, self.escapedCert) + self.verifyCertNotExist(escapedCertPath) + self.runHTTPSTraffic() + self.verifyCertNotExist(escapedCertPath) + + UnsafeSniTest().run() + + class NoSniTest: + httpsReplayFile = "replays/https-no-sni.replay.yaml" + certPathSrc = os.path.join(urtest.TestDirectory, "certs") + certPathDest = "" + + def __init__(self): + self.setupOriginServer() + self.setupTS() + + def setupOriginServer(self): + self.server = urtest.MakeVerifierServerProcess("verifier-server4", self.httpsReplayFile) + + def setupTS(self): + self.ts = urtest.MakeATSProcess("ts4", enable_tls=True) + self.ts.addDefaultSSLFiles() + self.certPathDest = os.path.join(self.ts.Variables.CONFIGDIR, "certifier-certs") + prepare_certifier_storage(self.certPathSrc, self.certPathDest) + self.ts.Disk.records_config.update( + { + "proxy.config.diags.debug.enabled": 1, + "proxy.config.diags.debug.tags": "http|certifier|ssl", + "proxy.config.ssl.server.cert.path": f'{self.ts.Variables.SSLDir}', + "proxy.config.ssl.server.private_key.path": f'{self.ts.Variables.SSLDir}', + }) + self.ts.Disk.ssl_multicert_yaml.AddLines( + """ + ssl_multicert: + - dest_ip: "*" + ssl_cert_name: server.pem + ssl_key_name: server.key + """.split("\n")) + self.ts.Disk.remap_config.AddLine(f"map / http://127.0.0.1:{self.server.Variables.http_port}/",) + self.ts.Disk.plugin_config.AddLine( + f'certifier.so -s {os.path.join(self.certPathDest, "store")} -m 1000 -c {os.path.join(self.certPathDest, "ca.cert")} -k {os.path.join(self.certPathDest, "ca.key")} -r {os.path.join(self.certPathDest, "ca-serial.txt")}' + ) + self.ts.Disk.traffic_out.Content += Testers.ContainsExpression( + "no SNI available; using default certificate", "Should continue the handshake when no SNI is available.") + + def runHTTPSTraffic(self): + tr = urtest.AddTestRun("Test missing SNI falls back to default cert") + tr.AddVerifierClientProcess( + "client4", self.httpsReplayFile, http_ports=[self.ts.Variables.port], https_ports=[self.ts.Variables.ssl_port]) + tr.Processes.Default.StartBefore(self.server) + tr.Processes.Default.StartBefore(self.ts) + tr.StillRunningAfter = self.server + tr.StillRunningAfter = self.ts + + def run(self): + self.runHTTPSTraffic() + + NoSniTest().run() + urtest.execute() diff --git a/tests/gold_tests/pluginTest/client_context_dump/ssl/one.com.pem b/tests/uranium_tests/pluginTest/client_context_dump/ssl/one.com.pem similarity index 100% rename from tests/gold_tests/pluginTest/client_context_dump/ssl/one.com.pem rename to tests/uranium_tests/pluginTest/client_context_dump/ssl/one.com.pem diff --git a/tests/gold_tests/pluginTest/client_context_dump/ssl/two.com.pem b/tests/uranium_tests/pluginTest/client_context_dump/ssl/two.com.pem similarity index 100% rename from tests/gold_tests/pluginTest/client_context_dump/ssl/two.com.pem rename to tests/uranium_tests/pluginTest/client_context_dump/ssl/two.com.pem diff --git a/tests/uranium_tests/pluginTest/client_context_dump/test_client_context_dump.py b/tests/uranium_tests/pluginTest/client_context_dump/test_client_context_dump.py new file mode 100644 index 00000000000..a69fefd8b39 --- /dev/null +++ b/tests/uranium_tests/pluginTest/client_context_dump/test_client_context_dump.py @@ -0,0 +1,102 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_client_context_dump(urtest: UraniumTest) -> None: + ''' + Test the client_context_dump plugin. + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + urtest.Summary = ''' + Test client_context_dump plugin + ''' + + urtest.SkipUnless(Condition.PluginExists('client_context_dump.so')) + + # Set up ATS + ts = urtest.MakeATSProcess("ts", enable_tls=True) + + # Set up ssl files + ts.addSSLfile("ssl/one.com.pem") + ts.addSSLfile("ssl/two.com.pem") + + ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'client_context_dump', + 'proxy.config.ssl.server.cert.path': '{}'.format(ts.Variables.SSLDir), + 'proxy.config.ssl.server.private_key.path': '{}'.format(ts.Variables.SSLDir), + 'proxy.config.ssl.client.cert.path': '{}'.format(ts.Variables.SSLDir), + 'proxy.config.ssl.client.private_key.path': '{}'.format(ts.Variables.SSLDir), + }) + + ts.Disk.ssl_multicert_yaml.AddLines( + """ + ssl_multicert: + - dest_ip: "*" + ssl_cert_name: one.com.pem + ssl_key_name: one.com.pem + """.split("\n")) + + ts.Disk.sni_yaml.AddLines( + [ + 'sni:', + '- fqdn: "*one.com"', + ' client_cert: "one.com.pem"', + '- fqdn: "*two.com"', + ' client_cert: "two.com.pem"', + ]) + + # Set up plugin + urtest.PrepareInstalledPlugin('client_context_dump.so', ts) + + # custom log comparison. Verify the two certs we have loaded are dumped + log = urtest.Disk.File(ts.Variables.LOGDIR + '/client_context_dump.log', exists=True) + log.Content = Testers.ContainsExpression('CN=two.com', 'Info on two.com.pem should dump') + log.Content += Testers.ContainsExpression("CN=one.com", "Info on one.com.pem should dump") + + # traffic server test + t = urtest.AddTestRun("Test traffic server started properly") + t.StillRunningAfter = urtest.Processes.ts + + p = t.Processes.Default + t.MakeCurlCommand("http://127.0.0.1:{0}".format(ts.Variables.port), ts=ts) + p.ReturnCode = 0 + p.StartBefore(urtest.Processes.ts) + + # Client contexts test + tr = urtest.AddTestRun() + tr.Processes.Default.Env = ts.Env + tr.Processes.Default.Command = ('{0}/traffic_ctl plugin msg client_context_dump.t 1'.format(ts.Variables.BINDIR)) + tr.Processes.Default.ReturnCode = 0 + urtest.execute() diff --git a/tests/gold_tests/pluginTest/combo_handler/combo_handler_files/cache_control_aggregation.gold b/tests/uranium_tests/pluginTest/combo_handler/combo_handler_files/cache_control_aggregation.gold similarity index 100% rename from tests/gold_tests/pluginTest/combo_handler/combo_handler_files/cache_control_aggregation.gold rename to tests/uranium_tests/pluginTest/combo_handler/combo_handler_files/cache_control_aggregation.gold diff --git a/tests/gold_tests/pluginTest/combo_handler/combo_handler_files/max_age_zero.gold b/tests/uranium_tests/pluginTest/combo_handler/combo_handler_files/max_age_zero.gold similarity index 100% rename from tests/gold_tests/pluginTest/combo_handler/combo_handler_files/max_age_zero.gold rename to tests/uranium_tests/pluginTest/combo_handler/combo_handler_files/max_age_zero.gold diff --git a/tests/gold_tests/pluginTest/combo_handler/combo_handler_files/tr1.gold b/tests/uranium_tests/pluginTest/combo_handler/combo_handler_files/tr1.gold similarity index 100% rename from tests/gold_tests/pluginTest/combo_handler/combo_handler_files/tr1.gold rename to tests/uranium_tests/pluginTest/combo_handler/combo_handler_files/tr1.gold diff --git a/tests/gold_tests/pluginTest/combo_handler/combo_handler_files/tr2.gold b/tests/uranium_tests/pluginTest/combo_handler/combo_handler_files/tr2.gold similarity index 100% rename from tests/gold_tests/pluginTest/combo_handler/combo_handler_files/tr2.gold rename to tests/uranium_tests/pluginTest/combo_handler/combo_handler_files/tr2.gold diff --git a/tests/gold_tests/pluginTest/combo_handler/combo_handler_files/tr3.gold b/tests/uranium_tests/pluginTest/combo_handler/combo_handler_files/tr3.gold similarity index 100% rename from tests/gold_tests/pluginTest/combo_handler/combo_handler_files/tr3.gold rename to tests/uranium_tests/pluginTest/combo_handler/combo_handler_files/tr3.gold diff --git a/tests/gold_tests/pluginTest/combo_handler/ctwl.txt b/tests/uranium_tests/pluginTest/combo_handler/ctwl.txt similarity index 100% rename from tests/gold_tests/pluginTest/combo_handler/ctwl.txt rename to tests/uranium_tests/pluginTest/combo_handler/ctwl.txt diff --git a/tests/uranium_tests/pluginTest/combo_handler/test_combo_handler.py b/tests/uranium_tests/pluginTest/combo_handler/test_combo_handler.py new file mode 100644 index 00000000000..c8fe19f7d2c --- /dev/null +++ b/tests/uranium_tests/pluginTest/combo_handler/test_combo_handler.py @@ -0,0 +1,195 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_combo_handler(urtest: UraniumTest) -> None: + ''' + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import os + import sys + + urtest.Summary = ''' + Test combo_handler plugin + ''' + + # Skip if plugin not present. + # + urtest.SkipUnless(Condition.PluginExists('combo_handler.so'),) + + # Function to generate a unique data file path (in the top level of the test's run directory), put data (in string 'data') into + # the file, and return the file name. + # + _data_file__file_count = 0 + + def data_file(data): + nonlocal _data_file__file_count + file_path = urtest.RunDirectory + f"/tcp_client_in_{_data_file__file_count}" + _data_file__file_count += 1 + with open(file_path, "x") as f: + f.write(data) + return file_path + + # Function to return command (string) to run tcp_client.py tool. 'host' 'port', and 'file_path' are the parameters to tcp_client. + # + + def tcp_client_cmd(host, port, file_path): + return f"{sys.executable} {urtest.Variables.AtsTestToolsDir}/tcp_client.py {host} {port} {file_path}" + + # Function to return command (string) to run tcp_client.py tool. 'host' and 'port' are the first two parameters to tcp_client. + # 'data' is the data to put in the data file input to tcp_client. + # + + def tcp_client(host, port, data): + return tcp_client_cmd(host, port, data_file(data)) + + server = urtest.MakeOriginServer("server") + + def add_server_obj(content_type, path, cache_control="public, max-age=31536000"): + request_header = { + "headers": "GET " + path + " HTTP/1.1\r\n" + "Host: just.any.thing\r\n\r\n", + "timestamp": "1469733493.993", + "body": "" + } + response_header = { + "headers": + "HTTP/1.1 200 OK\r\n" + "Connection: close\r\n" + 'Etag: "359670651"\r\n' + "Cache-Control: " + cache_control + + "\r\n" + "Accept-Ranges: bytes\r\n" + "Content-Type: " + content_type + "\r\n" + "\r\n", + "timestamp": "1469733493.993", + "body": "Content for " + path + "\n" + } + server.addResponse("sessionfile.log", request_header, response_header) + + add_server_obj("text/css ; charset=utf-8", "/obj1") + add_server_obj("text/javascript", "/sub/obj2") + add_server_obj("text/argh", "/obj3") + add_server_obj("application/javascript", "/obj4") + add_server_obj("application/javascript", "/s/assets/module:variant_v1.js") + # Empty Content-Type header: the field is present but carries no value. + # With an allowlist configured this must be rejected; otherwise it would + # silently bypass the type check. + add_server_obj("", "/obj_empty_ct") + # Exercises CacheControlHeader::update via the refactored + # parse_cache_control_value(): private must propagate to the combo + # response and the smaller max-age must win across objects. + add_server_obj("text/javascript", "/obj_priv_short", cache_control="private, max-age=60") + # max-age=0 is a valid directive ("must revalidate"). The combo must + # propagate it as the minimum, not silently fall back to the 10-year + # default. + add_server_obj("text/javascript", "/obj_revalidate", cache_control="public, max-age=0") + + ts = urtest.MakeATSProcess("ts") + + ts.Disk.records_config.update({ + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'http|combo_handler', + }) + + ts.Disk.plugin_config.AddLine("combo_handler.so - - - ctwl.txt") + + ts.Disk.remap_config.AddLine('map http://xyz/ http://127.0.0.1/ @plugin=combo_handler.so') + ts.Disk.remap_config.AddLine(f'map http://localhost/127.0.0.1/ http://127.0.0.1:{server.Variables.Port}/') + ts.Disk.remap_config.AddLine(f'map http://localhost/sub/ http://127.0.0.1:{server.Variables.Port}/sub/') + ts.Disk.remap_config.AddLine(f'map http://localhost/s/ http://127.0.0.1:{server.Variables.Port}/s/') + + # Configure the combo_handler's configuration file. + ts.Setup.Copy("ctwl.txt", ts.Variables.CONFIGDIR) + + tr = urtest.AddTestRun() + tr.Processes.Default.StartBefore(ts) + tr.Processes.Default.StartBefore(server) + tr.Processes.Default.Command = "echo start stuff" + tr.Processes.Default.ReturnCode = 0 + + tr = urtest.AddTestRun() + tr.Processes.Default.Command = tcp_client( + "127.0.0.1", ts.Variables.port, + "GET /admin/v1/combo?obj1&sub:obj2&obj3 HTTP/1.1\n" + "Host: xyz\n" + "Connection: close\n" + "\n") + tr.Processes.Default.ReturnCode = 0 + f = tr.Disk.File("_output/1-tr-Default/stream.all.txt") + f.Content = "combo_handler_files/tr1.gold" + + tr = urtest.AddTestRun() + tr.Processes.Default.Command = tcp_client( + "127.0.0.1", ts.Variables.port, + "GET /admin/v1/combo?obj1&sub:obj2&obj4 HTTP/1.1\n" + "Host: xyz\n" + "Connection: close\n" + "\n") + tr.Processes.Default.ReturnCode = 0 + f = tr.Disk.File("_output/2-tr-Default/stream.all.txt") + f.Content = "combo_handler_files/tr2.gold" + + tr = urtest.AddTestRun() + tr.Processes.Default.Command = tcp_client( + "127.0.0.1", ts.Variables.port, + "GET /admin/v1/combo?s:assets/module:variant_v1.js HTTP/1.1\n" + "Host: xyz\n" + "Connection: close\n" + "\n") + tr.Processes.Default.ReturnCode = 0 + f = tr.Disk.File("_output/3-tr-Default/stream.all.txt") + f.Content = Testers.ContainsExpression("HTTP/1.1 200 OK", "Should successfully combine an object with a colon in its path") + f.Content += Testers.ContainsExpression( + "Content for /s/assets/module:variant_v1.js", "Should fetch the object path after the first colon") + + # An object whose Content-Type header is present but empty must not slip + # past the allowlist. Pairing it with the allowed obj1 ensures the request + # would have succeeded if the empty value were not enforced -- so a 403 + # here is a regression check for the bypass fix. + tr = urtest.AddTestRun() + tr.Processes.Default.Command = tcp_client( + "127.0.0.1", ts.Variables.port, + "GET /admin/v1/combo?obj1&obj_empty_ct HTTP/1.1\n" + "Host: xyz\n" + "Connection: close\n" + "\n") + tr.Processes.Default.ReturnCode = 0 + f = tr.Disk.File("_output/4-tr-Default/stream.all.txt") + f.Content = "combo_handler_files/tr3.gold" + + # Combining a long-TTL public object with a short-TTL private object + # must yield a Cache-Control of "max-age=60, private". This exercises + # both parse paths in the refactored parse_cache_control_value(). + tr = urtest.AddTestRun() + tr.Processes.Default.Command = tcp_client( + "127.0.0.1", ts.Variables.port, + "GET /admin/v1/combo?obj1&obj_priv_short HTTP/1.1\n" + "Host: xyz\n" + "Connection: close\n" + "\n") + tr.Processes.Default.ReturnCode = 0 + f = tr.Disk.File("_output/5-tr-Default/stream.all.txt") + f.Content = "combo_handler_files/cache_control_aggregation.gold" + + # An object with max-age=0 must drive the combined response down to + # max-age=0 instead of being filtered out as if it were absent. Pairs + # with the long-TTL obj1 to confirm the min-merge respects zero. + tr = urtest.AddTestRun() + tr.Processes.Default.Command = tcp_client( + "127.0.0.1", ts.Variables.port, + "GET /admin/v1/combo?obj1&obj_revalidate HTTP/1.1\n" + "Host: xyz\n" + "Connection: close\n" + "\n") + tr.Processes.Default.ReturnCode = 0 + f = tr.Disk.File("_output/6-tr-Default/stream.all.txt") + f.Content = "combo_handler_files/max_age_zero.gold" + + ts.Disk.diags_log.Content = Testers.ContainsExpression("ERROR", "Some tests are failure tests") + urtest.execute() diff --git a/tests/gold_tests/pluginTest/compress/compress.config b/tests/uranium_tests/pluginTest/compress/compress.config similarity index 100% rename from tests/gold_tests/pluginTest/compress/compress.config rename to tests/uranium_tests/pluginTest/compress/compress.config diff --git a/tests/gold_tests/pluginTest/compress/compress.gold b/tests/uranium_tests/pluginTest/compress/compress.gold similarity index 100% rename from tests/gold_tests/pluginTest/compress/compress.gold rename to tests/uranium_tests/pluginTest/compress/compress.gold diff --git a/tests/gold_tests/pluginTest/compress/compress2.config b/tests/uranium_tests/pluginTest/compress/compress2.config similarity index 100% rename from tests/gold_tests/pluginTest/compress/compress2.config rename to tests/uranium_tests/pluginTest/compress/compress2.config diff --git a/tests/gold_tests/pluginTest/compress/compress3.config b/tests/uranium_tests/pluginTest/compress/compress3.config similarity index 100% rename from tests/gold_tests/pluginTest/compress/compress3.config rename to tests/uranium_tests/pluginTest/compress/compress3.config diff --git a/tests/gold_tests/pluginTest/compress/compress_100_continue_origin.py b/tests/uranium_tests/pluginTest/compress/compress_100_continue_origin.py similarity index 100% rename from tests/gold_tests/pluginTest/compress/compress_100_continue_origin.py rename to tests/uranium_tests/pluginTest/compress/compress_100_continue_origin.py diff --git a/tests/gold_tests/pluginTest/compress/compress_observer.py b/tests/uranium_tests/pluginTest/compress/compress_observer.py similarity index 94% rename from tests/gold_tests/pluginTest/compress/compress_observer.py rename to tests/uranium_tests/pluginTest/compress/compress_observer.py index dbdecbf7efd..73ad763c5cf 100755 --- a/tests/gold_tests/pluginTest/compress/compress_observer.py +++ b/tests/uranium_tests/pluginTest/compress/compress_observer.py @@ -1,5 +1,5 @@ ''' -For compress gold test, observer for micro-server. +For compress Uranium test, observer for micro-server. ''' # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file diff --git a/tests/gold_tests/pluginTest/compress/compress_userver.gold b/tests/uranium_tests/pluginTest/compress/compress_userver.gold similarity index 100% rename from tests/gold_tests/pluginTest/compress/compress_userver.gold rename to tests/uranium_tests/pluginTest/compress/compress_userver.gold diff --git a/tests/gold_tests/pluginTest/compress/compress_vary.gold b/tests/uranium_tests/pluginTest/compress/compress_vary.gold similarity index 100% rename from tests/gold_tests/pluginTest/compress/compress_vary.gold rename to tests/uranium_tests/pluginTest/compress/compress_vary.gold diff --git a/tests/gold_tests/pluginTest/compress/etc/cache-true-no-compression.config b/tests/uranium_tests/pluginTest/compress/etc/cache-true-no-compression.config similarity index 100% rename from tests/gold_tests/pluginTest/compress/etc/cache-true-no-compression.config rename to tests/uranium_tests/pluginTest/compress/etc/cache-true-no-compression.config diff --git a/tests/gold_tests/pluginTest/compress/etc/cache-true-remove-accept-encoding.config b/tests/uranium_tests/pluginTest/compress/etc/cache-true-remove-accept-encoding.config similarity index 100% rename from tests/gold_tests/pluginTest/compress/etc/cache-true-remove-accept-encoding.config rename to tests/uranium_tests/pluginTest/compress/etc/cache-true-remove-accept-encoding.config diff --git a/tests/gold_tests/pluginTest/compress/etc/cache-true-remove-range.config b/tests/uranium_tests/pluginTest/compress/etc/cache-true-remove-range.config similarity index 100% rename from tests/gold_tests/pluginTest/compress/etc/cache-true-remove-range.config rename to tests/uranium_tests/pluginTest/compress/etc/cache-true-remove-range.config diff --git a/tests/gold_tests/pluginTest/compress/etc/compress-cache-false.config b/tests/uranium_tests/pluginTest/compress/etc/compress-cache-false.config similarity index 100% rename from tests/gold_tests/pluginTest/compress/etc/compress-cache-false.config rename to tests/uranium_tests/pluginTest/compress/etc/compress-cache-false.config diff --git a/tests/gold_tests/pluginTest/compress/etc/ignore-params-false.config b/tests/uranium_tests/pluginTest/compress/etc/ignore-params-false.config similarity index 100% rename from tests/gold_tests/pluginTest/compress/etc/ignore-params-false.config rename to tests/uranium_tests/pluginTest/compress/etc/ignore-params-false.config diff --git a/tests/gold_tests/pluginTest/compress/etc/ignore-params-true.config b/tests/uranium_tests/pluginTest/compress/etc/ignore-params-true.config similarity index 100% rename from tests/gold_tests/pluginTest/compress/etc/ignore-params-true.config rename to tests/uranium_tests/pluginTest/compress/etc/ignore-params-true.config diff --git a/tests/gold_tests/pluginTest/compress/greplog.sh b/tests/uranium_tests/pluginTest/compress/greplog.sh similarity index 100% rename from tests/gold_tests/pluginTest/compress/greplog.sh rename to tests/uranium_tests/pluginTest/compress/greplog.sh diff --git a/tests/gold_tests/pluginTest/compress/replay/compress-and-range.replay.yaml b/tests/uranium_tests/pluginTest/compress/replay/compress-and-range.replay.yaml similarity index 100% rename from tests/gold_tests/pluginTest/compress/replay/compress-and-range.replay.yaml rename to tests/uranium_tests/pluginTest/compress/replay/compress-and-range.replay.yaml diff --git a/tests/gold_tests/pluginTest/compress/replay/compress-content-type-params.replay.yaml b/tests/uranium_tests/pluginTest/compress/replay/compress-content-type-params.replay.yaml similarity index 100% rename from tests/gold_tests/pluginTest/compress/replay/compress-content-type-params.replay.yaml rename to tests/uranium_tests/pluginTest/compress/replay/compress-content-type-params.replay.yaml diff --git a/tests/uranium_tests/pluginTest/compress/test_compress.py b/tests/uranium_tests/pluginTest/compress/test_compress.py new file mode 100644 index 00000000000..5077f762001 --- /dev/null +++ b/tests/uranium_tests/pluginTest/compress/test_compress.py @@ -0,0 +1,343 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_compress(urtest: UraniumTest) -> None: + ''' + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + urtest.Summary = ''' + Test compress plugin + ''' + + # This test case is very bare-bones. It only covers a few scenarios that have caused problems. + + # Skip if plugins not present. + # + urtest.SkipUnless( + Condition.PluginExists('compress.so'), Condition.PluginExists('conf_remap.so'), Condition.HasATSFeature('TS_HAS_BROTLI'), + Condition.HasATSFeature('TS_HAS_ZSTD')) + + server = urtest.MakeOriginServer("server", options={'--load': f'{urtest.TestDirectory}/compress_observer.py'}) + + # Need a fairly big body, otherwise the plugin will refuse to compress + line = "lets go surfin now everybodys learnin how" + body = f'{line}\n' * 24 + line + orig_path = f'{urtest.RunDirectory}/orig.txt' + open(orig_path, 'w').write(body) + + # expected response from the origin server + response_header = { + "headers": + "HTTP/1.1 200 OK\r\nConnection: close\r\n" + 'Etag: "359670651"\r\n' + "Cache-Control: public, max-age=31536000\r\n" + + "Accept-Ranges: bytes\r\n" + "Content-Type: text/javascript\r\n" + "\r\n", + "timestamp": "1469733493.993", + "body": body + } + for i in range(6): + # add request/response to the server dictionary + request_header = { + "headers": f"GET /obj{i} HTTP/1.1\r\nHost: just.any.thing\r\n\r\n", + "timestamp": "1469733493.993", + "body": "" + } + server.addResponse("sessionfile.log", request_header, response_header) + + # post for the origin server + post_request_header = { + "headers": + "POST /obj3 HTTP/1.1\r\nHost: just.any.thing\r\nContent-Type: application/x-www-form-urlencoded\r\nContent-Length: 11\r\n\r\n", + "timestamp": "1469733493.993", + "body": "knock knock" + } + server.addResponse("sessionfile.log", post_request_header, response_header) + + def curl(ts, idx, encodingList, out_path): + return ( + f"-o {out_path} --verbose --proxy http://127.0.0.1:{ts.Variables.port}" + f" --header 'X-Ats-Compress-Test: {idx}/{encodingList}'" + f" --header 'Accept-Encoding: {encodingList}' 'http://ae-{idx}/obj{idx}'" + " 2>> compress_long.log ; printf '\n===\n' >> compress_long.log") + + def curl_post(ts, idx, encodingList, out_path): + return ( + f"-o {out_path} --verbose -d 'knock knock' --proxy http://127.0.0.1:{ts.Variables.port}" + f" --header 'X-Ats-Compress-Test: {idx}/{encodingList}'" + f" --header 'Accept-Encoding: {encodingList}' 'http://ae-{idx}/obj{idx}'" + " 2>> compress_long.log ; printf '\n===\n' >> compress_long.log") + + waitForServer = True + + waitForTs = True + + ts = urtest.MakeATSProcess("ts", enable_cache=False) + + ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'compress', + 'proxy.config.http.normalize_ae': 0, + }) + + ts.Setup.Copy("compress.config") + ts.Setup.Copy("compress2.config") + ts.Setup.Copy("compress3.config") + + ts.Disk.remap_config.AddLine( + f'map http://ae-0/ http://127.0.0.1:{server.Variables.Port}/' + + f' @plugin=compress.so @pparam={urtest.RunDirectory}/compress.config') + ts.Disk.remap_config.AddLine( + f'map http://ae-1/ http://127.0.0.1:{server.Variables.Port}/' + + ' @plugin=conf_remap.so @pparam=proxy.config.http.normalize_ae=1' + + f' @plugin=compress.so @pparam={urtest.RunDirectory}/compress.config') + ts.Disk.remap_config.AddLine( + f'map http://ae-2/ http://127.0.0.1:{server.Variables.Port}/' + + ' @plugin=conf_remap.so @pparam=proxy.config.http.normalize_ae=2' + + f' @plugin=compress.so @pparam={urtest.RunDirectory}/compress2.config') + ts.Disk.remap_config.AddLine( + f'map http://ae-3/ http://127.0.0.1:{server.Variables.Port}/' + + ' @plugin=conf_remap.so @pparam=proxy.config.http.normalize_ae=3' + + f' @plugin=compress.so @pparam={urtest.RunDirectory}/compress2.config') + ts.Disk.remap_config.AddLine( + f'map http://ae-4/ http://127.0.0.1:{server.Variables.Port}/' + + ' @plugin=conf_remap.so @pparam=proxy.config.http.normalize_ae=4' + + f' @plugin=compress.so @pparam={urtest.RunDirectory}/compress3.config') + ts.Disk.remap_config.AddLine( + f'map http://ae-5/ http://127.0.0.1:{server.Variables.Port}/' + + ' @plugin=conf_remap.so @pparam=proxy.config.http.normalize_ae=5' + + f' @plugin=compress.so @pparam={urtest.RunDirectory}/compress3.config') + + out_path_counter = 0 + + def get_out_path(): + nonlocal out_path_counter + out_path = f'{urtest.RunDirectory}/curl_out_{out_path_counter}' + out_path_counter += 1 + return out_path + + deflate_path = f'{urtest.RunDirectory}/deflate.txt' + + def get_verify_command(out_path, decrompressor): + return f"{decrompressor} {out_path} > {deflate_path} && diff {deflate_path} {orig_path}" + + for i in range(6): + + tr = urtest.AddTestRun(f'gzip, deflate, sdch, br, zstd: {i}') + if (waitForTs): + tr.Processes.Default.StartBefore(ts) + waitForTs = False + if (waitForServer): + tr.Processes.Default.StartBefore(server, ready=When.PortOpen(server.Variables.Port)) + waitForServer = False + tr.Processes.Default.ReturnCode = 0 + out_path = get_out_path() + tr.MakeCurlCommand(curl(ts, i, 'gzip, deflate, sdch, br, zstd', out_path), ts=ts) + tr = urtest.AddTestRun(f'verify gzip, deflate, sdch, br, zstd: {i}') + tr.ReturnCode = 0 + if i == 0: + tr.Processes.Default.Command = get_verify_command(out_path, "brotli -d -c") + elif i == 1: + tr.Processes.Default.Command = get_verify_command(out_path, "gunzip -k -c") + elif i == 2: + tr.Processes.Default.Command = get_verify_command(out_path, "brotli -d -c") + elif i == 3: + tr.Processes.Default.Command = get_verify_command(out_path, "brotli -d -c") + elif i == 4: + tr.Processes.Default.Command = get_verify_command(out_path, "zstd -d -c") + elif i == 5: + tr.Processes.Default.Command = get_verify_command(out_path, "zstd -d -c") + + tr = urtest.AddTestRun(f'gzip: {i}') + tr.Processes.Default.ReturnCode = 0 + out_path = get_out_path() + tr.MakeCurlCommand(curl(ts, i, "gzip", out_path), ts=ts) + tr = urtest.AddTestRun(f'verify gzip: {i}') + tr.ReturnCode = 0 + tr.Processes.Default.Command = get_verify_command(out_path, "gunzip -k -c") + + tr = urtest.AddTestRun(f'br: {i}') + tr.Processes.Default.ReturnCode = 0 + out_path = get_out_path() + tr.MakeCurlCommand(curl(ts, i, "br", out_path), ts=ts) + tr = urtest.AddTestRun(f'verify br: {i}') + tr.ReturnCode = 0 + if i == 1: + tr.Processes.Default.Command = f"diff {out_path} {orig_path}" + else: + tr.Processes.Default.Command = get_verify_command(out_path, "brotli -d -c") + + tr = urtest.AddTestRun(f'deflate: {i}') + tr.Processes.Default.ReturnCode = 0 + out_path = get_out_path() + tr.MakeCurlCommand(curl(ts, i, "deflate", out_path), ts=ts) + tr = urtest.AddTestRun(f'verify deflate: {i}') + tr.ReturnCode = 0 + tr.Processes.Default.Command = f"diff {out_path} {orig_path}" + + tr = urtest.AddTestRun(f'zstd: {i}') + tr.Processes.Default.ReturnCode = 0 + out_path = get_out_path() + tr.MakeCurlCommand(curl(ts, i, "zstd", out_path)) + tr = urtest.AddTestRun(f'verify zstd: {i}') + tr.ReturnCode = 0 + if i == 4 or i == 5: + tr.Processes.Default.Command = get_verify_command(out_path, "zstd -d -c") + else: + tr.Processes.Default.Command = get_verify_command(out_path, "cat") + + # Test Accept-Encoding normalization. + + tr = urtest.AddTestRun() + tr.Processes.Default.ReturnCode = 0 + out_path = get_out_path() + tr.MakeCurlCommand(curl(ts, 0, "gzip;q=0.666", out_path), ts=ts) + tr = urtest.AddTestRun(f'verify gzip;q=0.666') + tr.ReturnCode = 0 + tr.Processes.Default.Command = get_verify_command(out_path, "gunzip -k -c") + + tr = urtest.AddTestRun() + tr.Processes.Default.ReturnCode = 0 + out_path = get_out_path() + tr.MakeCurlCommand(curl(ts, 0, "gzip;q=0.666x", out_path), ts=ts) + tr = urtest.AddTestRun(f'verify gzip;q=0.666x') + tr.ReturnCode = 0 + tr.Processes.Default.Command = get_verify_command(out_path, "gunzip -k -c") + + tr = urtest.AddTestRun() + tr.Processes.Default.ReturnCode = 0 + out_path = get_out_path() + tr.MakeCurlCommand(curl(ts, 0, "gzip;q=#0.666", out_path), ts=ts) + tr = urtest.AddTestRun(f'verify gzip;q=#0.666') + tr.ReturnCode = 0 + tr.Processes.Default.Command = get_verify_command(out_path, "gunzip -k -c") + + tr = urtest.AddTestRun() + tr.Processes.Default.ReturnCode = 0 + out_path = get_out_path() + tr.MakeCurlCommand(curl(ts, 0, "gzip; Q = 0.666", out_path), ts=ts) + tr = urtest.AddTestRun(f'verify gzip; Q = 0.666') + tr.ReturnCode = 0 + tr.Processes.Default.Command = get_verify_command(out_path, "gunzip -k -c") + + tr = urtest.AddTestRun() + tr.Processes.Default.ReturnCode = 0 + out_path = get_out_path() + tr.MakeCurlCommand(curl(ts, 0, "gzip;q=0.0", out_path), ts=ts) + tr = urtest.AddTestRun(f'verify gzip;q=0.0') + tr.ReturnCode = 0 + tr.Processes.Default.Command = f"diff {out_path} {orig_path}" + + tr = urtest.AddTestRun() + tr.Processes.Default.ReturnCode = 0 + out_path = get_out_path() + tr.MakeCurlCommand(curl(ts, 0, "gzip;q=-0.1", out_path), ts=ts) + tr = urtest.AddTestRun(f'verify gzip;q=-0.1') + tr.ReturnCode = 0 + tr.Processes.Default.Command = get_verify_command(out_path, "gunzip -k -c") + + tr = urtest.AddTestRun() + tr.Processes.Default.ReturnCode = 0 + out_path = get_out_path() + tr.MakeCurlCommand(curl(ts, 0, "aaa, gzip;q=0.666, bbb", out_path), ts=ts) + tr = urtest.AddTestRun(f'verify aaa, gzip;q=0.666, bbb') + tr.ReturnCode = 0 + tr.Processes.Default.Command = get_verify_command(out_path, "gunzip -k -c") + + tr = urtest.AddTestRun() + tr.Processes.Default.ReturnCode = 0 + out_path = get_out_path() + tr.MakeCurlCommand(curl(ts, 0, " br ; q=0.666, bbb", out_path), ts=ts) + tr = urtest.AddTestRun(f'verify br ; q=0.666, bbb') + tr.ReturnCode = 0 + tr.Processes.Default.Command = get_verify_command(out_path, "brotli -d -c") + + tr = urtest.AddTestRun() + tr.Processes.Default.ReturnCode = 0 + out_path = get_out_path() + tr.MakeCurlCommand(curl(ts, 0, "aaa, gzip;q=0.666 , ", out_path), ts=ts) + tr = urtest.AddTestRun(f'verify aaa, gzip;q=0.666 , ') + tr.ReturnCode = 0 + tr.Processes.Default.Command = get_verify_command(out_path, "gunzip -k -c") + + # post + tr = urtest.AddTestRun() + tr.Processes.Default.ReturnCode = 0 + out_path = get_out_path() + tr.MakeCurlCommand(curl_post(ts, 3, "gzip", out_path), ts=ts) + tr = urtest.AddTestRun(f'verify gzip post') + tr.ReturnCode = 0 + tr.Processes.Default.Command = get_verify_command(out_path, "gunzip -k -c") + + # Test Vary header: compressible content without Accept-Encoding should get Vary: Accept-Encoding + tr = urtest.AddTestRun('vary header test: no accept-encoding') + tr.Processes.Default.ReturnCode = 0 + out_path = get_out_path() + tr.MakeCurlCommand( + f"-o {out_path} --verbose --proxy http://127.0.0.1:{ts.Variables.port}" + f" --header 'X-Ats-Compress-Test: vary-no-accept-encoding'" + f" 'http://ae-0/obj0'" + " 2>> compress_vary.log", + ts=ts) + + # Test Vary header: compressible content with unsupported Accept-Encoding should get Vary: Accept-Encoding + tr = urtest.AddTestRun('vary header test: unsupported accept-encoding') + tr.Processes.Default.ReturnCode = 0 + out_path = get_out_path() + tr.MakeCurlCommand(curl(ts, 0, "compress, identity", out_path).replace("compress_long.log", "compress_vary.log"), ts=ts) + + # Verify Vary header is present in both cases + tr = urtest.AddTestRun('verify vary headers') + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Command = ( + r"tr -d '\r' < compress_vary.log | grep -i 'vary:.*accept-encoding' | sort > compress_vary_short.log") + f = tr.Disk.File("compress_vary_short.log") + f.Content = "compress_vary.gold" + + # compress_long.log contains all the output from the curl commands. The tr removes the carriage returns for easier + # readability. Curl seems to have a bug, where it will neglect to output an end of line before outputting an HTTP + # message header line. The sed command is a work-around for this problem. greplog.sh uses the grep command to + # select HTTP request/response line that should be consistent every time the test runs. + # + tr = urtest.AddTestRun() + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Command = ( + r"tr -d '\r' < compress_long.log | sed 's/\(..*\)\([<>]\)/\1\n\2/' | {0}/greplog.sh > compress_short.log".format( + urtest.TestDirectory)) + f = tr.Disk.File("compress_short.log") + f.Content = "compress.gold" + + tr = urtest.AddTestRun() + tr.Processes.Default.Command = "echo" + f = tr.Disk.File("compress_userver.log") + f.Content = "compress_userver.gold" + urtest.execute() diff --git a/tests/uranium_tests/pluginTest/compress/test_compress_cache_untransformed.py b/tests/uranium_tests/pluginTest/compress/test_compress_cache_untransformed.py new file mode 100644 index 00000000000..c097b1fadec --- /dev/null +++ b/tests/uranium_tests/pluginTest/compress/test_compress_cache_untransformed.py @@ -0,0 +1,137 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_compress_cache_untransformed(urtest: UraniumTest) -> None: + ''' + Regression test for https://github.com/apache/trafficserver/issues/12244 + + The crash requires two conditions in the same transaction: + 1. An intermediate response (100 Continue) is forwarded to the client, which + sets client_response_hdr_bytes to the intermediate header size via + setup_100_continue_transfer(). + 2. The final response goes through a compress transform with untransformed + cache writing (cache=true), which calls setup_server_transfer_to_transform(). + For non-chunked responses, client_response_hdr_bytes is NOT reset to 0. + + perform_cache_write_action() then passes the stale client_response_hdr_bytes + as skip_bytes to the cache-write consumer, but the server-to-transform tunnel + buffer contains only body data (no headers). The assertion in + HttpTunnel::producer_run fires: + + c->skip_bytes <= c->buffer_reader->read_avail() + + This test uses a custom origin that sends "100 Continue" followed by a + compressible, non-chunked 200 OK to trigger the exact crash path. + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import sys + + from ports import get_port + + urtest.Summary = ''' + Regression test for compress plugin with cache=true causing assertion failure + when origin sends 100 Continue before a compressible response (#12244) + ''' + + urtest.SkipUnless(Condition.PluginExists('compress.so')) + + class CompressCacheUntransformedTest: + + def __init__(self): + self.setupTS() + self.run() + + def setupTS(self): + self.ts = urtest.MakeATSProcess("ts", enable_cache=True) + + def run(self): + tr = urtest.AddTestRun() + + # Copy scripts into the test run directory. + tr.Setup.CopyAs("compress_100_continue_origin.py") + tr.Setup.Copy("etc/compress-cache-false.config") + + # Create and configure the custom origin server process. + origin = tr.Processes.Process("origin") + origin_port = get_port(origin, 'http_port') + origin.Command = (f'{sys.executable} compress_100_continue_origin.py' + f' --port {origin_port}') + origin.Ready = When.PortOpenv4(origin_port) + origin.ReturnCode = 0 + + # Configure ATS. + self.ts.Disk.records_config.update( + { + "proxy.config.diags.debug.enabled": 1, + "proxy.config.diags.debug.tags": "http|compress|http_tunnel", + # Do NOT send 100 Continue from ATS - let the origin send it. + # This ensures ATS processes the origin's 100 via + # handle_100_continue_response -> setup_100_continue_transfer, + # which sets client_response_hdr_bytes. + "proxy.config.http.send_100_continue_response": 0, + # Enable POST caching so that the 200 OK is cached, triggering + # the cache write path where the stale client_response_hdr_bytes + # causes the crash. + "proxy.config.http.cache.post_method": 1, + }) + + self.ts.Disk.remap_config.AddLine( + f'map / http://127.0.0.1:{origin_port}/' + f' @plugin=compress.so' + f' @pparam={urtest.RunDirectory}/compress-cache-false.config') + + # Client sends a POST with Expect: 100-continue but does not wait for + # the 100 response before sending the body (--expect100-timeout 0). + # The crash is triggered by ATS processing the origin's 100 Continue, + # not by the client's behaviour during the handshake. + client = tr.Processes.Default + client.Command = ( + f'curl --http1.1 -s -o /dev/null' + f' -X POST' + f' -H "Accept-Encoding: gzip"' + f' -H "Expect: 100-continue"' + f' --expect100-timeout 0' + f' --data "test body data"' + f' http://127.0.0.1:{self.ts.Variables.port}/test/resource.js') + client.ReturnCode = 0 + client.StartBefore(origin) + client.StartBefore(self.ts) + + # The key assertion: ATS must still be running after the test. + # Without the fix, ATS would have crashed with a failed assertion + # in HttpTunnel::producer_run. + tr.StillRunningAfter = self.ts + + CompressCacheUntransformedTest() + urtest.execute() diff --git a/tests/uranium_tests/pluginTest/compress/test_compress_content_type_params.py b/tests/uranium_tests/pluginTest/compress/test_compress_content_type_params.py new file mode 100644 index 00000000000..4f143675554 --- /dev/null +++ b/tests/uranium_tests/pluginTest/compress/test_compress_content_type_params.py @@ -0,0 +1,86 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_compress_content_type_params(urtest: UraniumTest) -> None: + ''' + Test compress plugin matching with Content-Type parameters + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + urtest.Summary = ''' + Verify content_type_ignore_parameters affects compressible-content-type matching + ''' + + urtest.SkipUnless(Condition.PluginExists('compress.so')) + + class CompressPluginContentTypeParamsTest: + replayFile = "replay/compress-content-type-params.replay.yaml" + + def __init__(self): + self.setupOriginServer() + self.setupTS() + + def setupOriginServer(self): + self.server = urtest.MakeVerifierServerProcess("verifier-server", self.replayFile) + + def setupTS(self): + self.ts = urtest.MakeATSProcess("ts") + self.ts.Disk.records_config.update( + { + "proxy.config.diags.debug.enabled": 1, + "proxy.config.diags.debug.tags": "http|compress", + "proxy.config.http.insert_response_via_str": 2, + }) + + # Copy configs into the run directory + self.ts.Setup.Copy("etc/ignore-params-false.config") + self.ts.Setup.Copy("etc/ignore-params-true.config") + + self.ts.Disk.remap_config.AddLines( + [ + f'map /ignore-params-false/ http://127.0.0.1:{self.server.Variables.http_port}/ @plugin=compress.so' + + f' @pparam={urtest.RunDirectory}/ignore-params-false.config', + f'map /ignore-params-true/ http://127.0.0.1:{self.server.Variables.http_port}/ @plugin=compress.so' + + f' @pparam={urtest.RunDirectory}/ignore-params-true.config', + ]) + + def run(self): + tr = urtest.AddTestRun() + tr.AddVerifierClientProcess("verifier-client", self.replayFile, http_ports=[self.ts.Variables.port]) + tr.Processes.Default.StartBefore(self.ts) + tr.Processes.Default.StartBefore(self.server) + tr.StillRunningAfter = self.ts + tr.StillRunningAfter = self.server + + CompressPluginContentTypeParamsTest().run() + urtest.execute() diff --git a/tests/uranium_tests/pluginTest/compress/test_compress_range.py b/tests/uranium_tests/pluginTest/compress/test_compress_range.py new file mode 100644 index 00000000000..15036cdda45 --- /dev/null +++ b/tests/uranium_tests/pluginTest/compress/test_compress_range.py @@ -0,0 +1,92 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_compress_range(urtest: UraniumTest) -> None: + ''' + Test compress plugin with range request + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + urtest.Summary = ''' + Test compress plugin with range request + ''' + + urtest.SkipUnless(Condition.PluginExists('compress.so')) + + class CompressPluginTest: + replayFile = "replay/compress-and-range.replay.yaml" + + def __init__(self): + self.setupOriginServer() + self.setupTS() + + def setupOriginServer(self): + self.server = urtest.MakeVerifierServerProcess("verifier-server", self.replayFile) + + def setupTS(self): + self.ts = urtest.MakeATSProcess("ts") + self.ts.Disk.records_config.update( + { + "proxy.config.diags.debug.enabled": 1, + "proxy.config.diags.debug.tags": "http|compress", + "proxy.config.http.insert_response_via_str": 2, + }) + + self.ts.Setup.Copy("etc/cache-true-remove-range.config") + self.ts.Setup.Copy("etc/cache-true-remove-accept-encoding.config") + self.ts.Setup.Copy("etc/cache-true-no-compression.config") + + self.ts.Disk.remap_config.AddLines( + { + f""" + map /cache-true-remove-range/ http://127.0.0.1:{self.server.Variables.http_port}/ \ + @plugin=compress.so \ + @pparam={urtest.RunDirectory}/cache-true-remove-range.config + map /cache-true-remove-accept-encoding/ http://127.0.0.1:{self.server.Variables.http_port}/ \ + @plugin=compress.so \ + @pparam={urtest.RunDirectory}/cache-true-remove-accept-encoding.config + map /cache-true-no-compression/ http://127.0.0.1:{self.server.Variables.http_port}/ \ + @plugin=compress.so \ + @pparam={urtest.RunDirectory}/cache-true-no-compression.config + """ + }) + + def run(self): + tr = urtest.AddTestRun() + tr.AddVerifierClientProcess("verifier-client", self.replayFile, http_ports=[self.ts.Variables.port]) + tr.Processes.Default.StartBefore(self.ts) + tr.Processes.Default.StartBefore(self.server) + tr.StillRunningAfter = self.ts + tr.StillRunningAfter = self.server + + CompressPluginTest().run() + urtest.execute() diff --git a/tests/gold_tests/pluginTest/conf_remap/etc/negative_caching_list.yaml b/tests/uranium_tests/pluginTest/conf_remap/etc/negative_caching_list.yaml similarity index 100% rename from tests/gold_tests/pluginTest/conf_remap/etc/negative_caching_list.yaml rename to tests/uranium_tests/pluginTest/conf_remap/etc/negative_caching_list.yaml diff --git a/tests/gold_tests/pluginTest/conf_remap/replay/conf_remap.replay.yaml b/tests/uranium_tests/pluginTest/conf_remap/replay/conf_remap.replay.yaml similarity index 100% rename from tests/gold_tests/pluginTest/conf_remap/replay/conf_remap.replay.yaml rename to tests/uranium_tests/pluginTest/conf_remap/replay/conf_remap.replay.yaml diff --git a/tests/uranium_tests/pluginTest/conf_remap/test_conf_remap.py b/tests/uranium_tests/pluginTest/conf_remap/test_conf_remap.py new file mode 100644 index 00000000000..b0a44492883 --- /dev/null +++ b/tests/uranium_tests/pluginTest/conf_remap/test_conf_remap.py @@ -0,0 +1,91 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_conf_remap(urtest: UraniumTest) -> None: + ''' + Test conf_reamp plugin + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + urtest.Summary = ''' + Test conf_remap plugin + ''' + + urtest.SkipUnless(Condition.PluginExists('conf_remap.so')) + + class ConfRemapPluginTest: + + def __init__(self): + self._tr = urtest.AddTestRun("conf_remap") + self._replay_file = "replay/conf_remap.replay.yaml" + + self.__setupOriginServer() + self.__setupTS() + self.__setupClient() + + def __setupClient(self): + self._tr.AddVerifierClientProcess("verifier-client", self._replay_file, http_ports=[self._ts.Variables.port]) + + def __setupOriginServer(self): + self._server = self._tr.AddVerifierServerProcess("verifier-server", self._replay_file) + + def __setupTS(self): + self._ts = urtest.MakeATSProcess("ts", enable_cache=True) + self._ts.Disk.records_config.update( + { + "proxy.config.diags.debug.enabled": 1, + "proxy.config.diags.debug.tags": "http|conf_remap", + "proxy.config.http.insert_response_via_str": 2, + "proxy.config.http.negative_caching_enabled": 0 + }) + + self._ts.Setup.Copy("etc/negative_caching_list.yaml") + + self._ts.Disk.remap_config.AddLines( + { + f""" + map /default_negative_caching_list/ \ + http://127.0.0.1:{self._server.Variables.http_port}/default_negative_caching_list/ \ + @plugin=conf_remap.so @pparam=proxy.config.http.negative_caching_enabled=1 + + map /custom_negative_caching_list/ \ + http://127.0.0.1:{self._server.Variables.http_port}/custom_negative_caching_list/ \ + @plugin=conf_remap.so @pparam={urtest.RunDirectory}/negative_caching_list.yaml + """ + }) + + def run(self): + self._tr.Processes.Default.StartBefore(self._ts) + self._tr.StillRunningAfter = self._ts + + ConfRemapPluginTest().run() + urtest.execute() diff --git a/tests/gold_tests/pluginTest/cookie_remap/configs/bucketconfig.txt b/tests/uranium_tests/pluginTest/cookie_remap/configs/bucketconfig.txt similarity index 100% rename from tests/gold_tests/pluginTest/cookie_remap/configs/bucketconfig.txt rename to tests/uranium_tests/pluginTest/cookie_remap/configs/bucketconfig.txt diff --git a/tests/gold_tests/pluginTest/cookie_remap/configs/collapseconfig.txt b/tests/uranium_tests/pluginTest/cookie_remap/configs/collapseconfig.txt similarity index 100% rename from tests/gold_tests/pluginTest/cookie_remap/configs/collapseconfig.txt rename to tests/uranium_tests/pluginTest/cookie_remap/configs/collapseconfig.txt diff --git a/tests/gold_tests/pluginTest/cookie_remap/configs/connectorconfig.txt b/tests/uranium_tests/pluginTest/cookie_remap/configs/connectorconfig.txt similarity index 100% rename from tests/gold_tests/pluginTest/cookie_remap/configs/connectorconfig.txt rename to tests/uranium_tests/pluginTest/cookie_remap/configs/connectorconfig.txt diff --git a/tests/gold_tests/pluginTest/cookie_remap/configs/disable_pristine_host_hdr_config_false.txt b/tests/uranium_tests/pluginTest/cookie_remap/configs/disable_pristine_host_hdr_config_false.txt similarity index 100% rename from tests/gold_tests/pluginTest/cookie_remap/configs/disable_pristine_host_hdr_config_false.txt rename to tests/uranium_tests/pluginTest/cookie_remap/configs/disable_pristine_host_hdr_config_false.txt diff --git a/tests/gold_tests/pluginTest/cookie_remap/configs/disable_pristine_host_hdr_config_true.txt b/tests/uranium_tests/pluginTest/cookie_remap/configs/disable_pristine_host_hdr_config_true.txt similarity index 100% rename from tests/gold_tests/pluginTest/cookie_remap/configs/disable_pristine_host_hdr_config_true.txt rename to tests/uranium_tests/pluginTest/cookie_remap/configs/disable_pristine_host_hdr_config_true.txt diff --git a/tests/gold_tests/pluginTest/cookie_remap/configs/existsconfig.txt b/tests/uranium_tests/pluginTest/cookie_remap/configs/existsconfig.txt similarity index 100% rename from tests/gold_tests/pluginTest/cookie_remap/configs/existsconfig.txt rename to tests/uranium_tests/pluginTest/cookie_remap/configs/existsconfig.txt diff --git a/tests/gold_tests/pluginTest/cookie_remap/configs/matchconfig.txt b/tests/uranium_tests/pluginTest/cookie_remap/configs/matchconfig.txt similarity index 100% rename from tests/gold_tests/pluginTest/cookie_remap/configs/matchconfig.txt rename to tests/uranium_tests/pluginTest/cookie_remap/configs/matchconfig.txt diff --git a/tests/gold_tests/pluginTest/cookie_remap/configs/matchuriconfig.txt b/tests/uranium_tests/pluginTest/cookie_remap/configs/matchuriconfig.txt similarity index 100% rename from tests/gold_tests/pluginTest/cookie_remap/configs/matchuriconfig.txt rename to tests/uranium_tests/pluginTest/cookie_remap/configs/matchuriconfig.txt diff --git a/tests/gold_tests/pluginTest/cookie_remap/configs/notexistsconfig.txt b/tests/uranium_tests/pluginTest/cookie_remap/configs/notexistsconfig.txt similarity index 100% rename from tests/gold_tests/pluginTest/cookie_remap/configs/notexistsconfig.txt rename to tests/uranium_tests/pluginTest/cookie_remap/configs/notexistsconfig.txt diff --git a/tests/gold_tests/pluginTest/cookie_remap/configs/pcollapseconfig.txt b/tests/uranium_tests/pluginTest/cookie_remap/configs/pcollapseconfig.txt similarity index 100% rename from tests/gold_tests/pluginTest/cookie_remap/configs/pcollapseconfig.txt rename to tests/uranium_tests/pluginTest/cookie_remap/configs/pcollapseconfig.txt diff --git a/tests/gold_tests/pluginTest/cookie_remap/configs/psubstituteconfig.txt b/tests/uranium_tests/pluginTest/cookie_remap/configs/psubstituteconfig.txt similarity index 100% rename from tests/gold_tests/pluginTest/cookie_remap/configs/psubstituteconfig.txt rename to tests/uranium_tests/pluginTest/cookie_remap/configs/psubstituteconfig.txt diff --git a/tests/gold_tests/pluginTest/cookie_remap/configs/regexconfig.txt b/tests/uranium_tests/pluginTest/cookie_remap/configs/regexconfig.txt similarity index 100% rename from tests/gold_tests/pluginTest/cookie_remap/configs/regexconfig.txt rename to tests/uranium_tests/pluginTest/cookie_remap/configs/regexconfig.txt diff --git a/tests/gold_tests/pluginTest/cookie_remap/configs/statusconfig.txt b/tests/uranium_tests/pluginTest/cookie_remap/configs/statusconfig.txt similarity index 100% rename from tests/gold_tests/pluginTest/cookie_remap/configs/statusconfig.txt rename to tests/uranium_tests/pluginTest/cookie_remap/configs/statusconfig.txt diff --git a/tests/gold_tests/pluginTest/cookie_remap/configs/subcookie.txt b/tests/uranium_tests/pluginTest/cookie_remap/configs/subcookie.txt similarity index 100% rename from tests/gold_tests/pluginTest/cookie_remap/configs/subcookie.txt rename to tests/uranium_tests/pluginTest/cookie_remap/configs/subcookie.txt diff --git a/tests/gold_tests/pluginTest/cookie_remap/configs/substituteconfig.txt b/tests/uranium_tests/pluginTest/cookie_remap/configs/substituteconfig.txt similarity index 100% rename from tests/gold_tests/pluginTest/cookie_remap/configs/substituteconfig.txt rename to tests/uranium_tests/pluginTest/cookie_remap/configs/substituteconfig.txt diff --git a/tests/gold_tests/pluginTest/cookie_remap/disable_pristine_host_hdr_false.replay.yaml b/tests/uranium_tests/pluginTest/cookie_remap/disable_pristine_host_hdr_false.replay.yaml similarity index 100% rename from tests/gold_tests/pluginTest/cookie_remap/disable_pristine_host_hdr_false.replay.yaml rename to tests/uranium_tests/pluginTest/cookie_remap/disable_pristine_host_hdr_false.replay.yaml diff --git a/tests/gold_tests/pluginTest/cookie_remap/disable_pristine_host_hdr_server_canary_false.replay.yaml b/tests/uranium_tests/pluginTest/cookie_remap/disable_pristine_host_hdr_server_canary_false.replay.yaml similarity index 100% rename from tests/gold_tests/pluginTest/cookie_remap/disable_pristine_host_hdr_server_canary_false.replay.yaml rename to tests/uranium_tests/pluginTest/cookie_remap/disable_pristine_host_hdr_server_canary_false.replay.yaml diff --git a/tests/gold_tests/pluginTest/cookie_remap/disable_pristine_host_hdr_server_canary_true.replay.yaml b/tests/uranium_tests/pluginTest/cookie_remap/disable_pristine_host_hdr_server_canary_true.replay.yaml similarity index 100% rename from tests/gold_tests/pluginTest/cookie_remap/disable_pristine_host_hdr_server_canary_true.replay.yaml rename to tests/uranium_tests/pluginTest/cookie_remap/disable_pristine_host_hdr_server_canary_true.replay.yaml diff --git a/tests/gold_tests/pluginTest/cookie_remap/disable_pristine_host_hdr_server_stable.replay.yaml b/tests/uranium_tests/pluginTest/cookie_remap/disable_pristine_host_hdr_server_stable.replay.yaml similarity index 100% rename from tests/gold_tests/pluginTest/cookie_remap/disable_pristine_host_hdr_server_stable.replay.yaml rename to tests/uranium_tests/pluginTest/cookie_remap/disable_pristine_host_hdr_server_stable.replay.yaml diff --git a/tests/gold_tests/pluginTest/cookie_remap/disable_pristine_host_hdr_true.replay.yaml b/tests/uranium_tests/pluginTest/cookie_remap/disable_pristine_host_hdr_true.replay.yaml similarity index 100% rename from tests/gold_tests/pluginTest/cookie_remap/disable_pristine_host_hdr_true.replay.yaml rename to tests/uranium_tests/pluginTest/cookie_remap/disable_pristine_host_hdr_true.replay.yaml diff --git a/tests/gold_tests/pluginTest/cookie_remap/gold/collapseslashes.gold b/tests/uranium_tests/pluginTest/cookie_remap/gold/collapseslashes.gold similarity index 100% rename from tests/gold_tests/pluginTest/cookie_remap/gold/collapseslashes.gold rename to tests/uranium_tests/pluginTest/cookie_remap/gold/collapseslashes.gold diff --git a/tests/gold_tests/pluginTest/cookie_remap/gold/doesntexistcookie.gold b/tests/uranium_tests/pluginTest/cookie_remap/gold/doesntexistcookie.gold similarity index 100% rename from tests/gold_tests/pluginTest/cookie_remap/gold/doesntexistcookie.gold rename to tests/uranium_tests/pluginTest/cookie_remap/gold/doesntexistcookie.gold diff --git a/tests/gold_tests/pluginTest/cookie_remap/gold/existscookie.gold b/tests/uranium_tests/pluginTest/cookie_remap/gold/existscookie.gold similarity index 100% rename from tests/gold_tests/pluginTest/cookie_remap/gold/existscookie.gold rename to tests/uranium_tests/pluginTest/cookie_remap/gold/existscookie.gold diff --git a/tests/gold_tests/pluginTest/cookie_remap/gold/matchcookie.gold b/tests/uranium_tests/pluginTest/cookie_remap/gold/matchcookie.gold similarity index 100% rename from tests/gold_tests/pluginTest/cookie_remap/gold/matchcookie.gold rename to tests/uranium_tests/pluginTest/cookie_remap/gold/matchcookie.gold diff --git a/tests/gold_tests/pluginTest/cookie_remap/gold/matchcookie2.gold b/tests/uranium_tests/pluginTest/cookie_remap/gold/matchcookie2.gold similarity index 100% rename from tests/gold_tests/pluginTest/cookie_remap/gold/matchcookie2.gold rename to tests/uranium_tests/pluginTest/cookie_remap/gold/matchcookie2.gold diff --git a/tests/gold_tests/pluginTest/cookie_remap/gold/matchelsestatus.gold b/tests/uranium_tests/pluginTest/cookie_remap/gold/matchelsestatus.gold similarity index 100% rename from tests/gold_tests/pluginTest/cookie_remap/gold/matchelsestatus.gold rename to tests/uranium_tests/pluginTest/cookie_remap/gold/matchelsestatus.gold diff --git a/tests/gold_tests/pluginTest/cookie_remap/gold/matchstatus.gold b/tests/uranium_tests/pluginTest/cookie_remap/gold/matchstatus.gold similarity index 100% rename from tests/gold_tests/pluginTest/cookie_remap/gold/matchstatus.gold rename to tests/uranium_tests/pluginTest/cookie_remap/gold/matchstatus.gold diff --git a/tests/gold_tests/pluginTest/cookie_remap/gold/psubstitute.gold b/tests/uranium_tests/pluginTest/cookie_remap/gold/psubstitute.gold similarity index 100% rename from tests/gold_tests/pluginTest/cookie_remap/gold/psubstitute.gold rename to tests/uranium_tests/pluginTest/cookie_remap/gold/psubstitute.gold diff --git a/tests/gold_tests/pluginTest/cookie_remap/gold/regexdoesntmatch.gold b/tests/uranium_tests/pluginTest/cookie_remap/gold/regexdoesntmatch.gold similarity index 100% rename from tests/gold_tests/pluginTest/cookie_remap/gold/regexdoesntmatch.gold rename to tests/uranium_tests/pluginTest/cookie_remap/gold/regexdoesntmatch.gold diff --git a/tests/gold_tests/pluginTest/cookie_remap/gold/regexmatches.gold b/tests/uranium_tests/pluginTest/cookie_remap/gold/regexmatches.gold similarity index 100% rename from tests/gold_tests/pluginTest/cookie_remap/gold/regexmatches.gold rename to tests/uranium_tests/pluginTest/cookie_remap/gold/regexmatches.gold diff --git a/tests/gold_tests/pluginTest/cookie_remap/gold/substitute.gold b/tests/uranium_tests/pluginTest/cookie_remap/gold/substitute.gold similarity index 100% rename from tests/gold_tests/pluginTest/cookie_remap/gold/substitute.gold rename to tests/uranium_tests/pluginTest/cookie_remap/gold/substitute.gold diff --git a/tests/gold_tests/pluginTest/cookie_remap/gold/wontmatchcookie.gold b/tests/uranium_tests/pluginTest/cookie_remap/gold/wontmatchcookie.gold similarity index 100% rename from tests/gold_tests/pluginTest/cookie_remap/gold/wontmatchcookie.gold rename to tests/uranium_tests/pluginTest/cookie_remap/gold/wontmatchcookie.gold diff --git a/tests/gold_tests/pluginTest/cookie_remap/gold/wontmatchcookie2.gold b/tests/uranium_tests/pluginTest/cookie_remap/gold/wontmatchcookie2.gold similarity index 100% rename from tests/gold_tests/pluginTest/cookie_remap/gold/wontmatchcookie2.gold rename to tests/uranium_tests/pluginTest/cookie_remap/gold/wontmatchcookie2.gold diff --git a/tests/uranium_tests/pluginTest/cookie_remap/test_bucketcookie.py b/tests/uranium_tests/pluginTest/cookie_remap/test_bucketcookie.py new file mode 100644 index 00000000000..3438658cf9f --- /dev/null +++ b/tests/uranium_tests/pluginTest/cookie_remap/test_bucketcookie.py @@ -0,0 +1,130 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_bucketcookie(urtest: UraniumTest) -> None: + ''' + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import os + + urtest.Summary = ''' + + ''' + urtest.SkipUnless(Condition.PluginExists('cookie_remap.so')) + urtest.ContinueOnFail = True + urtest.testName = "cookie_remap: cookie in bucket or not" + + # Define default ATS + ts = urtest.MakeATSProcess("ts") + + # First server is run during first test and + # second server is run during second test + server = urtest.MakeOriginServer("server", ip='127.0.0.10') + + request_header = { + "headers": "GET /cookiematches HTTP/1.1\r\nHost: www.example.com\r\n\r\n", + "timestamp": "1469733493.993", + "body": "" + } + # expected response from the origin server + response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + + # add response to the server dictionary + server.addResponse("sessionfile.log", request_header, response_header) + + server2 = urtest.MakeOriginServer("server2", ip='127.0.0.11') + request_header2 = { + "headers": "GET /cookiedoesntmatch HTTP/1.1\r\nHost: www.example.com\r\n\r\n", + "timestamp": "1469733493.993", + "body": "" + } + # expected response from the origin server + response_header2 = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + + # add response to the server dictionary + server2.addResponse("sessionfile.log", request_header2, response_header2) + + # Setup the remap configuration + config_path = os.path.join(urtest.TestDirectory, "configs/bucketconfig.txt") + with open(config_path, 'r') as config_file: + config1 = config_file.read() + + ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'cookie_remap.*|http.*|dns.*', + }) + + config1 = config1.replace("$PORT", str(server.Variables.Port)) + config1 = config1.replace("$ALTPORT", str(server2.Variables.Port)) + + ts.Disk.File(ts.Variables.CONFIGDIR + "/bucketconfig.txt", id="config1") + ts.Disk.config1.WriteOn(config1) + + ts.Disk.remap_config.AddLine( + 'map http://www.example.com/magic http://shouldnothit.com @plugin=cookie_remap.so @pparam=config/bucketconfig.txt') + + # Cookie value in bucket + tr = urtest.AddTestRun("cookie value in bucket") + tr.MakeCurlCommand( + ''' --proxy 127.0.0.1:{0} \ + "http://www.example.com/magic" \ + -H"Cookie: fpbeta=333" \ + -H "Proxy-Connection: keep-alive" \ + --verbose \ + '''.format(ts.Variables.port), + ts=ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.StartBefore(server, ready=When.PortOpen(server.Variables.Port)) + tr.Processes.Default.StartBefore(urtest.Processes.ts) + tr.StillRunningAfter = ts + + server.Streams.All = "gold/matchcookie.gold" + + # cookie value not in bucket + tr = urtest.AddTestRun("cooke value not in bucket") + tr.MakeCurlCommand( + ''' --proxy 127.0.0.1:{0} \ + "http://www.example.com/magic" \ + -H"Cookie: fpbeta=etc" \ + -H "Proxy-Connection: keep-alive" \ + --verbose \ + '''.format(ts.Variables.port), + ts=ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.StartBefore(server2, ready=When.PortOpen(server2.Variables.Port)) + tr.StillRunningAfter = ts + + server2.Streams.All = "gold/wontmatchcookie.gold" + urtest.execute() diff --git a/tests/uranium_tests/pluginTest/cookie_remap/test_collapseslashes.py b/tests/uranium_tests/pluginTest/cookie_remap/test_collapseslashes.py new file mode 100644 index 00000000000..34833e74773 --- /dev/null +++ b/tests/uranium_tests/pluginTest/cookie_remap/test_collapseslashes.py @@ -0,0 +1,99 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_collapseslashes(urtest: UraniumTest) -> None: + ''' + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import os + + urtest.Summary = ''' + + ''' + urtest.SkipUnless(Condition.PluginExists('cookie_remap.so')) + urtest.ContinueOnFail = True + urtest.testName = "cookie_remap: plugin collapses consecutive slashes" + + # Define default ATS + ts = urtest.MakeATSProcess("ts") + + # We only run a server to capture ATS outbound requests + # and verify it collapsed the double // + server = urtest.MakeOriginServer("server", ip='127.0.0.10') + + request_header = { + "headers": "GET /i/like/cheetos?.done=http://finance.yahoo.com HTTP/1.1\r\nHost: www.example.com\r\n\r\n", + "timestamp": "1469733493.993", + "body": "" + } + # expected response from the origin server + response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + + # add response to the server dictionary + server.addResponse("sessionfile.log", request_header, response_header) + + # Setup the remap configuration + config_path = os.path.join(urtest.TestDirectory, "configs/collapseconfig.txt") + with open(config_path, 'r') as config_file: + config1 = config_file.read() + + ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'cookie_remap.*|http.*|dns.*', + }) + + config1 = config1.replace("$PORT", str(server.Variables.Port)) + + ts.Disk.File(ts.Variables.CONFIGDIR + "/collapseconfig.txt", id="config1") + ts.Disk.config1.WriteOn(config1) + + ts.Disk.remap_config.AddLine( + 'map http://www.example.com/magic http://shouldnothit.com/magic @plugin=cookie_remap.so @pparam=config/collapseconfig.txt') + + tr = urtest.AddTestRun("collapse consecutive forward slashes") + tr.MakeCurlCommand( + ''' --proxy 127.0.0.1:{0} \ + "http://www.example.com/magic" \ + -H "Proxy-Connection: keep-alive" \ + --verbose \ + '''.format(ts.Variables.port), + ts=ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.StartBefore(server, ready=When.PortOpen(server.Variables.Port)) + tr.Processes.Default.StartBefore(urtest.Processes.ts) + tr.StillRunningAfter = ts + + server.Streams.All = "gold/collapseslashes.gold" + urtest.execute() diff --git a/tests/uranium_tests/pluginTest/cookie_remap/test_connector.py b/tests/uranium_tests/pluginTest/cookie_remap/test_connector.py new file mode 100644 index 00000000000..3d1c7d76157 --- /dev/null +++ b/tests/uranium_tests/pluginTest/cookie_remap/test_connector.py @@ -0,0 +1,130 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_connector(urtest: UraniumTest) -> None: + ''' + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import os + + urtest.Summary = ''' + + ''' + urtest.SkipUnless(Condition.PluginExists('cookie_remap.so')) + urtest.ContinueOnFail = True + urtest.testName = "cookie_remap: test connector" + + # Define default ATS + ts = urtest.MakeATSProcess("ts") + + # First server is run during first test and + # second server is run during second test + server = urtest.MakeOriginServer("server", ip='127.0.0.10') + + request_header = { + "headers": "GET /cookiematches HTTP/1.1\r\nHost: www.example.com\r\n\r\n", + "timestamp": "1469733493.993", + "body": "" + } + # expected response from the origin server + response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + + # add response to the server dictionary + server.addResponse("sessionfile.log", request_header, response_header) + + server2 = urtest.MakeOriginServer("server2", ip='127.0.0.11') + request_header2 = { + "headers": "GET /cookiedoesntmatch HTTP/1.1\r\nHost: www.example.com\r\n\r\n", + "timestamp": "1469733493.993", + "body": "" + } + # expected response from the origin server + response_header2 = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + + # add response to the server dictionary + server2.addResponse("sessionfile.log", request_header2, response_header2) + + # Setup the remap configuration + config_path = os.path.join(urtest.TestDirectory, "configs/connectorconfig.txt") + with open(config_path, 'r') as config_file: + config1 = config_file.read() + + ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'cookie_remap.*|http.*|dns.*', + }) + + config1 = config1.replace("$PORT", str(server.Variables.Port)) + config1 = config1.replace("$ALTPORT", str(server2.Variables.Port)) + + ts.Disk.File(ts.Variables.CONFIGDIR + "/connectorconfig.txt", id="config1") + ts.Disk.config1.WriteOn(config1) + + ts.Disk.remap_config.AddLine( + 'map http://www.example.com/magic http://shouldnothit.com/magic @plugin=cookie_remap.so @pparam=config/connectorconfig.txt') + + # Positive test case that remaps because all connected operations pass + tr = urtest.AddTestRun("cookie value matches") + tr.MakeCurlCommand( + ''' --proxy 127.0.0.1:{0} \ + "http://www.example.com/magic" \ + -H"Cookie: fpbeta=abcd icecream=donteat" \ + -H "Proxy-Connection: keep-alive" \ + --verbose \ + '''.format(ts.Variables.port), + ts=ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.StartBefore(server, ready=When.PortOpen(server.Variables.Port)) + tr.Processes.Default.StartBefore(urtest.Processes.ts) + tr.StillRunningAfter = ts + + server.Streams.All = "gold/matchcookie.gold" + + # Negative test case that doesn't remap because not all subops pass + tr = urtest.AddTestRun("cookie value doesn't match") + tr.MakeCurlCommand( + ''' --proxy 127.0.0.1:{0} \ + "http://www.example.com/magic" \ + -H"Cookie: fpbeta=somethingelse" \ + -H "Proxy-Connection: keep-alive" \ + --verbose \ + '''.format(ts.Variables.port), + ts=ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.StartBefore(server2, ready=When.PortOpen(server2.Variables.Port)) + tr.StillRunningAfter = ts + + server2.Streams.All = "gold/wontmatchcookie.gold" + urtest.execute() diff --git a/tests/uranium_tests/pluginTest/cookie_remap/test_disable_pristine_host_hdr.py b/tests/uranium_tests/pluginTest/cookie_remap/test_disable_pristine_host_hdr.py new file mode 100644 index 00000000000..a2cc570a06e --- /dev/null +++ b/tests/uranium_tests/pluginTest/cookie_remap/test_disable_pristine_host_hdr.py @@ -0,0 +1,150 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_disable_pristine_host_hdr(urtest: UraniumTest) -> None: + ''' + Verify cookie_remap plugin's disable_pristine_host_hdr functionality. + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import os + + urtest.Summary = ''' + Test cookie_remap plugin's disable_pristine_host_hdr functionality. Verifies that + when disable_pristine_host_hdr is set to true, the pristine host header setting + is disabled for the matched transaction, allowing the Host header to be updated + to match the remapped destination. + ''' + urtest.SkipUnless(Condition.PluginExists('cookie_remap.so')) + urtest.ContinueOnFail = True + + class TestUpdateHostHeader: + """ + Test the disable_pristine_host_hdr feature of cookie_remap plugin. + + This test verifies that: + 1. Cookie bucket routing works correctly + 2. When disable_pristine_host_hdr is enabled, the Host header is updated + to match the sendto URL destination + 3. When disable_pristine_host_hdr is not set, the Host header remains as + the original client value + """ + + # Counter for unique process names across multiple test instances + test_counter: int = 0 + + def __init__(self, disable_pristine_host_hdr=True): + """Initialize the test by setting up servers and ATS configuration. + :param disable_pristine_host_hdr: Whether to configure disable_pristine_host_hdr + in the cookie_remap configuration. + """ + self.test_id = TestUpdateHostHeader.test_counter + TestUpdateHostHeader.test_counter += 1 + self.disable_pristine_host_hdr = disable_pristine_host_hdr + self.replay_file = f'disable_pristine_host_hdr_{"true" if disable_pristine_host_hdr else "false"}.replay.yaml' + self.canary_replay_file = ( + 'disable_pristine_host_hdr_server_canary_true.replay.yaml' + if disable_pristine_host_hdr else 'disable_pristine_host_hdr_server_canary_false.replay.yaml') + self.stable_replay_file = 'disable_pristine_host_hdr_server_stable.replay.yaml' + self._setupDns() + self._setupServers() + self._setupTS(disable_pristine_host_hdr) + self._setupClient() + + def _setupDns(self): + """Configure the DNS server.""" + self._dns = urtest.MakeDNServer(f"dns_{self.test_id}", default='127.0.0.1') + + def _setupServers(self): + """ + Configure the origin servers using proxy-verifier. + + Creates two servers to simulate canary and stable environments. + """ + self._server_canary = urtest.MakeVerifierServerProcess(f"server_canary_{self.test_id}", self.canary_replay_file) + expected_host = 'canary.com' if self.disable_pristine_host_hdr else 'example.com' + self._server_canary.Streams.All += Testers.ContainsExpression(expected_host, f'Host header should be {expected_host}') + + self._server_stable = urtest.MakeVerifierServerProcess(f"server_stable_{self.test_id}", self.stable_replay_file) + # The else path always preserves pristine host header (example.com). + expected_host = 'example.com' + self._server_stable.Streams.All += Testers.ContainsExpression(expected_host, f'Host header should be {expected_host}') + + def _setupTS(self, disable_pristine_host_hdr): + """Configure Traffic Server with cookie_remap plugin. + :param disable_pristine_host_hdr: Whether to configure disable_pristine_host_hdr in cookie_remap. + """ + ts = urtest.MakeATSProcess(f"ts_{self.test_id}", enable_cache=False) + self._ts = ts + + # Enable debug logging for cookie_remap and enable pristine_host_hdr + # (simulating production environment) + ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'cookie_remap|http', + 'proxy.config.dns.nameservers': f"127.0.0.1:{self._dns.Variables.Port}", + 'proxy.config.dns.resolv_conf': 'NULL', + 'proxy.config.url_remap.pristine_host_hdr': 1, + }) + + # Read and configure the cookie_remap configuration file + config_filename = f'disable_pristine_host_hdr_config_{"true" if disable_pristine_host_hdr else "false"}.txt' + config_path = os.path.join(urtest.TestDirectory, f"configs/{config_filename}") + with open(config_path, 'r') as config_file: + config_content = config_file.read() + config_content = config_content.replace("$CANARY_PORT", str(self._server_canary.Variables.http_port)) + config_content = config_content.replace("$STABLE_PORT", str(self._server_stable.Variables.http_port)) + ts.Disk.File(ts.Variables.CONFIGDIR + f"/{config_filename}", id="cookie_config") + ts.Disk.cookie_config.WriteOn(config_content) + + # Configure remap rule with cookie_remap plugin + ts.Disk.remap_config.AddLine( + 'map http://example.com http://shouldnothit.com ' + f'@plugin=cookie_remap.so @pparam=config/{config_filename}') + + def _setupClient(self): + """Setup the client for the test.""" + enabled_str = "enabled" if self.disable_pristine_host_hdr else "disabled" + tr = urtest.AddTestRun(f'Test cookie bucket routing with disable_pristine_host_hdr {enabled_str}') + + p = tr.AddVerifierClientProcess(f'client_{self.test_id}', self.replay_file, http_ports=[self._ts.Variables.port]) + p.StartBefore(self._dns) + p.StartBefore(self._ts) + p.StartBefore(self._server_canary) + p.StartBefore(self._server_stable) + + # Execute the test + TestUpdateHostHeader(disable_pristine_host_hdr=True) + TestUpdateHostHeader(disable_pristine_host_hdr=False) + urtest.execute() diff --git a/tests/uranium_tests/pluginTest/cookie_remap/test_existscookie.py b/tests/uranium_tests/pluginTest/cookie_remap/test_existscookie.py new file mode 100644 index 00000000000..4554a8df63d --- /dev/null +++ b/tests/uranium_tests/pluginTest/cookie_remap/test_existscookie.py @@ -0,0 +1,130 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_existscookie(urtest: UraniumTest) -> None: + ''' + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import os + + urtest.Summary = ''' + + ''' + urtest.SkipUnless(Condition.PluginExists('cookie_remap.so')) + urtest.ContinueOnFail = True + urtest.testName = "cookie_remap: cookie exists" + + # Define default ATS + ts = urtest.MakeATSProcess("ts") + + # First server is run during first test and + # second server is run during second test + server = urtest.MakeOriginServer("server", ip='127.0.0.10') + + request_header = { + "headers": "GET /cookieexists HTTP/1.1\r\nHost: www.example.com\r\n\r\n", + "timestamp": "1469733493.993", + "body": "" + } + # expected response from the origin server + response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + + # add response to the server dictionary + server.addResponse("sessionfile.log", request_header, response_header) + + server2 = urtest.MakeOriginServer("server2", ip='127.0.0.11') + request_header2 = { + "headers": "GET /cookiedoesntexist HTTP/1.1\r\nHost: www.example.com\r\n\r\n", + "timestamp": "1469733493.993", + "body": "" + } + # expected response from the origin server + response_header2 = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + + # add response to the server dictionary + server2.addResponse("sessionfile.log", request_header2, response_header2) + + # Setup the remap configuration + config_path = os.path.join(urtest.TestDirectory, "configs/existsconfig.txt") + with open(config_path, 'r') as config_file: + config1 = config_file.read() + + ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'cookie_remap.*|http.*|dns.*', + }) + + config1 = config1.replace("$PORT", str(server.Variables.Port)) + config1 = config1.replace("$ALTPORT", str(server2.Variables.Port)) + + ts.Disk.File(ts.Variables.CONFIGDIR + "/existsconfig.txt", id="config1") + ts.Disk.config1.WriteOn(config1) + + ts.Disk.remap_config.AddLine( + 'map http://www.example.com/magic http://shouldnothit.com @plugin=cookie_remap.so @pparam=config/existsconfig.txt') + + # Positive test case that remaps because cookie exists + tr = urtest.AddTestRun("cookie fpbeta exists") + tr.MakeCurlCommand( + ''' --proxy 127.0.0.1:{0} \ + "http://www.example.com/magic" \ + -H"Cookie: fpbeta=fdkfkdfkdfkdkdfkdfk" \ + -H "Proxy-Connection: keep-alive" \ + --verbose \ + '''.format(ts.Variables.port), + ts=ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.StartBefore(server, ready=When.PortOpen(server.Variables.Port)) + tr.Processes.Default.StartBefore(urtest.Processes.ts) + tr.StillRunningAfter = ts + + server.Streams.All = "gold/existscookie.gold" + + # Negative test case that doesn't remap because cookie doesn't exist + tr = urtest.AddTestRun("cooke fpbeta doesnt exist") + tr.MakeCurlCommand( + ''' --proxy 127.0.0.1:{0} \ + "http://www.example.com/magic" \ + -H"Cookie: breadcrumb=etc" \ + -H "Proxy-Connection: keep-alive" \ + --verbose \ + '''.format(ts.Variables.port), + ts=ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.StartBefore(server2, ready=When.PortOpen(server2.Variables.Port)) + tr.StillRunningAfter = ts + + server2.Streams.All = "gold/doesntexistcookie.gold" + urtest.execute() diff --git a/tests/uranium_tests/pluginTest/cookie_remap/test_matchcookie.py b/tests/uranium_tests/pluginTest/cookie_remap/test_matchcookie.py new file mode 100644 index 00000000000..4a8274b0a9f --- /dev/null +++ b/tests/uranium_tests/pluginTest/cookie_remap/test_matchcookie.py @@ -0,0 +1,130 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_matchcookie(urtest: UraniumTest) -> None: + ''' + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import os + + urtest.Summary = ''' + + ''' + urtest.SkipUnless(Condition.PluginExists('cookie_remap.so')) + urtest.ContinueOnFail = True + urtest.testName = "cookie_remap: match cookie" + + # Define default ATS + ts = urtest.MakeATSProcess("ts") + + # First server is run during first test and + # second server is run during second test + server = urtest.MakeOriginServer("server", ip='127.0.0.10') + + request_header = { + "headers": "GET /cookiematches?a=1&b=2&c=3 HTTP/1.1\r\nHost: www.example.com\r\n\r\n", + "timestamp": "1469733493.993", + "body": "" + } + # expected response from the origin server + response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + + # add response to the server dictionary + server.addResponse("sessionfile.log", request_header, response_header) + + server2 = urtest.MakeOriginServer("server2", ip='127.0.0.11') + request_header2 = { + "headers": "GET /cookiedoesntmatch?a=1&b=2&c=3 HTTP/1.1\r\nHost: www.example.com\r\n\r\n", + "timestamp": "1469733493.993", + "body": "" + } + # expected response from the origin server + response_header2 = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + + # add response to the server dictionary + server2.addResponse("sessionfile.log", request_header2, response_header2) + + # Setup the remap configuration + config_path = os.path.join(urtest.TestDirectory, "configs/matchconfig.txt") + with open(config_path, 'r') as config_file: + config1 = config_file.read() + + ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'cookie_remap.*|http.*|dns.*', + }) + + config1 = config1.replace("$PORT", str(server.Variables.Port)) + config1 = config1.replace("$ALTPORT", str(server2.Variables.Port)) + + ts.Disk.File(ts.Variables.CONFIGDIR + "/matchconfig.txt", id="config1") + ts.Disk.config1.WriteOn(config1) + + ts.Disk.remap_config.AddLine( + 'map http://www.example.com/magic http://shouldnothit.com @plugin=cookie_remap.so @pparam=config/matchconfig.txt') + + # Positive test case that remaps because cookie matches + tr = urtest.AddTestRun("cookie value matches") + tr.MakeCurlCommand( + ''' --proxy 127.0.0.1:{0} \ + "http://www.example.com/magic?a=1&b=2&c=3" \ + -H"Cookie: fpbeta=magic" \ + -H "Proxy-Connection: keep-alive" \ + --verbose \ + '''.format(ts.Variables.port), + ts=ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.StartBefore(server, ready=When.PortOpen(server.Variables.Port)) + tr.Processes.Default.StartBefore(urtest.Processes.ts) + tr.StillRunningAfter = ts + + server.Streams.All = "gold/matchcookie2.gold" + + # Negative test case that doesn't remap because cookie doesn't match + tr = urtest.AddTestRun("cookie regex doesn't match") + tr.MakeCurlCommand( + ''' --proxy 127.0.0.1:{0} \ + "http://www.example.com/magic?a=1&b=2&c=3" \ + -H"Cookie: fpbeta=magit" \ + -H "Proxy-Connection: keep-alive" \ + --verbose \ + '''.format(ts.Variables.port), + ts=ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.StartBefore(server2, ready=When.PortOpen(server2.Variables.Port)) + tr.StillRunningAfter = ts + + server2.Streams.All = "gold/wontmatchcookie2.gold" + urtest.execute() diff --git a/tests/uranium_tests/pluginTest/cookie_remap/test_matchuri.py b/tests/uranium_tests/pluginTest/cookie_remap/test_matchuri.py new file mode 100644 index 00000000000..3eeec31d07c --- /dev/null +++ b/tests/uranium_tests/pluginTest/cookie_remap/test_matchuri.py @@ -0,0 +1,129 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_matchuri(urtest: UraniumTest) -> None: + ''' + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import os + + urtest.Summary = ''' + + ''' + urtest.SkipUnless(Condition.PluginExists('cookie_remap.so')) + urtest.ContinueOnFail = True + urtest.testName = "cookie_remap: match cookie" + + # Define default ATS + ts = urtest.MakeATSProcess("ts") + + # First server is run during first test and + # second server is run during second test + server = urtest.MakeOriginServer("server", ip='127.0.0.10') + + request_header = { + "headers": "GET /cookiematches HTTP/1.1\r\nHost: www.example.com\r\n\r\n", + "timestamp": "1469733493.993", + "body": "" + } + # expected response from the origin server + response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + + # add response to the server dictionary + server.addResponse("sessionfile.log", request_header, response_header) + + server2 = urtest.MakeOriginServer("server2", ip='127.0.0.11') + request_header2 = { + "headers": "GET /cookiedoesntmatch HTTP/1.1\r\nHost: www.example.com\r\n\r\n", + "timestamp": "1469733493.993", + "body": "" + } + # expected response from the origin server + response_header2 = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + + # add response to the server dictionary + server2.addResponse("sessionfile.log", request_header2, response_header2) + + # Setup the remap configuration + config_path = os.path.join(urtest.TestDirectory, "configs/matchuriconfig.txt") + with open(config_path, 'r') as config_file: + config1 = config_file.read() + + ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'cookie_remap.*|http.*|dns.*', + }) + + config1 = config1.replace("$PORT", str(server.Variables.Port)) + config1 = config1.replace("$ALTPORT", str(server2.Variables.Port)) + + ts.Disk.File(ts.Variables.CONFIGDIR + "/matchuriconfig.txt", id="config1") + ts.Disk.config1.WriteOn(config1) + + ts.Disk.remap_config.AddLine( + 'map http://www.example.com/magic http://shouldnothit.com @plugin=cookie_remap.so @pparam=config/matchuriconfig.txt') + + # Positive test case, URI matches rule + tr = urtest.AddTestRun("URI value matches") + tr.MakeCurlCommand( + ''' --proxy 127.0.0.1:{0} \ + "http://www.example.com/magic/thisispartofthepath" \ + -H "Proxy-Connection: keep-alive" \ + --verbose \ + '''.format(ts.Variables.port), + ts=ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.StartBefore(server, ready=When.PortOpen(server.Variables.Port)) + tr.Processes.Default.StartBefore(urtest.Processes.ts) + tr.StillRunningAfter = ts + + server.Streams.All = "gold/matchcookie.gold" + + # Negative test case that doesn't remap because URI doesn't match + tr = urtest.AddTestRun("URI value doesn't match") + tr.MakeCurlCommand( + ''' --proxy 127.0.0.1:{0} \ + "http://www.example.com/magic" \ + -H"Cookie: fpbeta=etc" \ + -H "Proxy-Connection: keep-alive" \ + --verbose \ + '''.format(ts.Variables.port), + ts=ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.StartBefore(server2, ready=When.PortOpen(server2.Variables.Port)) + tr.StillRunningAfter = ts + + server2.Streams.All = "gold/wontmatchcookie.gold" + urtest.execute() diff --git a/tests/uranium_tests/pluginTest/cookie_remap/test_notexistscookie.py b/tests/uranium_tests/pluginTest/cookie_remap/test_notexistscookie.py new file mode 100644 index 00000000000..471b2796534 --- /dev/null +++ b/tests/uranium_tests/pluginTest/cookie_remap/test_notexistscookie.py @@ -0,0 +1,129 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_notexistscookie(urtest: UraniumTest) -> None: + ''' + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import os + + urtest.Summary = ''' + + ''' + urtest.SkipUnless(Condition.PluginExists('cookie_remap.so')) + urtest.ContinueOnFail = True + urtest.testName = "cookie_remap: cookie not exists" + + # Define default ATS + ts = urtest.MakeATSProcess("ts") + + # First server is run during first test and + # second server is run during second test + server = urtest.MakeOriginServer("server", ip='127.0.0.10') + + request_header = { + "headers": "GET /cookiedoesntexist HTTP/1.1\r\nHost: www.example.com\r\n\r\n", + "timestamp": "1469733493.993", + "body": "" + } + # expected response from the origin server + response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + + # add response to the server dictionary + server.addResponse("sessionfile.log", request_header, response_header) + + server2 = urtest.MakeOriginServer("server2", ip='127.0.0.11') + request_header2 = { + "headers": "GET /cookieexists HTTP/1.1\r\nHost: www.example.com\r\n\r\n", + "timestamp": "1469733493.993", + "body": "" + } + # expected response from the origin server + response_header2 = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + + # add response to the server dictionary + server2.addResponse("sessionfile.log", request_header2, response_header2) + + # Setup the remap configuration + config_path = os.path.join(urtest.TestDirectory, "configs/notexistsconfig.txt") + with open(config_path, 'r') as config_file: + config1 = config_file.read() + + ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'cookie_remap.*|http.*|dns.*', + }) + + config1 = config1.replace("$PORT", str(server.Variables.Port)) + config1 = config1.replace("$ALTPORT", str(server2.Variables.Port)) + + ts.Disk.File(ts.Variables.CONFIGDIR + "/notexistsconfig.txt", id="config1") + ts.Disk.config1.WriteOn(config1) + + ts.Disk.remap_config.AddLine( + 'map http://www.example.com/magic http://shouldnothit.com @plugin=cookie_remap.so @pparam=config/notexistsconfig.txt') + + # Positive test case that remaps because cookie doesn't exist + tr = urtest.AddTestRun("cookie fpbeta doesn't exist") + tr.MakeCurlCommand( + ''' --proxy 127.0.0.1:{0} \ + "http://www.example.com/magic" \ + -H "Proxy-Connection: keep-alive" \ + --verbose \ + '''.format(ts.Variables.port), + ts=ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.StartBefore(server, ready=When.PortOpen(server.Variables.Port)) + tr.Processes.Default.StartBefore(urtest.Processes.ts) + tr.StillRunningAfter = ts + + server.Streams.All = "gold/doesntexistcookie.gold" + + # Negative test case that doesn't remap because cookie exists + tr = urtest.AddTestRun("cooke fpbeta exists") + tr.MakeCurlCommand( + ''' --proxy 127.0.0.1:{0} \ + "http://www.example.com/magic" \ + -H"Cookie: fpbeta=etc" \ + -H "Proxy-Connection: keep-alive" \ + --verbose \ + '''.format(ts.Variables.port), + ts=ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.StartBefore(server2, ready=When.PortOpen(server2.Variables.Port)) + tr.StillRunningAfter = ts + + server2.Streams.All = "gold/existscookie.gold" + urtest.execute() diff --git a/tests/uranium_tests/pluginTest/cookie_remap/test_pcollapseslashes.py b/tests/uranium_tests/pluginTest/cookie_remap/test_pcollapseslashes.py new file mode 100644 index 00000000000..7be87dc770a --- /dev/null +++ b/tests/uranium_tests/pluginTest/cookie_remap/test_pcollapseslashes.py @@ -0,0 +1,100 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_pcollapseslashes(urtest: UraniumTest) -> None: + ''' + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import os + + urtest.Summary = ''' + + ''' + urtest.SkipUnless(Condition.PluginExists('cookie_remap.so')) + urtest.ContinueOnFail = True + urtest.testName = "cookie_remap: plugin collapses consecutive slashes (pre-remap URI)" + + # Define default ATS + ts = urtest.MakeATSProcess("ts") + + # We only run a server to capture ATS outbound requests + # and verify it collapsed the double // + server = urtest.MakeOriginServer("server", ip='127.0.0.10') + + request_header = { + "headers": "GET /i/like/cheetos?.done=http://finance.yahoo.com HTTP/1.1\r\nHost: www.example.com\r\n\r\n", + "timestamp": "1469733493.993", + "body": "" + } + # expected response from the origin server + response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + + # add response to the server dictionary + server.addResponse("sessionfile.log", request_header, response_header) + + # Setup the remap configuration + config_path = os.path.join(urtest.TestDirectory, "configs/pcollapseconfig.txt") + with open(config_path, 'r') as config_file: + config1 = config_file.read() + + ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'cookie_remap.*|http.*|dns.*', + }) + + config1 = config1.replace("$PORT", str(server.Variables.Port)) + + ts.Disk.File(ts.Variables.CONFIGDIR + "/collapseconfig.txt", id="config1") + ts.Disk.config1.WriteOn(config1) + + ts.Disk.remap_config.AddLine( + 'map http://www.example.com/orig_path http://shouldnothit.com/magic @plugin=cookie_remap.so @pparam=config/collapseconfig.txt' + ) + + tr = urtest.AddTestRun("collapse consecutive forward slashes") + tr.MakeCurlCommand( + ''' --proxy 127.0.0.1:{0} \ + "http://www.example.com/orig_path" \ + -H "Proxy-Connection: keep-alive" \ + --verbose \ + '''.format(ts.Variables.port), + ts=ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.StartBefore(server, ready=When.PortOpen(server.Variables.Port)) + tr.Processes.Default.StartBefore(urtest.Processes.ts) + tr.StillRunningAfter = ts + + server.Streams.All = "gold/collapseslashes.gold" + urtest.execute() diff --git a/tests/uranium_tests/pluginTest/cookie_remap/test_psubstitute.py b/tests/uranium_tests/pluginTest/cookie_remap/test_psubstitute.py new file mode 100644 index 00000000000..1370ec9b21d --- /dev/null +++ b/tests/uranium_tests/pluginTest/cookie_remap/test_psubstitute.py @@ -0,0 +1,164 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_psubstitute(urtest: UraniumTest) -> None: + ''' + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import os + + urtest.Summary = ''' + + ''' + urtest.SkipUnless(Condition.PluginExists('cookie_remap.so')) + urtest.ContinueOnFail = True + urtest.testName = "cookie_remap: Substitute variables (pre-remap URL)" + + # Define default ATS + ts = urtest.MakeATSProcess("ts") + + server = urtest.MakeOriginServer("server", ip='127.0.0.10') + + request_header = { + "headers": "GET /photos/search?query=magic HTTP/1.1\r\nHost: www.example.com\r\n\r\n", + "timestamp": "1469733493.993", + "body": "" + } + response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + + server.addResponse("sessionfile.log", request_header, response_header) + + request_header = { + "headers": "GET /photos/search?query=/theunmatchedpath HTTP/1.1\r\nHost: www.example.com\r\n\r\n", + "timestamp": "1469733493.993", + "body": "" + } + response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + + server.addResponse("sessionfile.log", request_header, response_header) + + request_header = { + "headers": "GET /photos/search/magic/foobar HTTP/1.1\r\nHost: www.example.com\r\n\r\n", + "timestamp": "1469733493.993", + "body": "" + } + response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + + server.addResponse("sessionfile.log", request_header, response_header) + + request_header = { + "headers": + "GET /photos/search/cr_substitutions?query=%28http%3A%2F%2Fwww%2Eexample%2Ecom%2Fmagic HTTP/1.1\r\nHost: www.example.com\r\n\r\n", + "timestamp": "1469733493.993", + "body": "" + } + response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + + server.addResponse("sessionfile.log", request_header, response_header) + + # Setup the remap configuration + config_path = os.path.join(urtest.TestDirectory, "configs/psubstituteconfig.txt") + with open(config_path, 'r') as config_file: + config1 = config_file.read() + + ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'cookie_remap.*|http.*|dns.*', + }) + + config1 = config1.replace("$PORT", str(server.Variables.Port)) + + ts.Disk.File(ts.Variables.CONFIGDIR + "/substituteconfig.txt", id="config1") + ts.Disk.config1.WriteOn(config1) + + ts.Disk.remap_config.AddLine( + 'map http://www.example.com/magic http://shouldnothit.com/not-used @plugin=cookie_remap.so @pparam=config/substituteconfig.txt' + ) + + tr = urtest.AddTestRun("Substitute $ppath in the dest query") + tr.MakeCurlCommand( + ''' --proxy 127.0.0.1:{0} \ + "http://www.example.com/magic" \ + -H"Cookie: fpbeta=abcd" \ + -H "Proxy-Connection: keep-alive" \ + --verbose \ + '''.format(ts.Variables.port), + ts=ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.StartBefore(server, ready=When.PortOpen(server.Variables.Port)) + tr.Processes.Default.StartBefore(urtest.Processes.ts) + tr.StillRunningAfter = ts + tr.StillRunningAfter = server + + tr = urtest.AddTestRun("Substitute $unmatched_ppath in the dest query") + tr.MakeCurlCommand( + ''' --proxy 127.0.0.1:{0} \ + "http://www.example.com/magic/theunmatchedpath" \ + -H"Cookie: oxalpha=3333" \ + -H "Proxy-Connection: keep-alive" \ + --verbose \ + '''.format(ts.Variables.port), + ts=ts) + tr.Processes.Default.ReturnCode = 0 + tr.StillRunningAfter = ts + tr.StillRunningAfter = server + + tr = urtest.AddTestRun("Substitute $cr_req_purl using $cr_urlencode") + tr.MakeCurlCommand( + ''' --proxy 127.0.0.1:{0} \ + "http://www.example.com/magic" \ + -H"Cookie: acgamma=dfndfdfd" \ + -H "Proxy-Connection: keep-alive" \ + --verbose \ + '''.format(ts.Variables.port), + ts=ts) + tr.Processes.Default.ReturnCode = 0 + tr.StillRunningAfter = ts + tr.StillRunningAfter = server + + tr = urtest.AddTestRun("Substitute $ppath as is in outgoing path") + tr.MakeCurlCommand( + ''' --proxy 127.0.0.1:{0} \ + "http://www.example.com/magic/foobar" \ + -H "Proxy-Connection: keep-alive" \ + --verbose \ + '''.format(ts.Variables.port), + ts=ts) + tr.Processes.Default.ReturnCode = 0 + tr.StillRunningAfter = ts + tr.StillRunningAfter = server + + server.Streams.All = "gold/psubstitute.gold" + urtest.execute() diff --git a/tests/uranium_tests/pluginTest/cookie_remap/test_regexcookie.py b/tests/uranium_tests/pluginTest/cookie_remap/test_regexcookie.py new file mode 100644 index 00000000000..9a436d92f5c --- /dev/null +++ b/tests/uranium_tests/pluginTest/cookie_remap/test_regexcookie.py @@ -0,0 +1,130 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_regexcookie(urtest: UraniumTest) -> None: + ''' + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import os + + urtest.Summary = ''' + + ''' + urtest.SkipUnless(Condition.PluginExists('cookie_remap.so')) + urtest.ContinueOnFail = True + urtest.testName = "cookie_remap: cookie regex match and substitution" + + # Define default ATS + ts = urtest.MakeATSProcess("ts") + + # First server is run during first test and + # second server is run during second test + server = urtest.MakeOriginServer("server", ip='127.0.0.10') + + request_header = { + "headers": "GET /regexmatches?cookies=oreos-chipsahoy-icecream HTTP/1.1\r\nHost: www.example.com\r\n\r\n", + "timestamp": "1469733493.993", + "body": "" + } + # expected response from the origin server + response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + + # add response to the server dictionary + server.addResponse("sessionfile.log", request_header, response_header) + + server2 = urtest.MakeOriginServer("server2", ip='127.0.0.11') + request_header2 = { + "headers": "GET /regexdoesntmatch HTTP/1.1\r\nHost: www.example.com\r\n\r\n", + "timestamp": "1469733493.993", + "body": "" + } + # expected response from the origin server + response_header2 = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + + # add response to the server dictionary + server2.addResponse("sessionfile.log", request_header2, response_header2) + + # Setup the remap configuration + config_path = os.path.join(urtest.TestDirectory, "configs/regexconfig.txt") + with open(config_path, 'r') as config_file: + config1 = config_file.read() + + ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'cookie_remap.*|http.*|dns.*', + }) + + config1 = config1.replace("$PORT", str(server.Variables.Port)) + config1 = config1.replace("$ALTPORT", str(server2.Variables.Port)) + + ts.Disk.File(ts.Variables.CONFIGDIR + "/regexconfig.txt", id="config1") + ts.Disk.config1.WriteOn(config1) + + ts.Disk.remap_config.AddLine( + 'map http://www.example.com/magic http://shouldnothit.com @plugin=cookie_remap.so @pparam=config/regexconfig.txt') + + # Positive test case that remaps because cookie regex matches + tr = urtest.AddTestRun("cookie regex matches") + tr.MakeCurlCommand( + ''' --proxy 127.0.0.1:{0} \ + "http://www.example.com/magic" \ + -H"Cookie: fpbeta=ilove-oreos-chipsahoy-icecream" \ + -H "Proxy-Connection: keep-alive" \ + --verbose \ + '''.format(ts.Variables.port), + ts=ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.StartBefore(server, ready=When.PortOpen(server.Variables.Port)) + tr.Processes.Default.StartBefore(urtest.Processes.ts) + tr.StillRunningAfter = ts + + server.Streams.All = "gold/regexmatches.gold" + + # Negative test case that doesn't remap because cookie regex doesn't match + tr = urtest.AddTestRun("cookie regex doesn't match") + tr.MakeCurlCommand( + ''' --proxy 127.0.0.1:{0} \ + "http://www.example.com/magic" \ + -H"Cookie: fpbeta=etc" \ + -H "Proxy-Connection: keep-alive" \ + --verbose \ + '''.format(ts.Variables.port), + ts=ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.StartBefore(server2, ready=When.PortOpen(server2.Variables.Port)) + tr.StillRunningAfter = ts + + server2.Streams.All = "gold/regexdoesntmatch.gold" + urtest.execute() diff --git a/tests/uranium_tests/pluginTest/cookie_remap/test_setstatus.py b/tests/uranium_tests/pluginTest/cookie_remap/test_setstatus.py new file mode 100644 index 00000000000..e9ca5f8759f --- /dev/null +++ b/tests/uranium_tests/pluginTest/cookie_remap/test_setstatus.py @@ -0,0 +1,98 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_setstatus(urtest: UraniumTest) -> None: + ''' + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import os + + urtest.Summary = ''' + + ''' + urtest.SkipUnless(Condition.PluginExists('cookie_remap.so')) + urtest.ContinueOnFail = True + urtest.testName = "cookie_remap: set HTTP status of the response" + + # Define default ATS + ts = urtest.MakeATSProcess("ts") + + # Setup the remap configuration + config_path = os.path.join(urtest.TestDirectory, "configs/statusconfig.txt") + with open(config_path, 'r') as config_file: + config1 = config_file.read() + + ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'cookie_remap.*|http.*|dns.*', + }) + + ts.Disk.File(ts.Variables.CONFIGDIR + "/statusconfig.txt", id="config1") + ts.Disk.config1.WriteOn(config1) + + ts.Disk.remap_config.AddLine( + 'map http://www.example.com/magic http://shouldnothit.com @plugin=cookie_remap.so @pparam=config/statusconfig.txt') + + # Plugin sets the HTTP status because first rule matches + tr = urtest.AddTestRun("Sets the status to 205") + tr.MakeCurlCommand( + ''' --proxy 127.0.0.1:{0} \ + "http://www.example.com/magic" \ + -H"Cookie: fpbeta=magic" \ + -H "Proxy-Connection: keep-alive" \ + --verbose \ + '''.format(ts.Variables.port), + ts=ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.StartBefore(urtest.Processes.ts) + tr.StillRunningAfter = ts + + tr.Streams.All = "gold/matchstatus.gold" + + # Plugin sets the HTTP status because the else rule matches (i.e. no match) + tr = urtest.AddTestRun("Sets the else status to 400") + tr.MakeCurlCommand( + ''' --proxy 127.0.0.1:{0} \ + "http://www.example.com/magic" \ + -H "Proxy-Connection: keep-alive" \ + -H "Proxy-Connection: keep-alive" \ + --verbose \ + '''.format(ts.Variables.port), + ts=ts) + tr.Processes.Default.ReturnCode = 0 + tr.StillRunningAfter = ts + + tr.Streams.All = "gold/matchelsestatus.gold" + urtest.execute() diff --git a/tests/uranium_tests/pluginTest/cookie_remap/test_subcookie.py b/tests/uranium_tests/pluginTest/cookie_remap/test_subcookie.py new file mode 100644 index 00000000000..a5f21acd462 --- /dev/null +++ b/tests/uranium_tests/pluginTest/cookie_remap/test_subcookie.py @@ -0,0 +1,132 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_subcookie(urtest: UraniumTest) -> None: + ''' + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import os + + urtest.Summary = ''' + + ''' + urtest.SkipUnless(Condition.PluginExists('cookie_remap.so')) + urtest.ContinueOnFail = True + urtest.testName = "cookie_remap: test connector" + + # Define default ATS + ts = urtest.MakeATSProcess("ts") + + # First server is run during first test and + # second server is run during second test + server = urtest.MakeOriginServer("server", ip='127.0.0.10') + + request_header = { + "headers": "GET /cookiematches HTTP/1.1\r\nHost: www.example.com\r\n\r\n", + "timestamp": "1469733493.993", + "body": "" + } + # expected response from the origin server + response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + + # add response to the server dictionary + server.addResponse("sessionfile.log", request_header, response_header) + + server2 = urtest.MakeOriginServer("server2", ip='127.0.0.11') + request_header2 = { + "headers": "GET /cookiedoesntmatch HTTP/1.1\r\nHost: www.example.com\r\n\r\n", + "timestamp": "1469733493.993", + "body": "" + } + # expected response from the origin server + response_header2 = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + + # add response to the server dictionary + server2.addResponse("sessionfile.log", request_header2, response_header2) + + # Setup the remap configuration + config_path = os.path.join(urtest.TestDirectory, "configs/subcookie.txt") + with open(config_path, 'r') as config_file: + config1 = config_file.read() + + ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'cookie_remap.*|http.*|dns.*', + }) + + config1 = config1.replace("$PORT", str(server.Variables.Port)) + config1 = config1.replace("$ALTPORT", str(server2.Variables.Port)) + + ts.Disk.File(ts.Variables.CONFIGDIR + "/subcookie.txt", id="config1") + ts.Disk.config1.WriteOn(config1) + + ts.Disk.remap_config.AddLine( + 'map http://www.example.com http://shouldnothit.com/magic @plugin=cookie_remap.so @pparam=config/subcookie.txt') + + # Positive test case that remaps because all connected operations pass + tr = urtest.AddTestRun("cookie value matches") + # Unlike in other places I am using a single line string because the & seems to + # be interpreted by the autest framework or the shell (tried escaping with \) + tr.MakeCurlCommand( + '--proxy 127.0.0.1:{0} "http://www.example.com" -H"Cookie: fpbeta=a=1&b=2&c=3" -H "Proxy-Connection: keep-alive" --verbose ' + .format(ts.Variables.port), + ts=ts) + # tr.Processes.Default.Command = ''' + # curl + # --proxy 127.0.0.1:{0} + # "http://www.example.com" + # -H\"Cookie: fpbeta=a=1&b=2&c=3\" + # -H "Proxy-Connection: keep-alive" + # --verbose + # '''.format(ts.Variables.port) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.StartBefore(server, ready=When.PortOpen(server.Variables.Port)) + tr.Processes.Default.StartBefore(urtest.Processes.ts) + tr.StillRunningAfter = ts + + server.Streams.All = "gold/matchcookie.gold" + + # Negative test case that doesn't remap because not all subops pass + tr = urtest.AddTestRun("cookie value doesn't match") + tr.MakeCurlCommand( + '--proxy 127.0.0.1:{0} "http://www.example.com" -H"Cookie: fpbeta=a=1&b=2&c=4" -H "Proxy-Connection: keep-alive" --verbose ' + .format(ts.Variables.port), + ts=ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.StartBefore(server2, ready=When.PortOpen(server2.Variables.Port)) + tr.StillRunningAfter = ts + + server2.Streams.All = "gold/wontmatchcookie.gold" + urtest.execute() diff --git a/tests/uranium_tests/pluginTest/cookie_remap/test_substitute.py b/tests/uranium_tests/pluginTest/cookie_remap/test_substitute.py new file mode 100644 index 00000000000..18192dfd0fd --- /dev/null +++ b/tests/uranium_tests/pluginTest/cookie_remap/test_substitute.py @@ -0,0 +1,154 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_substitute(urtest: UraniumTest) -> None: + ''' + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import os + + urtest.Summary = ''' + + ''' + urtest.SkipUnless(Condition.PluginExists('cookie_remap.so')) + urtest.ContinueOnFail = True + urtest.testName = "cookie_remap: Substitute variables" + + # Define default ATS + ts = urtest.MakeATSProcess("ts") + + server = urtest.MakeOriginServer("server", ip='127.0.0.10') + + request_header = { + "headers": "GET /photos/search?query=magic HTTP/1.1\r\nHost: www.example.com\r\n\r\n", + "timestamp": "1469733493.993", + "body": "" + } + response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + + server.addResponse("sessionfile.log", request_header, response_header) + + request_header_2 = { + "headers": "GET /photos/search?query=/theunmatchedpath HTTP/1.1\r\nHost: www.example.com\r\n\r\n", + "timestamp": "1469733493.993", + "body": "" + } + response_header_2 = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + + server.addResponse("sessionfile.log", request_header_2, response_header_2) + + request_header_3 = { + "headers": "GET /photos/search/magic/foobar HTTP/1.1\r\nHost: www.example.com\r\n\r\n", + "timestamp": "1469733493.993", + "body": "" + } + response_header_3 = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + + server.addResponse("sessionfile.log", request_header_3, response_header_3) + + # Setup the remap configuration + config_path = os.path.join(urtest.TestDirectory, "configs/substituteconfig.txt") + with open(config_path, 'r') as config_file: + config1 = config_file.read() + + ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'cookie_remap.*|http.*|dns.*', + }) + + config1 = config1.replace("$PORT", str(server.Variables.Port)) + + ts.Disk.File(ts.Variables.CONFIGDIR + "/substituteconfig.txt", id="config1") + ts.Disk.config1.WriteOn(config1) + + ts.Disk.remap_config.AddLine( + 'map http://www.example.com/magic http://shouldnothit.com/magic @plugin=cookie_remap.so @pparam=config/substituteconfig.txt' + ) + + tr = urtest.AddTestRun("Substitute $path in the dest query") + tr.MakeCurlCommand( + ''' --proxy 127.0.0.1:{0} \ + "http://www.example.com/magic" \ + -H"Cookie: fpbeta=abcd" \ + -H "Proxy-Connection: keep-alive" \ + --verbose \ + '''.format(ts.Variables.port), + ts=ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.StartBefore(server, ready=When.PortOpen(server.Variables.Port)) + tr.Processes.Default.StartBefore(urtest.Processes.ts) + tr.StillRunningAfter = ts + tr.StillRunningAfter = server + + tr = urtest.AddTestRun("Substitute $unmatched_path in the dest query") + tr.MakeCurlCommand( + ''' --proxy 127.0.0.1:{0} \ + "http://www.example.com/magic/theunmatchedpath" \ + -H"Cookie: oxalpha=3333" \ + -H "Proxy-Connection: keep-alive" \ + --verbose \ + '''.format(ts.Variables.port), + ts=ts) + tr.Processes.Default.ReturnCode = 0 + tr.StillRunningAfter = ts + tr.StillRunningAfter = server + + tr = urtest.AddTestRun("Substitute $cr_req_url using $cr_urlencode") + tr.MakeCurlCommand( + ''' --proxy 127.0.0.1:{0} \ + "http://www.example.com/magic" \ + -H"Cookie: acgamma=dfndfdfd" \ + -H "Proxy-Connection: keep-alive" \ + --verbose \ + '''.format(ts.Variables.port), + ts=ts) + tr.Processes.Default.ReturnCode = 0 + tr.StillRunningAfter = ts + tr.StillRunningAfter = server + + tr = urtest.AddTestRun("Substitute $path as is in outgoing path") + tr.MakeCurlCommand( + ''' --proxy 127.0.0.1:{0} \ + "http://www.example.com/magic/foobar" \ + -H "Proxy-Connection: keep-alive" \ + --verbose \ + '''.format(ts.Variables.port), + ts=ts) + tr.Processes.Default.ReturnCode = 0 + tr.StillRunningAfter = ts + tr.StillRunningAfter = server + + server.Streams.All = "gold/substitute.gold" + urtest.execute() diff --git a/tests/uranium_tests/pluginTest/cppapi/test_cppapi.py b/tests/uranium_tests/pluginTest/cppapi/test_cppapi.py new file mode 100644 index 00000000000..4f43e635c94 --- /dev/null +++ b/tests/uranium_tests/pluginTest/cppapi/test_cppapi.py @@ -0,0 +1,52 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_cppapi(urtest: UraniumTest) -> None: + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import os + + urtest.Summary = ''' + Execute plugin with cppapi tests. + ''' + + ts = urtest.MakeATSProcess("ts") + + urtest.PrepareTestPlugin(os.path.join(urtest.Variables.AtsTestPluginsDir, 'test_cppapi.so'), ts) + + tr = urtest.AddTestRun() + tr.Processes.Default.StartBefore(urtest.Processes.ts) + tr.Processes.Default.Command = "echo run test_cppapi plugin" + tr.Processes.Default.ReturnCode = 0 + tr.Processes.StillRunningAfter = ts + urtest.execute() diff --git a/tests/gold_tests/pluginTest/crash_test/CMakeLists.txt b/tests/uranium_tests/pluginTest/crash_test/CMakeLists.txt similarity index 95% rename from tests/gold_tests/pluginTest/crash_test/CMakeLists.txt rename to tests/uranium_tests/pluginTest/crash_test/CMakeLists.txt index c6c8ab89ccf..0298d9bb54d 100644 --- a/tests/gold_tests/pluginTest/crash_test/CMakeLists.txt +++ b/tests/uranium_tests/pluginTest/crash_test/CMakeLists.txt @@ -15,4 +15,4 @@ # ####################### -add_autest_plugin(crash_test crash_test.cc) +add_urtest_plugin(crash_test crash_test.cc) diff --git a/tests/gold_tests/pluginTest/crash_test/crash_test.cc b/tests/uranium_tests/pluginTest/crash_test/crash_test.cc similarity index 100% rename from tests/gold_tests/pluginTest/crash_test/crash_test.cc rename to tests/uranium_tests/pluginTest/crash_test/crash_test.cc diff --git a/tests/uranium_tests/pluginTest/crash_test/test_crash_test.py b/tests/uranium_tests/pluginTest/crash_test/test_crash_test.py new file mode 100644 index 00000000000..0123497ddbe --- /dev/null +++ b/tests/uranium_tests/pluginTest/crash_test/test_crash_test.py @@ -0,0 +1,122 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_crash_test(urtest: UraniumTest) -> None: + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + """ + Test that crash logs are generated with backtraces when traffic_server crashes. + + This test intentionally crashes traffic_server using a plugin that dereferences + a null pointer when it receives a specific header. It then verifies that: + 1. A crash log file was created + 2. The crash log contains thread information + """ + + import os + + urtest.Summary = ''' + Test crash log generation with backtrace. + ''' + + # Create an origin server for the test. + server = urtest.MakeOriginServer("server") + + request_header = {"headers": "GET / HTTP/1.1\r\nHost: example.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": "Hello"} + server.addResponse("sessionlog.json", request_header, response_header) + + ts = urtest.MakeATSProcess("ts") + + # We expect ATS to crash with SIGSEGV, allowing us to test crash logging. + ts.ReturnCode = -11 + + ts.Disk.records_config.update( + { + 'proxy.config.proxy_name': 'test_proxy', + 'proxy.config.url_remap.remap_required': 0, + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'crash_test', + # Enable the crash log helper. + 'proxy.config.crash_log_helper': 'traffic_crashlog', + }) + + # Copy the crash_test plugin. + plugin_path = os.path.join(urtest.Variables.AtsBuildUraniumTestsDir, 'pluginTest', 'crash_test', '.libs', 'crash_test.so') + ts.Setup.Copy(plugin_path, ts.Env['PROXY_CONFIG_PLUGIN_PLUGIN_DIR']) + + ts.Disk.plugin_config.AddLine("crash_test.so") + + ts.Disk.remap_config.AddLine(f"map / http://127.0.0.1:{server.Variables.Port}/") + + ts.Disk.diags_log.Content += Testers.ContainsExpression( + "Received crash trigger header - crashing now!", "Expect the log indicating the intentional crash.") + ts.Disk.diags_log.Content += Testers.ExcludesExpression( + "This should never be reached.", "Expect to not see the log after the crash.") + + # Test 1: Make a normal request to verify the server is running. + tr = urtest.AddTestRun("Verify server is running") + tr.Processes.Default.StartBefore(server) + tr.Processes.Default.StartBefore(ts) + tr.MakeCurlCommand(f'-s -o /dev/null -w "%{{http_code}}" http://127.0.0.1:{ts.Variables.port}/', ts=ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.stdout = Testers.ContainsExpression("200", "Expected 200 OK response") + tr.StillRunningAfter = ts + tr.StillRunningAfter = server + + # Test 2: Send the crash trigger header. + tr = urtest.AddTestRun("Trigger crash") + # The curl command should fail since ATS will crash. + tr.MakeCurlCommand(f'-s -o /dev/null -H "X-Crash-Test: now" http://127.0.0.1:{ts.Variables.port}/', ts=ts) + tr.Processes.Default.ReturnCode = 52 + + # Test 3: Wait for a crash log to be created. + tr = urtest.AddTestRun("Wait for crash log") + crash_log_glob = f'{ts.Variables.LOGDIR}/crash-*.log' + # Wait up to 60 seconds for a crash log file to appear, then 1 extra second for it to be written. + tr.Processes.Default.Command = (f"{os.path.join(urtest.Variables.AtsTestToolsDir, 'condwait')} 60 1 -f '{crash_log_glob}'") + tr.Processes.Default.ReturnCode = 0 + + # Test 4: Verify crash log contains expected content. + tr = urtest.AddTestRun("Check crash log content") + tr.Processes.Default.Command = (f'cat {ts.Variables.LOGDIR}/crash-*.log 2>&1') + tr.Processes.Default.ReturnCode = 0 + # The crash log should contain signal information (always present). + tr.Processes.Default.Streams.stdout += Testers.ContainsExpression( + "Segmentation fault", "Expected crash log to show segmentation fault signal") + # The crash log should contain the crashing thread information first. + # The crashing thread should be listed first. + tr.Processes.Default.Streams.stdout += Testers.ContainsExpression("Crashing Thread", "Expected crashing thread backtrace first") + # The other threads should be listed after. + tr.Processes.Default.Streams.stdout += Testers.ContainsExpression( + "Other Non-Crashing Threads:", "Expected other non-crashing threads section") + urtest.execute() diff --git a/tests/gold_tests/pluginTest/escalate/escalate_failover.replay.yaml b/tests/uranium_tests/pluginTest/escalate/escalate_failover.replay.yaml similarity index 100% rename from tests/gold_tests/pluginTest/escalate/escalate_failover.replay.yaml rename to tests/uranium_tests/pluginTest/escalate/escalate_failover.replay.yaml diff --git a/tests/gold_tests/pluginTest/escalate/escalate_failover_server_default.replay.yaml b/tests/uranium_tests/pluginTest/escalate/escalate_failover_server_default.replay.yaml similarity index 100% rename from tests/gold_tests/pluginTest/escalate/escalate_failover_server_default.replay.yaml rename to tests/uranium_tests/pluginTest/escalate/escalate_failover_server_default.replay.yaml diff --git a/tests/gold_tests/pluginTest/escalate/escalate_failover_server_non_get.replay.yaml b/tests/uranium_tests/pluginTest/escalate/escalate_failover_server_non_get.replay.yaml similarity index 100% rename from tests/gold_tests/pluginTest/escalate/escalate_failover_server_non_get.replay.yaml rename to tests/uranium_tests/pluginTest/escalate/escalate_failover_server_non_get.replay.yaml diff --git a/tests/gold_tests/pluginTest/escalate/escalate_non_get_methods.replay.yaml b/tests/uranium_tests/pluginTest/escalate/escalate_non_get_methods.replay.yaml similarity index 100% rename from tests/gold_tests/pluginTest/escalate/escalate_non_get_methods.replay.yaml rename to tests/uranium_tests/pluginTest/escalate/escalate_non_get_methods.replay.yaml diff --git a/tests/gold_tests/pluginTest/escalate/escalate_original.replay.yaml b/tests/uranium_tests/pluginTest/escalate/escalate_original.replay.yaml similarity index 100% rename from tests/gold_tests/pluginTest/escalate/escalate_original.replay.yaml rename to tests/uranium_tests/pluginTest/escalate/escalate_original.replay.yaml diff --git a/tests/gold_tests/pluginTest/escalate/escalate_original_server_default.replay.yaml b/tests/uranium_tests/pluginTest/escalate/escalate_original_server_default.replay.yaml similarity index 100% rename from tests/gold_tests/pluginTest/escalate/escalate_original_server_default.replay.yaml rename to tests/uranium_tests/pluginTest/escalate/escalate_original_server_default.replay.yaml diff --git a/tests/gold_tests/pluginTest/escalate/escalate_original_server_non_get.replay.yaml b/tests/uranium_tests/pluginTest/escalate/escalate_original_server_non_get.replay.yaml similarity index 100% rename from tests/gold_tests/pluginTest/escalate/escalate_original_server_non_get.replay.yaml rename to tests/uranium_tests/pluginTest/escalate/escalate_original_server_non_get.replay.yaml diff --git a/tests/uranium_tests/pluginTest/escalate/test_escalate.py b/tests/uranium_tests/pluginTest/escalate/test_escalate.py new file mode 100644 index 00000000000..68179a8abe0 --- /dev/null +++ b/tests/uranium_tests/pluginTest/escalate/test_escalate.py @@ -0,0 +1,315 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_escalate(urtest: UraniumTest) -> None: + ''' + Verify escalate plugin behavior. + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import os + from ports import get_port + + urtest.Summary = ''' + Test the escalate plugin. + ''' + + urtest.SkipUnless(Condition.PluginExists('escalate.so')) + + class EscalateTest: + """ + Test the escalate plugin default behavior (GET requests only). + """ + + _replay_original_file: str = 'escalate_original.replay.yaml' + _replay_failover_file: str = 'escalate_failover.replay.yaml' + _server_original_file: str = 'escalate_original_server_default.replay.yaml' + _server_failover_file: str = 'escalate_failover_server_default.replay.yaml' + _process_counter: int = 0 + + def __init__(self, disable_redirect_header: bool = False, enable_cache: bool = False) -> None: + '''Configure the test run. + :param disable_redirect_header: Whether to use --no-redirect-header. + :param enable_cache: Whether to exercise the cached redirect path. + ''' + tr = urtest.AddTestRun( + f'Test escalate plugin. disable_redirect_header={disable_redirect_header}, enable_cache={enable_cache}') + self._enable_cache = enable_cache + self._setup_dns(tr) + self._setup_servers(tr, disable_redirect_header) + self._setup_ts(tr, disable_redirect_header) + self._setup_client(tr) + EscalateTest._process_counter += 1 + + def _setup_dns(self, tr: 'TestRun') -> None: + '''Set up the DNS server. + + :param tr: The test run to add the DNS server to. + ''' + process_name = f"dns_{EscalateTest._process_counter}" + self._dns = tr.MakeDNServer(process_name, default='127.0.0.1') + + def _setup_servers(self, tr: 'TestRun', disable_redirect_header: bool) -> None: + '''Set up the origin and failover servers. + + :param tr: The test run to add the servers to. + :param disable_redirect_header: Whether ATS was configured with --no-redirect-header. + ''' + tr.Setup.Copy(self._replay_original_file) + tr.Setup.Copy(self._replay_failover_file) + tr.Setup.Copy(self._server_original_file) + tr.Setup.Copy(self._server_failover_file) + process_name = f"server_origin_{EscalateTest._process_counter}" + self._server_origin = tr.AddVerifierServerProcess(process_name, self._server_original_file) + process_name = f"server_failover_{EscalateTest._process_counter}" + self._server_failover = tr.AddVerifierServerProcess(process_name, self._server_failover_file) + + self._server_origin.Streams.All += Testers.ContainsExpression( + 'uuid: GET', "Verify the origin server received the GET request.") + self._server_origin.Streams.All += Testers.ContainsExpression( + 'uuid: GET_chunked', "Verify the origin server GET request for chunked content.") + self._server_origin.Streams.All += Testers.ContainsExpression( + 'uuid: GET_failed', "Verify the origin server received the GET request that it returns a 502 with.") + self._server_origin.Streams.All += Testers.ContainsExpression( + 'uuid: HEAD_fail_not_escalated', "Verify the origin server received the HEAD request that should not be escalated.") + self._server_origin.Streams.All += Testers.ContainsExpression( + 'uuid: POST_fail_not_escalated', "Verify the origin server received the POST request that should not be escalated.") + self._server_origin.Streams.All += Testers.ExcludesExpression( + 'uuid: GET_down_origin', "Verify the origin server did not receive the down origin request.") + self._server_origin.Streams.All += Testers.ExcludesExpression( + 'x-escalate-redirect', "Verify the origin server should never receive the x-escalate-redirect header.") + + self._server_failover.Streams.All += Testers.ContainsExpression( + 'uuid: GET_failed', "Verify the failover server received the failed GET request.") + self._server_failover.Streams.All += Testers.ContainsExpression( + 'uuid: GET_down_origin', "Verify the failover server received the GET request for the down origin.") + + self._server_failover.Streams.All += Testers.ExcludesExpression( + 'x-request: first', "Verify the failover server did not receive the GET request.") + self._server_failover.Streams.All += Testers.ExcludesExpression( + 'uuid: GET_chunked', "Verify the failover server did not receive the GET request for chunked content.") + # By default, non-GET methods should NOT be escalated to failover + self._server_failover.Streams.All += Testers.ExcludesExpression( + 'uuid: HEAD_fail_not_escalated', + "Verify the failover server did not receive the HEAD request that should not be escalated.") + self._server_failover.Streams.All += Testers.ExcludesExpression( + 'uuid: POST_fail_not_escalated', + "Verify the failover server did not receive the POST request that should not be escalated.") + + if disable_redirect_header: + self._server_failover.Streams.All += Testers.ExcludesExpression( + 'x-escalate-redirect', "Verify the failover server did not receive the x-escalate-redirect header.") + else: + self._server_failover.Streams.All += Testers.ContainsExpression( + 'x-escalate-redirect: 1', "Verify the failover server received the x-escalate-redirect header.") + + def _setup_ts(self, tr: 'Process', disable_redirect_header: bool) -> None: + '''Set up Traffic Server. + + :param tr: The test run to add Traffic Server to. + :param disable_redirect_header: Whether ATS should be configured with --no-redirect-header. + ''' + process_name = f"ts_{EscalateTest._process_counter}" + self._ts = tr.MakeATSProcess(process_name, enable_cache=self._enable_cache) + # Select a port that is guaranteed to not be used at the moment. + dead_port = get_port(self._ts, "dead_port") + self._ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'http|escalate', + 'proxy.config.dns.nameservers': f'127.0.0.1:{self._dns.Variables.Port}', + 'proxy.config.dns.resolv_conf': 'NULL', + 'proxy.config.http.redirect.actions': 'self:follow', + 'proxy.config.http.number_of_redirections': 4, + }) + params = '' + if disable_redirect_header: + params = '@pparam=--no-redirect-header' + self._ts.Disk.remap_config.AddLines( + [ + f'map http://origin.server.com http://backend.origin.server.com:{self._server_origin.Variables.http_port} ' + f'@plugin=escalate.so @pparam=500,502:failover.server.com:{self._server_failover.Variables.http_port} {params}', + + # Now create remap entries for the multiplexed hosts: one that + # verifies HTTP, and another that verifies HTTPS. + f'map http://down_origin.server.com http://backend.down_origin.server.com:{dead_port} ' + f'@plugin=escalate.so @pparam=500,502:failover.server.com:{self._server_failover.Variables.http_port} {params}', + ]) + + def _setup_client(self, tr: 'Process') -> None: + '''Set up the client. + + :param tr: The test run to add the client to. + ''' + process_name = f"client_{EscalateTest._process_counter}" + client = tr.AddVerifierClientProcess(process_name, self._replay_original_file, http_ports=[self._ts.Variables.port]) + client.StartBefore(self._dns) + client.StartBefore(self._server_origin) + client.StartBefore(self._server_failover) + client.StartBefore(self._ts) + + client.Streams.All += Testers.ExcludesExpression(r'\[ERROR\]', 'Verify there were no errors in the replay.') + client.Streams.All += Testers.ExcludesExpression('400 Bad', 'Verify none of the 400 responses make it to the client.') + client.Streams.All += Testers.ExcludesExpression( + '500 Internal', 'Verify none of the 500 responses make it to the client.') + # GET requests should be escalated and return 200 + client.Streams.All += Testers.ContainsExpression('x-response: first', 'Verify that the first response was received.') + client.Streams.All += Testers.ContainsExpression('x-response: second', 'Verify that the second response was received.') + client.Streams.All += Testers.ContainsExpression('x-response: third', 'Verify that the third response was received.') + client.Streams.All += Testers.ContainsExpression('x-response: fourth', 'Verify that the fourth response was received.') + # Non-GET requests should NOT be escalated and return 502 (default behavior) + client.Streams.All += Testers.ContainsExpression( + 'x-response: head_fail_not_escalated', 'Verify that the HEAD response was received (502).') + client.Streams.All += Testers.ContainsExpression( + 'x-response: post_fail_not_escalated', 'Verify that the POST response was received (502).') + client.Streams.All += Testers.ContainsExpression( + '502 Bad Gateway', 'Verify that non-GET requests return 502 (not escalated by default).') + + class EscalateNonGetMethodsTest: + """ + Test the escalate plugin with --escalate-non-get-methods option to verify non-GET requests are also escalated. + """ + + _replay_get_method_file: str = 'escalate_non_get_methods.replay.yaml' + _replay_failover_file: str = 'escalate_failover.replay.yaml' + _server_origin_file: str = 'escalate_original_server_non_get.replay.yaml' + _server_failover_file: str = 'escalate_failover_server_non_get.replay.yaml' + + def __init__(self): + '''Configure the test run for escalating non-GET methods testing.''' + tr = urtest.AddTestRun('Test escalate plugin with --escalate-non-get-methods option.') + self._setup_dns(tr) + self._setup_servers(tr) + self._setup_ts(tr) + self._setup_client(tr) + + def _setup_dns(self, tr: 'TestRun') -> None: + '''Set up the DNS server.''' + self._dns = tr.MakeDNServer("dns_non_get_methods", default='127.0.0.1') + + def _setup_servers(self, tr: 'TestRun') -> None: + '''Set up the origin and failover servers for non-GET methods testing.''' + tr.Setup.Copy(self._replay_get_method_file) + tr.Setup.Copy(self._replay_failover_file) + tr.Setup.Copy(self._server_origin_file) + tr.Setup.Copy(self._server_failover_file) + self._server_origin = tr.AddVerifierServerProcess("server_origin_non_get_methods", self._server_origin_file) + self._server_failover = tr.AddVerifierServerProcess("server_failover_non_get_methods", self._server_failover_file) + + # Verify the origin server received all requests + self._server_origin.Streams.All += Testers.ContainsExpression( + 'uuid: GET', "Verify the origin server received the first GET request.") + self._server_origin.Streams.All += Testers.ContainsExpression( + 'uuid: GET_chunked', "Verify the origin server received the chunked GET request.") + self._server_origin.Streams.All += Testers.ContainsExpression( + 'uuid: GET_failed', "Verify the origin server received the failed GET request.") + self._server_origin.Streams.All += Testers.ContainsExpression( + 'uuid: POST_success', "Verify the origin server received the successful POST request.") + self._server_origin.Streams.All += Testers.ContainsExpression( + 'uuid: HEAD_fail_escalated', "Verify the origin server received the HEAD request that will be escalated.") + + # The down origin request should NOT be received by this server + self._server_origin.Streams.All += Testers.ExcludesExpression( + 'uuid: GET_down_origin', "Verify the origin server did not receive the down origin request.") + + # Verify failover server receives escalated requests including non-GET methods + self._server_failover.Streams.All += Testers.ContainsExpression( + 'uuid: GET_failed', "Verify the failover server received the failed GET request.") + self._server_failover.Streams.All += Testers.ContainsExpression( + 'uuid: GET_down_origin', "Verify the failover server received the down origin GET request.") + # With --escalate-non-get-methods, the HEAD request should now be escalated + self._server_failover.Streams.All += Testers.ContainsExpression( + 'uuid: HEAD_fail_escalated', "Verify the failover server received the HEAD that is now escalated.") + # The successful POST should also not reach failover (since it succeeds on origin) + self._server_failover.Streams.All += Testers.ExcludesExpression( + 'uuid: POST_success', "Verify the failover server did not receive the successful POST request.") + + def _setup_ts(self, tr: 'TestRun') -> None: + '''Set up Traffic Server with --escalate-non-get-methods option.''' + self._ts = tr.MakeATSProcess("ts_non_get_methods", enable_cache=False) + + self._ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'http|escalate', + 'proxy.config.dns.nameservers': f'127.0.0.1:{self._dns.Variables.Port}', + 'proxy.config.dns.resolv_conf': 'NULL', + 'proxy.config.http.redirect.actions': 'self:follow', + 'proxy.config.http.number_of_redirections': 4, + }) + + # Set up a dead port for the down origin scenario + dead_port = get_port(self._ts, "dead_port") + + # Configure escalate plugin with --escalate-non-get-methods option + self._ts.Disk.remap_config.AddLines( + [ + f'map http://origin.server.com http://backend.origin.server.com:{self._server_origin.Variables.http_port} ' + f'@plugin=escalate.so @pparam=500,502:failover.server.com:{self._server_failover.Variables.http_port} @pparam=--escalate-non-get-methods', + f'map http://down_origin.server.com http://backend.down_origin.server.com:{dead_port} ' + f'@plugin=escalate.so @pparam=500,502:failover.server.com:{self._server_failover.Variables.http_port} @pparam=--escalate-non-get-methods', + ]) + + def _setup_client(self, tr: 'TestRun') -> None: + '''Set up the client for non-GET methods testing.''' + client = tr.AddVerifierClientProcess( + "client_non_get_methods", self._replay_get_method_file, http_ports=[self._ts.Variables.port]) + + client.StartBefore(self._dns) + client.StartBefore(self._server_origin) + client.StartBefore(self._server_failover) + client.StartBefore(self._ts) + + # Verify that successful responses are returned for successful requests and escalated failures + client.Streams.All += Testers.ContainsExpression('x-response: first', 'Verify first GET response received.') + client.Streams.All += Testers.ContainsExpression('x-response: second', 'Verify second GET response received.') + client.Streams.All += Testers.ContainsExpression('x-response: third', 'Verify third GET response received (escalated).') + client.Streams.All += Testers.ContainsExpression( + 'x-response: fourth', 'Verify fourth GET response received (escalated).') + client.Streams.All += Testers.ContainsExpression( + 'x-response: post_success', 'Verify successful POST response received.') + client.Streams.All += Testers.ContainsExpression( + 'x-response: head_fail_escalated', 'Verify escalated HEAD response received.') + + # With --escalate-non-get-methods, POST and HEAD failures should now be escalated and return 200 + client.Streams.All += Testers.ExcludesExpression( + '502 Bad Gateway', 'Verify failed POST and HEAD requests are now escalated') + + # The test should complete without errors + client.Streams.All += Testers.ExcludesExpression(r'\[ERROR\]', 'Verify there were no errors in the replay.') + + EscalateTest(disable_redirect_header=False) + EscalateTest(disable_redirect_header=True) + EscalateTest(disable_redirect_header=False, enable_cache=True) + EscalateNonGetMethodsTest() + urtest.execute() diff --git a/tests/gold_tests/pluginTest/esi/esi_nested_html_comment.replay.yaml b/tests/uranium_tests/pluginTest/esi/esi_nested_html_comment.replay.yaml similarity index 100% rename from tests/gold_tests/pluginTest/esi/esi_nested_html_comment.replay.yaml rename to tests/uranium_tests/pluginTest/esi/esi_nested_html_comment.replay.yaml diff --git a/tests/gold_tests/pluginTest/esi/esi_nested_html_comment_reject.replay.yaml b/tests/uranium_tests/pluginTest/esi/esi_nested_html_comment_reject.replay.yaml similarity index 100% rename from tests/gold_tests/pluginTest/esi/esi_nested_html_comment_reject.replay.yaml rename to tests/uranium_tests/pluginTest/esi/esi_nested_html_comment_reject.replay.yaml diff --git a/tests/gold_tests/pluginTest/esi/esi_nested_include.replay.yaml b/tests/uranium_tests/pluginTest/esi/esi_nested_include.replay.yaml similarity index 100% rename from tests/gold_tests/pluginTest/esi/esi_nested_include.replay.yaml rename to tests/uranium_tests/pluginTest/esi/esi_nested_include.replay.yaml diff --git a/tests/gold_tests/pluginTest/esi/esi_ssrf_validate.replay.yaml b/tests/uranium_tests/pluginTest/esi/esi_ssrf_validate.replay.yaml similarity index 100% rename from tests/gold_tests/pluginTest/esi/esi_ssrf_validate.replay.yaml rename to tests/uranium_tests/pluginTest/esi/esi_ssrf_validate.replay.yaml diff --git a/tests/gold_tests/pluginTest/esi/gold/empty_response_body.gold b/tests/uranium_tests/pluginTest/esi/gold/empty_response_body.gold similarity index 100% rename from tests/gold_tests/pluginTest/esi/gold/empty_response_body.gold rename to tests/uranium_tests/pluginTest/esi/gold/empty_response_body.gold diff --git a/tests/gold_tests/pluginTest/esi/gold/esi_body.gold b/tests/uranium_tests/pluginTest/esi/gold/esi_body.gold similarity index 100% rename from tests/gold_tests/pluginTest/esi/gold/esi_body.gold rename to tests/uranium_tests/pluginTest/esi/gold/esi_body.gold diff --git a/tests/gold_tests/pluginTest/esi/gold/esi_gzipped.gold b/tests/uranium_tests/pluginTest/esi/gold/esi_gzipped.gold similarity index 100% rename from tests/gold_tests/pluginTest/esi/gold/esi_gzipped.gold rename to tests/uranium_tests/pluginTest/esi/gold/esi_gzipped.gold diff --git a/tests/gold_tests/pluginTest/esi/gold/esi_headers.gold b/tests/uranium_tests/pluginTest/esi/gold/esi_headers.gold similarity index 100% rename from tests/gold_tests/pluginTest/esi/gold/esi_headers.gold rename to tests/uranium_tests/pluginTest/esi/gold/esi_headers.gold diff --git a/tests/gold_tests/pluginTest/esi/gold/esi_private_headers.gold b/tests/uranium_tests/pluginTest/esi/gold/esi_private_headers.gold similarity index 100% rename from tests/gold_tests/pluginTest/esi/gold/esi_private_headers.gold rename to tests/uranium_tests/pluginTest/esi/gold/esi_private_headers.gold diff --git a/tests/gold_tests/pluginTest/esi/gold/nested_include_body.gold b/tests/uranium_tests/pluginTest/esi/gold/nested_include_body.gold similarity index 100% rename from tests/gold_tests/pluginTest/esi/gold/nested_include_body.gold rename to tests/uranium_tests/pluginTest/esi/gold/nested_include_body.gold diff --git a/tests/uranium_tests/pluginTest/esi/test_esi.py b/tests/uranium_tests/pluginTest/esi/test_esi.py new file mode 100644 index 00000000000..476b5897397 --- /dev/null +++ b/tests/uranium_tests/pluginTest/esi/test_esi.py @@ -0,0 +1,399 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_esi(urtest: UraniumTest) -> None: + ''' + Test the ESI plugin. + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import os + import re + + urtest.Summary = ''' + Test the ESI plugin. + ''' + + urtest.SkipUnless(Condition.PluginExists('esi.so'),) + + # An enum of expected plugin behaviors for Cache-Control headers. + class CcBehaviorT(): + REMOVE_CC = 0 + MAKE_PRIVATE = 1 + + class EsiTest(): + """ + A class that encapsulates the configuration and execution of a set of EPI + test cases. + """ + """ static: The same server Process is used across all tests. """ + _server = None + """ static: A counter to keep the ATS process names unique across tests. """ + _ts_counter = 0 + """ static: A counter to keep any output file names unique across tests. """ + _output_counter = 0 + """ The ATS process for this set of test cases. """ + _ts = None + + def __init__(self, plugin_config, cc_behavior=CcBehaviorT.REMOVE_CC): + """ + Args: + plugin_config (str): The base config line to place in plugin.config for + the ATS process. + cc_behavior (CcBehaviorT): The expected behavior of the ESI plugin + with respect to the Cache-Control header. + """ + if EsiTest._server is None: + EsiTest._server = EsiTest._create_server() + + self._plugin_config = plugin_config + self._cc_behavior = cc_behavior + self._ts = EsiTest._create_ats(self, plugin_config) + + @staticmethod + def _create_server(): + """ + Create and start a server process. + """ + # Configure our server. + server = urtest.MakeOriginServer("server") + + # Generate the set of ESI responses derived right from our ESI docs. + # See: + # doc/admin-guide/plugins/esi.en.rst + request_header = { + "headers": ("GET /esi.php HTTP/1.1\r\n" + "Host: www.example.com\r\n" + "Content-Length: 0\r\n\r\n"), + "timestamp": "1469733493.993", + "body": "" + } + esi_body = r''' + + + Hello, + + + ''' + response_header = { + "headers": + ( + "HTTP/1.1 200 OK\r\n" + "Content-Type: text/html\r\n" + "X-Esi: 1\r\n" + "Connection: close\r\n" + "Content-Length: {}\r\n" + "Cache-Control: max-age=300\r\n" + "\r\n".format(len(esi_body))), + "timestamp": "1469733493.993", + "body": esi_body + } + server.addResponse("sessionfile.log", request_header, response_header) + request_header = { + "headers": ("GET /date.php HTTP/1.1\r\n" + "Host: www.example.com\r\n" + "Content-Length: 0\r\n\r\n"), + "timestamp": "1469733493.993", + "body": "" + } + date_body = r''' + ''' + response_header = { + "headers": + ( + "HTTP/1.1 200 OK\r\n" + "Content-Type: text/html\r\n" + "Connection: close\r\n" + "Content-Length: {}\r\n" + "Cache-Control: max-age=300\r\n" + "\r\n".format(len(date_body))), + "timestamp": "1469733493.993", + "body": date_body + } + server.addResponse("sessionfile.log", request_header, response_header) + # Verify correct functionality with an empty body. + request_header = { + "headers": ("GET /expect_empty_body HTTP/1.1\r\n" + "Host: www.example.com\r\n" + "Content-Length: 0\r\n\r\n"), + "timestamp": "1469733493.993", + "body": "" + } + response_header = { + "headers": + ( + "HTTP/1.1 200 OK\r\n" + "X-ESI: On\r\n" + "Content-Length: 0\r\n" + "Connection: close\r\n" + "Content-Type: text/html; charset=UTF-8\r\n" + "\r\n"), + "timestamp": "1469733493.993", + "body": "" + } + server.addResponse("sessionfile.log", request_header, response_header) + + # Create a run to start the server. + tr = urtest.AddTestRun("Start the server.") + tr.Processes.Default.StartBefore(server) + tr.Processes.Default.Command = "echo starting the server" + tr.Processes.Default.ReturnCode = 0 + tr.StillRunningAfter = server + + return server + + @staticmethod + def _create_ats(self, plugin_config): + """ + Create and start an ATS process. + """ + EsiTest._ts_counter += 1 + + # Configure ATS with a vanilla ESI plugin configuration. + ts = urtest.MakeATSProcess("ts{}".format(EsiTest._ts_counter)) + ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'http|plugin_esi', + }) + ts.Disk.remap_config.AddLine(f'map http://www.example.com/ http://127.0.0.1:{EsiTest._server.Variables.Port}') + ts.Disk.plugin_config.AddLine(plugin_config) + + # Create a run to start the ATS process. + tr = urtest.AddTestRun("Start the ATS process.") + tr.Processes.Default.StartBefore(ts) + tr.Processes.Default.Command = "echo starting ATS" + tr.Processes.Default.ReturnCode = 0 + tr.StillRunningAfter = ts + return ts + + def _configure_client_output_expectations(self, client_process): + client_process.Streams.stderr = "gold/esi_headers.gold" + client_process.Streams.stdout = "gold/esi_body.gold" + if self._cc_behavior == CcBehaviorT.REMOVE_CC: + client_process.Streams.stderr += Testers.ExcludesExpression( + 'cache-control:', 'The Cache-Control field should not be present in the response', reflags=re.IGNORECASE) + client_process.Streams.stderr += Testers.ExcludesExpression( + 'expires:', 'The Expires field should not be present in the response', reflags=re.IGNORECASE) + if self._cc_behavior == CcBehaviorT.MAKE_PRIVATE: + client_process.Streams.stderr += Testers.ContainsExpression( + 'cache-control:.*max-age=0, private', + 'The private response directive should be present in the response', + reflags=re.IGNORECASE) + client_process.Streams.stderr += Testers.ContainsExpression( + 'expires: -1', 'The Expires field should be set to -1', reflags=re.IGNORECASE) + + def run_cases_expecting_gzip(self): + # Test 1: Verify basic ESI functionality. + tr = urtest.AddTestRun(f"First request for esi.php: not cached: {self._plugin_config}") + tr.MakeCurlCommand( + f'http://127.0.0.1:{self._ts.Variables.port}/esi.php -H"Host: www.example.com" ' + '-H"Accept: */*" --verbose', + ts=self._ts) + tr.Processes.Default.ReturnCode = 0 + self._configure_client_output_expectations(tr.Processes.Default) + tr.StillRunningAfter = self._server + tr.StillRunningAfter = self._ts + + # Test 2: Repeat the above, should now be cached. + tr = urtest.AddTestRun(f"Second request for esi.php: will be cached: {self._plugin_config}") + tr.MakeCurlCommand( + f'http://127.0.0.1:{self._ts.Variables.port}/esi.php -H"Host: www.example.com" ' + '-H"Accept: */*" --verbose', + ts=self._ts) + tr.Processes.Default.ReturnCode = 0 + self._configure_client_output_expectations(tr.Processes.Default) + tr.StillRunningAfter = self._server + tr.StillRunningAfter = self._ts + + # Test 3: Verify the ESI plugin can gzip a response when the client accepts it. + tr = urtest.AddTestRun(f"Verify the ESI plugin can gzip a response: {self._plugin_config}") + EsiTest._output_counter += 1 + unzipped_body_file = os.path.join(tr.RunDirectory, f"non_empty_curl_output_{EsiTest._output_counter}") + gzipped_body_file = unzipped_body_file + ".gz" + tr.MakeCurlCommand( + f'http://127.0.0.1:{self._ts.Variables.port}/esi.php -H"Host: www.example.com" ' + f'-H "Accept-Encoding: gzip" -H"Accept: */*" --verbose --output {gzipped_body_file}', + ts=self._ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Ready = When.FileExists(gzipped_body_file) + tr.Processes.Default.Streams.stderr = "gold/esi_gzipped.gold" + tr.StillRunningAfter = self._server + tr.StillRunningAfter = self._ts + zipped_body_disk_file = tr.Disk.File(gzipped_body_file) + zipped_body_disk_file.Exists = True + + # Now, unzip the file and make sure its size is the expected body. + tr = urtest.AddTestRun(f"Verify the file unzips to the expected body: {self._plugin_config}") + tr.Processes.Default.Command = f"gunzip {gzipped_body_file}" + tr.Processes.Default.Ready = When.FileExists(unzipped_body_file) + tr.Processes.Default.ReturnCode = 0 + unzipped_body_disk_file = tr.Disk.File(unzipped_body_file) + unzipped_body_disk_file.Content = "gold/esi_body.gold" + + # Test 4: Verify correct handling of a gzipped empty response body. + tr = urtest.AddTestRun(f"Verify we can handle an empty response: {self._plugin_config}") + EsiTest._output_counter += 1 + empty_body_file = os.path.join(tr.RunDirectory, f"empty_curl_output_{EsiTest._output_counter}") + gzipped_empty_body = empty_body_file + ".gz" + tr.MakeCurlCommand( + f'http://127.0.0.1:{self._ts.Variables.port}/expect_empty_body ' + '-H"Host: www.example.com" -H"Accept-Encoding: gzip" -H"Accept: */*" ' + f'--verbose --output {gzipped_empty_body}', + ts=self._ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Ready = When.FileExists(gzipped_empty_body) + tr.Processes.Default.Streams.stderr = "gold/empty_response_body.gold" + tr.StillRunningAfter = self._server + tr.StillRunningAfter = self._ts + # The gzipped output file should be greater than 0, even though 0 bytes are + # compressed. + gz_disk_file = tr.Disk.File(gzipped_empty_body) + gz_disk_file.Size = Testers.GreaterThan(0) + + # Now, unzip the file and make sure its size is the original 0 size body. + tr = urtest.AddTestRun(f"Verify the file unzips to a zero sized file: {self._plugin_config}") + tr.Processes.Default.Command = f"gunzip {gzipped_empty_body}" + tr.Processes.Default.Ready = When.FileExists(empty_body_file) + tr.Processes.Default.ReturnCode = 0 + unzipped_disk_file = tr.Disk.File(empty_body_file) + unzipped_disk_file.Size = 0 + + def run_case_max_doc_size_too_small(self): + tr = urtest.AddTestRun(f"Max doc size too small: {self._plugin_config}") + tr.MakeCurlCommand( + f'http://127.0.0.1:{self._ts.Variables.port}/esi.php ' + '-H"Host: www.example.com" -H"Accept: */*" --verbose', + ts=self._ts) + tr.Processes.Default.ReturnCode = 0 + self._ts.Disk.diags_log.Content = Testers.ContainsExpression( + r"ERROR: \[_setup\] Cannot allow attempted doc of size 121; Max allowed size is 100 for URL \[.*esi\.php.*\]", + "max doc size test should have doc size error log") + tr.StillRunningAfter = self._server + tr.StillRunningAfter = self._ts + + def run_cases_expecting_no_gzip(self): + # Test 1: Run an ESI test where the client does not accept gzip. + tr = urtest.AddTestRun(f"First request for esi.php: gzip not accepted: {self._plugin_config}") + tr.MakeCurlCommand( + f'http://127.0.0.1:{self._ts.Variables.port}/esi.php ' + '-H"Host: www.example.com" -H"Accept: */*" --verbose', + ts=self._ts) + tr.Processes.Default.ReturnCode = 0 + self._configure_client_output_expectations(tr.Processes.Default) + tr.StillRunningAfter = self._server + tr.StillRunningAfter = self._ts + + # Test 2: Verify the ESI plugin does not gzip the response even if the + # client accepts the gzip encoding. + tr = urtest.AddTestRun(f"Verify the ESI plugin refuses to gzip responses with: {self._plugin_config}") + tr.MakeCurlCommand( + f'http://127.0.0.1:{self._ts.Variables.port}/esi.php ' + '-H"Host: www.example.com" -H "Accept-Encoding: gzip" -H"Accept: */*" --verbose', + ts=self._ts) + tr.Processes.Default.ReturnCode = 0 + self._configure_client_output_expectations(tr.Processes.Default) + tr.StillRunningAfter = self._server + tr.StillRunningAfter = self._ts + + def run_cases_expecting_no_transformation(self): + tr = urtest.AddTestRun(f"Verify the ESI plugin does not transform responses: {self._plugin_config}") + client = tr.MakeCurlCommand( + f'http://127.0.0.1:{self._ts.Variables.port}/esi.php ' + '-H"Host: www.example.com" -H"Accept: */*" --verbose', + ts=self._ts) + client.ReturnCode = 0 + + # Expect no transformation: the tag should be present without any transformation. + client.Streams.stdout += Testers.ContainsExpression( + 'Hello, ', + 'The response should not be transformed', + reflags=re.IGNORECASE) + tr.StillRunningAfter = self._server + tr.StillRunningAfter = self._ts + + # + # Configure and run the test cases. + # + + # Run the tests with ESI configured with no parameters. + vanilla_test = EsiTest(plugin_config='esi.so', cc_behavior=CcBehaviorT.REMOVE_CC) + vanilla_test.run_cases_expecting_gzip() + + private_response_test = EsiTest(plugin_config='esi.so --private-response', cc_behavior=CcBehaviorT.MAKE_PRIVATE) + private_response_test.run_cases_expecting_gzip() + + # For these test cases, the behavior should remain the same with + # --first-byte-flush set. + first_byte_flush_test = EsiTest(plugin_config='esi.so --first-byte-flush') + first_byte_flush_test.run_cases_expecting_gzip() + + # For these test cases, the behavior should remain the same with + # --packed-node-support set. + # + # Packed node support is incomplete and the following test does not work. Our + # documentation advises users not to use the --packed-node-support feature. + # This test is left here, commented out, so that it is conveniently available + # for and potential future development on this feature if desired. + # + # packed_node_support_test = EsiTest(plugin_config='esi.so --packed-node-support') + # packed_node_support_test.run_cases_expecting_gzip() + + # Run a set of cases verifying that the plugin does not zip content if + # --disable-gzip-output is set. + gzip_disabled_test = EsiTest(plugin_config='esi.so --disable-gzip-output') + gzip_disabled_test.run_cases_expecting_no_gzip() + + # Run the tests with too small max doc size. + max_doc_100_test = EsiTest(plugin_config='esi.so --max-doc-size 100') + max_doc_100_test.run_case_max_doc_size_too_small() + + # Run the tests with no default, but sufficient, max doc size. + max_doc_2K_test = EsiTest(plugin_config='esi.so --max-doc-size 2K') + max_doc_2K_test.run_cases_expecting_gzip() + max_doc_20M_test = EsiTest(plugin_config='esi.so --max-doc-size 20M') + max_doc_20M_test.run_cases_expecting_gzip() + + # The test doesn't use 304 redirect, so restricting the allowed response codes to 200 + # should not affect the test. + allowed_response_codes_test = EsiTest(plugin_config='esi.so --allowed-response-codes 200') + allowed_response_codes_test.run_cases_expecting_gzip() + + # Do not allow transforming the 200 OK response. Since the test uses a 200 OK response, + # the plugin should not transform it. + response_not_allowed_test = EsiTest(plugin_config='esi.so --allowed-response-codes 304') + response_not_allowed_test.run_cases_expecting_no_transformation() + urtest.execute() diff --git a/tests/uranium_tests/pluginTest/esi/test_esi_304.py b/tests/uranium_tests/pluginTest/esi/test_esi_304.py new file mode 100644 index 00000000000..04566f4c1bd --- /dev/null +++ b/tests/uranium_tests/pluginTest/esi/test_esi_304.py @@ -0,0 +1,203 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_esi_304(urtest: UraniumTest) -> None: + ''' + Test the ESI plugin when origin returns 304 response. + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import os + + urtest.Summary = ''' + Test the ESI plugin when origin returns 304 response. + ''' + + urtest.SkipUnless(Condition.PluginExists('esi.so'),) + + class EsiTest(): + """ + A class that encapsulates the configuration and execution of a set of ESI + test cases. + """ + """ static: The same server Process is used across all tests. """ + _server = None + """ static: A counter to keep the ATS process names unique across tests. """ + _ts_counter = 0 + """ static: A counter to keep any output file names unique across tests. """ + _output_counter = 0 + """ The ATS process for this set of test cases. """ + _ts = None + + def __init__(self, plugin_config): + """ + Args: + plugin_config (str): The config line to place in plugin.config for + the ATS process. + """ + if EsiTest._server is None: + EsiTest._server = EsiTest._create_server() + + self._ts = EsiTest._create_ats(self, plugin_config) + + @staticmethod + def _create_server(): + """ + Create and start a server process. + """ + # Configure our server. + server = urtest.MakeOriginServer("server", lookup_key="{%uuid}") + + # Generate the set of ESI responses. + request_header = { + "headers": + "GET /esi_etag.php HTTP/1.1\r\n" + "Host: www.example.com\r\n" + "uuid: first\r\n" + + "Content-Length: 0\r\n\r\n", + "timestamp": "1469733493.993", + "body": "" + } + esi_body = r''' + + Hello, ESI 304 test + + + ''' + response_header = { + "headers": + "HTTP/1.1 200 OK\r\n" + "X-Esi: 1\r\n" + "Cache-Control: public, max-age=0\r\n" + 'Etag: "esi_304_test"\r\n' + + "Content-Type: text/html\r\n" + "Connection: close\r\n" + "Content-Length: {}\r\n".format(len(esi_body)) + + "\r\n", + "timestamp": "1469733493.993", + "body": esi_body + } + server.addResponse("sessionfile.log", request_header, response_header) + + request_header = { + "headers": + "GET /esi_etag.php HTTP/1.1\r\n" + "Host: www.example.com\r\n" + "uuid: second\r\n" + + 'If-None-Match: "esi_304_test"\r\n' + "Content-Length: 0\r\n\r\n", + "timestamp": "1469733493.993", + "body": "" + } + response_header = { + "headers": + "HTTP/1.1 304 Not Modified\r\n" + "Content-Type: text/html\r\n" + "Connection: close\r\n" + + "Content-Length: 0\r\n" + "\r\n", + "timestamp": "1469733493.993", + "body": "" + } + server.addResponse("sessionfile.log", request_header, response_header) + + request_header = { + "headers": + "GET /date.php HTTP/1.1\r\n" + "Host: www.example.com\r\n" + "uuid: date\r\n" + "Content-Length: 0\r\n\r\n", + "timestamp": "1469733493.993", + "body": "" + } + date_body = r'''ESI 304 test + No Date + ''' + response_header = { + "headers": + "HTTP/1.1 200 OK\r\n" + "Content-Type: text/html\r\n" + "Connection: close\r\n" + + "Content-Length: {}\r\n".format(len(date_body)) + "\r\n", + "timestamp": "1469733493.993", + "body": date_body + } + server.addResponse("sessionfile.log", request_header, response_header) + + # Create a run to start the server. + tr = urtest.AddTestRun("Start the server.") + tr.Processes.Default.StartBefore(server) + tr.Processes.Default.Command = "echo starting the server" + tr.Processes.Default.ReturnCode = 0 + tr.StillRunningAfter = server + + return server + + @staticmethod + def _create_ats(self, plugin_config): + """ + Create and start an ATS process. + """ + EsiTest._ts_counter += 1 + + # Configure ATS with a vanilla ESI plugin configuration. + ts = urtest.MakeATSProcess("ts{}".format(EsiTest._ts_counter)) + ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'http|plugin_esi', + }) + ts.Disk.remap_config.AddLine('map http://www.example.com/ http://127.0.0.1:{0}'.format(EsiTest._server.Variables.Port)) + ts.Disk.plugin_config.AddLine(plugin_config) + + # Create a run to start the ATS process. + tr = urtest.AddTestRun("Start the ATS process.") + tr.Processes.Default.StartBefore(ts) + tr.Processes.Default.Command = "echo starting ATS" + tr.Processes.Default.ReturnCode = 0 + tr.StillRunningAfter = ts + return ts + + def run_cases(self): + # Test 1: Verify basic ESI functionality. + tr = urtest.AddTestRun("First request for esi_etag.php: not cached") + tr.MakeCurlCommand( + 'http://127.0.0.1:{0}/esi_etag.php -H"Host: www.example.com" ' + '-H"Accept: */*" -H"uuid: first" --verbose -o /dev/stderr'.format(self._ts.Variables.port), + ts=self._ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.stderr = "gold/esi_private_headers.gold" + tr.StillRunningAfter = self._server + tr.StillRunningAfter = self._ts + + # Test 2: Repeat the above, origin should now be returning 304 response. + tr = urtest.AddTestRun("Second request for esi_etag.php: will be cached") + tr.MakeCurlCommand( + 'http://127.0.0.1:{0}/esi_etag.php -H"Host: www.example.com" ' + '-H"Accept: */*" -H"uuid: second" --verbose -o /dev/stderr'.format(self._ts.Variables.port), + ts=self._ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.stderr = "gold/esi_private_headers.gold" + tr.StillRunningAfter = self._server + tr.StillRunningAfter = self._ts + + # + # Configure and run the test cases. + # + + # Run the tests with ESI configured with private response. + private_response_test = EsiTest(plugin_config='esi.so --private-response') + private_response_test.run_cases() + urtest.execute() diff --git a/tests/uranium_tests/pluginTest/esi/test_esi_nested_html_comment.py b/tests/uranium_tests/pluginTest/esi/test_esi_nested_html_comment.py new file mode 100644 index 00000000000..0c198bff549 --- /dev/null +++ b/tests/uranium_tests/pluginTest/esi/test_esi_nested_html_comment.py @@ -0,0 +1,174 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_esi_nested_html_comment(urtest: UraniumTest) -> None: + ''' + Regression test for the ESI comment wrapper handling. + + Covers the sec-035 fix that rejects nested wrappers inside + another . Two scenarios: + + 1. Legitimate single-level wrapper: must still be expanded normally and + the nested-wrapper guard must NOT fire. + + 2. Nested wrapper hidden inside / child_nodes: + must be rejected by the guard and the diags log must record the + "Nested inside " error. This is the + deeper case the prior top-level-only check missed. + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + urtest.Summary = ''' + Verify the ESI plugin processes a legitimate single-level wrapper + and rejects a nested hidden inside /. + ''' + + urtest.SkipUnless(Condition.PluginExists('esi.so'),) + + class EsiHtmlCommentTest(): + """ + Drive a single request through ATS whose origin response contains an + `` wrapper, and verify the plugin + expands it without tripping the nested-wrapper guard. + """ + + _replay_file: str = "esi_nested_html_comment.replay.yaml" + + def __init__(self, plugin_config: str) -> None: + tr = urtest.AddTestRun("ESI single-level wrapper is processed") + self._create_server(tr) + self._create_ats(tr, plugin_config) + self._create_client(tr) + + def _create_server(self, tr: 'TestRun') -> 'Process': + server = tr.AddVerifierServerProcess("server", self._replay_file, other_args='--format "{url}"') + self._server = server + + server.Streams.All += Testers.ContainsExpression( + 'GET /esi-html-comment.php', 'Verify the server received the ESI document request.') + return server + + def _create_ats(self, tr: 'TestRun', plugin_config: str) -> 'Process': + ts = tr.MakeATSProcess("ts") + self._ts = ts + ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'http|plugin_esi|plugin_esi_procesor', + }) + server_port = self._server.Variables.http_port + ts.Disk.remap_config.AddLine(f'map http://www.example.com/ http://127.0.0.1:{server_port}') + ts.Disk.plugin_config.AddLine(plugin_config) + + # The nested-wrapper guard added for sec-035 must NOT fire on a + # legitimate single-level wrapper. + ts.Disk.diags_log.Content = Testers.ExcludesExpression( + r'Nested inside ', 'The nested-wrapper guard must not fire on legitimate input.') + return ts + + def _create_client(self, tr: 'TestRun') -> None: + p = tr.AddVerifierClientProcess( + "client", + self._replay_file, + http_ports=[self._ts.Variables.port], + other_args='--format "{url}" --keys /esi-html-comment.php') + p.ReturnCode = 0 + p.StartBefore(self._server) + p.StartBefore(self._ts) + + # The body must contain the expanded variable from inside the + # wrapper. + p.Streams.stdout += Testers.ContainsExpression('www.example.com', 'Verify the client received the expanded ESI body.') + + class EsiNestedHtmlCommentRejectTest(): + """ + Drive a request whose origin response contains a nested + hidden inside / child_nodes, and verify the + processor rejects it via the raw-substring guard. The prior fix only + scanned the top level of inner_nodes and would have let this through. + """ + + _replay_file: str = "esi_nested_html_comment_reject.replay.yaml" + + def __init__(self, plugin_config: str) -> None: + tr = urtest.AddTestRun("ESI nested wrapper is rejected") + self._create_server(tr) + self._create_ats(tr, plugin_config) + self._create_client(tr) + + def _create_server(self, tr: 'TestRun') -> 'Process': + server = tr.AddVerifierServerProcess("server-reject", self._replay_file, other_args='--format "{url}"') + self._server = server + + server.Streams.All += Testers.ContainsExpression( + 'GET /esi-nested-reject.php', 'Verify the server received the nested-wrapper request.') + return server + + def _create_ats(self, tr: 'TestRun', plugin_config: str) -> 'Process': + ts = tr.MakeATSProcess("ts-reject") + self._ts = ts + ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'http|plugin_esi|plugin_esi_procesor', + }) + server_port = self._server.Variables.http_port + ts.Disk.remap_config.AddLine(f'map http://www.example.com/ http://127.0.0.1:{server_port}') + ts.Disk.plugin_config.AddLine(plugin_config) + + # The guard must fire on a nested wrapper, even when the nested + # is hidden inside / rather + # than appearing at the top level of the outer wrapper's content. + ts.Disk.diags_log.Content = Testers.ContainsExpression( + r'Nested inside is not allowed', + 'The nested-wrapper guard must fire on a nested ESI comment hidden inside /.') + return ts + + def _create_client(self, tr: 'TestRun') -> None: + p = tr.AddVerifierClientProcess( + "client-reject", + self._replay_file, + http_ports=[self._ts.Variables.port], + other_args='--format "{url}" --keys /esi-nested-reject.php') + p.ReturnCode = 0 + p.StartBefore(self._server) + p.StartBefore(self._ts) + # The strong signal that the guard fired is the diags_log + # ContainsExpression in _create_ats. We deliberately do not + # assert on the client body here, since the failure path + # (pass-through vs. error response) is orthogonal to the + # guard and may evolve independently. + + EsiHtmlCommentTest(plugin_config='esi.so') + EsiNestedHtmlCommentRejectTest(plugin_config='esi.so') + urtest.execute() diff --git a/tests/uranium_tests/pluginTest/esi/test_esi_nested_include.py b/tests/uranium_tests/pluginTest/esi/test_esi_nested_include.py new file mode 100644 index 00000000000..42ee0b0a4e2 --- /dev/null +++ b/tests/uranium_tests/pluginTest/esi/test_esi_nested_include.py @@ -0,0 +1,125 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_esi_nested_include(urtest: UraniumTest) -> None: + ''' + Test nested include for the ESI plugin. + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + urtest.Summary = ''' + Test nested include for the ESI plugin. + ''' + + urtest.SkipUnless(Condition.PluginExists('esi.so'),) + + class EsiTest(): + """ + A class that encapsulates the configuration and execution of a set of ESI + test cases. + """ + + _replay_file: str = "esi_nested_include.replay.yaml" + + def __init__(self, plugin_config) -> None: + """ + :param plugin_config: esi.so configuration for plugin.config. + """ + tr = urtest.AddTestRun("Request the ESI generated document") + self._create_server(tr) + self._create_ats(tr, plugin_config) + self._create_client(tr) + + def _create_server(self, tr: 'TestRun') -> 'Process': + """ Create and start a server process. + :param tr: The test run to add the server to. + :return: The server process. + """ + # Configure our server using proxy verifier. + server = tr.AddVerifierServerProcess("server", self._replay_file, other_args='--format "{url}"') + self._server = server + + # Validate server traffic + server.Streams.All += Testers.ContainsExpression('GET /main.php', 'Verify the server received the initial request.') + server.Streams.All += Testers.ContainsExpression( + 'GET /esi-nested-include.html', 'Verify the server received the nested include request.') + + return server + + def _create_ats(self, tr: 'TestRun', plugin_config: str) -> 'Process': + """ Create and start an ATS process. + :param tr: The test run to add the ATS to. + :param plugin_config: The plugin configuration to use. + :return: The ATS process. + """ + # Configure ATS with a vanilla ESI plugin configuration. + ts = tr.MakeATSProcess(f"ts") + self._ts = ts + ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'http|plugin_esi', + }) + server_port = self._server.Variables.http_port + ts.Disk.remap_config.AddLine(f'map http://www.example.com/ http://127.0.0.1:{server_port}') + ts.Disk.plugin_config.AddLine(plugin_config) + + ts.Disk.diags_log.Content = Testers.ContainsExpression( + r'The current esi inclusion depth \(3\) is larger than or equal to the max \(3\)', + 'Verify the ESI error concerning the max inclusion depth') + return ts + + def _create_client(self, tr: 'TestRun') -> None: + """ Create and start a client process to generate the request. + :param tr: The test run to add the client to. + """ + # Note, just request the main.php file. Otherwise the client will do the + # ESI requests in the replay file as well. + p = tr.AddVerifierClientProcess( + "client", self._replay_file, http_ports=[self._ts.Variables.port], other_args='--format "{url}" --keys /main.php') + p.ReturnCode = 0 + p.StartBefore(self._server) + p.StartBefore(self._ts) + + # Double check that the client received the response. + p.Streams.stdout += Testers.ContainsExpression( + 'Received an HTTP/1 chunked body', 'Verify the client received the response.') + + p.Streams.stdout += Testers.ContainsExpression( + 'esi:include src="http://www.example.com/esi-nested-include.html"/>', 'Verify the ATS received the esi include.') + + # + # Configure and run the test cases. + # + EsiTest(plugin_config='esi.so') + urtest.execute() diff --git a/tests/uranium_tests/pluginTest/esi/test_esi_request_size_cap.py b/tests/uranium_tests/pluginTest/esi/test_esi_request_size_cap.py new file mode 100644 index 00000000000..2bf815fc75d --- /dev/null +++ b/tests/uranium_tests/pluginTest/esi/test_esi_request_size_cap.py @@ -0,0 +1,112 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_esi_request_size_cap(urtest: UraniumTest) -> None: + ''' + Test the ESI plugin's HTTP fetch request size cap. + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + urtest.Summary = ''' + Verify HttpDataFetcherImpl rejects ESI include fetches whose total HTTP + request size (request line + URL + forwarded headers) would exceed + MAX_REQ_LEN (32 KB), logging an error rather than allocating a buffer + sized by an overflowed size_t. + ''' + + urtest.SkipUnless(Condition.PluginExists('esi.so'),) + + # Matches MAX_REQ_LEN in plugins/esi/fetcher/HttpDataFetcherImpl.cc. + MAX_REQ_LEN = 32 * 1024 + + # total_len in addFetchRequest is: + # sizeof("GET ") - 1 -> 4 + # + url.length() + # + sizeof(" HTTP/1.0\r\n") - 1 -> 11 + # + _headers_str.length() + # + sizeof("\r\n") - 1 -> 2 + # A path one byte longer than MAX_REQ_LEN guarantees the cap is tripped + # regardless of how many headers end up being forwarded. + oversized_path = 'A' * (MAX_REQ_LEN + 1) + esi_body = ('\n\n' + f'Hello, \n' + '\n\n') + + server = urtest.MakeOriginServer("server") + server.addResponse( + "sessionfile.log", { + "headers": ("GET /oversized.php HTTP/1.1\r\n" + "Host: www.example.com\r\n" + "Content-Length: 0\r\n\r\n"), + "timestamp": "1469733493.993", + "body": "" + }, { + "headers": + ( + "HTTP/1.1 200 OK\r\n" + "Content-Type: text/html\r\n" + "X-Esi: 1\r\n" + "Connection: close\r\n" + f"Content-Length: {len(esi_body)}\r\n" + "Cache-Control: max-age=300\r\n\r\n"), + "timestamp": "1469733493.993", + "body": esi_body + }) + + ts = urtest.MakeATSProcess("ts") + ts.Disk.records_config.update({ + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'http|plugin_esi', + }) + ts.Disk.remap_config.AddLine(f'map http://www.example.com/ http://127.0.0.1:{server.Variables.Port}') + ts.Disk.plugin_config.AddLine('esi.so') + + tr = urtest.AddTestRun("Start the server and ATS.") + tr.Processes.Default.StartBefore(server) + tr.Processes.Default.StartBefore(ts) + tr.Processes.Default.Command = "echo starting" + tr.Processes.Default.ReturnCode = 0 + tr.StillRunningAfter = server + tr.StillRunningAfter = ts + + tr = urtest.AddTestRun("Issue a request whose ESI include URL exceeds the 32 KB fetch cap.") + tr.MakeCurlCommand( + f'http://127.0.0.1:{ts.Variables.port}/oversized.php ' + '-H"Host: www.example.com" -H"Accept: */*" --output /dev/null --silent', + ts=ts) + tr.Processes.Default.ReturnCode = 0 + ts.Disk.diags_log.Content = Testers.ContainsExpression( + r"HTTP request size exceeds maximum 32768", "ESI fetcher must log the MAX_REQ_LEN cap error for oversize requests") + tr.StillRunningAfter = server + tr.StillRunningAfter = ts + urtest.execute() diff --git a/tests/uranium_tests/pluginTest/esi/test_esi_ssrf_validate.py b/tests/uranium_tests/pluginTest/esi/test_esi_ssrf_validate.py new file mode 100644 index 00000000000..42ced6359f0 --- /dev/null +++ b/tests/uranium_tests/pluginTest/esi/test_esi_ssrf_validate.py @@ -0,0 +1,134 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_esi_ssrf_validate(urtest: UraniumTest) -> None: + ''' + Test SSRF validation for ESI include src= URLs. + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + urtest.Summary = ''' + Verify the ESI plugin's SSRF guard rejects include URLs with private-IP + hosts, non-http(s) schemes, and attacker-controlled variable expansion, + while still allowing ordinary includes to the configured upstream. + ''' + + urtest.SkipUnless(Condition.PluginExists('esi.so'),) + + class EsiSsrfTest: + """Drives the replay file through ATS with the SSRF guard enabled and + confirms each rejection path logs the expected reason while the + allowed include is still fetched and inlined.""" + + _replay_file: str = "esi_ssrf_validate.replay.yaml" + + def __init__(self) -> None: + tr = urtest.AddTestRun("ESI include URLs are validated for SSRF") + self._create_server(tr) + self._create_ats(tr) + self._create_client(tr) + + def _create_server(self, tr: 'TestRun') -> None: + server = tr.AddVerifierServerProcess("server", self._replay_file, other_args='--format "{url}"') + self._server = server + + # The snippet for the allowed-include case must reach the origin. + # The rejected cases must never reach any backend other than the + # five top-level documents themselves; we don't assert their + # absence on the server stream because the rejected hosts are + # not mapped to this verifier. + server.Streams.All += Testers.ContainsExpression( + 'GET /allowed.php', 'Verify the allowed top-level request reached origin.') + server.Streams.All += Testers.ContainsExpression( + 'GET /snippet.html', 'Verify the snippet for the allowed include was fetched.') + + def _create_ats(self, tr: 'TestRun') -> None: + ts = tr.MakeATSProcess("ts") + self._ts = ts + ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'http|plugin_esi', + }) + + server_port = self._server.Variables.http_port + ts.Disk.remap_config.AddLine(f'map http://www.example.com/ http://127.0.0.1:{server_port}') + + # Default-deny private/loopback hosts; no allow-regex configured, + # so any non-private host is permitted (the upstream maps via + # www.example.com). + ts.Disk.plugin_config.AddLine('esi.so') + + # Each rejection must be logged with its reason. Reason strings + # come from IncludeUrlValidator::reasonString(). + # TSError emits one line per rejection that contains both the + # offending URL substring and the reason token. Match each pair + # loosely so the assertion survives bracket/paren formatting + # differences in the diags log. ``.`` is newline-bounded by + # default, so each regex stays single-line. + ts.Disk.diags_log.Content = Testers.ContainsExpression( + r'Rejecting include URL.*169\.254\.169\.254.*private-host', + 'Cloud-metadata IPv4 literal must be rejected as private-host.') + ts.Disk.diags_log.Content += Testers.ContainsExpression( + r'Rejecting include URL.*127\.0\.0\.1/admin.*private-host', + 'Loopback IPv4 literal must be rejected as private-host.') + ts.Disk.diags_log.Content += Testers.ContainsExpression( + r'Rejecting include URL.*gopher://internal\.svc/x.*bad-scheme', + 'Non-http(s) scheme must be rejected as bad-scheme.') + ts.Disk.diags_log.Content += Testers.ContainsExpression( + r'Rejecting include URL.*10\.0\.0\.5/secret.*private-host', + 'Attacker-influenced variable expansion must be rejected after \\$\\(...\\) is expanded.') + + # The allowed case must NOT show up as a rejection. + ts.Disk.diags_log.Content += Testers.ExcludesExpression( + r'Rejecting include URL.*www\.example\.com/snippet\.html', + 'The legitimate include to the mapped upstream must not be rejected.') + + def _create_client(self, tr: 'TestRun') -> None: + # Proxy Verifier's default --keys format in this codebase keys + # transactions by URL (see esi_nested_include.test.py), not by the + # `uuid` request header. Match on the top-level document URLs so + # the client drives the four rejection cases and the one allowed + # case; the /snippet.html transaction is omitted because ATS + # fetches it internally via the ESI include. + p = tr.AddVerifierClientProcess( + "client", + self._replay_file, + http_ports=[self._ts.Variables.port], + other_args='--format "{url}" --keys /metadata.php /loopback.php /badscheme.php /varinject.php /allowed.php') + p.ReturnCode = 0 + p.StartBefore(self._server) + p.StartBefore(self._ts) + + EsiSsrfTest() + urtest.execute() diff --git a/tests/gold_tests/pluginTest/filter_body/config/filter_body_request_block.yaml b/tests/uranium_tests/pluginTest/filter_body/config/filter_body_request_block.yaml similarity index 100% rename from tests/gold_tests/pluginTest/filter_body/config/filter_body_request_block.yaml rename to tests/uranium_tests/pluginTest/filter_body/config/filter_body_request_block.yaml diff --git a/tests/gold_tests/pluginTest/filter_body/config/filter_body_request_header.yaml b/tests/uranium_tests/pluginTest/filter_body/config/filter_body_request_header.yaml similarity index 100% rename from tests/gold_tests/pluginTest/filter_body/config/filter_body_request_header.yaml rename to tests/uranium_tests/pluginTest/filter_body/config/filter_body_request_header.yaml diff --git a/tests/gold_tests/pluginTest/filter_body/config/filter_body_request_log.yaml b/tests/uranium_tests/pluginTest/filter_body/config/filter_body_request_log.yaml similarity index 100% rename from tests/gold_tests/pluginTest/filter_body/config/filter_body_request_log.yaml rename to tests/uranium_tests/pluginTest/filter_body/config/filter_body_request_log.yaml diff --git a/tests/gold_tests/pluginTest/filter_body/config/filter_body_response_block.yaml b/tests/uranium_tests/pluginTest/filter_body/config/filter_body_response_block.yaml similarity index 100% rename from tests/gold_tests/pluginTest/filter_body/config/filter_body_response_block.yaml rename to tests/uranium_tests/pluginTest/filter_body/config/filter_body_response_block.yaml diff --git a/tests/gold_tests/pluginTest/filter_body/config/filter_body_response_header.yaml b/tests/uranium_tests/pluginTest/filter_body/config/filter_body_response_header.yaml similarity index 100% rename from tests/gold_tests/pluginTest/filter_body/config/filter_body_response_header.yaml rename to tests/uranium_tests/pluginTest/filter_body/config/filter_body_response_header.yaml diff --git a/tests/gold_tests/pluginTest/filter_body/config/filter_body_response_log.yaml b/tests/uranium_tests/pluginTest/filter_body/config/filter_body_response_log.yaml similarity index 100% rename from tests/gold_tests/pluginTest/filter_body/config/filter_body_response_log.yaml rename to tests/uranium_tests/pluginTest/filter_body/config/filter_body_response_log.yaml diff --git a/tests/gold_tests/pluginTest/filter_body/filter_body.test.yaml b/tests/uranium_tests/pluginTest/filter_body/filter_body.test.yaml similarity index 99% rename from tests/gold_tests/pluginTest/filter_body/filter_body.test.yaml rename to tests/uranium_tests/pluginTest/filter_body/filter_body.test.yaml index cc652dda4e5..6a81d7b5af1 100644 --- a/tests/gold_tests/pluginTest/filter_body/filter_body.test.yaml +++ b/tests/uranium_tests/pluginTest/filter_body/filter_body.test.yaml @@ -21,7 +21,7 @@ meta: version: "1.0" -autest: +urtest: description: 'Verify filter_body plugin for request/response body content filtering' requires: plugins: [filter_body.so] diff --git a/tests/gold_tests/pluginTest/header_rewrite/gold/header_rewrite-set_body_from_200.gold b/tests/uranium_tests/pluginTest/header_rewrite/gold/header_rewrite-set_body_from_200.gold similarity index 100% rename from tests/gold_tests/pluginTest/header_rewrite/gold/header_rewrite-set_body_from_200.gold rename to tests/uranium_tests/pluginTest/header_rewrite/gold/header_rewrite-set_body_from_200.gold diff --git a/tests/gold_tests/pluginTest/header_rewrite/gold/header_rewrite-set_body_from_conn_fail.gold b/tests/uranium_tests/pluginTest/header_rewrite/gold/header_rewrite-set_body_from_conn_fail.gold similarity index 100% rename from tests/gold_tests/pluginTest/header_rewrite/gold/header_rewrite-set_body_from_conn_fail.gold rename to tests/uranium_tests/pluginTest/header_rewrite/gold/header_rewrite-set_body_from_conn_fail.gold diff --git a/tests/gold_tests/pluginTest/header_rewrite/gold/header_rewrite-set_body_from_remap_fail.gold b/tests/uranium_tests/pluginTest/header_rewrite/gold/header_rewrite-set_body_from_remap_fail.gold similarity index 100% rename from tests/gold_tests/pluginTest/header_rewrite/gold/header_rewrite-set_body_from_remap_fail.gold rename to tests/uranium_tests/pluginTest/header_rewrite/gold/header_rewrite-set_body_from_remap_fail.gold diff --git a/tests/gold_tests/pluginTest/header_rewrite/gold/header_rewrite-set_body_from_success.gold b/tests/uranium_tests/pluginTest/header_rewrite/gold/header_rewrite-set_body_from_success.gold similarity index 100% rename from tests/gold_tests/pluginTest/header_rewrite/gold/header_rewrite-set_body_from_success.gold rename to tests/uranium_tests/pluginTest/header_rewrite/gold/header_rewrite-set_body_from_success.gold diff --git a/tests/gold_tests/pluginTest/header_rewrite/gold/header_rewrite-tag.gold b/tests/uranium_tests/pluginTest/header_rewrite/gold/header_rewrite-tag.gold similarity index 100% rename from tests/gold_tests/pluginTest/header_rewrite/gold/header_rewrite-tag.gold rename to tests/uranium_tests/pluginTest/header_rewrite/gold/header_rewrite-tag.gold diff --git a/tests/gold_tests/pluginTest/header_rewrite/gold/plugin-status-test.gold b/tests/uranium_tests/pluginTest/header_rewrite/gold/plugin-status-test.gold similarity index 100% rename from tests/gold_tests/pluginTest/header_rewrite/gold/plugin-status-test.gold rename to tests/uranium_tests/pluginTest/header_rewrite/gold/plugin-status-test.gold diff --git a/tests/gold_tests/pluginTest/header_rewrite/gold/set-redirect-glob.gold b/tests/uranium_tests/pluginTest/header_rewrite/gold/set-redirect-glob.gold similarity index 100% rename from tests/gold_tests/pluginTest/header_rewrite/gold/set-redirect-glob.gold rename to tests/uranium_tests/pluginTest/header_rewrite/gold/set-redirect-glob.gold diff --git a/tests/gold_tests/pluginTest/header_rewrite/header_rewrite_bundle.test.yaml b/tests/uranium_tests/pluginTest/header_rewrite/header_rewrite_bundle.test.yaml similarity index 99% rename from tests/gold_tests/pluginTest/header_rewrite/header_rewrite_bundle.test.yaml rename to tests/uranium_tests/pluginTest/header_rewrite/header_rewrite_bundle.test.yaml index c67ad080b6a..777d5167334 100644 --- a/tests/gold_tests/pluginTest/header_rewrite/header_rewrite_bundle.test.yaml +++ b/tests/uranium_tests/pluginTest/header_rewrite/header_rewrite_bundle.test.yaml @@ -17,8 +17,8 @@ meta: version: "1.0" -# Configuration section for autest integration -autest: +# Configuration for the Uranium test runner +urtest: description: 'Test header_rewrite with URL conditions and operators' dns: diff --git a/tests/gold_tests/pluginTest/header_rewrite/header_rewrite_post_remap.test.yaml b/tests/uranium_tests/pluginTest/header_rewrite/header_rewrite_post_remap.test.yaml similarity index 99% rename from tests/gold_tests/pluginTest/header_rewrite/header_rewrite_post_remap.test.yaml rename to tests/uranium_tests/pluginTest/header_rewrite/header_rewrite_post_remap.test.yaml index d3cf64c259d..29224ab67b7 100644 --- a/tests/gold_tests/pluginTest/header_rewrite/header_rewrite_post_remap.test.yaml +++ b/tests/uranium_tests/pluginTest/header_rewrite/header_rewrite_post_remap.test.yaml @@ -17,7 +17,7 @@ meta: version: "1.0" -autest: +urtest: description: 'Test header_rewrite POST_REMAP_HOOK support (global plugin)' dns: diff --git a/tests/gold_tests/pluginTest/header_rewrite/post_remap.conf b/tests/uranium_tests/pluginTest/header_rewrite/post_remap.conf similarity index 100% rename from tests/gold_tests/pluginTest/header_rewrite/post_remap.conf rename to tests/uranium_tests/pluginTest/header_rewrite/post_remap.conf diff --git a/tests/gold_tests/pluginTest/header_rewrite/rules/complex_logics.conf b/tests/uranium_tests/pluginTest/header_rewrite/rules/complex_logics.conf similarity index 100% rename from tests/gold_tests/pluginTest/header_rewrite/rules/complex_logics.conf rename to tests/uranium_tests/pluginTest/header_rewrite/rules/complex_logics.conf diff --git a/tests/gold_tests/pluginTest/header_rewrite/rules/glob_set_redirect.conf b/tests/uranium_tests/pluginTest/header_rewrite/rules/glob_set_redirect.conf similarity index 100% rename from tests/gold_tests/pluginTest/header_rewrite/rules/glob_set_redirect.conf rename to tests/uranium_tests/pluginTest/header_rewrite/rules/glob_set_redirect.conf diff --git a/tests/gold_tests/pluginTest/header_rewrite/rules/implicit_hook.conf b/tests/uranium_tests/pluginTest/header_rewrite/rules/implicit_hook.conf similarity index 100% rename from tests/gold_tests/pluginTest/header_rewrite/rules/implicit_hook.conf rename to tests/uranium_tests/pluginTest/header_rewrite/rules/implicit_hook.conf diff --git a/tests/gold_tests/pluginTest/header_rewrite/rules/nested_ifs.conf b/tests/uranium_tests/pluginTest/header_rewrite/rules/nested_ifs.conf similarity index 100% rename from tests/gold_tests/pluginTest/header_rewrite/rules/nested_ifs.conf rename to tests/uranium_tests/pluginTest/header_rewrite/rules/nested_ifs.conf diff --git a/tests/gold_tests/pluginTest/header_rewrite/rules/query_sub_key.conf b/tests/uranium_tests/pluginTest/header_rewrite/rules/query_sub_key.conf similarity index 100% rename from tests/gold_tests/pluginTest/header_rewrite/rules/query_sub_key.conf rename to tests/uranium_tests/pluginTest/header_rewrite/rules/query_sub_key.conf diff --git a/tests/gold_tests/pluginTest/header_rewrite/rules/regex_tests.conf b/tests/uranium_tests/pluginTest/header_rewrite/rules/regex_tests.conf similarity index 100% rename from tests/gold_tests/pluginTest/header_rewrite/rules/regex_tests.conf rename to tests/uranium_tests/pluginTest/header_rewrite/rules/regex_tests.conf diff --git a/tests/gold_tests/pluginTest/header_rewrite/rules/rule.conf b/tests/uranium_tests/pluginTest/header_rewrite/rules/rule.conf similarity index 100% rename from tests/gold_tests/pluginTest/header_rewrite/rules/rule.conf rename to tests/uranium_tests/pluginTest/header_rewrite/rules/rule.conf diff --git a/tests/gold_tests/pluginTest/header_rewrite/rules/rule_add_cache_result_header.conf b/tests/uranium_tests/pluginTest/header_rewrite/rules/rule_add_cache_result_header.conf similarity index 100% rename from tests/gold_tests/pluginTest/header_rewrite/rules/rule_add_cache_result_header.conf rename to tests/uranium_tests/pluginTest/header_rewrite/rules/rule_add_cache_result_header.conf diff --git a/tests/gold_tests/pluginTest/header_rewrite/rules/rule_cidr.conf b/tests/uranium_tests/pluginTest/header_rewrite/rules/rule_cidr.conf similarity index 100% rename from tests/gold_tests/pluginTest/header_rewrite/rules/rule_cidr.conf rename to tests/uranium_tests/pluginTest/header_rewrite/rules/rule_cidr.conf diff --git a/tests/gold_tests/pluginTest/header_rewrite/rules/rule_client.conf b/tests/uranium_tests/pluginTest/header_rewrite/rules/rule_client.conf similarity index 100% rename from tests/gold_tests/pluginTest/header_rewrite/rules/rule_client.conf rename to tests/uranium_tests/pluginTest/header_rewrite/rules/rule_client.conf diff --git a/tests/gold_tests/pluginTest/header_rewrite/rules/rule_cond_method.conf b/tests/uranium_tests/pluginTest/header_rewrite/rules/rule_cond_method.conf similarity index 100% rename from tests/gold_tests/pluginTest/header_rewrite/rules/rule_cond_method.conf rename to tests/uranium_tests/pluginTest/header_rewrite/rules/rule_cond_method.conf diff --git a/tests/gold_tests/pluginTest/header_rewrite/rules/rule_cookie.conf b/tests/uranium_tests/pluginTest/header_rewrite/rules/rule_cookie.conf similarity index 100% rename from tests/gold_tests/pluginTest/header_rewrite/rules/rule_cookie.conf rename to tests/uranium_tests/pluginTest/header_rewrite/rules/rule_cookie.conf diff --git a/tests/gold_tests/pluginTest/header_rewrite/rules/rule_effective_address.conf b/tests/uranium_tests/pluginTest/header_rewrite/rules/rule_effective_address.conf similarity index 100% rename from tests/gold_tests/pluginTest/header_rewrite/rules/rule_effective_address.conf rename to tests/uranium_tests/pluginTest/header_rewrite/rules/rule_effective_address.conf diff --git a/tests/gold_tests/pluginTest/header_rewrite/rules/rule_empty_body.conf b/tests/uranium_tests/pluginTest/header_rewrite/rules/rule_empty_body.conf similarity index 100% rename from tests/gold_tests/pluginTest/header_rewrite/rules/rule_empty_body.conf rename to tests/uranium_tests/pluginTest/header_rewrite/rules/rule_empty_body.conf diff --git a/tests/gold_tests/pluginTest/header_rewrite/rules/rule_l_value.conf b/tests/uranium_tests/pluginTest/header_rewrite/rules/rule_l_value.conf similarity index 100% rename from tests/gold_tests/pluginTest/header_rewrite/rules/rule_l_value.conf rename to tests/uranium_tests/pluginTest/header_rewrite/rules/rule_l_value.conf diff --git a/tests/gold_tests/pluginTest/header_rewrite/rules/rule_server_conditions.conf b/tests/uranium_tests/pluginTest/header_rewrite/rules/rule_server_conditions.conf similarity index 100% rename from tests/gold_tests/pluginTest/header_rewrite/rules/rule_server_conditions.conf rename to tests/uranium_tests/pluginTest/header_rewrite/rules/rule_server_conditions.conf diff --git a/tests/gold_tests/pluginTest/header_rewrite/rules/rule_session_vars.conf b/tests/uranium_tests/pluginTest/header_rewrite/rules/rule_session_vars.conf similarity index 100% rename from tests/gold_tests/pluginTest/header_rewrite/rules/rule_session_vars.conf rename to tests/uranium_tests/pluginTest/header_rewrite/rules/rule_session_vars.conf diff --git a/tests/gold_tests/pluginTest/header_rewrite/rules/rule_set_body_from_plugin.conf b/tests/uranium_tests/pluginTest/header_rewrite/rules/rule_set_body_from_plugin.conf similarity index 100% rename from tests/gold_tests/pluginTest/header_rewrite/rules/rule_set_body_from_plugin.conf rename to tests/uranium_tests/pluginTest/header_rewrite/rules/rule_set_body_from_plugin.conf diff --git a/tests/gold_tests/pluginTest/header_rewrite/rules/rule_set_body_from_remap.conf b/tests/uranium_tests/pluginTest/header_rewrite/rules/rule_set_body_from_remap.conf similarity index 100% rename from tests/gold_tests/pluginTest/header_rewrite/rules/rule_set_body_from_remap.conf rename to tests/uranium_tests/pluginTest/header_rewrite/rules/rule_set_body_from_remap.conf diff --git a/tests/gold_tests/pluginTest/header_rewrite/rules/rule_set_body_status.conf b/tests/uranium_tests/pluginTest/header_rewrite/rules/rule_set_body_status.conf similarity index 100% rename from tests/gold_tests/pluginTest/header_rewrite/rules/rule_set_body_status.conf rename to tests/uranium_tests/pluginTest/header_rewrite/rules/rule_set_body_status.conf diff --git a/tests/gold_tests/pluginTest/header_rewrite/rules/rule_set_header_after_ssn_txn_count.conf b/tests/uranium_tests/pluginTest/header_rewrite/rules/rule_set_header_after_ssn_txn_count.conf similarity index 100% rename from tests/gold_tests/pluginTest/header_rewrite/rules/rule_set_header_after_ssn_txn_count.conf rename to tests/uranium_tests/pluginTest/header_rewrite/rules/rule_set_header_after_ssn_txn_count.conf diff --git a/tests/gold_tests/pluginTest/header_rewrite/rules/run_plugin.conf b/tests/uranium_tests/pluginTest/header_rewrite/rules/run_plugin.conf similarity index 100% rename from tests/gold_tests/pluginTest/header_rewrite/rules/run_plugin.conf rename to tests/uranium_tests/pluginTest/header_rewrite/rules/run_plugin.conf diff --git a/tests/gold_tests/pluginTest/header_rewrite/rules/set_redirect.conf b/tests/uranium_tests/pluginTest/header_rewrite/rules/set_redirect.conf similarity index 100% rename from tests/gold_tests/pluginTest/header_rewrite/rules/set_redirect.conf rename to tests/uranium_tests/pluginTest/header_rewrite/rules/set_redirect.conf diff --git a/tests/gold_tests/pluginTest/header_rewrite/rules/set_status_in_if.conf b/tests/uranium_tests/pluginTest/header_rewrite/rules/set_status_in_if.conf similarity index 100% rename from tests/gold_tests/pluginTest/header_rewrite/rules/set_status_in_if.conf rename to tests/uranium_tests/pluginTest/header_rewrite/rules/set_status_in_if.conf diff --git a/tests/uranium_tests/pluginTest/header_rewrite/test_header_rewrite_bad_run_plugin.py b/tests/uranium_tests/pluginTest/header_rewrite/test_header_rewrite_bad_run_plugin.py new file mode 100644 index 00000000000..f14c12d6cad --- /dev/null +++ b/tests/uranium_tests/pluginTest/header_rewrite/test_header_rewrite_bad_run_plugin.py @@ -0,0 +1,178 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_header_rewrite_bad_run_plugin(urtest: UraniumTest) -> None: + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + ''' + Verify header_rewrite rejects a run-plugin operator whose target plugin fails to + load. The failure must be caught at config load time, not aborted at request time. + ''' + + urtest.Summary = ''' + header_rewrite must reject a run-plugin whose target plugin fails to load, at + config load time, rather than aborting the server on the first request. + ''' + + # Reproduce the reported crash: run-plugin against a plugin whose instance-init fails + # (conf_remap + a missing file) hands header_rewrite a null instance, which old code aborted on. + urtest.SkipUnless( + Condition.PluginExists('header_rewrite.so'), + Condition.PluginExists('conf_remap.so'), + ) + + class TestBadRunPlugin: + '''Verify failed run-plugin initialization is rejected safely.''' + + ERROR_MARKER: str = 'run-plugin unable to load' + BAD_RULE_LINES: list[str] = [ + 'cond %{REMAP_PSEUDO_HOOK}', + ' run-plugin conf_remap.so no_such_conf_remap_file.yaml', + ] + NESTED_BAD_RULE_LINES: list[str] = [ + 'cond %{REMAP_PSEUDO_HOOK}', + ' if', + ' cond %{TRUE}', + ' run-plugin conf_remap.so no_such_conf_remap_file.yaml', + ' endif', + ] + + def __init__(self) -> None: + '''Configure startup and reload rejection scenarios.''' + self._configure_startup_rejection() + self._server = self._configure_origin_server() + self._ts = self._configure_traffic_server() + self._configure_baseline_request() + self._configure_bad_remap_install() + self._configure_failed_reload() + self._configure_post_reload_request() + self._ts.Disk.diags_log.Content = Testers.IncludesExpression( + self.ERROR_MARKER, 'the rejected reload should log the run-plugin failure') + + def _configure_startup_rejection(self) -> None: + '''Verify a bad top-level run-plugin fails startup cleanly.''' + ts = urtest.MakeATSProcess("ts-startup", disable_log_checks=True) + ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'header_rewrite', + }) + ts.Disk.MakeConfigFile('bad_run_plugin.conf').AddLines(self.BAD_RULE_LINES) + ts.Disk.remap_config.AddLine( + 'map http://startup.example.com/ http://127.0.0.1/ ' + '@plugin=header_rewrite.so @pparam=bad_run_plugin.conf') + + # Invalid remap.config triggers a controlled exit rather than SIGABRT. + ts.ReturnCode = 33 + ts.Ready = 0 + ts.Disk.diags_log.Content = Testers.IncludesExpression( + self.ERROR_MARKER, 'header_rewrite must report the failed run-plugin load') + ts.Disk.traffic_out.Content = Testers.ExcludesExpression( + 'Traffic Server is fully initialized', 'ATS must not initialize with a bad run-plugin config') + + tr = urtest.AddTestRun("Bad run-plugin config fails startup instead of crashing") + tr.Processes.Default.Command = 'echo verifying startup rejection' + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.StartBefore(ts) + + def _configure_origin_server(self) -> 'Process': + '''Configure the origin used to verify reload behavior.''' + server = urtest.MakeOriginServer("server") + request_header = { + "headers": "GET / HTTP/1.1\r\nHost: reload.example.com\r\n\r\n", + "timestamp": "1469733493.993", + "body": "" + } + response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + server.addResponse("sessionfile.log", request_header, response_header) + return server + + def _configure_traffic_server(self) -> 'Process': + '''Configure ATS with a valid initial remap table.''' + ts = urtest.MakeATSProcess("ts-reload", disable_log_checks=True) + ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'header_rewrite', + }) + ts.Disk.MakeConfigFile('nested_bad_run_plugin.conf').AddLines(self.NESTED_BAD_RULE_LINES) + ts.Disk.remap_config.AddLine(f'map http://reload.example.com http://127.0.0.1:{self._server.Variables.Port}') + return ts + + def _configure_curl_run(self, name: str, expectation: str) -> 'TestRun': + '''Configure a request that verifies ATS still serves traffic.''' + tr = urtest.AddTestRun(name) + tr.MakeCurlCommand( + f'--proxy 127.0.0.1:{self._ts.Variables.port} "http://reload.example.com" ' + '-H "Proxy-Connection: keep-alive" --verbose', + ts=self._ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.stderr = Testers.IncludesExpression('200 OK', expectation) + tr.StillRunningAfter = self._ts + tr.StillRunningAfter = self._server + return tr + + def _configure_baseline_request(self) -> None: + '''Verify the valid initial configuration serves requests.''' + tr = self._configure_curl_run("Baseline request is served before reload", 'baseline request should be served') + tr.Processes.Default.StartBefore(self._server) + tr.Processes.Default.StartBefore(self._ts) + + def _configure_bad_remap_install(self) -> None: + '''Replace remap.config with one containing a bad nested run-plugin.''' + tr = urtest.AddTestRun("Install a remap.config with a bad run-plugin") + remap_path = self._ts.Disk.remap_config.AbsPath + tr.Disk.File(remap_path, id="remap_bad", typename="ats:config") + tr.Disk.remap_bad.AddLine( + f'map http://reload.example.com http://127.0.0.1:{self._server.Variables.Port} ' + '@plugin=header_rewrite.so @pparam=nested_bad_run_plugin.conf') + tr.Processes.Default.Command = 'echo installed bad remap.config' + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Env = self._ts.Env + tr.StillRunningAfter = self._ts + tr.StillRunningAfter = self._server + + def _configure_failed_reload(self) -> None: + '''Verify the bad remap table is rejected without stopping ATS.''' + tr = urtest.AddConfigReload( + self._ts, expect="fail", delay_start=2, description="Reload with bad run-plugin must be rejected, not fatal") + tr.StillRunningAfter = self._ts + tr.StillRunningAfter = self._server + + def _configure_post_reload_request(self) -> None: + '''Verify the rejected reload leaves the old configuration active.''' + self._configure_curl_run( + "Server still serves the old config after the rejected reload", + 'old config should still serve after a rejected reload') + + TestBadRunPlugin() + urtest.execute() diff --git a/tests/uranium_tests/pluginTest/header_rewrite/test_header_rewrite_set_body_from.py b/tests/uranium_tests/pluginTest/header_rewrite/test_header_rewrite_set_body_from.py new file mode 100644 index 00000000000..8388fd81918 --- /dev/null +++ b/tests/uranium_tests/pluginTest/header_rewrite/test_header_rewrite_set_body_from.py @@ -0,0 +1,224 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_header_rewrite_set_body_from(urtest: UraniumTest) -> None: + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + urtest.Summary = ''' + Test for successful response manipulation using set-body-from + ''' + urtest.ContinueOnFail = True + + class HeaderRewriteSetBodyFromTest: + + def __init__(self): + self.setUpOriginServer() + self.setUpTS() + + def setUpOriginServer(self): + self.server = urtest.MakeOriginServer("server") + + # Response for original transaction + response_header = {"headers": "HTTP/1.1 404 Not Found\r\nConnection: close\r\n\r\n", "body": "404 Not Found"} + + # Request/response for original transaction where transaction returns a 200 status code + remap_success_request_header = {"headers": "GET /200 HTTP/1.1\r\nHost: www.example.com\r\n\r\n"} + ooo = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "body": "200 OK"} + + self.server.addResponse("sessionfile.log", remap_success_request_header, ooo) + + # Request/response for original transaction with failed second tranasaction + remap_fail_1_request_header = {"headers": "GET /remap_fail HTTP/1.1\r\nHost: www.example.com\r\n\r\n"} + self.server.addResponse("sessionfile.log", remap_fail_1_request_header, response_header) + + plugin_fail_1_request_header = {"headers": "GET /plugin_fail HTTP/1.1\r\nHost: www.example.com\r\n\r\n"} + self.server.addResponse("sessionfile.log", plugin_fail_1_request_header, response_header) + + # Request/response for original successful transaction with successful second tranasaction + remap_success_1_request_header = {"headers": "GET /remap_success HTTP/1.1\r\nHost: www.example.com\r\n\r\n"} + self.server.addResponse("sessionfile.log", remap_success_1_request_header, response_header) + + plugin_success_1_request_header = {"headers": "GET /plugin_success HTTP/1.1\r\nHost: www.example.com\r\n\r\n"} + self.server.addResponse("sessionfile.log", plugin_success_1_request_header, response_header) + + # Request/response for custom body transaction that successfully retrieves body + success_2_request_header = {"headers": "GET /404.html HTTP/1.1\r\nHost: www.example.com\r\n\r\n"} + success_2_response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "body": "Custom body found\n"} + self.server.addResponse("sessionfile.log", success_2_request_header, success_2_response_header) + + # Request/response for original transaction that triggers binary set-body-from + remap_binary_request_header = {"headers": "GET /remap_binary HTTP/1.1\r\nHost: www.example.com\r\n\r\n"} + self.server.addResponse("sessionfile.log", remap_binary_request_header, response_header) + + # Response for the set-body-from fetch: body has an internal NUL. The strdup + # bug truncated everything after the NUL and emitted heap garbage instead. + binary_body_request_header = {"headers": "GET /binary_body HTTP/1.1\r\nHost: www.example.com\r\n\r\n"} + binary_body_response_header = { + "headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", + "body": "BeforeNUL\x00AfterNUL_AAAAAAAAAAAAAAAAAAAAAAAA" + } + self.server.addResponse("sessionfile.log", binary_body_request_header, binary_body_response_header) + + def setUpTS(self): + self.ts = urtest.MakeATSProcess("ts") + + # Set header rewrite rules + self.ts.Setup.CopyAs('rules/rule_set_body_from_remap.conf', urtest.RunDirectory) + self.ts.Setup.CopyAs('rules/rule_set_body_from_plugin.conf', urtest.RunDirectory) + + self.ts.Disk.remap_config.AddLine( + """\ + map http://www.example.com/remap_success http://127.0.0.1:{0}/remap_success @plugin=header_rewrite.so @pparam={1}/rule_set_body_from_remap.conf + map http://www.example.com/200 http://127.0.0.1:{0}/200 @plugin=header_rewrite.so @pparam={1}/rule_set_body_from_remap.conf + map http://www.example.com/remap_fail http://127.0.0.1:{0}/remap_fail @plugin=header_rewrite.so @pparam={1}/rule_set_body_from_remap.conf + map http://www.example.com/remap_binary http://127.0.0.1:{0}/remap_binary @plugin=header_rewrite.so @pparam={1}/rule_set_body_from_remap.conf + map http://www.example.com/binary_body http://127.0.0.1:{0}/binary_body + map http://www.example.com/plugin_success http://127.0.0.1:{0}/plugin_success + map http://www.example.com/plugin_fail http://127.0.0.1:{0}/plugin_fail + map http://www.example.com/404.html http://127.0.0.1:{0}/404.html + map http://www.example.com/plugin_no_server http://127.0.0.1::{2}/plugin_no_server + """.format(self.server.Variables.Port, urtest.RunDirectory, urtest.GetTcpPort("bad_port"))) + self.ts.Disk.plugin_config.AddLine('header_rewrite.so {0}/rule_set_body_from_plugin.conf'.format(urtest.RunDirectory)) + + def test_setBodyFromFails_remap(self): + ''' + Test where set-body-from request fails + Triggered from remap file + This uses the case where no remap rule is provided + ''' + tr = urtest.AddTestRun() + tr.MakeCurlCommand( + '-s -v --proxy 127.0.0.1:{0} "http://www.example.com/remap_fail"'.format(self.ts.Variables.port), ts=self.ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.StartBefore(self.server) + tr.Processes.Default.StartBefore(self.ts) + tr.Processes.Default.Streams.stdout = "gold/header_rewrite-set_body_from_remap_fail.gold" + tr.Processes.Default.Streams.stderr.Content = Testers.ContainsExpression("404 Not Found", "Expected 404 response") + tr.StillRunningAfter = self.server + + def test_setBodyFromSucceeds_remap(self): + ''' + Test where set-body-from request succeeds + Triggered from remap file + ''' + tr = urtest.AddTestRun() + tr.MakeCurlCommand( + '-s -v --proxy 127.0.0.1:{0} "http://www.example.com/remap_success"'.format(self.ts.Variables.port), ts=self.ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.stdout = "gold/header_rewrite-set_body_from_success.gold" + tr.Processes.Default.Streams.stderr.Content = Testers.ContainsExpression("404 Not Found", "Expected 404 response") + tr.StillRunningAfter = self.server + + def test_setBodyFromSucceeds_plugin(self): + ''' + Test where set-body-from request succeeds + Triggered from plugin file + ''' + tr = urtest.AddTestRun() + tr.MakeCurlCommand( + '-s -v --proxy 127.0.0.1:{0} "http://www.example.com/plugin_success"'.format(self.ts.Variables.port), ts=self.ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.stdout = "gold/header_rewrite-set_body_from_success.gold" + tr.Processes.Default.Streams.stderr.Content = Testers.ContainsExpression("404 Not Found", "Expected 404 response") + tr.StillRunningAfter = self.server + + def test_setBodyFromFails_plugin(self): + ''' + Test where set-body-from request fails + This uses the case where the second endpoint cannot connect to the requested server + Triggered from plugin file + ''' + tr = urtest.AddTestRun() + tr.MakeCurlCommand( + '-s -v --proxy 127.0.0.1:{0} "http://www.example.com/plugin_fail"'.format(self.ts.Variables.port), ts=self.ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.stdout = "gold/header_rewrite-set_body_from_conn_fail.gold" + tr.Processes.Default.Streams.stderr.Content = Testers.ContainsExpression("404 Not Found", "Expected 404 response") + tr.StillRunningAfter = self.server + + def test_setBodyFromSucceeds_200(self): + ''' + Test where set-body-from request succeeds and returns 200 OK + Triggered from remap file + This is tested because right now, TSHttpTxnErrorBodySet will change OK status codes to 500 INKApi Error + Ideally, this would not occur. + ''' + tr = urtest.AddTestRun() + tr.MakeCurlCommand( + '-s -v --proxy 127.0.0.1:{0} "http://www.example.com/200"'.format(self.ts.Variables.port), ts=self.ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.stdout = "gold/header_rewrite-set_body_from_200.gold" + tr.Processes.Default.Streams.stderr.Content = Testers.ContainsExpression("500 INKApi Error", "Expected 500 response") + tr.StillRunningAfter = self.server + + def test_setBodyFromBinary(self): + ''' + set-body-from must preserve binary bodies that contain internal NUL + bytes. The previous TSstrdup path truncated at the first NUL and + emitted heap bytes for the remainder; check the SHA256 of the body + the client actually receives. + ''' + body_path = f"{urtest.RunDirectory}/binary_body.out" + # SHA256 of b"BeforeNUL\x00AfterNUL_AAAAAAAAAAAAAAAAAAAAAAAA" (43 bytes) + expected_sha = "a529bbd61061b739b611ca67a7b76fc433b3d3a9cc3bcc2e385b812f00b0fe63" + + tr = urtest.AddTestRun() + tr.MakeCurlCommand( + f'-s --proxy 127.0.0.1:{self.ts.Variables.port} -o {body_path} "http://www.example.com/remap_binary"', ts=self.ts) + tr.Processes.Default.ReturnCode = 0 + tr.StillRunningAfter = self.server + + # Compute the hash via Python instead of sha256sum/shasum so the + # check does not depend on which CLI hash tool the host happens + # to ship. + tr = urtest.AddTestRun() + tr.Processes.Default.Command = ( + f'python3 -c \'import hashlib; ' + f'print(hashlib.sha256(open("{body_path}", "rb").read()).hexdigest())\'') + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.stdout.Content = Testers.ContainsExpression( + expected_sha, "Client must receive the exact binary body bytes") + + def runTraffic(self): + self.test_setBodyFromFails_remap() + self.test_setBodyFromSucceeds_remap() + self.test_setBodyFromSucceeds_plugin() + self.test_setBodyFromFails_plugin() + self.test_setBodyFromSucceeds_200() + self.test_setBodyFromBinary() + + def run(self): + self.runTraffic() + + HeaderRewriteSetBodyFromTest().run() + urtest.execute() diff --git a/tests/uranium_tests/pluginTest/header_rewrite/test_header_rewrite_url_glob.py b/tests/uranium_tests/pluginTest/header_rewrite/test_header_rewrite_url_glob.py new file mode 100644 index 00000000000..1bc6b4e22ad --- /dev/null +++ b/tests/uranium_tests/pluginTest/header_rewrite/test_header_rewrite_url_glob.py @@ -0,0 +1,85 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_header_rewrite_url_glob(urtest: UraniumTest) -> None: + ''' + Test global header_rewrite with set-redirect operator. + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + urtest.Summary = ''' + Test global header_rewrite with set-redirect operator. + ''' + + urtest.ContinueOnFail = True + ts = urtest.MakeATSProcess("ts", block_for_debug=False) + server = urtest.MakeOriginServer("server") + + # Configure the server to return 200 responses. The rewrite rules below set a + # non-200 status, so if curl gets a 200 response something went wrong. + urtest.testName = "" + request_header = {"headers": "GET / HTTP/1.1\r\nHost: no_path.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + server.addResponse("sessionfile.log", request_header, response_header) + + ts.Disk.records_config.update( + { + 'proxy.config.url_remap.remap_required': 0, + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.show_location': 0, + 'proxy.config.diags.debug.tags': 'header', + }) + ts.Setup.CopyAs('rules/glob_set_redirect.conf', urtest.RunDirectory) + + ts.Disk.plugin_config.AddLine(f'header_rewrite.so {urtest.RunDirectory}/glob_set_redirect.conf') + + # Run operator on Read Response Hdr hook (ID:REQUEST == 0). + tr = urtest.AddTestRun() + tr.Processes.Default.StartBefore(ts) + tr.Processes.Default.StartBefore(server) + tr.MakeCurlCommand(f'--head 127.0.0.1:{ts.Variables.port} -H "Host: 127.0.0.1:{server.Variables.Port}" --verbose', ts=ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.stderr = "gold/set-redirect-glob.gold" + tr.StillRunningAfter = server + tr.StillRunningAfter = ts + ts.Disk.traffic_out.Content = "gold/header_rewrite-tag.gold" + + # Run operator on Send Response Hdr hook (ID:REQUEST == 1). + tr = urtest.AddTestRun() + tr.MakeCurlCommand(f'--head 127.0.0.1:{ts.Variables.port} -H "Host: 127.0.0.1:{server.Variables.Port}" --verbose', ts=ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.stderr = "gold/set-redirect-glob.gold" + tr.StillRunningAfter = server + tr.StillRunningAfter = ts + ts.Disk.traffic_out.Content = "gold/header_rewrite-tag.gold" + urtest.execute() diff --git a/tests/gold_tests/pluginTest/healthchecks/acme b/tests/uranium_tests/pluginTest/healthchecks/acme similarity index 100% rename from tests/gold_tests/pluginTest/healthchecks/acme rename to tests/uranium_tests/pluginTest/healthchecks/acme diff --git a/tests/gold_tests/pluginTest/healthchecks/acme-ssl b/tests/uranium_tests/pluginTest/healthchecks/acme-ssl similarity index 100% rename from tests/gold_tests/pluginTest/healthchecks/acme-ssl rename to tests/uranium_tests/pluginTest/healthchecks/acme-ssl diff --git a/tests/uranium_tests/pluginTest/healthchecks/test_healthchecks.py b/tests/uranium_tests/pluginTest/healthchecks/test_healthchecks.py new file mode 100644 index 00000000000..71cfb36ff72 --- /dev/null +++ b/tests/uranium_tests/pluginTest/healthchecks/test_healthchecks.py @@ -0,0 +1,235 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_healthchecks(urtest: UraniumTest) -> None: + """ + Verify healthchecks plugin behavior. + """ + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import os + + urtest.Summary = '''Verify healthchecks plugin behavior.''' + + urtest.SkipUnless(Condition.PluginExists('healthchecks.so')) + + CONFIG = f''' + /acme {urtest.RunDirectory}/acme text/plain 200 404 + /acme-ssl {urtest.RunDirectory}/acme-ssl text/plain 200 404 + ''' + + CONTENT = 'Some generic content.' + + class TestFileChangeBehavior: + '''Verify healthchecks plugin file change detection behavior.''' + + def __init__(self) -> None: + '''Initialize the TestRun.''' + self._ts_started = False + self._positive_hc_counter = 0 + self._configure_global_ts() + self._expect_positive_healthchecks() + self._remove_acme_ssl() + self._expect_acme_ssl_404() + self._re_add_acme_ssl() + self._expect_positive_healthchecks() + self._expect_full_buffer_acme_body() + self._rewrite_acme_while_serving() + self._expect_rewritten_acme_body() + + def _configure_global_ts(self) -> None: + '''Configure a global Traffic Server instance for the test runs. + :param tr: The TestRun to associate the ATS instance with. + :return: The Traffic Server Process. + ''' + ts = urtest.MakeATSProcess('ts', enable_tls=True) + self._ts = ts + + # healthchecks plugin configuration. + config_name = "healthchecks.config" + config_path = os.path.join(ts.Variables.CONFIGDIR, config_name) + ts.Disk.File(config_path, id='healthchecks_config', typename="ats:config") + config = ts.Disk.healthchecks_config + config.AddLine(f'/acme {urtest.RunDirectory}/acme text/plain 200 404') + config.AddLine(f'/acme-ssl {urtest.RunDirectory}/acme-ssl text/plain 200 404') + ts.Setup.Copy('acme') + ts.Setup.Copy('acme-ssl') + ts.Disk.plugin_config.AddLine(f'healthchecks.so {ts.Variables.CONFIGDIR}/healthchecks.config') + + # TLS configuration. + ts.addDefaultSSLFiles() + ts.Disk.records_config.update( + { + "proxy.config.ssl.server.cert.path": f'{ts.Variables.SSLDir}', + "proxy.config.ssl.server.private_key.path": f'{ts.Variables.SSLDir}', + "proxy.config.ssl.client.verify.server.policy": 'PERMISSIVE', + }) + ts.Disk.ssl_multicert_yaml.AddLines( + """ + ssl_multicert: + - dest_ip: "*" + ssl_cert_name: server.pem + ssl_key_name: server.key + """.split("\n")) + + # Other configuration. + ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'healthchecks', + }) + return ts + + def _expect_positive_healthchecks(self) -> None: + '''Configure a positive healthcheck for the test runs. + :return: None + ''' + self._positive_hc_counter += 1 + counter = self._positive_hc_counter + tr = urtest.AddTestRun(f'Positive acme healthchecks: {counter}') + tr.MakeCurlCommand(f'-v http://127.0.0.1:{self._ts.Variables.port}/acme', ts=self._ts) + curl_acme = tr.Processes.Default + if not self._ts_started: + curl_acme.StartBefore(self._ts) + self._ts_started = True + curl_acme.Streams.All += Testers.ContainsExpression('HTTP/1.1 200', 'Verify 200 response for /acme') + + if not Condition.CurlUsingUnixDomainSocket(): + # Repeat for acme-ssl + tr2 = urtest.AddTestRun(f'Positive acme-ssl healthchecks: {counter}') + tr2.MakeCurlCommand(f'-kv https://127.0.0.1:{self._ts.Variables.ssl_port}/acme-ssl', ts=self._ts) + curl_acme_ssl = tr2.Processes.Default + if not self._ts_started: + curl_acme_ssl.StartBefore(self._ts) + self._ts_started = True + curl_acme_ssl.Streams.All += Testers.ContainsExpression('HTTP/2 200', 'Verify 200 response for /acme-ssl') + + def _remove_acme_ssl(self) -> None: + '''Remove the acme-ssl file to trigger a file change detection. + :return: None + ''' + if not Condition.CurlUsingUnixDomainSocket(): + tr = urtest.AddTestRun('Remove acme-ssl file') + p = tr.Processes.Default + p.Command = f'rm {urtest.RunDirectory}/acme-ssl && sleep 1' + tr.Processes.Default.ReturnCode = 0 + + def _expect_acme_ssl_404(self) -> None: + '''Expect a 404 response for the removed acme-ssl file. + :return: None + ''' + tr = urtest.AddTestRun('Expect 200 for acme after acme-ssl removal') + tr.MakeCurlCommand(f'-v http://127.0.0.1:{self._ts.Variables.port}/acme', ts=self._ts) + curl_acme = tr.Processes.Default + if not self._ts_started: + curl_acme.StartBefore(self._ts) + self._ts_started = True + curl_acme.Streams.All += Testers.ContainsExpression( + 'HTTP/1.1 200', 'Verify 200 response for /acme after acme-ssl removal') + + if not Condition.CurlUsingUnixDomainSocket(): + tr2 = urtest.AddTestRun('Expect 404 for acme-ssl after removal') + tr2.MakeCurlCommand(f'-kv https://127.0.0.1:{self._ts.Variables.ssl_port}/acme-ssl', ts=self._ts) + curl_acme_ssl = tr2.Processes.Default + if not self._ts_started: + curl_acme_ssl.StartBefore(self._ts) + self._ts_started = True + curl_acme_ssl.Streams.All += Testers.ContainsExpression( + 'HTTP/2 404', 'Verify 404 response for /acme-ssl after removal') + + def _re_add_acme_ssl(self) -> None: + '''Re-add the acme-ssl file to restore the healthcheck. + :return: None + ''' + if not Condition.CurlUsingUnixDomainSocket(): + tr = urtest.AddTestRun('Re-add acme-ssl file') + tr.Setup.Copy('acme-ssl', urtest.RunDirectory) + p = tr.Processes.Default + p.Command = 'sleep 1' + p.ReturnCode = 0 + + def _rewrite_acme_while_serving(self) -> None: + '''Rewrite the acme file repeatedly while healthcheck requests are in flight. + + The plugin replaces the health check file data underneath transactions which may still be + reading the previous data. This drives that replacement so that an ASan enabled build + catches the old data being released while it is still referenced. + :return: None + ''' + tr = urtest.AddTestRun('Rewrite acme while healthchecks are being served') + acme_file = os.path.join(urtest.RunDirectory, 'acme') + url = f'http://127.0.0.1:{self._ts.Variables.port}/acme' + + # Note that autest runs the command through string.Template, so shell variables cannot be + # used here. The loop is therefore unrolled. + commands = [] + for iteration in range(10): + commands.append(f'echo "{CONTENT} {iteration}" > {acme_file};') + commands.append('{curl} -s -o /dev/null ' + url + ' &') + commands.append('{curl} -s -o /dev/null ' + url + ' &') + commands.append('wait') + + tr.MakeCurlCommandMulti(' '.join(commands), ts=self._ts) + tr.Processes.Default.ReturnCode = 0 + + def _expect_full_buffer_acme_body(self) -> None: + '''Verify that a MAX_BODY_LEN-sized file is not reported as empty. + :return: None + ''' + tr = urtest.AddTestRun('Expect a full-sized healthcheck response body') + acme_file = os.path.join(urtest.RunDirectory, 'acme') + url = f'http://127.0.0.1:{self._ts.Variables.port}/acme' + command = ( + f'dd if=/dev/zero of={acme_file} bs=16384 count=1 2>/dev/null && sleep 1 && ' + '{curl} -s ' + url + ' | wc -c') + tr.MakeCurlCommandMulti(command, ts=self._ts) + p = tr.Processes.Default + p.ReturnCode = 0 + p.Streams.All += Testers.ContainsExpression('16384', 'Verify the response contains 16 KiB') + + def _expect_rewritten_acme_body(self) -> None: + '''Verify that the most recently written acme content is what gets served. + :return: None + ''' + tr = urtest.AddTestRun('Expect the last written acme content in the response body') + acme_file = os.path.join(urtest.RunDirectory, 'acme') + url = f'http://127.0.0.1:{self._ts.Variables.port}/acme' + command = f'echo "{CONTENT} final" > {acme_file} && sleep 1 && ' + '{curl} -v ' + url + tr.MakeCurlCommandMulti(command, ts=self._ts) + p = tr.Processes.Default + p.ReturnCode = 0 + p.Streams.All += Testers.ContainsExpression('HTTP/1.1 200', 'Verify 200 response for /acme') + p.Streams.All += Testers.ContainsExpression(f'{CONTENT} final', 'Verify the reloaded acme content is served') + + # Instantiate the test + TestFileChangeBehavior() + urtest.execute() diff --git a/tests/gold_tests/pluginTest/ja3_fingerprint/ja3_fingerprint_global.replay.yaml b/tests/uranium_tests/pluginTest/ja3_fingerprint/ja3_fingerprint_global.replay.yaml similarity index 100% rename from tests/gold_tests/pluginTest/ja3_fingerprint/ja3_fingerprint_global.replay.yaml rename to tests/uranium_tests/pluginTest/ja3_fingerprint/ja3_fingerprint_global.replay.yaml diff --git a/tests/gold_tests/pluginTest/ja3_fingerprint/ja3_fingerprint_remap.replay.yaml b/tests/uranium_tests/pluginTest/ja3_fingerprint/ja3_fingerprint_remap.replay.yaml similarity index 100% rename from tests/gold_tests/pluginTest/ja3_fingerprint/ja3_fingerprint_remap.replay.yaml rename to tests/uranium_tests/pluginTest/ja3_fingerprint/ja3_fingerprint_remap.replay.yaml diff --git a/tests/gold_tests/pluginTest/ja3_fingerprint/modify-incoming-client.gold b/tests/uranium_tests/pluginTest/ja3_fingerprint/modify-incoming-client.gold similarity index 100% rename from tests/gold_tests/pluginTest/ja3_fingerprint/modify-incoming-client.gold rename to tests/uranium_tests/pluginTest/ja3_fingerprint/modify-incoming-client.gold diff --git a/tests/gold_tests/pluginTest/ja3_fingerprint/modify-incoming-proxy.gold b/tests/uranium_tests/pluginTest/ja3_fingerprint/modify-incoming-proxy.gold similarity index 100% rename from tests/gold_tests/pluginTest/ja3_fingerprint/modify-incoming-proxy.gold rename to tests/uranium_tests/pluginTest/ja3_fingerprint/modify-incoming-proxy.gold diff --git a/tests/gold_tests/pluginTest/ja3_fingerprint/modify-sent-client.gold b/tests/uranium_tests/pluginTest/ja3_fingerprint/modify-sent-client.gold similarity index 100% rename from tests/gold_tests/pluginTest/ja3_fingerprint/modify-sent-client.gold rename to tests/uranium_tests/pluginTest/ja3_fingerprint/modify-sent-client.gold diff --git a/tests/gold_tests/pluginTest/ja3_fingerprint/modify-sent-proxy-global.gold b/tests/uranium_tests/pluginTest/ja3_fingerprint/modify-sent-proxy-global.gold similarity index 100% rename from tests/gold_tests/pluginTest/ja3_fingerprint/modify-sent-proxy-global.gold rename to tests/uranium_tests/pluginTest/ja3_fingerprint/modify-sent-proxy-global.gold diff --git a/tests/gold_tests/pluginTest/ja3_fingerprint/modify-sent-proxy-remap.gold b/tests/uranium_tests/pluginTest/ja3_fingerprint/modify-sent-proxy-remap.gold similarity index 100% rename from tests/gold_tests/pluginTest/ja3_fingerprint/modify-sent-proxy-remap.gold rename to tests/uranium_tests/pluginTest/ja3_fingerprint/modify-sent-proxy-remap.gold diff --git a/tests/uranium_tests/pluginTest/ja3_fingerprint/test_ja3_fingerprint.py b/tests/uranium_tests/pluginTest/ja3_fingerprint/test_ja3_fingerprint.py new file mode 100644 index 00000000000..91292c99fa2 --- /dev/null +++ b/tests/uranium_tests/pluginTest/ja3_fingerprint/test_ja3_fingerprint.py @@ -0,0 +1,228 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_ja3_fingerprint(urtest: UraniumTest) -> None: + ''' + Verify the behavior of the JA3 fingerprint plugin. + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import os + import re + + urtest.Summary = __doc__ + + # Skip if plugin not present. Plugin gets built only if openssl is used(not BoringSSL). + urtest.SkipUnless(Condition.PluginExists('ja3_fingerprint.so')) + + class JA3FingerprintTest: + """Verify the behavior of the JA3 fingerprint plugin.""" + + _dns_counter: int = 0 + _server_counter: int = 0 + _ts_counter: int = 0 + _client_counter: int = 0 + + def __init__(self, test_remap: bool, modify_incoming: bool) -> None: + """Configure the test processes in preparation for the TestRun. + + :param test_remap: Whether to configure the plugin as a remap plugin + instead of as a global plugin. + :param modify_incoming: Whether ja3_fingerprint should be configured to + modify the client request rather than the proxy request. + """ + if test_remap and modify_incoming: + raise ValueError('modify-incoming is only allowed as a global plugin.') + + self._test_remap = test_remap + if test_remap: + self._replay_file = 'ja3_fingerprint_remap.replay.yaml' + else: + self._replay_file = 'ja3_fingerprint_global.replay.yaml' + + self._modify_incoming = modify_incoming + + tr = urtest.AddTestRun(f'test_remap: {test_remap}, modify_incoming: {modify_incoming}') + self._configure_dns(tr) + self._configure_server(tr) + self._configure_trafficserver() + self._configure_client(tr) + self._await_ja3log() + self._verify_internal_headers() + + def _configure_dns(self, tr: 'TestRun') -> None: + """Configure a nameserver for the test. + + :param tr: The TestRun to add the nameserver to. + """ + name = f'dns{self._dns_counter}' + self._dns = tr.MakeDNServer(name, default='127.0.0.1') + JA3FingerprintTest._dns_counter += 1 + + def _configure_server(self, tr: 'TestRun') -> None: + """Configure the server to be used in the test. + + :param tr: The TestRun to add the server to. + """ + name = f'server{self._server_counter}' + self._server = tr.AddVerifierServerProcess(name, self._replay_file) + JA3FingerprintTest._server_counter += 1 + self._server.Streams.All += Testers.ContainsExpression("https-request", "Verify the HTTPS request was received.") + self._server.Streams.All += Testers.ContainsExpression("http2-request", "Verify the HTTP/2 request was received.") + if not self._test_remap: + # The first request has no existing JA3 headers, so headers are added. + self._server.Streams.All += Testers.ContainsExpression( + "x-ja3-raw: .*,", "Verify the new raw header was added.", reflags=re.IGNORECASE) + self._server.Streams.All += Testers.ContainsExpression( + "x-ja3-via: test.proxy.com", "The x-ja3-via string was added.") + # The second request has existing JA3 headers. With --preserve, + # no new JA3 headers are added (including x-ja3-sig). The replay + # file verifies x-ja3-sig is absent for the http2 transaction. + self._server.Streams.All += Testers.ContainsExpression( + "x-ja3-raw: first-signature", "Verify the already-existing raw header was preserved.", reflags=re.IGNORECASE) + + def _configure_trafficserver(self) -> None: + """Configure Traffic Server to be used in the test.""" + # Associate ATS with the Test so that metrics can be verified. + name = f'ts{self._ts_counter}' + self._ts = urtest.MakeATSProcess(name, enable_cache=False, enable_tls=True) + JA3FingerprintTest._ts_counter += 1 + self._ts.addDefaultSSLFiles() + self._ts.Disk.ssl_multicert_yaml.AddLines( + """ + ssl_multicert: + - dest_ip: "*" + ssl_cert_name: server.pem + ssl_key_name: server.key + """.split("\n")) + server_port = self._server.Variables.https_port + self._ts.Disk.remap_config.AddLine(f'map https://https.server.com https://https.backend.com:{server_port}') + + if self._test_remap: + self._ts.Disk.remap_config.AddLine( + f'map https://http2.server.com https://http2.backend.com:{server_port} ' + '@plugin=ja3_fingerprint.so @pparam=--ja3log') + else: + arguments = '--ja3log --ja3raw --preserve' + if self._modify_incoming: + arguments += ' --modify-incoming' + self._ts.Disk.plugin_config.AddLine(f'ja3_fingerprint.so {arguments}') + self._ts.Disk.remap_config.AddLine(f'map https://http2.server.com https://http2.backend.com:{server_port}') + + self._ts.Disk.records_config.update( + { + 'proxy.config.ssl.server.cert.path': self._ts.Variables.SSLDir, + 'proxy.config.ssl.server.private_key.path': self._ts.Variables.SSLDir, + 'proxy.config.ssl.client.verify.server.policy': 'PERMISSIVE', + 'proxy.config.dns.nameservers': f"127.0.0.1:{self._dns.Variables.Port}", + 'proxy.config.dns.resolv_conf': 'NULL', + 'proxy.config.proxy_name': 'test.proxy.com', + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'http|ja3_fingerprint', + }) + + ja3log_path = os.path.join(self._ts.Variables.LOGDIR, "ja3_fingerprint.log") + self._ts.Disk.File(ja3log_path, id='ja3_log') + + if self._test_remap: + # Only the http2 request should be logged because only that request + # had the plugin configured for that remap rule. + regex = r'(.*JA3.*MD5){1}' + else: + regex = r'(.*JA3.*MD5){2}' + + self._ts.Disk.ja3_log.Content += Testers.ContainsExpression( + regex, "Verify the JA3 log contains a JA3 line.", reflags=re.MULTILINE | re.DOTALL) + + def _configure_client(self, tr: 'TestRun') -> None: + """Configure the TestRun. + + :param tr: The TestRun to add the client to. + """ + name = f'client{self._client_counter}' + p = tr.AddVerifierClientProcess( + name, self._replay_file, http_ports=[self._ts.Variables.port], https_ports=[self._ts.Variables.ssl_port]) + JA3FingerprintTest._client_counter += 1 + + p.StartBefore(self._dns) + p.StartBefore(self._server) + p.StartBefore(self._ts) + tr.StillRunningAfter = self._ts + + p.Streams.All += Testers.ContainsExpression("https-response", "Verify the HTTPS response was received.") + p.Streams.All += Testers.ContainsExpression("http2-response", "Verify the HTTP/2 response was received.") + + def _await_ja3log(self) -> None: + """Await the creation of the JA3 log.""" + tr = urtest.AddTestRun('Await the contents of the JA3 log.') + + waiter = tr.Processes.Process('waiter', 'sleep 30') + ja3_path = self._ts.Disk.ja3_log.AbsPath + waiter.Ready = When.FileContains(ja3_path, "JA3") + + p = tr.Processes.Default + p.Command = f'echo await {ja3_path} creation' + p.StartBefore(waiter) + + def _verify_internal_headers(self) -> None: + """Verify that the correct headers were modified.""" + + # We use the grep command to get the small snippet of output we want + # from the traffic.out file. Gold file matching against long files, like + # traffic.out, is exceedingly slow. + + tr = urtest.AddTestRun('Verify the internal client request headers.') + traffic_out = self._ts.Disk.traffic_out.AbsPath + p = tr.Processes.Default + p.Command = f'grep --after-context=20 "Incoming Request" {traffic_out}' + + if self._modify_incoming: + p.Streams.All += "modify-incoming-client.gold" + else: + p.Streams.All += "modify-sent-client.gold" + + tr = urtest.AddTestRun('Verify the internal proxy request headers.') + p = tr.Processes.Default + p.Command = f'grep --after-context=20 "Proxy\'s Request after hooks" {traffic_out}' + + if self._modify_incoming: + p.Streams.All += "modify-incoming-proxy.gold" + elif self._test_remap: + p.Streams.All += "modify-sent-proxy-remap.gold" + else: + p.Streams.All += "modify-sent-proxy-global.gold" + + JA3FingerprintTest(test_remap=False, modify_incoming=False) + JA3FingerprintTest(test_remap=True, modify_incoming=False) + JA3FingerprintTest(test_remap=False, modify_incoming=True) + urtest.execute() diff --git a/tests/gold_tests/pluginTest/ja4_fingerprint/ja4_fingerprint.replay.yaml b/tests/uranium_tests/pluginTest/ja4_fingerprint/ja4_fingerprint.replay.yaml similarity index 100% rename from tests/gold_tests/pluginTest/ja4_fingerprint/ja4_fingerprint.replay.yaml rename to tests/uranium_tests/pluginTest/ja4_fingerprint/ja4_fingerprint.replay.yaml diff --git a/tests/gold_tests/pluginTest/ja4_fingerprint/ja4_fingerprint_basic_server.replay.yaml b/tests/uranium_tests/pluginTest/ja4_fingerprint/ja4_fingerprint_basic_server.replay.yaml similarity index 100% rename from tests/gold_tests/pluginTest/ja4_fingerprint/ja4_fingerprint_basic_server.replay.yaml rename to tests/uranium_tests/pluginTest/ja4_fingerprint/ja4_fingerprint_basic_server.replay.yaml diff --git a/tests/uranium_tests/pluginTest/ja4_fingerprint/test_ja4_fingerprint.py b/tests/uranium_tests/pluginTest/ja4_fingerprint/test_ja4_fingerprint.py new file mode 100644 index 00000000000..1fe430b7c20 --- /dev/null +++ b/tests/uranium_tests/pluginTest/ja4_fingerprint/test_ja4_fingerprint.py @@ -0,0 +1,226 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_ja4_fingerprint(urtest: UraniumTest) -> None: + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # 'License'); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import functools + import os + import re + from typing import Any, Callable, Dict, Optional + + from ports import get_port + + urtest.Summary = 'Tests the ja4_fingerprint plugin.' + # The plugin is experimental and therefore may not always be built. It also + # doesn't support QUIC yet. + urtest.SkipIf(Condition.CurlUsingUnixDomainSocket()) + urtest.SkipUnless(Condition.PluginExists('ja4_fingerprint.so')) + + TestParams = Dict[str, Any] + + class TestJA4Fingerprint: + '''Configure a test for ja4_fingerprint.''' + + replay_filepath: str = 'ja4_fingerprint.replay.yaml' + basic_server_replay_filepath: str = 'ja4_fingerprint_basic_server.replay.yaml' + client_counter: int = 0 + server_counter: int = 0 + ts_counter: int = 0 + + def __init__(self, name: str, /, autorun: bool, use_preserve: bool = False) -> None: + '''Initialize the test. + + :param name: The name of the test. + :param use_preserve: Whether to use the --preserve flag. + ''' + self.name = name + self.autorun = autorun + self.use_preserve = use_preserve + + def _init_run(self) -> 'TestRun': + '''Initialize processes for the test run.''' + + replay_filepath = self.replay_filepath if self.use_preserve else self.basic_server_replay_filepath + server_one = TestJA4Fingerprint.configure_server('yay.com', replay_filepath=replay_filepath) + self._configure_traffic_server(server_one) + + tr = urtest.AddTestRun(self.name) + tr.Processes.Default.StartBefore(server_one) + tr.Processes.Default.StartBefore(self._ts) + + waiting_tr = self._configure_wait_for_log(server_one) + + return { + 'tr': tr, + 'waiting_tr': waiting_tr, + 'ts': self._ts, + 'server_one': server_one, + 'port_one': self._port_one, + } + + @classmethod + def runner(cls, name: str, autorun: bool = True, **kwargs) -> Optional[Callable]: + '''Create a runner for a test case. + + :param autorun: Run the test case once it's set up. Default is True. + :return: Returns a runner that can be used as a decorator. + ''' + test = cls(name, autorun=autorun, **kwargs)._prepare_test_case + return test + + def _prepare_test_case(self, func: Callable) -> Callable: + '''Set up a test case and possibly run it. + + :param func: The test case to set up. + :return: Returns a wrapped function that will have its test params + passed to it on invocation. + ''' + functools.wraps(func) + test_params = self._init_run() + + def wrapper(*args, **kwargs) -> Any: + return func(test_params, *args, **kwargs) + + if self.autorun: + wrapper() + return wrapper + + @staticmethod + def configure_server(domain: str, replay_filepath: str): + server = urtest.MakeVerifierServerProcess( + f'server{TestJA4Fingerprint.server_counter + 1}.{domain}', replay_filepath, other_args="--format '{url}'") + TestJA4Fingerprint.server_counter += 1 + + return server + + def _configure_traffic_server(self, server_one: 'Process'): + '''Configure Traffic Server. + + :param server_one: The origin server process. + ''' + ts = urtest.MakeATSProcess(f'ts-{TestJA4Fingerprint.ts_counter + 1}', enable_tls=True) + TestJA4Fingerprint.ts_counter += 1 + + ts.addDefaultSSLFiles() + self._port_one = get_port(ts, 'PortOne') + ts.Disk.records_config.update( + { + 'proxy.config.ssl.server.cert.path': f'{ts.Variables.SSLDir}', + 'proxy.config.ssl.server.private_key.path': f'{ts.Variables.SSLDir}', + 'proxy.config.http.server_ports': f'{self._port_one}:ssl', + 'proxy.config.proxy_name': 'test.proxy.com', + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'ja4_fingerprint|http', + }) + + ts.Disk.remap_config.AddLine(f'map / http://localhost:{server_one.Variables.http_port}') + + ts.Disk.ssl_multicert_yaml.AddLines( + """ + ssl_multicert: + - dest_ip: "*" + ssl_cert_name: server.pem + ssl_key_name: server.key + """.split("\n")) + + plugin_args = 'ja4_fingerprint.so' + if self.use_preserve: + plugin_args += ' --preserve' + ts.Disk.plugin_config.AddLine(plugin_args) + + log_path = os.path.join(ts.Variables.LOGDIR, "ja4_fingerprint.log") + ts.Disk.File(log_path, id='log_file') + + self._ts = ts + + def _configure_wait_for_log(self, server_one: 'Process'): + waiting_tr = urtest.AddAwaitFileContainsTestRun(self.name, self._ts.Disk.log_file.Name, 'JA4') + return waiting_tr + + # Tests start. + + + @TestJA4Fingerprint.runner('When we send a request, ' \ + 'then a JA4 header should be attached.') + def test1(params: TestParams) -> None: + client = params['tr'].Processes.Default + params['tr'].MakeCurlCommand( + '-k -v -H "uuid: no-existing-headers" "https://localhost:{0}/resource"'.format(params['port_one']), ts=params['ts']) + + client.ReturnCode = 0 + client.Streams.stdout += Testers.ContainsExpression(r'Yay!', 'We should receive the expected body.') + params['ts'].Disk.traffic_out.Content += Testers.ContainsExpression( + r'JA4 fingerprint:', 'We should receive the expected log message.') + + @TestJA4Fingerprint.runner('With --preserve, existing JA4 headers should be preserved.', use_preserve=True) + def test_preserve(params: TestParams) -> None: + '''Test that --preserve skips adding headers when JA4 headers exist.''' + tr = params['tr'] + client = tr.Processes.Default + server = params['server_one'] + + # Request 1: No existing JA4 headers - should add them. + tr.MakeCurlCommand( + '-k -v -H "uuid: no-existing-headers" "https://localhost:{0}/resource"'.format(params['port_one']), ts=params['ts']) + client.ReturnCode = 0 + client.Streams.stdout += Testers.ContainsExpression(r'Yay!', 'First request should succeed.') + + # Request 2: With existing JA4 headers - should preserve them. + tr2 = urtest.AddTestRun('Verify preserve skips adding JA4 headers when they exist.') + tr2.MakeCurlCommand( + '-k -v -H "uuid: existing-ja4-headers" -H "ja4: upstream-fingerprint" -H "x-ja4-via: upstream.proxy.com" ' + '"https://localhost:{0}/resource-with-headers"'.format(params['port_one']), + ts=params['ts']) + tr2.Processes.Default.ReturnCode = 0 + tr2.Processes.Default.Streams.stdout += Testers.ContainsExpression(r'Preserved!', 'Second request should preserve headers.') + + # Request 3: With only x-ja4-via - should also trigger preserve. + tr3 = urtest.AddTestRun('Verify preserve triggers when only x-ja4-via exists.') + tr3.MakeCurlCommand( + '-k -v -H "uuid: existing-via-only" -H "x-ja4-via: upstream.proxy.com" ' + '"https://localhost:{0}/resource-via-only"'.format(params['port_one']), + ts=params['ts']) + tr3.Processes.Default.ReturnCode = 0 + tr3.Processes.Default.Streams.stdout += Testers.ContainsExpression(r'Via only!', 'Third request should succeed.') + + # Verify the replay file's proxy-request validations pass (checked per-request). + # Also verify via Proxy Verifier's validation logs. + server.Streams.All += Testers.ContainsExpression( + r'Equals Success.*"/resource-with-headers".*ja4.*upstream-fingerprint', + 'Preserved ja4 header should match original value.', + reflags=re.DOTALL) + server.Streams.All += Testers.ContainsExpression( + r'Absence Success.*"/resource-via-only".*ja4', 'ja4 should be absent when only x-ja4-via exists.', reflags=re.DOTALL) + + urtest.execute() diff --git a/tests/gold_tests/pluginTest/jax_fingerprint/jax_fingerprint_all_methods.replay.yaml b/tests/uranium_tests/pluginTest/jax_fingerprint/jax_fingerprint_all_methods.replay.yaml similarity index 100% rename from tests/gold_tests/pluginTest/jax_fingerprint/jax_fingerprint_all_methods.replay.yaml rename to tests/uranium_tests/pluginTest/jax_fingerprint/jax_fingerprint_all_methods.replay.yaml diff --git a/tests/gold_tests/pluginTest/jax_fingerprint/jax_fingerprint_append.replay.yaml b/tests/uranium_tests/pluginTest/jax_fingerprint/jax_fingerprint_append.replay.yaml similarity index 100% rename from tests/gold_tests/pluginTest/jax_fingerprint/jax_fingerprint_append.replay.yaml rename to tests/uranium_tests/pluginTest/jax_fingerprint/jax_fingerprint_append.replay.yaml diff --git a/tests/gold_tests/pluginTest/jax_fingerprint/jax_fingerprint_global.replay.yaml b/tests/uranium_tests/pluginTest/jax_fingerprint/jax_fingerprint_global.replay.yaml similarity index 100% rename from tests/gold_tests/pluginTest/jax_fingerprint/jax_fingerprint_global.replay.yaml rename to tests/uranium_tests/pluginTest/jax_fingerprint/jax_fingerprint_global.replay.yaml diff --git a/tests/gold_tests/pluginTest/jax_fingerprint/jax_fingerprint_global_h2.replay.yaml b/tests/uranium_tests/pluginTest/jax_fingerprint/jax_fingerprint_global_h2.replay.yaml similarity index 100% rename from tests/gold_tests/pluginTest/jax_fingerprint/jax_fingerprint_global_h2.replay.yaml rename to tests/uranium_tests/pluginTest/jax_fingerprint/jax_fingerprint_global_h2.replay.yaml diff --git a/tests/gold_tests/pluginTest/jax_fingerprint/jax_fingerprint_hybrid_servernames.replay.yaml b/tests/uranium_tests/pluginTest/jax_fingerprint/jax_fingerprint_hybrid_servernames.replay.yaml similarity index 100% rename from tests/gold_tests/pluginTest/jax_fingerprint/jax_fingerprint_hybrid_servernames.replay.yaml rename to tests/uranium_tests/pluginTest/jax_fingerprint/jax_fingerprint_hybrid_servernames.replay.yaml diff --git a/tests/gold_tests/pluginTest/jax_fingerprint/jax_fingerprint_keep.replay.yaml b/tests/uranium_tests/pluginTest/jax_fingerprint/jax_fingerprint_keep.replay.yaml similarity index 100% rename from tests/gold_tests/pluginTest/jax_fingerprint/jax_fingerprint_keep.replay.yaml rename to tests/uranium_tests/pluginTest/jax_fingerprint/jax_fingerprint_keep.replay.yaml diff --git a/tests/gold_tests/pluginTest/jax_fingerprint/jax_fingerprint_remap.replay.yaml b/tests/uranium_tests/pluginTest/jax_fingerprint/jax_fingerprint_remap.replay.yaml similarity index 100% rename from tests/gold_tests/pluginTest/jax_fingerprint/jax_fingerprint_remap.replay.yaml rename to tests/uranium_tests/pluginTest/jax_fingerprint/jax_fingerprint_remap.replay.yaml diff --git a/tests/gold_tests/pluginTest/jax_fingerprint/jax_fingerprint_remap_tls.replay.yaml b/tests/uranium_tests/pluginTest/jax_fingerprint/jax_fingerprint_remap_tls.replay.yaml similarity index 100% rename from tests/gold_tests/pluginTest/jax_fingerprint/jax_fingerprint_remap_tls.replay.yaml rename to tests/uranium_tests/pluginTest/jax_fingerprint/jax_fingerprint_remap_tls.replay.yaml diff --git a/tests/gold_tests/pluginTest/jax_fingerprint/jax_fingerprint_servernames.replay.yaml b/tests/uranium_tests/pluginTest/jax_fingerprint/jax_fingerprint_servernames.replay.yaml similarity index 100% rename from tests/gold_tests/pluginTest/jax_fingerprint/jax_fingerprint_servernames.replay.yaml rename to tests/uranium_tests/pluginTest/jax_fingerprint/jax_fingerprint_servernames.replay.yaml diff --git a/tests/gold_tests/pluginTest/jax_fingerprint/jax_fingerprint_tls.replay.yaml b/tests/uranium_tests/pluginTest/jax_fingerprint/jax_fingerprint_tls.replay.yaml similarity index 100% rename from tests/gold_tests/pluginTest/jax_fingerprint/jax_fingerprint_tls.replay.yaml rename to tests/uranium_tests/pluginTest/jax_fingerprint/jax_fingerprint_tls.replay.yaml diff --git a/tests/uranium_tests/pluginTest/jax_fingerprint/test_jax_fingerprint.py b/tests/uranium_tests/pluginTest/jax_fingerprint/test_jax_fingerprint.py new file mode 100644 index 00000000000..458c850b468 --- /dev/null +++ b/tests/uranium_tests/pluginTest/jax_fingerprint/test_jax_fingerprint.py @@ -0,0 +1,528 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_jax_fingerprint(urtest: UraniumTest) -> None: + ''' + Verify the behavior of the jax_fingerprint plugin. + + Covers all supported run modes (overwrite, keep, append), setup types + (global, remap, hybrid), fingerprint methods (JA3, JA4, JA4H), and both + HTTP/1.1 and HTTP/2 client connections. + + Global setup: plugin in plugin.config, applies to every request. + Remap setup: plugin per remap rule, applies only to matched routes. + Hybrid setup: global plugin captures the TLS client hello (creates + context) while a remap plugin reads that context and + sets headers only on matched routes. + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import os + import re + + urtest.Summary = __doc__ + urtest.SkipUnless(Condition.PluginExists('jax_fingerprint.so')) + + class JaxFingerprintTest: + '''Verify the behavior of the jax_fingerprint plugin.''' + + _dns_counter: int = 0 + _server_counter: int = 0 + _ts_counter: int = 0 + _client_counter: int = 0 + + def __init__( + self, + name: str, + method: str, + setup: str, + mode: str = 'overwrite', + http2: bool = False, + servernames: str = '', + log_field: str = '') -> None: + '''Configure test processes for the jax_fingerprint plugin. + + :param name: Descriptive name for this test run. + :param method: Fingerprint method: 'JA3', 'JA4', or 'JA4H'. + :param setup: Plugin setup type: 'global', 'remap', or 'hybrid'. + :param mode: Header write mode: 'overwrite', 'keep', or 'append'. + :param http2: If True, the client connects to ATS over HTTP/2 (h2 + over TLS). For JA3/JA4 the h2 ALPN in the ClientHello produces + a fingerprint distinct from the HTTP/1.1 case. For JA4H, + get_version() detects HTTP/2 via the protocol stack. + :param servernames: Comma-separated SNI allowlist passed as + --servernames to the plugin. Connections whose SNI is not in + the list are skipped entirely: handle_client_hello returns early, + no context is created, and handle_read_request_hdr is a no-op. + Only meaningful for CONNECTION_BASED methods (JA3/JA4) in global + setup. + :param log_field: Symbol name for --log-field option. When set, + configures logging.yaml with a custom format using the symbol + and verifies the fingerprint appears in the ATS access log. + Only supported with global setup. + + Method notes: + - JA3 / JA4 are CONNECTION_BASED (triggered on TLS client hello) + and require TLS between the client and ATS. + - JA4H is REQUEST_BASED (triggered on HTTP request read) and + works over plain HTTP, HTTPS, and HTTP/2. + + Setup notes: + - global: plugin.config entry with --standalone so ATS registers + the READ_REQUEST_HDR hook and modifies every request. + - remap: @plugin in remap.config. For CONNECTION_BASED methods + --standalone must also be passed so the remap plugin registers + the SSL_CLIENT_HELLO_HOOK globally and can populate the vconn + context that TSRemapDoRemap reads later. + - hybrid: global plugin (no --standalone) captures the TLS client + hello and stores context on the vconn; a remap plugin (no + --standalone) reads that shared context and sets headers only + on matched routes. Both instances share the same user-arg slot + because TSUserArgIndexReserve is idempotent for identical names. + ''' + self._name = name + self._method = method + self._setup = setup + self._mode = mode + self._http2 = http2 + self._servernames = servernames + self._log_field = log_field + # HTTP/2 always runs over TLS (h2 requires TLS). + self._needs_tls = method in ('JA3', 'JA4') or http2 + self._replay_file = self._choose_replay_file() + self._fingerprint_pattern = rf'{self._method}: [a-z0-9_-]+' + + tr = urtest.AddTestRun(name) + self._configure_dns(tr) + self._configure_server(tr) + self._configure_trafficserver() + self._configure_client(tr) + urtest.AddAwaitFileContainsTestRun( + f'Await jax_fingerprint.log for: {self._name}', self._ts.Disk.jax_log.AbsPath, self._fingerprint_pattern) + + if self._log_field: + log_field_path = os.path.join(self._ts.Variables.LOGDIR, 'jax_log_field.log') + # Verify the log contains a fingerprint (not just a dash placeholder). + urtest.AddAwaitFileContainsTestRun( + f'Await jax_log_field.log for: {self._name}', log_field_path, f'{self._method}: [a-z0-9]') + + # ------------------------------------------------------------------ + # Helpers + # ------------------------------------------------------------------ + + def _choose_replay_file(self) -> str: + '''Return the replay YAML to drive this test. + + Key: (servernames, setup, needs_tls, http2, mode) + ''' + key = (bool(self._servernames), self._setup, self._needs_tls, self._http2, self._mode) + mapping = { + (False, 'global', False, False, 'overwrite'): 'jax_fingerprint_global.replay.yaml', + (False, 'global', False, False, 'keep'): 'jax_fingerprint_keep.replay.yaml', + (False, 'global', False, False, 'append'): 'jax_fingerprint_append.replay.yaml', + (False, 'global', True, False, 'overwrite'): 'jax_fingerprint_tls.replay.yaml', + (False, 'global', True, True, 'overwrite'): 'jax_fingerprint_global_h2.replay.yaml', + (False, 'remap', False, False, 'overwrite'): 'jax_fingerprint_remap.replay.yaml', + (False, 'remap', True, False, 'overwrite'): 'jax_fingerprint_remap_tls.replay.yaml', + (False, 'hybrid', True, False, 'overwrite'): 'jax_fingerprint_remap_tls.replay.yaml', + (True, 'global', True, False, 'overwrite'): 'jax_fingerprint_servernames.replay.yaml', + (True, 'hybrid', True, False, 'overwrite'): 'jax_fingerprint_hybrid_servernames.replay.yaml', + } + return mapping[key] + + def _build_remap_plugin_line(self, add_standalone: bool = False) -> str: + '''Build the @plugin / @pparam fragment for a remap.config line.''' + parts = [ + '@plugin=jax_fingerprint.so', + '@pparam=--method', + f'@pparam={self._method}', + '@pparam=--header', + '@pparam=x-jax', + '@pparam=--via-header', + '@pparam=x-jax-via', + '@pparam=--log-filename', + '@pparam=jax_fingerprint', + ] + if self._mode != 'overwrite': + parts.extend(['@pparam=--mode', f'@pparam={self._mode}']) + if add_standalone: + parts.append('@pparam=--standalone') + return ' '.join(parts) + + # ------------------------------------------------------------------ + # Test-process configuration + # ------------------------------------------------------------------ + + def _configure_dns(self, tr: 'TestRun') -> None: + '''Configure a nameserver for the test.''' + name = f'dns{JaxFingerprintTest._dns_counter}' + self._dns = tr.MakeDNServer(name, default='127.0.0.1') + JaxFingerprintTest._dns_counter += 1 + + def _configure_server(self, tr: 'TestRun') -> None: + '''Configure the origin (verifier) server.''' + name = f'server{JaxFingerprintTest._server_counter}' + self._server = tr.AddVerifierServerProcess(name, self._replay_file) + JaxFingerprintTest._server_counter += 1 + + # For remap / hybrid tests the second session reaches the server + # with the fingerprint header already appended by ATS; verify it. + if self._servernames: + # Only the SNI-matched session carries the fingerprint header. + uuid_key = 'hybrid-servernames-in-filter' if self._setup == 'hybrid' else 'servernames-in-filter' + self._server.Streams.All += Testers.ContainsExpression( + uuid_key, 'Verify the allowed-SNI request reached the server.') + self._server.Streams.All += Testers.ContainsExpression( + r'x-jax:', 'Verify the fingerprint header was forwarded for the allowed SNI.', reflags=re.IGNORECASE) + elif self._setup in ('remap', 'hybrid'): + uuid_key = 'remap-tls-plugin-request' if self._needs_tls else 'remap-plugin-request' + self._server.Streams.All += Testers.ContainsExpression( + uuid_key, 'Verify the matched-route request reached the server.') + self._server.Streams.All += Testers.ContainsExpression( + r'x-jax:', 'Verify the fingerprint header was forwarded.', reflags=re.IGNORECASE) + else: + # global tests - all requests carry the fingerprint header + self._server.Streams.All += Testers.ContainsExpression( + r'x-jax:', 'Verify the fingerprint header was forwarded.', reflags=re.IGNORECASE) + + def _configure_trafficserver(self) -> None: + '''Configure Traffic Server and its plugin / remap rules.''' + name = f'ts{JaxFingerprintTest._ts_counter}' + self._ts = urtest.MakeATSProcess(name, enable_cache=False, enable_tls=True) + JaxFingerprintTest._ts_counter += 1 + + self._ts.addDefaultSSLFiles() + self._ts.Disk.ssl_multicert_yaml.AddLines( + """ + ssl_multicert: + - dest_ip: "*" + ssl_cert_name: server.pem + ssl_key_name: server.key + """.split("\n")) + + if self._needs_tls: + server_port = self._server.Variables.https_port + scheme = 'https' + else: + server_port = self._server.Variables.http_port + scheme = 'http' + + self._ts.Disk.records_config.update( + { + 'proxy.config.ssl.server.cert.path': self._ts.Variables.SSLDir, + 'proxy.config.ssl.server.private_key.path': self._ts.Variables.SSLDir, + 'proxy.config.ssl.client.verify.server.policy': 'PERMISSIVE', + 'proxy.config.dns.nameservers': f"127.0.0.1:{self._dns.Variables.Port}", + 'proxy.config.dns.resolv_conf': 'NULL', + 'proxy.config.proxy_name': 'test.proxy.test', + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'jax_fingerprint|http', + }) + + log_path = os.path.join(self._ts.Variables.LOGDIR, 'jax_fingerprint.log') + self._ts.Disk.File(log_path, id='jax_log') + self._ts.Disk.jax_log.Content += Testers.ContainsExpression( + self._fingerprint_pattern, f'Verify the jax_fingerprint log contains a {self._method} fingerprint.') + + backend = f'{scheme}://jax.backend.test:{server_port}' + backend_no_plugin = f'{scheme}://jax.backend.test:{server_port}' + + if self._setup == 'global': + global_args = ( + f'--method {self._method} ' + f'--header x-jax ' + f'--via-header x-jax-via ' + f'--log-filename jax_fingerprint ' + f'--standalone') + if self._mode != 'overwrite': + global_args += f' --mode {self._mode}' + if self._servernames: + global_args += f' --servernames {self._servernames}' + if self._log_field: + global_args += f' --log-field {self._log_field}' + self._ts.Disk.plugin_config.AddLine(f'jax_fingerprint.so {global_args}') + self._ts.Disk.remap_config.AddLine(f'map {scheme}://jax.server.test {backend}') + if self._servernames: + # Second remap rule for the SNI that is NOT in the allowlist. + self._ts.Disk.remap_config.AddLine(f'map {scheme}://jax-filtered.server.test {backend}') + + elif self._setup == 'remap': + # Route without plugin (session 1 in replay file) + self._ts.Disk.remap_config.AddLine(f'map {scheme}://jax-no-plugin.server.test {backend_no_plugin}') + # Route with plugin (session 2 in replay file) + # CONNECTION_BASED methods need --standalone so the remap plugin + # registers the SSL_CLIENT_HELLO_HOOK to populate the vconn context. + remap_line = self._build_remap_plugin_line(add_standalone=self._needs_tls) + self._ts.Disk.remap_config.AddLine(f'map {scheme}://jax.server.test {backend} {remap_line}') + + elif self._setup == 'hybrid': + # Global plugin: registers SSL_CLIENT_HELLO_HOOK to capture the + # TLS handshake and store context on the vconn. No --standalone + # means no READ_REQUEST_HDR hook, so headers are never set here. + global_plugin_args = f'--method {self._method}' + if self._servernames: + global_plugin_args += f' --servernames {self._servernames}' + self._ts.Disk.plugin_config.AddLine(f'jax_fingerprint.so {global_plugin_args}') + remap_line = self._build_remap_plugin_line(add_standalone=False) + if self._servernames: + # Both routes have the remap plugin. Only the SNI-allowed + # connection has a context on the vconn, so only that request + # gets fingerprint headers even though both routes are mapped. + self._ts.Disk.remap_config.AddLine( + f'map https://jax.server.test https://jax.backend.test:{server_port} {remap_line}') + self._ts.Disk.remap_config.AddLine( + f'map https://jax-filtered.server.test https://jax.backend.test:{server_port} {remap_line}') + else: + # Route without remap plugin: context is captured but no headers set. + self._ts.Disk.remap_config.AddLine( + f'map https://jax-no-plugin.server.test https://jax.backend.test:{server_port}') + # Route with remap plugin: reads shared vconn context, sets headers. + self._ts.Disk.remap_config.AddLine( + f'map https://jax.server.test https://jax.backend.test:{server_port} {remap_line}') + + if self._log_field: + self._ts.Disk.logging_yaml.AddLines( + f''' + logging: + formats: + - name: jax_custom + format: '{self._method}: %<{self._log_field}>' + logs: + - filename: jax_log_field + format: jax_custom + '''.split("\n")) + + def _configure_client(self, tr: 'TestRun') -> None: + '''Configure the verifier client.''' + name = f'client{JaxFingerprintTest._client_counter}' + p = tr.AddVerifierClientProcess( + name, self._replay_file, http_ports=[self._ts.Variables.port], https_ports=[self._ts.Variables.ssl_port]) + JaxFingerprintTest._client_counter += 1 + + p.StartBefore(self._dns) + p.StartBefore(self._server) + p.StartBefore(self._ts) + tr.StillRunningAfter = self._ts + + # ====================================================================== + # Test instances + # ====================================================================== + + # --- Global setup ------------------------------------------------------- + + # All HTTP/1.1 requests receive a JA4H fingerprint header (overwrite mode). + JaxFingerprintTest('Global JA4H overwrite', 'JA4H', 'global') + + # All TLS requests receive a JA3 fingerprint header (overwrite mode). + JaxFingerprintTest('Global JA3 overwrite', 'JA3', 'global') + + # --- Remap setup -------------------------------------------------------- + + # Only requests matching the remap rule receive a JA4H header. + JaxFingerprintTest('Remap JA4H', 'JA4H', 'remap') + + # Remap plugin with --standalone captures TLS client hellos globally and + # sets JA3 headers only on the matched route. + JaxFingerprintTest('Remap JA3 standalone', 'JA3', 'remap') + + # --- Hybrid setup ------------------------------------------------------- + + # Global plugin captures TLS client hellos (creates vconn context). + # Remap plugin reads that shared context and sets headers on matched routes. + JaxFingerprintTest('Hybrid JA4', 'JA4', 'hybrid') + + # --- HTTP/2 (h2 over TLS) ----------------------------------------------- + + # JA4: the h2 ALPN in the ClientHello produces a different fingerprint than + # the HTTP/1.1 case – exercises the full TLS extension path with h2 ALPN. + JaxFingerprintTest('Global JA4 HTTP/2', 'JA4', 'global', http2=True) + + # JA4H: exercises the HTTP/2 branch of get_version() which detects h2 via + # TSHttpTxnClientProtocolStackContains. + JaxFingerprintTest('Global JA4H HTTP/2', 'JA4H', 'global', http2=True) + + # keep mode: existing x-jax / x-jax-via headers are not overwritten. + JaxFingerprintTest('Global JA4H keep mode', 'JA4H', 'global', mode='keep') + + # append mode: fingerprint / proxy name are appended to existing header values. + JaxFingerprintTest('Global JA4H append mode', 'JA4H', 'global', mode='append') + + # --- SNI allowlist (--servernames) -------------------------------------- + + # --servernames restricts fingerprinting to connections whose TLS SNI is in + # the list. Connections with a non-matching SNI are skipped at the client- + # hello hook: no context is created and handle_read_request_hdr is a no-op, + # so neither the fingerprint header nor the via header are set. + JaxFingerprintTest('Global JA4 servernames', 'JA4', 'global', servernames='jax.server.test') + + # --- Hybrid + SNI allowlist (most common production pattern) --------------- + + # Global plugin captures TLS client hellos for allowed SNIs only. + # Remap plugin sets headers on both routes, but only the SNI-allowed + # connection has a vconn context, so only that request gets headers. + JaxFingerprintTest('Hybrid JA4 servernames', 'JA4', 'hybrid', servernames='jax.server.test') + + # --- Custom log field (--log-field) ----------------------------------------- + + # Register a custom log field via --log-field and verify the fingerprint + # appears in the ATS access log configured in logging.yaml. + JaxFingerprintTest('Global JA4H log-field', 'JA4H', 'global', log_field='jaxja4h') + JaxFingerprintTest('Global JA4 log-field', 'JA4', 'global', log_field='jaxja4') + + # ====================================================================== + # All Methods Test - Verify shared context map works with multiple methods + # ====================================================================== + + class AllMethodsTest: + '''Test multiple fingerprint methods loaded simultaneously. + + When multiple jax_fingerprint instances are loaded, they share user arg + slots via a ContextMap. This test verifies that JA3/JA4 and JA4H can + coexist and produce fingerprints while sharing that context machinery + across their respective vconn and txn storage. + ''' + + _dns_counter: int = 0 + _server_counter: int = 0 + _ts_counter: int = 0 + _client_counter: int = 0 + + def __init__(self, name: str) -> None: + '''Configure test with multiple methods loaded.''' + self._name = name + self._replay_file = 'jax_fingerprint_all_methods.replay.yaml' + + tr = urtest.AddTestRun(name) + self._configure_dns(tr) + self._configure_server(tr) + self._configure_trafficserver() + self._configure_client(tr) + + def _configure_dns(self, tr: 'TestRun') -> None: + '''Configure a nameserver for the test.''' + name = f'dns_all{AllMethodsTest._dns_counter}' + self._dns = tr.MakeDNServer(name, default='127.0.0.1') + AllMethodsTest._dns_counter += 1 + + def _configure_server(self, tr: 'TestRun') -> None: + '''Configure the origin (verifier) server.''' + name = f'server_all{AllMethodsTest._server_counter}' + self._server = tr.AddVerifierServerProcess(name, self._replay_file) + AllMethodsTest._server_counter += 1 + + # Verify all headers were forwarded to the origin. + for header in ['x-ja3', 'x-ja4', 'x-ja4h']: + self._server.Streams.All += Testers.ContainsExpression( + rf'{header}:', f'Verify {header} header was forwarded.', reflags=re.IGNORECASE) + + def _configure_trafficserver(self) -> None: + '''Configure Traffic Server with multiple methods.''' + name = f'ts_all{AllMethodsTest._ts_counter}' + self._ts = urtest.MakeATSProcess(name, enable_cache=False, enable_tls=True) + AllMethodsTest._ts_counter += 1 + + self._ts.addDefaultSSLFiles() + self._ts.Disk.ssl_multicert_yaml.AddLines( + """ + ssl_multicert: + - dest_ip: "*" + ssl_cert_name: server.pem + ssl_key_name: server.key + """.split("\n")) + + server_port = self._server.Variables.https_port + + self._ts.Disk.records_config.update( + { + 'proxy.config.ssl.server.cert.path': self._ts.Variables.SSLDir, + 'proxy.config.ssl.server.private_key.path': self._ts.Variables.SSLDir, + 'proxy.config.ssl.client.verify.server.policy': 'PERMISSIVE', + 'proxy.config.dns.nameservers': f"127.0.0.1:{self._dns.Variables.Port}", + 'proxy.config.dns.resolv_conf': 'NULL', + 'proxy.config.proxy_name': 'test.proxy.test', + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'jax_fingerprint', + }) + + # Each of the following pairs makes sure that the expression exists + # exactly once. + self._ts.Disk.traffic_out.Content += Testers.ContainsExpression( + r'Reserved shared user_arg slot: type=vconn, index=\d+', 'Verify a shared vconn user arg slot was reserved.') + self._ts.Disk.traffic_out.Content += Testers.ExcludesExpression( + r'Reserved shared user_arg slot: type=vconn, index=\d+.*Reserved shared user_arg slot: type=vconn, index=\d+', + 'Verify the shared vconn user arg slot was reserved only once.', + reflags=re.MULTILINE | re.DOTALL) + + self._ts.Disk.traffic_out.Content += Testers.ContainsExpression( + r'Reserved shared user_arg slot: type=txn, index=\d+', 'Verify a shared txn user arg slot was reserved.') + self._ts.Disk.traffic_out.Content += Testers.ExcludesExpression( + r'Reserved shared user_arg slot: type=txn, index=\d+.*Reserved shared user_arg slot: type=txn, index=\d+', + 'Verify the shared txn user arg slot was reserved only once.', + reflags=re.MULTILINE | re.DOTALL) + + # Ensure that JA3 and JA4 share the same vconn user arg slot. + self._ts.Disk.traffic_out.Content += Testers.ContainsExpression( + r'Using shared user_arg: type=vconn, method=JA3, index=(\d+).*Using shared user_arg: type=vconn, method=JA4, index=\1', + 'Verify JA3 and JA4 share the same vconn user arg slot.', + reflags=re.MULTILINE | re.DOTALL) + # Note that JA4H is on txn not vconn as JA3 and JA4 above. + self._ts.Disk.traffic_out.Content += Testers.ContainsExpression( + r'Using shared user_arg: type=txn, method=JA4H, index=\d+', 'Verify JA4H uses the shared txn user arg slot.') + + # Load multiple methods - all share the same user arg slot via ContextMap. + self._ts.Disk.plugin_config.AddLines( + [ + 'jax_fingerprint.so --method JA3 --header x-ja3 --standalone', + 'jax_fingerprint.so --method JA4 --header x-ja4 --standalone', + 'jax_fingerprint.so --method JA4H --header x-ja4h --standalone', + ]) + + self._ts.Disk.remap_config.AddLine(f'map https://jax.server.test https://jax.backend.test:{server_port}') + + def _configure_client(self, tr: 'TestRun') -> None: + '''Configure the verifier client.''' + name = f'client_all{AllMethodsTest._client_counter}' + p = tr.AddVerifierClientProcess( + name, self._replay_file, http_ports=[self._ts.Variables.port], https_ports=[self._ts.Variables.ssl_port]) + AllMethodsTest._client_counter += 1 + + p.StartBefore(self._dns) + p.StartBefore(self._server) + p.StartBefore(self._ts) + tr.StillRunningAfter = self._ts + + # --- All Methods Test -------------------------------------------------------- + + # Multiple methods loaded simultaneously, verifying shared context map works. + AllMethodsTest('Multiple methods loaded simultaneously') + urtest.execute() diff --git a/tests/gold_tests/pluginTest/lua/client_hook.lua b/tests/uranium_tests/pluginTest/lua/client_hook.lua similarity index 100% rename from tests/gold_tests/pluginTest/lua/client_hook.lua rename to tests/uranium_tests/pluginTest/lua/client_hook.lua diff --git a/tests/gold_tests/pluginTest/lua/fetch_ipv6_cliaddr.lua b/tests/uranium_tests/pluginTest/lua/fetch_ipv6_cliaddr.lua similarity index 100% rename from tests/gold_tests/pluginTest/lua/fetch_ipv6_cliaddr.lua rename to tests/uranium_tests/pluginTest/lua/fetch_ipv6_cliaddr.lua diff --git a/tests/gold_tests/pluginTest/lua/global.lua b/tests/uranium_tests/pluginTest/lua/global.lua similarity index 100% rename from tests/gold_tests/pluginTest/lua/global.lua rename to tests/uranium_tests/pluginTest/lua/global.lua diff --git a/tests/gold_tests/pluginTest/lua/global_shutdown.lua b/tests/uranium_tests/pluginTest/lua/global_shutdown.lua similarity index 100% rename from tests/gold_tests/pluginTest/lua/global_shutdown.lua rename to tests/uranium_tests/pluginTest/lua/global_shutdown.lua diff --git a/tests/gold_tests/pluginTest/lua/gold/lifecycle.gold b/tests/uranium_tests/pluginTest/lua/gold/lifecycle.gold similarity index 100% rename from tests/gold_tests/pluginTest/lua/gold/lifecycle.gold rename to tests/uranium_tests/pluginTest/lua/gold/lifecycle.gold diff --git a/tests/gold_tests/pluginTest/lua/gold/lua_proxy_protocol.gold b/tests/uranium_tests/pluginTest/lua/gold/lua_proxy_protocol.gold similarity index 100% rename from tests/gold_tests/pluginTest/lua/gold/lua_proxy_protocol.gold rename to tests/uranium_tests/pluginTest/lua/gold/lua_proxy_protocol.gold diff --git a/tests/gold_tests/pluginTest/lua/gold/metrics.gold b/tests/uranium_tests/pluginTest/lua/gold/metrics.gold similarity index 100% rename from tests/gold_tests/pluginTest/lua/gold/metrics.gold rename to tests/uranium_tests/pluginTest/lua/gold/metrics.gold diff --git a/tests/gold_tests/pluginTest/lua/header_table.lua b/tests/uranium_tests/pluginTest/lua/header_table.lua similarity index 100% rename from tests/gold_tests/pluginTest/lua/header_table.lua rename to tests/uranium_tests/pluginTest/lua/header_table.lua diff --git a/tests/gold_tests/pluginTest/lua/hello.lua b/tests/uranium_tests/pluginTest/lua/hello.lua similarity index 100% rename from tests/gold_tests/pluginTest/lua/hello.lua rename to tests/uranium_tests/pluginTest/lua/hello.lua diff --git a/tests/gold_tests/pluginTest/lua/lifecycle_stats.sh b/tests/uranium_tests/pluginTest/lua/lifecycle_stats.sh similarity index 100% rename from tests/gold_tests/pluginTest/lua/lifecycle_stats.sh rename to tests/uranium_tests/pluginTest/lua/lifecycle_stats.sh diff --git a/tests/gold_tests/pluginTest/lua/metrics.sh b/tests/uranium_tests/pluginTest/lua/metrics.sh similarity index 100% rename from tests/gold_tests/pluginTest/lua/metrics.sh rename to tests/uranium_tests/pluginTest/lua/metrics.sh diff --git a/tests/gold_tests/pluginTest/lua/proxy_protocol.lua b/tests/uranium_tests/pluginTest/lua/proxy_protocol.lua similarity index 100% rename from tests/gold_tests/pluginTest/lua/proxy_protocol.lua rename to tests/uranium_tests/pluginTest/lua/proxy_protocol.lua diff --git a/tests/gold_tests/pluginTest/lua/remap_after_hook.lua b/tests/uranium_tests/pluginTest/lua/remap_after_hook.lua similarity index 100% rename from tests/gold_tests/pluginTest/lua/remap_after_hook.lua rename to tests/uranium_tests/pluginTest/lua/remap_after_hook.lua diff --git a/tests/gold_tests/pluginTest/lua/set_error_response.lua b/tests/uranium_tests/pluginTest/lua/set_error_response.lua similarity index 100% rename from tests/gold_tests/pluginTest/lua/set_error_response.lua rename to tests/uranium_tests/pluginTest/lua/set_error_response.lua diff --git a/tests/gold_tests/pluginTest/lua/set_error_response.replay.yaml b/tests/uranium_tests/pluginTest/lua/set_error_response.replay.yaml similarity index 100% rename from tests/gold_tests/pluginTest/lua/set_error_response.replay.yaml rename to tests/uranium_tests/pluginTest/lua/set_error_response.replay.yaml diff --git a/tests/gold_tests/pluginTest/lua/tags.lua b/tests/uranium_tests/pluginTest/lua/tags.lua similarity index 100% rename from tests/gold_tests/pluginTest/lua/tags.lua rename to tests/uranium_tests/pluginTest/lua/tags.lua diff --git a/tests/gold_tests/pluginTest/lua/tags.sh b/tests/uranium_tests/pluginTest/lua/tags.sh similarity index 100% rename from tests/gold_tests/pluginTest/lua/tags.sh rename to tests/uranium_tests/pluginTest/lua/tags.sh diff --git a/tests/uranium_tests/pluginTest/lua/test_lua_client_hook.py b/tests/uranium_tests/pluginTest/lua/test_lua_client_hook.py new file mode 100644 index 00000000000..d3d9dfe61dd --- /dev/null +++ b/tests/uranium_tests/pluginTest/lua/test_lua_client_hook.py @@ -0,0 +1,78 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_lua_client_hook(urtest: UraniumTest) -> None: + ''' + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import os + + urtest.Summary = ''' + Test lua functionality + ''' + + urtest.SkipUnless(Condition.PluginExists('tslua.so'),) + + urtest.ContinueOnFail = True + # Define default ATS + ts = urtest.MakeATSProcess("ts") + server = urtest.MakeOriginServer("server") + + urtest.testName = "" + request_header = {"headers": "GET / HTTP/1.1\r\nHost: www.example.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + # expected response from the origin server + response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": "AAAA"} + + # add response to the server dictionary + server.addResponse("sessionfile.log", request_header, response_header) + + ts.Disk.remap_config.AddLine( + 'map / http://127.0.0.1:{}/'.format(server.Variables.Port) + ' @plugin=tslua.so @pparam=client_hook.lua') + + # Configure the tslua's configuration file. + ts.Setup.Copy("client_hook.lua", ts.Variables.CONFIGDIR) + + ts.Disk.records_config.update({'proxy.config.diags.debug.enabled': 1, 'proxy.config.diags.debug.tags': 'ts_lua'}) + + # Test for watermark debug output + ts.Disk.traffic_out.Content = Testers.ContainsExpression(r"AAAA", "Response is properly captured") + + # Test if watermark upstream is set + tr = urtest.AddTestRun("Lua Response Client Hook") + tr.MakeCurlCommand("-v http://127.0.0.1:{0}".format(ts.Variables.port), ts=ts) + tr.Processes.Default.StartBefore(server, ready=When.PortOpen(server.Variables.Port)) + + tr.Processes.Default.StartBefore(ts) + tr.StillRunningAfter = server + urtest.execute() diff --git a/tests/uranium_tests/pluginTest/lua/test_lua_debug_tags.py b/tests/uranium_tests/pluginTest/lua/test_lua_debug_tags.py new file mode 100644 index 00000000000..e851c22a1e4 --- /dev/null +++ b/tests/uranium_tests/pluginTest/lua/test_lua_debug_tags.py @@ -0,0 +1,72 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_lua_debug_tags(urtest: UraniumTest) -> None: + ''' + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import os + + urtest.Summary = ''' + Test lua is_debug_tag_set functionality + ''' + + urtest.SkipUnless(Condition.PluginExists('tslua.so'),) + + urtest.ContinueOnFail = False + # Define default ATS + ts = urtest.MakeATSProcess("ts") + + ts.Disk.remap_config.AddLine('map http://test http://127.0.0.1/ @plugin=tslua.so @pparam=tags.lua') + + # Configure the tslua's configuration file. + ts.Setup.Copy("tags.lua", ts.Variables.CONFIGDIR) + ts.Setup.Copy("tags.sh") + + ts.Disk.records_config.update({ + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'foo|ts_lua', + }) + + curl_and_args = f'{{curl}} -s -D /dev/stderr -o /dev/stdout -x localhost:{ts.Variables.port} http://test/test.html' + + # 0 Ensure no debug tag set + tr = urtest.AddTestRun("check tags") + ps = tr.Processes.Default + tr.StillRunningAfter = ts + ps.StartBefore(urtest.Processes.ts) + tr.MakeCurlCommandMulti(f"bash ./tags.sh {curl_and_args}", ts=ts) + ps.Env = ts.Env + ps.ReturnCode = 0 + urtest.execute() diff --git a/tests/uranium_tests/pluginTest/lua/test_lua_fetch_ipv6_cliaddr.py b/tests/uranium_tests/pluginTest/lua/test_lua_fetch_ipv6_cliaddr.py new file mode 100644 index 00000000000..05c36601f14 --- /dev/null +++ b/tests/uranium_tests/pluginTest/lua/test_lua_fetch_ipv6_cliaddr.py @@ -0,0 +1,80 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_lua_fetch_ipv6_cliaddr(urtest: UraniumTest) -> None: + ''' + Verify ts.fetch handles an IPv6 cliaddr option. + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + urtest.Summary = ''' + Verify ts.fetch parses an IPv6 cliaddr and the inner fetch completes. + ''' + + urtest.SkipUnless(Condition.PluginExists('tslua.so'),) + + urtest.ContinueOnFail = True + + ts = urtest.MakeATSProcess("ts") + server = urtest.MakeOriginServer("server") + + # Inner sub-request fetched by the Lua post_remap hook. + inner_req = {"headers": "GET /inner.txt HTTP/1.1\r\nHost: www.example.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + inner_resp = { + "headers": "HTTP/1.1 200 OK\r\nConnection: close\r\nContent-Length: 5\r\n\r\n", + "timestamp": "1469733493.993", + "body": "AAAAA" + } + server.addResponse("sessionfile.log", inner_req, inner_resp) + + # Outer request driven by curl below. + outer_req = {"headers": "GET / HTTP/1.1\r\nHost: www.example.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + outer_resp = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": "outer"} + server.addResponse("sessionfile.log", outer_req, outer_resp) + + ts.Disk.remap_config.AddLine( + 'map / http://127.0.0.1:{}/'.format(server.Variables.Port) + ' @plugin=tslua.so @pparam=fetch_ipv6_cliaddr.lua') + + ts.Setup.Copy("fetch_ipv6_cliaddr.lua", ts.Variables.CONFIGDIR) + + ts.Disk.records_config.update({'proxy.config.diags.debug.enabled': 1, 'proxy.config.diags.debug.tags': 'ts_lua'}) + + tr = urtest.AddTestRun("ts.fetch with IPv6 cliaddr") + tr.MakeCurlCommand("-s -D - http://127.0.0.1:{0}/".format(ts.Variables.port), ts=ts) + tr.Processes.Default.StartBefore(server, ready=When.PortOpen(server.Variables.Port)) + tr.Processes.Default.StartBefore(ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.stdout.Content = Testers.ContainsExpression( + "Sub-Body-Len: 5", "Inner fetch using IPv6 cliaddr should return the full origin body") + tr.StillRunningAfter = server + urtest.execute() diff --git a/tests/uranium_tests/pluginTest/lua/test_lua_global_shutdown.py b/tests/uranium_tests/pluginTest/lua/test_lua_global_shutdown.py new file mode 100644 index 00000000000..ed10e550e24 --- /dev/null +++ b/tests/uranium_tests/pluginTest/lua/test_lua_global_shutdown.py @@ -0,0 +1,86 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_lua_global_shutdown(urtest: UraniumTest) -> None: + ''' + Test __shutdown__ lua global plugin hook. + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + urtest.Summary = ''' + Test __shutdown__ lua global plugin hook + ''' + + urtest.SkipUnless(Condition.PluginExists('tslua.so'),) + + urtest.ContinueOnFail = True + + server = urtest.MakeOriginServer("server") + ts = urtest.MakeATSProcess("ts") + + urtest.Setup.Copy("global_shutdown.lua") + + request_header = {"headers": "GET / HTTP/1.1\r\nHost: www.example.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + server.addResponse("sessionfile.log", request_header, response_header) + + ts.Disk.remap_config.AddLine('map / http://127.0.0.1:{}/'.format(server.Variables.Port)) + + # Use 2 states so the shutdown handler is called a predictable number of times. + ts.Disk.plugin_config.AddLine('tslua.so --states=2 {}/global_shutdown.lua'.format(urtest.RunDirectory)) + + ts.Disk.records_config.update({ + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'ts_lua', + }) + + curl_and_args = '-s -D /dev/stdout -o /dev/stderr -x localhost:{} '.format(ts.Variables.port) + + # 0 Test - Send a request to confirm the global plugin is active. + tr = urtest.AddTestRun("Lua global read request hook fires for HTTP requests") + ps = tr.Processes.Default + ps.StartBefore(server, ready=When.PortOpen(server.Variables.Port)) + ps.StartBefore(urtest.Processes.ts) + tr.MakeCurlCommand(curl_and_args + 'http://www.example.com/', ts=ts) + ps.ReturnCode = 0 + tr.StillRunningAfter = ts + + # Verify do_global_read_request was invoked for the HTTP request above. + ts.Disk.traffic_out.Content = Testers.ContainsExpression( + r'do_global_read_request called', 'do_global_read_request should be called for HTTP requests') + + # After all test runs complete AuTest stops ATS, which fires TS_LIFECYCLE_SHUTDOWN_HOOK. + # The shutdown handler calls __shutdown__ once per Lua state (2 states configured). + ts.Disk.traffic_out.Content += Testers.ContainsExpression( + r'__shutdown__ called', '__shutdown__ should be called on ATS shutdown') + urtest.execute() diff --git a/tests/uranium_tests/pluginTest/lua/test_lua_header_table.py b/tests/uranium_tests/pluginTest/lua/test_lua_header_table.py new file mode 100644 index 00000000000..812a3d6c2df --- /dev/null +++ b/tests/uranium_tests/pluginTest/lua/test_lua_header_table.py @@ -0,0 +1,62 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_lua_header_table(urtest: UraniumTest) -> None: + ''' + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + urtest.Summary = ''' + Test lua header table functionality + ''' + + urtest.SkipUnless(Condition.PluginExists('tslua.so'),) + + urtest.ContinueOnFail = True + # Define default ATS + ts = urtest.MakeATSProcess("ts") + + ts.Disk.remap_config.AddLine(f"map / http://127.0.0.1 @plugin=tslua.so @pparam=header_table.lua") + # Configure the tslua's configuration file. + ts.Setup.Copy("header_table.lua", ts.Variables.CONFIGDIR) + + # Test - Check for header table + tr = urtest.AddTestRun("Lua Header Table") + ps = tr.Processes.Default # alias + ps.StartBefore(urtest.Processes.ts) + tr.MakeCurlCommand(f"-s -D /dev/stderr -H 'X-Test: test1' -H 'X-Test: test2' http://127.0.0.1:{ts.Variables.port}", ts=ts) + ps.Env = ts.Env + ps.ReturnCode = 0 + ps.Streams.stdout.Content = Testers.ContainsExpression("test1test2", "expected header table results") + tr.StillRunningAfter = ts + urtest.execute() diff --git a/tests/uranium_tests/pluginTest/lua/test_lua_proxy_protocol.py b/tests/uranium_tests/pluginTest/lua/test_lua_proxy_protocol.py new file mode 100644 index 00000000000..b1b43bddddf --- /dev/null +++ b/tests/uranium_tests/pluginTest/lua/test_lua_proxy_protocol.py @@ -0,0 +1,103 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_lua_proxy_protocol(urtest: UraniumTest) -> None: + ''' + Test Lua plugin PROXY protocol support + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import os + + urtest.Summary = ''' + Test Lua plugin PROXY protocol API + ''' + + urtest.SkipUnless( + Condition.PluginExists('tslua.so'), + Condition.HasCurlOption("--haproxy-clientip"), + ) + + urtest.ContinueOnFail = True + + # ---- Setup Origin Server ---- + server = urtest.MakeOriginServer("server") + + request_header = {"headers": "GET / HTTP/1.1\r\nHost: www.example.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + response_header = { + "headers": "HTTP/1.1 200 OK\r\nConnection: close\r\nContent-Length: 4\r\n\r\n", + "timestamp": "1469733493.993", + "body": "TEST" + } + server.addResponse("sessionfile.log", request_header, response_header) + + # ---- Setup ATS ---- + ts = urtest.MakeATSProcess("ts", enable_proxy_protocol=True) + + ts.Disk.remap_config.AddLine( + 'map / http://127.0.0.1:{0}/'.format(server.Variables.Port) + ' @plugin=tslua.so @pparam=proxy_protocol.lua') + + ts.Setup.Copy("proxy_protocol.lua", ts.Variables.CONFIGDIR) + + ts.Disk.records_config.update({'proxy.config.diags.debug.enabled': 1, 'proxy.config.diags.debug.tags': 'ts_lua'}) + + # ---- Test with PROXY protocol ---- + tr = urtest.AddTestRun("Test with PROXY protocol v1") + tr.Processes.Default.Command = ( + f"curl --haproxy-protocol --haproxy-clientip 192.168.1.100 " + f"http://127.0.0.1:{ts.Variables.proxy_protocol_port}/") + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.StartBefore(server, ready=When.PortOpen(server.Variables.Port)) + tr.Processes.Default.StartBefore(ts) + tr.Processes.Default.Streams.stdout = Testers.ContainsExpression("TEST", "Response body should be received") + tr.StillRunningAfter = server + tr.StillRunningAfter = ts + + # Check debug log for PROXY protocol info + ts.Disk.traffic_out.Content += Testers.ContainsExpression(r"PP-Version: 1", "PROXY protocol version should be logged") + ts.Disk.traffic_out.Content += Testers.ContainsExpression( + r"PP-Source: 192\.168\.1\.100", "PROXY protocol source address should be logged") + ts.Disk.traffic_out.Content += Testers.ContainsExpression(r"PP-Protocol: 2", "PROXY protocol IP family should be logged") + ts.Disk.traffic_out.Content += Testers.ContainsExpression(r"PP-SocketType: 1", "PROXY protocol socket type should be logged") + + # ---- Test without PROXY protocol ---- + tr2 = urtest.AddTestRun("Test without PROXY protocol") + tr2.Processes.Default.Command = f"curl http://127.0.0.1:{ts.Variables.port}/" + tr2.Processes.Default.ReturnCode = 0 + tr2.Processes.Default.Streams.stdout = Testers.ContainsExpression("TEST", "Response body should be received") + tr2.StillRunningAfter = server + tr2.StillRunningAfter = ts + + # Check that PP-Not-Present is logged when PROXY protocol is not used + ts.Disk.traffic_out.Content += Testers.ContainsExpression(r"PP-Version: 0", "Should log when PROXY protocol is not present") + urtest.execute() diff --git a/tests/uranium_tests/pluginTest/lua/test_lua_remap_after_hook.py b/tests/uranium_tests/pluginTest/lua/test_lua_remap_after_hook.py new file mode 100644 index 00000000000..6cb883eabe2 --- /dev/null +++ b/tests/uranium_tests/pluginTest/lua/test_lua_remap_after_hook.py @@ -0,0 +1,76 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_lua_remap_after_hook(urtest: UraniumTest) -> None: + ''' + Verify ts.remap.* APIs respect their documented do_remap-only context. + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + urtest.Summary = ''' + The ts.remap.* family is documented as context: do_remap. Verify that + calling one (ts.remap.get_from_url_host) from a transaction hook + registered during do_remap returns nil and does not crash, while the + same call inside do_remap returns the from-URL host. + ''' + + urtest.SkipUnless(Condition.PluginExists('tslua.so'),) + + urtest.ContinueOnFail = True + + ts = urtest.MakeATSProcess("ts") + server = urtest.MakeOriginServer("server") + + req = {"headers": "GET / HTTP/1.1\r\nHost: www.example.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + resp = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": "ok"} + server.addResponse("sessionfile.log", req, resp) + + ts.Disk.remap_config.AddLine( + 'map http://www.example.com/ http://127.0.0.1:{}/'.format(server.Variables.Port) + + ' @plugin=tslua.so @pparam=remap_after_hook.lua') + + ts.Setup.Copy("remap_after_hook.lua", ts.Variables.CONFIGDIR) + + ts.Disk.records_config.update({'proxy.config.diags.debug.enabled': 1, 'proxy.config.diags.debug.tags': 'ts_lua'}) + + tr = urtest.AddTestRun("ts.remap.* in do_remap and from a txn hook") + tr.MakeCurlCommand("-s -D - -H 'Host: www.example.com' http://127.0.0.1:{0}/".format(ts.Variables.port), ts=ts) + tr.Processes.Default.StartBefore(server, ready=When.PortOpen(server.Variables.Port)) + tr.Processes.Default.StartBefore(ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.stdout.Content = Testers.ContainsExpression( + "Remap-From-Host: www.example.com", "ts.remap.* should return the from-URL host inside do_remap") + tr.Processes.Default.Streams.stdout.Content += Testers.ContainsExpression( + "Hook-From-Host: ", "ts.remap.* should return nil when called outside do_remap") + tr.StillRunningAfter = server + urtest.execute() diff --git a/tests/uranium_tests/pluginTest/lua/test_lua_set_error_response.py b/tests/uranium_tests/pluginTest/lua/test_lua_set_error_response.py new file mode 100644 index 00000000000..10fb199e7a8 --- /dev/null +++ b/tests/uranium_tests/pluginTest/lua/test_lua_set_error_response.py @@ -0,0 +1,118 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_lua_set_error_response(urtest: UraniumTest) -> None: + '''Verify ts.client_response.set_error_resp functionality.''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import os + + urtest.Summary = ''' + Test lua functionality + ''' + + urtest.SkipUnless(Condition.PluginExists('tslua.so'),) + + class TestLuaSetErrorResponse: + '''Verify ts.client_response.set_error_resp functionality.''' + + replay_file: str = 'set_error_response.replay.yaml' + lua_script: str = 'set_error_response.lua' + + def __init__(self): + tr = urtest.AddTestRun("Lua ts.client_response.set_error_resp") + self._configure_dns(tr) + self._configure_server(tr) + self._configure_ts(tr) + self._configure_client(tr) + + def _configure_dns(self, tr: 'TestRun') -> 'Process': + '''Configure the DNS server. + + :param tr: The test run to configure the DNS server in. + :return: The newly created DNS process. + ''' + dns = tr.MakeDNServer("dns", default='127.0.0.1') + self._dns = dns + return dns + + def _configure_server(self, tr: 'TestRun') -> 'Process': + '''Configure the origin server. + + :param tr: The test run to configure the server in. + :return: The newly created server process. + ''' + server = tr.AddVerifierServerProcess("server", self.replay_file) + self._server = server + return server + + def _configure_ts(self, tr: 'TestRun') -> 'Process': + '''Configure the ATS process. + + :param tr: The test run to configure the ATS process in. + :return: The newly created ATS process. + ''' + ts = tr.MakeATSProcess("ts", enable_cache=False) + self._ts = ts + ts.Setup.Copy(self.lua_script, ts.Variables.CONFIGDIR) + port = self._server.Variables.http_port + ts.Disk.remap_config.AddLine(f'map / http://backend.example.com:{port}/ @plugin=tslua.so @pparam={self.lua_script}') + ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'ts_lua|http', + 'proxy.config.dns.nameservers': f"127.0.0.1:{self._dns.Variables.Port}", + 'proxy.config.dns.resolv_conf': 'NULL' + }) + return ts + + def _configure_client(self, tr: 'TestRun') -> 'Process': + '''Configure the client process. + + :param tr: The test run to configure the client process in. + :return: The newly created client process. + ''' + port = self._ts.Variables.port + client = tr.AddVerifierClientProcess('client', self.replay_file, http_ports=[port]) + self._client = client + client.StartBefore(self._server) + client.StartBefore(self._dns) + client.StartBefore(self._ts) + client.Streams.All += Testers.ContainsExpression( + 'HTTP/1.1 418', 'The modified HTTP 418 response should be received by the client.') + client.Streams.All += Testers.ContainsExpression( + 'bad luck', 'The modified HTTP response body should contain "bad luck".') + return client + + TestLuaSetErrorResponse() + urtest.execute() diff --git a/tests/uranium_tests/pluginTest/lua/test_lua_states_stats.py b/tests/uranium_tests/pluginTest/lua/test_lua_states_stats.py new file mode 100644 index 00000000000..ba1ece967fd --- /dev/null +++ b/tests/uranium_tests/pluginTest/lua/test_lua_states_stats.py @@ -0,0 +1,117 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_lua_states_stats(urtest: UraniumTest) -> None: + ''' + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + urtest.Summary = ''' + Test lua states and stats functionality + ''' + + urtest.SkipUnless(Condition.PluginExists('tslua.so'),) + + urtest.ContinueOnFail = True + # Define default ATS + server = urtest.MakeOriginServer("server") + + ts = urtest.MakeATSProcess("ts") + + urtest.testName = "Lua states and stats" + + urtest.Setup.Copy("hello.lua") + urtest.Setup.Copy("global.lua") + urtest.Setup.Copy("metrics.sh") + urtest.Setup.Copy("lifecycle_stats.sh") + + # test to ensure origin server works + request_header = {"headers": "GET / HTTP/1.1\r\nHost: www.example.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + + # add response to the server dictionary + server.addResponse("sessionfile.log", request_header, response_header) + + ts.Disk.remap_config.AddLines( + { + 'map / http://127.0.0.1:{}/'.format(server.Variables.Port), + 'map http://hello http://127.0.0.1:{}/'.format(server.Variables.Port) + + ' @plugin=tslua.so @pparam={}/hello.lua'.format(urtest.RunDirectory) + }) + + ts.Disk.plugin_config.AddLine('tslua.so {}/global.lua'.format(urtest.RunDirectory)) + + ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'ts_lua', + 'proxy.config.plugin.lua.max_states': 4, + }) + + curl_and_args = '-s -D /dev/stdout -o /dev/stderr -x localhost:{} '.format(ts.Variables.port) + + # 0 Test - Check for configured lua states + tr = urtest.AddTestRun("Lua states") + ps = tr.Processes.Default # alias + ps.StartBefore(server, ready=When.PortOpen(server.Variables.Port)) + ps.StartBefore(urtest.Processes.ts) + ps.Command = "traffic_ctl config match lua" + ps.Env = ts.Env + ps.ReturnCode = 0 + ps.Streams.stdout.Content = Testers.ContainsExpression("proxy.config.plugin.lua.max_states: 4", "expected 4 states") + tr.StillRunningAfter = ts + + # 1 Test - Exercise lua script + tr = urtest.AddTestRun("Lua hello") + ps = tr.Processes.Default # alias + tr.MakeCurlCommand(curl_and_args + ' http://hello/hello', ts=ts) + ps.ReturnCode = 0 + ps.Streams.stderr.Content = Testers.ContainsExpression("Hello, World", "hello world content") + tr.StillRunningAfter = ts + + # 2 Test - Check for metrics + tr = urtest.AddTestRun("Check for metrics") + ps = tr.Processes.Default # alias + ps.Command = "bash -c ./metrics.sh" + ps.Env = ts.Env + ps.ReturnCode = 0 + tr.StillRunningAfter = ts + + # 3 Test - Check for developer lifecycle stats + tr = urtest.AddTestRun("Check for lifecycle stats") + ps = tr.Processes.Default # alias + ps.Command = "bash -c ./lifecycle_stats.sh" + ps.Env = ts.Env + ps.ReturnCode = 0 + tr.StillRunningAfter = ts + urtest.execute() diff --git a/tests/uranium_tests/pluginTest/lua/test_lua_verified_addr.py b/tests/uranium_tests/pluginTest/lua/test_lua_verified_addr.py new file mode 100644 index 00000000000..d814985d3a5 --- /dev/null +++ b/tests/uranium_tests/pluginTest/lua/test_lua_verified_addr.py @@ -0,0 +1,101 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_lua_verified_addr(urtest: UraniumTest) -> None: + ''' + Test lua verified address functionality + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + urtest.Summary = ''' + Test lua verified address get/set functionality + ''' + + urtest.SkipUnless(Condition.PluginExists('tslua.so'),) + + urtest.ContinueOnFail = True + + # Define ATS process + ts = urtest.MakeATSProcess("ts") + + # Configure remap + ts.Disk.remap_config.AddLine(f"map / http://127.0.0.1 @plugin=tslua.so @pparam=verified_addr.lua") + + # Copy the Lua script + ts.Setup.Copy("verified_addr.lua", ts.Variables.CONFIGDIR) + + # Enable debug logging + ts.Disk.records_config.update({'proxy.config.diags.debug.enabled': 1, 'proxy.config.diags.debug.tags': 'ts_lua'}) + + # Test 1: IPv4 verified address + tr = urtest.AddTestRun("Lua Verified Address - IPv4") + ps = tr.Processes.Default + ps.StartBefore(urtest.Processes.ts) + tr.MakeCurlCommand(f"-s -H 'X-Real-IP: 192.0.2.100' http://127.0.0.1:{ts.Variables.port}", ts=ts) + ps.Env = ts.Env + ps.ReturnCode = 0 + ps.Streams.stdout.Content = Testers.ContainsExpression( + "initial:nil;set:success;get:192.0.2.100:2;", "IPv4 verified address should be set and retrieved correctly") + tr.StillRunningAfter = ts + + # Test 2: IPv6 verified address + tr = urtest.AddTestRun("Lua Verified Address - IPv6") + ps = tr.Processes.Default + tr.MakeCurlCommand(f"-s -H 'X-Real-IP-V6: 2001:db8::1' http://127.0.0.1:{ts.Variables.port}", ts=ts) + ps.Env = ts.Env + ps.ReturnCode = 0 + ps.Streams.stdout.Content = Testers.ContainsExpression( + "initial:nil;setv6:success;getv6:2001:db8::1:10;", "IPv6 verified address should be set and retrieved correctly") + tr.StillRunningAfter = ts + + # Test 3: Invalid IP address (should be rejected) + tr = urtest.AddTestRun("Lua Verified Address - Invalid IP") + ps = tr.Processes.Default + tr.MakeCurlCommand(f"-s -H 'X-Invalid-IP: not.a.valid.ip' http://127.0.0.1:{ts.Variables.port}", ts=ts) + ps.Env = ts.Env + ps.ReturnCode = 0 + ps.Streams.stdout.Content = Testers.ContainsExpression("invalid:rejected;", "Invalid IP address should be rejected") + tr.StillRunningAfter = ts + + # Test 4: Both IPv4 and IPv6 in sequence + tr = urtest.AddTestRun("Lua Verified Address - IPv4 then IPv6") + ps = tr.Processes.Default + tr.MakeCurlCommand( + f"-s -H 'X-Real-IP: 203.0.113.42' -H 'X-Real-IP-V6: 2001:db8::42' http://127.0.0.1:{ts.Variables.port}", ts=ts) + ps.Env = ts.Env + ps.ReturnCode = 0 + ps.Streams.stdout.Content = Testers.ContainsExpression( + "initial:nil;set:success;get:203.0.113.42:2;setv6:success;getv6:2001:db8::42:10;", + "Both IPv4 and IPv6 verified addresses should work in sequence") + tr.StillRunningAfter = ts + urtest.execute() diff --git a/tests/uranium_tests/pluginTest/lua/test_lua_watermark.py b/tests/uranium_tests/pluginTest/lua/test_lua_watermark.py new file mode 100644 index 00000000000..05a2203b596 --- /dev/null +++ b/tests/uranium_tests/pluginTest/lua/test_lua_watermark.py @@ -0,0 +1,78 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_lua_watermark(urtest: UraniumTest) -> None: + ''' + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import os + + urtest.Summary = ''' + Test lua functionality + ''' + + urtest.SkipUnless(Condition.PluginExists('tslua.so'),) + + urtest.ContinueOnFail = True + # Define default ATS + ts = urtest.MakeATSProcess("ts") + server = urtest.MakeOriginServer("server") + + urtest.testName = "" + request_header = {"headers": "GET / HTTP/1.1\r\nHost: www.example.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + # expected response from the origin server + response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + + # add response to the server dictionary + server.addResponse("sessionfile.log", request_header, response_header) + + ts.Disk.remap_config.AddLine( + 'map / http://127.0.0.1:{}/'.format(server.Variables.Port) + ' @plugin=tslua.so @pparam=watermark.lua') + + # Configure the tslua's configuration file. + ts.Setup.Copy("watermark.lua", ts.Variables.CONFIGDIR) + + ts.Disk.records_config.update({'proxy.config.diags.debug.enabled': 1, 'proxy.config.diags.debug.tags': 'ts_lua'}) + + # Test for watermark debug output + ts.Disk.traffic_out.Content = Testers.ContainsExpression(r"WMbytes\(31337\)", "Upstream watermark should be properly set") + + # Test if watermark upstream is set + tr = urtest.AddTestRun("Lua Watermark") + tr.MakeCurlCommand("-v http://127.0.0.1:{0}".format(ts.Variables.port), ts=ts) + tr.Processes.Default.StartBefore(server, ready=When.PortOpen(server.Variables.Port)) + + tr.Processes.Default.StartBefore(ts) + tr.StillRunningAfter = server + urtest.execute() diff --git a/tests/gold_tests/pluginTest/lua/verified_addr.lua b/tests/uranium_tests/pluginTest/lua/verified_addr.lua similarity index 100% rename from tests/gold_tests/pluginTest/lua/verified_addr.lua rename to tests/uranium_tests/pluginTest/lua/verified_addr.lua diff --git a/tests/gold_tests/pluginTest/lua/watermark.lua b/tests/uranium_tests/pluginTest/lua/watermark.lua similarity index 100% rename from tests/gold_tests/pluginTest/lua/watermark.lua rename to tests/uranium_tests/pluginTest/lua/watermark.lua diff --git a/tests/gold_tests/pluginTest/money_trace/gold/global-log.gold b/tests/uranium_tests/pluginTest/money_trace/gold/global-log.gold similarity index 100% rename from tests/gold_tests/pluginTest/money_trace/gold/global-log.gold rename to tests/uranium_tests/pluginTest/money_trace/gold/global-log.gold diff --git a/tests/gold_tests/pluginTest/money_trace/gold/remap-log.gold b/tests/uranium_tests/pluginTest/money_trace/gold/remap-log.gold similarity index 100% rename from tests/gold_tests/pluginTest/money_trace/gold/remap-log.gold rename to tests/uranium_tests/pluginTest/money_trace/gold/remap-log.gold diff --git a/tests/uranium_tests/pluginTest/money_trace/test_money_trace.py b/tests/uranium_tests/pluginTest/money_trace/test_money_trace.py new file mode 100644 index 00000000000..cd7f911e29c --- /dev/null +++ b/tests/uranium_tests/pluginTest/money_trace/test_money_trace.py @@ -0,0 +1,229 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_money_trace(urtest: UraniumTest) -> None: + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + import os + + urtest.Summary = ''' + Test money_trace remap + ''' + + # Test description: + + urtest.SkipUnless(Condition.PluginExists('money_trace.so'),) + urtest.ContinueOnFail = False + urtest.testName = "money_trace remap" + + # Define ATS and configure + ts = urtest.MakeATSProcess("ts", enable_cache=False) + + # configure origin server + server = urtest.MakeOriginServer("server") + + req_chk = {"headers": "GET / HTTP/1.1\r\n" + "Host: origin\r\n" + "\r\n", "timestamp": "1469733493.993", "body": ""} + res_chk = {"headers": "HTTP/1.1 200 OK\r\n" + "Connection: close\r\n" + "\r\n", "timestamp": "1469733493.993", "body": ""} + server.addResponse("sessionlog.json", req_chk, res_chk) + + req_hdr = { + "headers": "GET /path HTTP/1.1\r\n" + "Host: www.example.com\r\n" + "\r\n", + "timestamp": "1469733493.993", + "body": "" + } + res_hdr = {"headers": "HTTP/1.1 200 OK\r\n" + "Connection: close\r\n" + "\r\n", "timestamp": "1469733493.993", "body": ""} + server.addResponse("sessionlog.json", req_hdr, res_hdr) + + ts.Disk.remap_config.AddLines( + [ + f"map http://none/ http://127.0.0.1:{server.Variables.Port}", + f"map http://basic/ http://127.0.0.1:{server.Variables.Port} @plugin=money_trace.so", + f"map http://header/ http://127.0.0.1:{server.Variables.Port} @plugin=money_trace.so @pparam=--header=mt", + f"map http://pregen/ http://127.0.0.1:{server.Variables.Port} @plugin=money_trace.so @pparam=--pregen-header=@pregen", + f"map http://pgh/ http://127.0.0.1:{server.Variables.Port} @plugin=money_trace.so @pparam=--header=mt @pparam=--pregen-header=@pregen", + f"map http://create/ http://127.0.0.1:{server.Variables.Port} @plugin=money_trace.so @pparam=--create-if-none=true", + f"map http://cheader/ http://127.0.0.1:{server.Variables.Port} @plugin=money_trace.so @pparam=--create-if-none=true @pparam=--header=mt", + f"map http://cpregen/ http://127.0.0.1:{server.Variables.Port} @plugin=money_trace.so @pparam=--create-if-none=true @pparam=--pregen-header=@pregen", + f"map http://passthru/ http://127.0.0.1:{server.Variables.Port} @plugin=money_trace.so @pparam=--passthru=true", + ]) + + # minimal configuration + ts.Disk.records_config.update({ + 'proxy.config.diags.debug.enabled': 0, + 'proxy.config.diags.debug.tags': 'money_trace', + }) + + ts.Disk.logging_yaml.AddLines( + ''' + logging: + formats: + - name: custom + format: 'cqh: %<{X-MoneyTrace}cqh> %<{mt}cqh> %<{@pregen}cqh> pqh: %<{X-MoneyTrace}pqh> %<{mt}pqh> psh: %<{X-MoneyTrace}psh> %<{mt}psh>' + logs: + - filename: remap + format: custom + '''.split("\n")) + + urtest.Disk.File(os.path.join(ts.Variables.LOGDIR, 'remap.log'), exists=True, content='gold/remap-log.gold') + + curl_and_args = f"-s -D /dev/stdout -o /dev/stderr -x 127.0.0.1:{ts.Variables.port}" + + # 0 Test + tr = urtest.AddTestRun("no plugin test") + ps = tr.Processes.Default + ps.StartBefore(server) + ps.StartBefore(urtest.Processes.ts) + tr.MakeCurlCommand(curl_and_args + " http://none/path", ts=ts) + ps.ReturnCode = 0 + tr.StillRunningAfter = ts + tr.StillRunningAfter = server + + # 1 Test + tr = urtest.AddTestRun("basic config, no money trace client header") + ps = tr.Processes.Default + tr.MakeCurlCommand(curl_and_args + " http://basic/path", ts=ts) + ps.ReturnCode = 0 + tr.StillRunningAfter = ts + tr.StillRunningAfter = server + + def maketrace(name): + return f'trace-id={name};parent-id=foo;span-id=bar' + + # 2 Test + tr = urtest.AddTestRun("basic config, money trace client header") + ps = tr.Processes.Default + tr.MakeCurlCommand(curl_and_args + ' http://basic/path -H "X-MoneyTrace: ' + maketrace("basic") + '"', ts=ts) + ps.ReturnCode = 0 + tr.StillRunningAfter = ts + tr.StillRunningAfter = server + + # 3 Test + tr = urtest.AddTestRun("header config, mt client header") + ps = tr.Processes.Default + tr.MakeCurlCommand(curl_and_args + ' http://header/path -H "mt: ' + maketrace("header") + '"', ts=ts) + ps.ReturnCode = 0 + tr.StillRunningAfter = ts + tr.StillRunningAfter = server + + # 4 Test + tr = urtest.AddTestRun("pregen config, but no header passed in") + ps = tr.Processes.Default + tr.MakeCurlCommand(curl_and_args + ' http://pregen/path', ts=ts) + ps.ReturnCode = 0 + tr.StillRunningAfter = ts + tr.StillRunningAfter = server + + # 5 Test + tr = urtest.AddTestRun("pregen config, money trace client header") + ps = tr.Processes.Default + tr.MakeCurlCommand(curl_and_args + ' http://pregen/path -H "X-MoneyTrace: ' + maketrace("pregen") + '"', ts=ts) + ps.ReturnCode = 0 + tr.StillRunningAfter = ts + tr.StillRunningAfter = server + + # 6 Test + tr = urtest.AddTestRun("pregen config, mt client header") + ps = tr.Processes.Default + tr.MakeCurlCommand(curl_and_args + ' http://pgh/path -H "mt: ' + maketrace("pgh") + '"', ts=ts) + ps.ReturnCode = 0 + tr.StillRunningAfter = ts + tr.StillRunningAfter = server + + # 7 Test + tr = urtest.AddTestRun("create config, money trace client header") + ps = tr.Processes.Default + tr.MakeCurlCommand(curl_and_args + ' http://create/path', ts=ts) + ps.ReturnCode = 0 + tr.StillRunningAfter = ts + tr.StillRunningAfter = server + + # 8 Test + tr = urtest.AddTestRun("create config, mt client header") + ps = tr.Processes.Default + tr.MakeCurlCommand(curl_and_args + ' http://cheader/path', ts=ts) + ps.ReturnCode = 0 + tr.StillRunningAfter = ts + tr.StillRunningAfter = server + + # 9 Test + tr = urtest.AddTestRun("create config, pregen client header") + ps = tr.Processes.Default + tr.MakeCurlCommand(curl_and_args + ' http://cpregen/path', ts=ts) + ps.ReturnCode = 0 + tr.StillRunningAfter = ts + tr.StillRunningAfter = server + + # 10 Test + tr = urtest.AddTestRun("passthru mode") + ps = tr.Processes.Default + tr.MakeCurlCommand(curl_and_args + ' http://passthru/path -H "X-MoneyTrace: ' + maketrace("passthru") + '"', ts=ts) + ps.ReturnCode = 0 + tr.StillRunningAfter = ts + tr.StillRunningAfter = server + + # Parsing robustness + + trace_strings = [ + "trace-id=spaces; parent-id=foo; span-id=bar", + "trace-id=noparent;span-id=8215111;span-name=this is some description", + "trace-id=badspan;span-id=", + "trace-id=traceonly", + "trace-id=traceonlysemi;", + "not a trace header", + ] + + # 11 Test + for trace in trace_strings: + tr = urtest.AddTestRun(trace) + ps = tr.Processes.Default + tr.MakeCurlCommand(curl_and_args + ' http://pregen/path -H "X-MoneyTrace: ' + trace + '"', ts=ts) + ps.ReturnCode = 0 + tr.StillRunningAfter = ts + tr.StillRunningAfter = server + + # 11 Test + for trace in trace_strings: + tr = urtest.AddTestRun(trace) + ps = tr.Processes.Default + tr.MakeCurlCommand(curl_and_args + ' http://cpregen/path -H "X-MoneyTrace: ' + trace + '"', ts=ts) + ps.ReturnCode = 0 + tr.StillRunningAfter = ts + tr.StillRunningAfter = server + + # Wait for the final log line to be written. + # 11 Test + urtest.AddAwaitFileContainsTestRun( + 'Await final money_trace remap log line.', + os.path.join(ts.Variables.LOGDIR, 'remap.log'), + r'^cqh: not a trace header - trace-id=.* pqh: trace-id=.* - psh: not a trace header -$', + ) + urtest.execute() diff --git a/tests/uranium_tests/pluginTest/money_trace/test_money_trace_global.py b/tests/uranium_tests/pluginTest/money_trace/test_money_trace_global.py new file mode 100644 index 00000000000..5f6c7f33e62 --- /dev/null +++ b/tests/uranium_tests/pluginTest/money_trace/test_money_trace_global.py @@ -0,0 +1,132 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_money_trace_global(urtest: UraniumTest) -> None: + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import os + + urtest.Summary = ''' + Test money_trace global + ''' + + # Test description: + + urtest.SkipUnless( + # Condition.PluginExists('xdebug.so'), + Condition.PluginExists('money_trace.so'),) + urtest.ContinueOnFail = False + urtest.testName = "money_trace global" + + # Define ATS and configure + ts = urtest.MakeATSProcess("ts", enable_cache=False) + + # configure origin server + server = urtest.MakeOriginServer("server") + + req_chk = {"headers": "GET / HTTP/1.1\r\n" + "Host: origin\r\n" + "\r\n", "timestamp": "1469733493.993", "body": ""} + res_chk = {"headers": "HTTP/1.1 200 OK\r\n" + "Connection: close\r\n" + "\r\n", "timestamp": "1469733493.993", "body": ""} + server.addResponse("sessionlog.json", req_chk, res_chk) + + req_hdr = { + "headers": "GET /path HTTP/1.1\r\n" + "Host: www.example.com\r\n" + "\r\n", + "timestamp": "1469733493.993", + "body": "" + } + res_hdr = {"headers": "HTTP/1.1 200 OK\r\n" + "Connection: close\r\n" + "\r\n", "timestamp": "1469733493.993", "body": ""} + server.addResponse("sessionlog.json", req_hdr, res_hdr) + + ts.Disk.remap_config.AddLines([ + f"map http://ats http://127.0.0.1:{server.Variables.Port}", + ]) + + ts.Disk.plugin_config.AddLine( + "money_trace.so --pregen-header=@pregen --header=MoneyTrace --create-if-none=true --global-skip-header=Skip-Global-MoneyTrace" + ) + + # minimal configuration + ts.Disk.records_config.update({ + 'proxy.config.diags.debug.enabled': 0, + 'proxy.config.diags.debug.tags': 'money_trace', + }) + + ts.Disk.logging_yaml.AddLines( + ''' + logging: + formats: + - name: custom + format: 'cqh: %<{MoneyTrace}cqh> %<{@pregen}cqh> pqh: %<{MoneyTrace}pqh> psh: %<{MoneyTrace}psh>' + logs: + - filename: global + format: custom + '''.split("\n")) + + urtest.Disk.File(os.path.join(ts.Variables.LOGDIR, 'global.log'), exists=True, content='gold/global-log.gold') + + curl_and_args = f"-s -D /dev/stdout -o /dev/stderr -x 127.0.0.1:{ts.Variables.port}" # -H 'X-Debug: Probe' " + + clientvalue = "trace-id=foo;parent-id=bar;span-id=baz" + + # 0 Test + tr = urtest.AddTestRun("normal header test") + ps = tr.Processes.Default + ps.StartBefore(server, ready=When.PortOpen(server.Variables.Port)) + ps.StartBefore(urtest.Processes.ts) + tr.MakeCurlCommand(curl_and_args + ' http://ats/path -H "MoneyTrace: ' + clientvalue + '"', ts=ts) + ps.ReturnCode = 0 + tr.StillRunningAfter = ts + tr.StillRunningAfter = server + + # 1 Test + tr = urtest.AddTestRun("skip plugin test - pregen will still be set") + ps = tr.Processes.Default + tr.MakeCurlCommand(curl_and_args + ' http://ats/path -H "Skip-Global-MoneyTrace: true"', ts=ts) + ps.ReturnCode = 0 + tr.StillRunningAfter = ts + tr.StillRunningAfter = server + + # 2 Test + tr = urtest.AddTestRun("create header test") + ps = tr.Processes.Default + tr.MakeCurlCommand(curl_and_args + ' http://ats/path', ts=ts) + ps.ReturnCode = 0 + tr.StillRunningAfter = ts + tr.StillRunningAfter = server + + # Wait for the final log line to be written. + urtest.AddAwaitFileContainsTestRun( + 'Await final money_trace global log line.', + os.path.join(ts.Variables.LOGDIR, 'global.log'), + r'^cqh: - trace-id=.* pqh: trace-id=.* psh: -$', + ) + urtest.execute() diff --git a/tests/gold_tests/pluginTest/multiplexer/replays/multiplexer_copy.replay.yaml b/tests/uranium_tests/pluginTest/multiplexer/replays/multiplexer_copy.replay.yaml similarity index 100% rename from tests/gold_tests/pluginTest/multiplexer/replays/multiplexer_copy.replay.yaml rename to tests/uranium_tests/pluginTest/multiplexer/replays/multiplexer_copy.replay.yaml diff --git a/tests/gold_tests/pluginTest/multiplexer/replays/multiplexer_copy_skip_post.replay.yaml b/tests/uranium_tests/pluginTest/multiplexer/replays/multiplexer_copy_skip_post.replay.yaml similarity index 100% rename from tests/gold_tests/pluginTest/multiplexer/replays/multiplexer_copy_skip_post.replay.yaml rename to tests/uranium_tests/pluginTest/multiplexer/replays/multiplexer_copy_skip_post.replay.yaml diff --git a/tests/gold_tests/pluginTest/multiplexer/replays/multiplexer_invalid_chunk_copy.replay.yaml b/tests/uranium_tests/pluginTest/multiplexer/replays/multiplexer_invalid_chunk_copy.replay.yaml similarity index 100% rename from tests/gold_tests/pluginTest/multiplexer/replays/multiplexer_invalid_chunk_copy.replay.yaml rename to tests/uranium_tests/pluginTest/multiplexer/replays/multiplexer_invalid_chunk_copy.replay.yaml diff --git a/tests/gold_tests/pluginTest/multiplexer/replays/multiplexer_invalid_chunk_original.replay.yaml b/tests/uranium_tests/pluginTest/multiplexer/replays/multiplexer_invalid_chunk_original.replay.yaml similarity index 100% rename from tests/gold_tests/pluginTest/multiplexer/replays/multiplexer_invalid_chunk_original.replay.yaml rename to tests/uranium_tests/pluginTest/multiplexer/replays/multiplexer_invalid_chunk_original.replay.yaml diff --git a/tests/gold_tests/pluginTest/multiplexer/replays/multiplexer_original.replay.yaml b/tests/uranium_tests/pluginTest/multiplexer/replays/multiplexer_original.replay.yaml similarity index 100% rename from tests/gold_tests/pluginTest/multiplexer/replays/multiplexer_original.replay.yaml rename to tests/uranium_tests/pluginTest/multiplexer/replays/multiplexer_original.replay.yaml diff --git a/tests/gold_tests/pluginTest/multiplexer/replays/multiplexer_original_skip_post.replay.yaml b/tests/uranium_tests/pluginTest/multiplexer/replays/multiplexer_original_skip_post.replay.yaml similarity index 100% rename from tests/gold_tests/pluginTest/multiplexer/replays/multiplexer_original_skip_post.replay.yaml rename to tests/uranium_tests/pluginTest/multiplexer/replays/multiplexer_original_skip_post.replay.yaml diff --git a/tests/uranium_tests/pluginTest/multiplexer/test_multiplexer.py b/tests/uranium_tests/pluginTest/multiplexer/test_multiplexer.py new file mode 100644 index 00000000000..b06364381a6 --- /dev/null +++ b/tests/uranium_tests/pluginTest/multiplexer/test_multiplexer.py @@ -0,0 +1,343 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_multiplexer(urtest: UraniumTest) -> None: + ''' + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import os + + urtest.Summary = ''' + Test the Multiplexer plugin. + ''' + + urtest.SkipUnless(Condition.PluginExists('multiplexer.so')) + + class MultiplexerTestBase: + """ + Encapsulates the base configuration used by each test. + """ + + client_counter = 0 + dns_counter = 0 + server_counter = 0 + ts_counter = 0 + + def __init__(self, replay_file, multiplexed_host_replay_file, skip_post): + self.replay_file = replay_file + self.multiplexed_host_replay_file = multiplexed_host_replay_file + + self.setupServers() + self.setupDns() + self.setupTS(skip_post) + + def setupDns(self): + counter = MultiplexerTestBase.dns_counter + MultiplexerTestBase.dns_counter += 1 + self.dns = urtest.MakeDNServer(f"dns_{counter}", default='127.0.0.1') + + def setupServers(self): + counter = MultiplexerTestBase.server_counter + MultiplexerTestBase.server_counter += 1 + self.server_origin = urtest.MakeVerifierServerProcess(f"server_origin_{counter}", self.replay_file) + self.server_http = urtest.MakeVerifierServerProcess(f"server_http_{counter}", self.multiplexed_host_replay_file) + self.server_https = urtest.MakeVerifierServerProcess(f"server_https_{counter}", self.multiplexed_host_replay_file) + + # The origin should never receive "X-Multiplexer: copy" + self.server_origin.Streams.All += Testers.ExcludesExpression( + 'X-Multiplexer: copy', 'Verify the original server target never receives a "copy".') + + # Nor should the multiplexed hosts receive an "original" X-Multiplexer value. + self.server_http.Streams.All += Testers.ExcludesExpression( + 'X-Multiplexer: original', 'Verify the HTTP multiplexed host does not receive an "original".') + self.server_https.Streams.All += Testers.ExcludesExpression( + 'X-Multiplexer: original', 'Verify the HTTPS multiplexed host does not receive an "original".') + self.server_https.Streams.All += Testers.ExcludesExpression(r'\[ERROR\]', 'Verify there were no errors in the replay.') + + # In addition, the original server should always receive the POST and + # PUT requests. + self.server_origin.Streams.All += Testers.ContainsExpression( + 'uuid: POST', "Verify the client's original target received the POST transaction.") + self.server_origin.Streams.All += Testers.ContainsExpression( + 'uuid: PUT', "Verify the client's original target received the PUT transaction.") + self.server_origin.Streams.All += Testers.ExcludesExpression(r'\[ERROR\]', 'Verify there were no errors in the replay.') + # The chunked POST should go to the origin. + self.server_origin.Streams.All += Testers.ContainsExpression( + 'uuid: CHUNKED_POST', "Verify the client's original target received the chunked POST transaction.") + + # Under all configurations, the GET request should be multiplexed. + self.server_origin.Streams.All += Testers.ContainsExpression( + 'X-Multiplexer: original', 'Verify the client\'s original target received the "original" request.') + self.server_origin.Streams.All += Testers.ContainsExpression( + 'uuid: GET', "Verify the client's original target received the GET request.") + + self.server_http.Streams.All += Testers.ContainsExpression( + 'X-Multiplexer: copy', 'Verify the HTTP server received a "copy" of the request.') + self.server_http.Streams.All += Testers.ContainsExpression( + 'uuid: GET', "Verify the HTTP server received the GET request.") + self.server_http.Streams.All += Testers.ExcludesExpression(r'\[ERROR\]', 'Verify there were no errors in the replay.') + # Chunked POST requests are not supported for multiplexing. + self.server_http.Streams.All += Testers.ExcludesExpression( + 'uuid: CHUNKED_POST', 'We do not expect a multiplexed chunked POST.') + + self.server_https.Streams.All += Testers.ContainsExpression( + 'X-Multiplexer: copy', 'Verify the HTTPS server received a "copy" of the request.') + self.server_https.Streams.All += Testers.ContainsExpression( + 'uuid: GET', "Verify the HTTPS server received the GET request.") + # Chunked POST requests are not supported for multiplexing. + self.server_https.Streams.All += Testers.ExcludesExpression( + 'uuid: CHUNKED_POST', 'We do not expect a multiplexed chunked POST.') + + # Verify that the CUSTOM_METHOD is sent to all servers. + self.server_origin.Streams.All += Testers.ContainsExpression( + 'uuid: MYCUSTOMMETHOD', "Verify the client's original target received the MYCUSTOMMETHOD transaction.") + self.server_http.Streams.All += Testers.ContainsExpression( + 'uuid: MYCUSTOMMETHOD', "Verify the HTTP server received the MYCUSTOMMETHOD request.") + self.server_http.Streams.All += Testers.ContainsExpression( + 'x-response: fifth', "Verify the HTTP server sent the MYCUSTOMMETHOD response.") + self.server_https.Streams.All += Testers.ContainsExpression( + 'uuid: MYCUSTOMMETHOD', "Verify the HTTPS server received the MYCUSTOMMETHOD request.") + self.server_https.Streams.All += Testers.ContainsExpression( + 'x-response: fifth', "Verify the HTTPS server sent the MYCUSTOMMETHOD response.") + + # Verify that the HTTPS server receives a TLS connection. + self.server_https.Streams.All += Testers.ContainsExpression( + 'Finished accept using TLSSession', "Verify the HTTPS was indeed used by the HTTPS server.") + + def setupTS(self, skip_post): + counter = MultiplexerTestBase.ts_counter + MultiplexerTestBase.ts_counter += 1 + self.ts = urtest.MakeATSProcess(f"ts_{counter}", enable_tls=True, enable_cache=False) + self.ts.addDefaultSSLFiles() + self.ts.Disk.records_config.update( + { + "proxy.config.ssl.server.cert.path": f'{self.ts.Variables.SSLDir}', + "proxy.config.ssl.server.private_key.path": f'{self.ts.Variables.SSLDir}', + "proxy.config.ssl.client.verify.server.policy": 'PERMISSIVE', + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'http|multiplexer', + 'proxy.config.dns.nameservers': f'127.0.0.1:{self.dns.Variables.Port}', + 'proxy.config.dns.resolv_conf': 'NULL', + }) + self.ts.Disk.ssl_multicert_yaml.AddLines( + """ + ssl_multicert: + - dest_ip: "*" + ssl_cert_name: server.pem + ssl_key_name: server.key + """.split("\n")) + skip_remap_param = '' + if skip_post: + skip_remap_param = ' @pparam=proxy.config.multiplexer.skip_post_put=1' + self.ts.Disk.remap_config.AddLines( + [ + f'map https://origin.server.com https://backend.origin.server.com:{self.server_origin.Variables.https_port} ' + f'@plugin=multiplexer.so @pparam=nontls.server.com @pparam=tls.server.com' + f'{skip_remap_param}', + + # Now create remap entries for the multiplexed hosts: one that + # verifies HTTP, and another that verifies HTTPS. + f'map http://nontls.server.com http://backend.nontls.server.com:{self.server_http.Variables.http_port}', + f'map http://tls.server.com https://backend.tls.server.com:{self.server_https.Variables.https_port}', + ]) + + def run(self): + tr = urtest.AddTestRun() + self.ts.StartBefore(self.dns) + tr.Processes.Default.StartBefore(self.server_origin) + tr.Processes.Default.StartBefore(self.server_http) + tr.Processes.Default.StartBefore(self.server_https) + tr.Processes.Default.StartBefore(self.ts) + + counter = MultiplexerTestBase.client_counter + MultiplexerTestBase.client_counter += 1 + client = tr.AddVerifierClientProcess(f"client_{counter}", self.replay_file, https_ports=[self.ts.Variables.ssl_port]) + client.Streams.All += Testers.ExcludesExpression(r'\[ERROR\]', 'Verify there were no errors in the replay.') + + class MultiplexerTest(MultiplexerTestBase): + """ + Exercise multiplexing without skip_post configuration. + """ + + replay_file = os.path.join("replays", "multiplexer_original.replay.yaml") + multiplexed_host_replay_file = os.path.join("replays", "multiplexer_copy.replay.yaml") + + def __init__(self): + super().__init__(MultiplexerTest.replay_file, MultiplexerTest.multiplexed_host_replay_file, skip_post=False) + + def setupServers(self): + super().setupServers() + + # Both of the multiplexed hosts should receive the POST because skip_post + # is disabled. + self.server_http.Streams.All += Testers.ContainsExpression( + 'uuid: POST', "Verify the HTTP server received the POST request.") + self.server_https.Streams.All += Testers.ContainsExpression( + 'uuid: POST', "Verify the HTTPS server received the POST request.") + self.server_http.Streams.All += Testers.ContainsExpression( + 'x-response: second', "Verify the HTTP server sent the POST response.") + self.server_https.Streams.All += Testers.ContainsExpression( + 'x-response: second', "Verify the HTTPS server sent the POST response.") + self.server_https.Streams.All += Testers.ContainsExpression( + 'x-response: fifth', "Verify the HTTPS server sent the custom method response.") + + # Same with PUT + self.server_http.Streams.All += Testers.ContainsExpression( + 'uuid: PUT', "Verify the HTTP server received the PUT request.") + self.server_https.Streams.All += Testers.ContainsExpression( + 'uuid: PUT', "Verify the HTTPS server received the PUT request.") + + # Same with MYCUSTOMMETHOD + self.server_http.Streams.All += Testers.ContainsExpression( + 'uuid: MYCUSTOMMETHOD', "Verify the HTTP server received the custom method request.") + self.server_https.Streams.All += Testers.ContainsExpression( + 'uuid: MYCUSTOMMETHOD', "Verify the HTTPS server received the custom method request.") + + class MultiplexerSkipPostTest(MultiplexerTestBase): + """ + Exercise multiplexing with skip_post configuration. + """ + + replay_file = os.path.join("replays", "multiplexer_original_skip_post.replay.yaml") + multiplexed_host_replay_file = os.path.join("replays", "multiplexer_copy_skip_post.replay.yaml") + + def __init__(self): + super().__init__( + MultiplexerSkipPostTest.replay_file, MultiplexerSkipPostTest.multiplexed_host_replay_file, skip_post=True) + + def setupServers(self): + super().setupServers() + + # Neither of the multiplexed hosts should receive the POST because skip_post + # is enabled. + self.server_http.Streams.All += Testers.ExcludesExpression( + 'uuid: POST', "Verify the HTTP server did not receive the POST request.") + self.server_https.Streams.All += Testers.ExcludesExpression( + 'uuid: POST', "Verify the HTTPS server did not receive the POST request.") + + # Same with PUT. + self.server_http.Streams.All += Testers.ExcludesExpression( + 'uuid: PUT', "Verify the HTTP server did not receive the PUT request.") + self.server_https.Streams.All += Testers.ExcludesExpression( + 'uuid: PUT', "Verify the HTTPS server did not receive the PUT request.") + + class MultiplexerInvalidChunkedResponseTest: + """ + Verify a copied upstream response with an oversized chunk-size does not + disrupt the original client transaction. + """ + + replay_file = os.path.join("replays", "multiplexer_invalid_chunk_original.replay.yaml") + multiplexed_host_replay_file = os.path.join("replays", "multiplexer_invalid_chunk_copy.replay.yaml") + + def __init__(self): + self.setupServers() + self.setupDns() + self.setupTS() + + def setupDns(self): + counter = MultiplexerTestBase.dns_counter + MultiplexerTestBase.dns_counter += 1 + self.dns = urtest.MakeDNServer(f"dns_{counter}", default='127.0.0.1') + + def setupServers(self): + counter = MultiplexerTestBase.server_counter + MultiplexerTestBase.server_counter += 1 + self.server_origin = urtest.MakeVerifierServerProcess(f"server_origin_{counter}", self.replay_file) + self.server_http = urtest.MakeVerifierServerProcess(f"server_http_{counter}", self.multiplexed_host_replay_file) + self.server_https = urtest.MakeVerifierServerProcess(f"server_https_{counter}", self.multiplexed_host_replay_file) + + self.server_origin.Streams.All += Testers.ContainsExpression( + 'uuid: INVALID_CHUNK', "Verify the original server received the invalid chunk test request.") + self.server_origin.Streams.All += Testers.ContainsExpression( + 'X-Multiplexer: original', 'Verify the original target received the "original" request.') + self.server_origin.Streams.All += Testers.ExcludesExpression(r'\[ERROR\]', 'Verify there were no errors in the replay.') + + for server in [self.server_http, self.server_https]: + server.Streams.All += Testers.ContainsExpression( + 'uuid: INVALID_CHUNK', "Verify the multiplexed server received the invalid chunk test request.") + server.Streams.All += Testers.ContainsExpression( + 'X-Multiplexer: copy', 'Verify the multiplexed server received a "copy" of the request.') + server.Streams.All += Testers.ExcludesExpression(r'\[ERROR\]', 'Verify there were no errors in the replay.') + + self.server_https.Streams.All += Testers.ContainsExpression( + 'Finished accept using TLSSession', "Verify the HTTPS was indeed used by the HTTPS server.") + + def setupTS(self): + counter = MultiplexerTestBase.ts_counter + MultiplexerTestBase.ts_counter += 1 + self.ts = urtest.MakeATSProcess(f"ts_{counter}", enable_tls=True, enable_cache=False) + self.ts.addDefaultSSLFiles() + self.ts.Disk.records_config.update( + { + "proxy.config.ssl.server.cert.path": f'{self.ts.Variables.SSLDir}', + "proxy.config.ssl.server.private_key.path": f'{self.ts.Variables.SSLDir}', + "proxy.config.ssl.client.verify.server.policy": 'PERMISSIVE', + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'http|multiplexer', + 'proxy.config.dns.nameservers': f'127.0.0.1:{self.dns.Variables.Port}', + 'proxy.config.dns.resolv_conf': 'NULL', + }) + self.ts.Disk.ssl_multicert_yaml.AddLines( + """ + ssl_multicert: + - dest_ip: "*" + ssl_cert_name: server.pem + ssl_key_name: server.key + """.split("\n")) + self.ts.Disk.remap_config.AddLines( + [ + f'map https://origin.server.com https://backend.origin.server.com:{self.server_origin.Variables.https_port} ' + f'@plugin=multiplexer.so @pparam=nontls.server.com @pparam=tls.server.com', + f'map http://nontls.server.com http://backend.nontls.server.com:{self.server_http.Variables.http_port}', + f'map http://tls.server.com https://backend.tls.server.com:{self.server_https.Variables.https_port}', + ]) + + def run(self): + tr = urtest.AddTestRun("Multiplexed response with oversized chunk-size") + self.ts.StartBefore(self.dns) + tr.Processes.Default.StartBefore(self.server_origin) + tr.Processes.Default.StartBefore(self.server_http) + tr.Processes.Default.StartBefore(self.server_https) + tr.Processes.Default.StartBefore(self.ts) + + counter = MultiplexerTestBase.client_counter + MultiplexerTestBase.client_counter += 1 + client = tr.AddVerifierClientProcess(f"client_{counter}", self.replay_file, https_ports=[self.ts.Variables.ssl_port]) + client.Streams.All += Testers.ExcludesExpression(r'\[ERROR\]', 'Verify there were no errors in the replay.') + + MultiplexerTest().run() + MultiplexerSkipPostTest().run() + MultiplexerInvalidChunkedResponseTest().run() + urtest.execute() diff --git a/tests/gold_tests/pluginTest/origin_server_auth/gold/origin_server_auth_parsing_ts.gold b/tests/uranium_tests/pluginTest/origin_server_auth/gold/origin_server_auth_parsing_ts.gold similarity index 100% rename from tests/gold_tests/pluginTest/origin_server_auth/gold/origin_server_auth_parsing_ts.gold rename to tests/uranium_tests/pluginTest/origin_server_auth/gold/origin_server_auth_parsing_ts.gold diff --git a/tests/gold_tests/pluginTest/origin_server_auth/gold/origin_server_auth_parsing_ts_uds 2.gold b/tests/uranium_tests/pluginTest/origin_server_auth/gold/origin_server_auth_parsing_ts_uds 2.gold similarity index 100% rename from tests/gold_tests/pluginTest/origin_server_auth/gold/origin_server_auth_parsing_ts_uds 2.gold rename to tests/uranium_tests/pluginTest/origin_server_auth/gold/origin_server_auth_parsing_ts_uds 2.gold diff --git a/tests/gold_tests/pluginTest/origin_server_auth/gold/origin_server_auth_parsing_ts_uds.gold b/tests/uranium_tests/pluginTest/origin_server_auth/gold/origin_server_auth_parsing_ts_uds.gold similarity index 100% rename from tests/gold_tests/pluginTest/origin_server_auth/gold/origin_server_auth_parsing_ts_uds.gold rename to tests/uranium_tests/pluginTest/origin_server_auth/gold/origin_server_auth_parsing_ts_uds.gold diff --git a/tests/gold_tests/pluginTest/origin_server_auth/rules/s3_url_encoding.test_input b/tests/uranium_tests/pluginTest/origin_server_auth/rules/s3_url_encoding.test_input similarity index 100% rename from tests/gold_tests/pluginTest/origin_server_auth/rules/s3_url_encoding.test_input rename to tests/uranium_tests/pluginTest/origin_server_auth/rules/s3_url_encoding.test_input diff --git a/tests/gold_tests/pluginTest/origin_server_auth/rules/v4-parse-test.test_input b/tests/uranium_tests/pluginTest/origin_server_auth/rules/v4-parse-test.test_input similarity index 100% rename from tests/gold_tests/pluginTest/origin_server_auth/rules/v4-parse-test.test_input rename to tests/uranium_tests/pluginTest/origin_server_auth/rules/v4-parse-test.test_input diff --git a/tests/uranium_tests/pluginTest/origin_server_auth/test_origin_server_auth.py b/tests/uranium_tests/pluginTest/origin_server_auth/test_origin_server_auth.py new file mode 100644 index 00000000000..ff92e70de01 --- /dev/null +++ b/tests/uranium_tests/pluginTest/origin_server_auth/test_origin_server_auth.py @@ -0,0 +1,156 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_origin_server_auth(urtest: UraniumTest) -> None: + ''' + Test origin_server_auth config parsing + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + urtest.testName = "origin_server_auth: config parsing" + urtest.Summary = ''' + Test for successful authentication to origin server + ''' + urtest.ContinueOnFail = True + + class OriginServerAuthTest: + + def __init__(self): + self.setUpOriginServer() + self.setUpTS() + + def setUpOriginServer(self): + self.server = urtest.MakeOriginServer("server") + # define the s3 request header and the desired response header + request_header = { + "headers": "GET /s3-bucket HTTP/1.1\r\nHost: www.example.com\r\n\r\n", + "timestamp": "1469733493.993", + "x-amz-security-token": + "hkMsi6/bfHyBKrSeM/H0hoXeyx8z1yZ/mJ0c+B/TqYx=tTJDjnQWtul38Z9iVJjeH1HB4VT2c=2o3yE3o=I9kmFs/lJDR85qWjB8e5asY/WbjyRpbAzmDipQpboIcYnUYg55bxrQFidV/q8gZa5A9MpR3n=op1C0lWjeBqcEJxpevNZxteSQTQfeGsi98Cdf+On=/SINVlKrNhMnmMsDOLMGx1YYt9d4UsRg1jtVrwxL4Vd/F7aHCZySAXKv+1rkhACR023wpa3dhp+xirGJxSO9LWwvcrTdM4xJo4RS8B40tGENOJ1NKixUJxwN/6og58Oft/u==uleR89Ja=7zszK2H7tX3DqmEYNvNDYQh/7VBRe5otghQtPwJzWpXAGk+Vme4hPPM5K6axH2LxipXzRiIV=oxNs0upKNu1FvuzbCQmkQdKQVmXl0344vngngrgN7wkEfrYtmKwICmpAS0cbW9jdSClgziVo4NaFc/hsIfok=4UA3hVtxIdw74lFNXD0RR7HKXkFPLIn85M7peOZsqMUCfO4gxr7KCfabszQQf0YcP/mt79XK50=WrSJG7oUyn+clUySPhlegqHAfT9a50uSK5WiQmOnGNGLF4wDO10sqKN1xRgQbYHPtwL+Ye0EMisvmYA3==kScorTSGaQWyibSWXAvxq9+IVGBYShVJ6S7DmTT=u/2d/fGEge+Xmbxlftza=cxJ=Md=k1Q71Lp6Boa56d7wtYRpK6tXHJ9I/2r7rN1E4OtwkFqb7SfWV3UXwyUrXyaaNPTIbqnAHnbgUGtuU6pgICpfREiIxVqvKBf6ErbxHRmMmAuYKxk5E9Mn6nnbxR4WTniweKYeDv2w39zge/tss+36Moeuio9d2eoyRFqXhq=rUGtDwX3fzXV0wV+dUojxOYQ57GQDl7+68PwHPcX794OIXuGOxBk83lNIYIcYz3Vc7qnGy6tFTz7f6S9+EZuSGN7TY5VKkT2eWye46DebrDF9Nwzs/FVpTzbPD/KGDIBtFIbazglhKoWe9txqb1QW8vFNNVOEhYa+cViO3g8ZmY1wG960US2zsnX5Eg8Q5a4h3+sxaJSJ4ONiXZWJuAgKRQzcrszu+M5C0ZVoCOv1goEgfNJeSm/yFc/3rx8wmeWLIJFtq65B7zF72HRKq1nthHAguaxXr20nguHpKkDpNBDVa=WwuJsbeGI", + "body": "" + } + + # desired response from the origin server + response_header = { + "headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", + "timestamp": "1469733493.993", + "body": "success!" + } + self.server.addResponse("sessionlog.log", request_header, response_header) + + # define the GCP request header and the desired response header + request_header = { + "headers": "GET /gcp HTTP/1.1\r\nHost: www.example.com\r\n\r\n", + "timestamp": "1469733493.993", + "Authorization": + "Bearer hkMsi6/bfHyBKrSeM/H0hoXeyx8z1yZ/mJ0c+B/TqYx=tTJDjnQWtul38Z9iVJjeH1HB4VT2c=2o3yE3o=I9kmFs/lJDR85qWjB8e5asY/WbjyRpbAzmDipQpboIcYnUYg55bxrQFidV/q8gZa5A9MpR3n=op1C0lWjeBqcEJxpevNZxteSQTQfeGsi98Cdf+On=/SINVlKrNhMnmMsDOLMGx1YYt9d4UsRg1jtVrwxL4Vd/F7aHCZySAXKv+1rkhACR023wpa3dhp+xirGJxSO9LWwvcrTdM4xJo4RS8B40tGENOJ1NKixUJxwN/6og58Oft/u==uleR89Ja=7zszK2H7tX3DqmEYNvNDYQh/7VBRe5otghQtPwJzWpXAGk+Vme4hPPM5K6axH2LxipXzRiIV=oxNs0upKNu1FvuzbCQmkQdKQVmXl0344vngngrgN7wkEfrYtmKwICmpAS0cbW9jdSClgziVo4NaFc/hsIfok=4UA3hVtxIdw74lFNXD0RR7HKXkFPLIn85M7peOZsqMUCfO4gxr7KCfabszQQf0YcP/mt79XK50=WrSJG7oUyn+clUySPhlegqHAfT9a50uSK5WiQmOnGNGLF4wDO10sqKN1xRgQbYHPtwL+Ye0EMisvmYA3==kScorTSGaQWyibSWXAvxq9+IVGBYShVJ6S7DmTT=u/2d/fGEge+Xmbxlftza=cxJ=Md=k1Q71Lp6Boa56d7wtYRpK6tXHJ9I/2r7rN1E4OtwkFqb7SfWV3UXwyUrXyaaNPTIbqnAHnbgUGtuU6pgICpfREiIxVqvKBf6ErbxHRmMmAuYKxk5E9Mn6nnbxR4WTniweKYeDv2w39zge/tss+36Moeuio9d2eoyRFqXhq=rUGtDwX3fzXV0wV+dUojxOYQ57GQDl7+68PwHPcX794OIXuGOxBk83lNIYIcYz3Vc7qnGy6tFTz7f6S9+EZuSGN7TY5VKkT2eWye46DebrDF9Nwzs/FVpTzbPD/KGDIBtFIbazglhKoWe9txqb1QW8vFNNVOEhYa+cViO3g8ZmY1wG960US2zsnX5Eg8Q5a4h3+sxaJSJ4ONiXZWJuAgKRQzcrszu+M5C0ZVoCOv1goEgfNJeSm/yFc/3rx8wmeWLIJFtq65B7zF72HRKq1nthHAguaxXr20nguHpKkDpNBDVa=WwuJsbeGI", + "body": "" + } + + # desired response from the origin server + response_header = { + "headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", + "timestamp": "1469733493.993", + "body": "success!" + } + + # add request/response + self.server.addResponse("sessionlog.log", request_header, response_header) + + def setUpTS(self): + self.ts = urtest.MakeATSProcess("ts") + + self.ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.show_location': 0, + 'proxy.config.diags.debug.tags': 'origin_server_auth', + }) + + self.ts.Setup.CopyAs('rules/v4-parse-test.test_input', urtest.RunDirectory) + + self.ts.Disk.remap_config.AddLine( + f'map http://www.example.com/s3-bucket http://127.0.0.1:{self.server.Variables.Port}/s3-bucket \ + @plugin=origin_server_auth.so \ + @pparam=--config @pparam={urtest.RunDirectory}/v4-parse-test.test_input') + + self.ts.Disk.remap_config.AddLine( + f'map http://www.example.com/gcp http://127.0.0.1:{self.server.Variables.Port}/gcp \ + @plugin=origin_server_auth.so \ + @pparam=--access_key @pparam=1234567 \ + @pparam=--session_token @pparam=hkMsi6/bfHyBKrSeM/H0hoXeyx8z1yZ/mJ0c+B/TqYx=tTJDjnQWtul38Z9iVJjeH1HB4VT2c=2o3yE3o=I9kmFs/lJDR85qWjB8e5asY/WbjyRpbAzmDipQpboIcYnUYg55bxrQFidV/q8gZa5A9MpR3n=op1C0lWjeBqcEJxpevNZxteSQTQfeGsi98Cdf+On=/SINVlKrNhMnmMsDOLMGx1YYt9d4UsRg1jtVrwxL4Vd/F7aHCZySAXKv+1rkhACR023wpa3dhp+xirGJxSO9LWwvcrTdM4xJo4RS8B40tGENOJ1NKixUJxwN/6og58Oft/u==uleR89Ja=7zszK2H7tX3DqmEYNvNDYQh/7VBRe5otghQtPwJzWpXAGk+Vme4hPPM5K6axH2LxipXzRiIV=oxNs0upKNu1FvuzbCQmkQdKQVmXl0344vngngrgN7wkEfrYtmKwICmpAS0cbW9jdSClgziVo4NaFc/hsIfok=4UA3hVtxIdw74lFNXD0RR7HKXkFPLIn85M7peOZsqMUCfO4gxr7KCfabszQQf0YcP/mt79XK50=WrSJG7oUyn+clUySPhlegqHAfT9a50uSK5WiQmOnGNGLF4wDO10sqKN1xRgQbYHPtwL+Ye0EMisvmYA3==kScorTSGaQWyibSWXAvxq9+IVGBYShVJ6S7DmTT=u/2d/fGEge+Xmbxlftza=cxJ=Md=k1Q71Lp6Boa56d7wtYRpK6tXHJ9I/2r7rN1E4OtwkFqb7SfWV3UXwyUrXyaaNPTIbqnAHnbgUGtuU6pgICpfREiIxVqvKBf6ErbxHRmMmAuYKxk5E9Mn6nnbxR4WTniweKYeDv2w39zge/tss+36Moeuio9d2eoyRFqXhq=rUGtDwX3fzXV0wV+dUojxOYQ57GQDl7+68PwHPcX794OIXuGOxBk83lNIYIcYz3Vc7qnGy6tFTz7f6S9+EZuSGN7TY5VKkT2eWye46DebrDF9Nwzs/FVpTzbPD/KGDIBtFIbazglhKoWe9txqb1QW8vFNNVOEhYa+cViO3g8ZmY1wG960US2zsnX5Eg8Q5a4h3+sxaJSJ4ONiXZWJuAgKRQzcrszu+M5C0ZVoCOv1goEgfNJeSm/yFc/3rx8wmeWLIJFtq65B7zF72HRKq1nthHAguaxXr20nguHpKkDpNBDVa=WwuJsbeGI \ + @pparam=--version @pparam=gcpv1') + + def test_s3_origin_remap(self): + ''' + Test s3 v4 origin server + ''' + tr = urtest.AddTestRun() + tr.MakeCurlCommand(f'-s -v -H "Host: www.example.com" http://127.0.0.1:{self.ts.Variables.port}/s3-bucket;', ts=self.ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.StartBefore(self.server) + tr.Processes.Default.StartBefore(self.ts) + tr.Processes.Default.Streams.stderr.Content = Testers.ContainsExpression("200 OK", "expected 200 response") + tr.Processes.Default.Streams.stderr.Content += Testers.ContainsExpression( + "Content-Length: 8", "expected content-length 8") + tr.StillRunningAfter = self.server + + def test_gcp_origin_remap(self): + ''' + Test GCP origin server + ''' + tr = urtest.AddTestRun() + tr.MakeCurlCommand(f'-s -v -H "Host: www.example.com" http://127.0.0.1:{self.ts.Variables.port}/gcp;', ts=self.ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.stderr.Content = Testers.ContainsExpression("200 OK", "expected 200 response") + tr.Processes.Default.Streams.stderr.Content += Testers.ContainsExpression( + "Content-Length: 8", "expected content-length 8") + tr.StillRunningAfter = self.server + + def checkLogOutput(self): + if Condition.CurlUsingUnixDomainSocket(): + self.ts.Disk.traffic_out.Content = "gold/origin_server_auth_parsing_ts_uds.gold" + else: + self.ts.Disk.traffic_out.Content = "gold/origin_server_auth_parsing_ts.gold" + self.ts.ReturnCode = 0 + + def runTraffic(self): + self.test_s3_origin_remap() + self.test_gcp_origin_remap() + self.checkLogOutput() + + def run(self): + self.runTraffic() + + OriginServerAuthTest().run() + urtest.execute() diff --git a/tests/uranium_tests/pluginTest/origin_server_auth/test_s3_url_encoding.py b/tests/uranium_tests/pluginTest/origin_server_auth/test_s3_url_encoding.py new file mode 100644 index 00000000000..19aa9b94015 --- /dev/null +++ b/tests/uranium_tests/pluginTest/origin_server_auth/test_s3_url_encoding.py @@ -0,0 +1,217 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_s3_url_encoding(urtest: UraniumTest) -> None: + ''' + Test origin_server_auth URL encoding fix (verifies fix for mixed-encoding bug) + + This test verifies the fix for a bug where mixed URL encoding in request paths + caused signature mismatch with S3. + + Fixed bug: When a URL had SOME characters encoded (e.g., %5B) but others NOT + encoded (e.g., parentheses), the old isUriEncoded() function incorrectly + returned true, causing canonicalEncode() to skip re-encoding. This resulted + in a signature calculated for a partially-encoded path, which didn't match + what S3 expected. + + With the fix, isUriEncoded() now correctly returns false for mixed-encoding + URLs, triggering the decode/re-encode path to normalize the URL. + + Example (now handled correctly): + Client sends: /app/(channel)/%5B%5Bparts%5D%5D/page.js + isUriEncoded(): Returns false (detects unencoded parentheses) + canonicalEncode(): Decodes then re-encodes properly + Signature for: /app/%28channel%29/%5B%5Bparts%5D%5D/page.js <- CORRECT + + This test sends requests with various URL encodings and verifies that + the origin receives correctly normalized Authorization headers. + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + urtest.testName = "origin_server_auth: URL encoding bug" + urtest.Summary = ''' + Test for S3 signature calculation with mixed URL encoding + ''' + urtest.ContinueOnFail = True + + class S3UrlEncodingTest: + """ + Test class for verifying URL encoding behavior in S3 auth signature calculation. + """ + + def __init__(self): + self.setupOriginServer() + self.setupTS() + + def setupOriginServer(self): + """ + Create an origin server that captures and logs the request path and headers. + ATS preserves URL encoding from the client, so we need to match exactly. + """ + self.server = urtest.MakeOriginServer("s3_mock") + + # Test case 1: Fully unencoded path with parentheses + # Client sends: /bucket/app/(channel)/test.js (unencoded) + # ATS forwards: /bucket/app/(channel)/test.js (as-is) + request1 = { + "headers": "GET /bucket/app/(channel)/test.js HTTP/1.1\r\nHost: s3.amazonaws.com\r\n\r\n", + "timestamp": "1469733493.993", + "body": "" + } + response1 = { + "headers": "HTTP/1.1 200 OK\r\nConnection: close\r\nContent-Length: 7\r\n\r\n", + "timestamp": "1469733493.993", + "body": "test1ok" + } + self.server.addResponse("sessionlog.log", request1, response1) + + # Test case 2: Fully encoded parentheses + # Client sends: /bucket/app/%28channel%29/test.js (encoded) + # ATS forwards: /bucket/app/%28channel%29/test.js (as-is, preserves encoding) + request2 = { + "headers": "GET /bucket/app/%28channel%29/test.js HTTP/1.1\r\nHost: s3.amazonaws.com\r\n\r\n", + "timestamp": "1469733493.993", + "body": "" + } + response2 = { + "headers": "HTTP/1.1 200 OK\r\nConnection: close\r\nContent-Length: 7\r\n\r\n", + "timestamp": "1469733493.993", + "body": "test2ok" + } + self.server.addResponse("sessionlog.log", request2, response2) + + # Test case 3: Mixed encoding - BUG CASE + # Client sends: /bucket/app/(channel)/%5B%5Bparts%5D%5D/page.js + # - Parentheses () NOT encoded + # - Brackets [] ARE encoded as %5B%5D + # ATS forwards: /bucket/app/(channel)/%5B%5Bparts%5D%5D/page.js (as-is) + # + # BUG: isUriEncoded() sees %5B -> returns true -> canonicalEncode() skips encoding + # Result: Signature calculated for /bucket/app/(channel)/%5B%5Bparts%5D%5D/page.js + # But S3 expects signature for /bucket/app/%28channel%29/%5B%5Bparts%5D%5D/page.js + request3 = { + "headers": "GET /bucket/app/(channel)/%5B%5Bparts%5D%5D/page.js HTTP/1.1\r\nHost: s3.amazonaws.com\r\n\r\n", + "timestamp": "1469733493.993", + "body": "" + } + response3 = { + "headers": "HTTP/1.1 200 OK\r\nConnection: close\r\nContent-Length: 7\r\n\r\n", + "timestamp": "1469733493.993", + "body": "test3ok" + } + self.server.addResponse("sessionlog.log", request3, response3) + + def setupTS(self): + """Configure ATS with the origin_server_auth plugin.""" + self.ts = urtest.MakeATSProcess("ts") + + self.ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'origin_server_auth', + 'proxy.config.url_remap.pristine_host_hdr': 1, + }) + + # Copy the config file to the test directory and use it + self.ts.Setup.CopyAs('rules/s3_url_encoding.test_input', urtest.RunDirectory) + + # Remap rule with S3 auth + self.ts.Disk.remap_config.AddLine( + f'map http://s3.amazonaws.com/ http://127.0.0.1:{self.server.Variables.Port}/ ' + f'@plugin=origin_server_auth.so ' + f'@pparam=--config @pparam={urtest.RunDirectory}/s3_url_encoding.test_input') + + def test_unencoded_parentheses(self): + """ + Test 1: Request with unencoded parentheses. + The plugin should encode () to %28%29 for the signature calculation. + """ + tr = urtest.AddTestRun("Unencoded parentheses in path") + tr.Processes.Default.Command = ( + f'curl -s -v -o /dev/null ' + f'-H "Host: s3.amazonaws.com" ' + f'"http://127.0.0.1:{self.ts.Variables.port}/bucket/app/(channel)/test.js"') + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.StartBefore(self.server) + tr.Processes.Default.StartBefore(self.ts) + tr.Processes.Default.Streams.stderr.Content = Testers.ContainsExpression("200 OK", "Expected 200 OK response") + tr.StillRunningAfter = self.ts + + def test_encoded_parentheses(self): + """ + Test 2: Request with URL-encoded parentheses. + ATS preserves the encoding when forwarding to origin. + """ + tr = urtest.AddTestRun("Encoded parentheses in path") + tr.Processes.Default.Command = ( + f'curl -s -v -o /dev/null ' + f'-H "Host: s3.amazonaws.com" ' + f'"http://127.0.0.1:{self.ts.Variables.port}/bucket/app/%28channel%29/test.js"') + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.stderr.Content = Testers.ContainsExpression("200 OK", "Expected 200 OK response") + tr.StillRunningAfter = self.ts + + def test_mixed_encoding_bug(self): + """ + Test 3: BUG CASE - Mixed encoding with unencoded parentheses and encoded brackets. + + Client URL: /bucket/app/(channel)/%5B%5Bparts%5D%5D/page.js + - Parentheses () are NOT encoded + - Brackets [] ARE encoded as %5B%5D + + BUG: isUriEncoded() finds %5B and returns true, so canonicalEncode() + returns the path as-is without encoding the parentheses. + + This causes signature mismatch because: + - Plugin calculates signature for: /bucket/app/(channel)/%5B%5Bparts%5D%5D/page.js + - S3 expects signature for: /bucket/app/%28channel%29/%5B%5Bparts%5D%5D/page.js + """ + tr = urtest.AddTestRun("Mixed encoding - BUG CASE") + tr.Processes.Default.Command = ( + f'curl -s -v -o /dev/null ' + f'-H "Host: s3.amazonaws.com" ' + f'"http://127.0.0.1:{self.ts.Variables.port}/bucket/app/(channel)/%5B%5Bparts%5D%5D/page.js"') + tr.Processes.Default.ReturnCode = 0 + # This should succeed with our mock server, but would fail with real S3 + # The test documents the bug - when fixed, the signature would be correct + tr.Processes.Default.Streams.stderr.Content = Testers.ContainsExpression( + "200 OK", "Expected 200 OK response (mock server accepts any signature)") + tr.StillRunningAfter = self.ts + + def run(self): + self.test_unencoded_parentheses() + self.test_encoded_parentheses() + self.test_mixed_encoding_bug() + + S3UrlEncodingTest().run() + urtest.execute() diff --git a/tests/uranium_tests/pluginTest/packet_mark/test_packet_mark.py b/tests/uranium_tests/pluginTest/packet_mark/test_packet_mark.py new file mode 100644 index 00000000000..8cb362e946e --- /dev/null +++ b/tests/uranium_tests/pluginTest/packet_mark/test_packet_mark.py @@ -0,0 +1,196 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_packet_mark(urtest: UraniumTest) -> None: + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import os + import socket + + urtest.Summary = ''' + Verify TSHttpTxnClientPacketMarkSet and TSHttpTxnServerPacketMarkSet set the + firewall mark on the client- and server-side connections respectively. Each is + driven by a test plugin that applies the mark and reads it back off the relevant + socket with getsockopt(SO_MARK), echoing the observed value into a response + header this test asserts on. + ''' + + def _can_set_so_mark() -> bool: + """Probe whether SO_MARK can actually be set on this host. + + Setting SO_MARK is Linux-only and requires CAP_NET_ADMIN or CAP_NET_RAW. + On any host that lacks the capability (or the platform), setsockopt raises, + and the applied value would be unobservable -- so the test is skipped + rather than failed. + """ + if not hasattr(socket, "SO_MARK"): + return False + try: + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as probe: + probe.setsockopt(socket.SOL_SOCKET, socket.SO_MARK, 0x1) + return True + except (OSError, PermissionError): + return False + + urtest.SkipUnless( + Condition.IsPlatform("linux"), + # pass_value defaults to True: run only when the probe reports SO_MARK is settable. + Condition(_can_set_so_mark, "Setting SO_MARK requires Linux with CAP_NET_ADMIN or CAP_NET_RAW"), + ) + + # SOCK_OPT_PACKET_MARK (0x10) | SOCK_OPT_NO_DELAY (0x1). The mark is only pushed + # to the socket when the PACKET_MARK bit is set in the sock option flag. + SOCK_OPT_FLAG_PACKET_MARK = 0x11 + + class PacketMarkTest: + """Drive a TSHttpTxn*PacketMarkSet API through a test plugin and assert on the + firewall mark read back off the relevant socket. + + This base holds the shared skeleton -- process setup, the common records, the + curl-and-assert case runner. Each subclass supplies its plugin and echo + header and extends _configure() (via super()) with the side-specific mark and + flag records. + """ + + # Value the plugin sets; the mark is expected to become exactly this. + SET_MARK = 0x0000000A + # Seeded starting mark, distinct from SET_MARK so a no-op would be visible. + SEED_MARK = 0x0000FF00 + + # Bumped per instance so each side gets uniquely-numbered processes. + _counter = 0 + + def __init__(self): + self._num = PacketMarkTest._counter + PacketMarkTest._counter += 1 + self._server = self._make_server() + self._ts = self._make_ats() + self._configure(self._ts) + self._started = False + + def _make_server(self): + server = urtest.MakeOriginServer(f"server{self._num}") + request_header = {"headers": "GET / HTTP/1.1\r\nHost: example.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + for _ in range(2): + server.addResponse("sessionlog.json", request_header, response_header) + return server + + def _make_ats(self): + return urtest.MakeATSProcess(f"ts{self._num}", enable_cache=False) + + def _configure(self, ts): + # Records and remap shared by both sides. Subclasses override to add the + # side-specific mark/flag records and load their plugin, calling super() + # for these. + ts.Disk.records_config.update( + { + 'proxy.config.url_remap.remap_required': 0, + # Keep ATS running as the invoking user inside sudo (no privilege drop). + 'proxy.config.admin.user_id': '#-1', + }) + ts.Disk.remap_config.AddLine(f"map / http://127.0.0.1:{self._server.Variables.Port}") + + def _add_case(self, echo_header: str, description: str, set_header: str): + # The mark is set to the supplied value, regardless of the seeded + # starting mark. The set is driven by whichever request header the plugin + # keys on; the observed mark is echoed into echo_header. The origin server + # and ATS are started before the first case, independent of the order in + # which cases are added. + tr = urtest.AddTestRun(description) + tr.Processes.Default.StartBefore(self._server) + tr.Processes.Default.StartBefore(self._ts) + if not self._started: + tr.StillRunningAfter = self._server + tr.StillRunningAfter = self._ts + self._started = True + tr.MakeCurlCommand( + f'--verbose --ipv4 --header "{set_header}: 0x{self.SET_MARK:08x}" http://localhost:{self._ts.Variables.port}/', + ts=self._ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.All += Testers.ContainsExpression( + f"{echo_header}: 0x{self.SET_MARK:08x}", f"Observed packet mark should be 0x{self.SET_MARK:08x}") + + class ClientPacketMarkTest(PacketMarkTest): + """Exercise TSHttpTxnClientPacketMarkSet. The client mark is seeded on the + inbound socket. + """ + + ECHO_HEADER = "X-Client-Packet-Mark" + + def _configure(self, ts): + super()._configure(ts) + ts.Disk.records_config.update( + { + 'proxy.config.net.sock_packet_mark_in': self.SEED_MARK, + 'proxy.config.net.sock_option_flag_in': SOCK_OPT_FLAG_PACKET_MARK, + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'http|client_packet_mark', + }) + urtest.PrepareTestPlugin(os.path.join(urtest.Variables.AtsTestPluginsDir, f'client_packet_mark.so'), ts) + + def run(self): + self._add_case(self.ECHO_HEADER, "client_packet_mark sets the client-side mark on the live connection", "X-Set-Mark") + + class ServerPacketMarkTest(PacketMarkTest): + """Exercise TSHttpTxnServerPacketMarkSet. The server mark is seeded on the + outbound socket. + + The server API additionally records the mark for a *future* origin + connection (TSHttpTxnConfigIntSet on TS_CONFIG_NET_SOCK_PACKET_MARK_OUT), + which the client API has no equivalent of. The server plugin exposes this by + honoring X-Set-Mark-Preconnect at READ_REQUEST_HDR, before any origin + connection exists -- so the mark can only reach the socket via that seed. + """ + + ECHO_HEADER = "X-Server-Packet-Mark" + + def _configure(self, ts): + super()._configure(ts) + ts.Disk.records_config.update( + { + 'proxy.config.net.sock_packet_mark_out': self.SEED_MARK, + 'proxy.config.net.sock_option_flag_out': SOCK_OPT_FLAG_PACKET_MARK, + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'http|server_packet_mark', + }) + urtest.PrepareTestPlugin(os.path.join(urtest.Variables.AtsTestPluginsDir, f'server_packet_mark.so'), ts) + + def run(self): + self._add_case(self.ECHO_HEADER, "server_packet_mark sets the server-side mark on the live connection", "X-Set-Mark") + self._add_case( + self.ECHO_HEADER, "server_packet_mark seeds the mark for a future origin connection", "X-Set-Mark-Preconnect") + + ClientPacketMarkTest().run() + ServerPacketMarkTest().run() + urtest.execute() diff --git a/tests/gold_tests/pluginTest/per_plugin_metrics/rules/global.conf b/tests/uranium_tests/pluginTest/per_plugin_metrics/rules/global.conf similarity index 100% rename from tests/gold_tests/pluginTest/per_plugin_metrics/rules/global.conf rename to tests/uranium_tests/pluginTest/per_plugin_metrics/rules/global.conf diff --git a/tests/uranium_tests/pluginTest/per_plugin_metrics/test_per_plugin_metrics.py b/tests/uranium_tests/pluginTest/per_plugin_metrics/test_per_plugin_metrics.py new file mode 100644 index 00000000000..15b69d722b8 --- /dev/null +++ b/tests/uranium_tests/pluginTest/per_plugin_metrics/test_per_plugin_metrics.py @@ -0,0 +1,132 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_per_plugin_metrics(urtest: UraniumTest) -> None: + ''' + Verify the per-plugin workload counters proxy.process.plugin..{invocations,bytes,transfers}. + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import re + + urtest.Summary = ''' + Verify the per-plugin workload counters proxy.process.plugin..invocations (global and remap + dispatch), .bytes and .transfers (PluginVC intercept transport). + ''' + + urtest.SkipUnless( + Condition.PluginExists('header_rewrite.so'), Condition.PluginExists('conf_remap.so'), + Condition.PluginExists('generator.so')) + + urtest.ContinueOnFail = True + + class TestPerPluginWorkloadCounters: + + def __init__(self): + self.setUpOriginServer() + self.setUpTS() + + def setUpOriginServer(self): + self.server = urtest.MakeOriginServer("server") + request_header = {"headers": "GET / HTTP/1.1\r\nHost: test.example\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + self.server.addResponse("sessionfile.log", request_header, response_header) + + def setUpTS(self): + self.ts = urtest.MakeATSProcess("ts") + + # header_rewrite (global) and conf_remap (remap) exercise the two invocation-counter sites; + # generator serves its body through a PluginVC intercept, exercising the bytes and transfers counters. + self.ts.Setup.CopyAs('rules/global.conf', urtest.RunDirectory) + self.ts.Disk.plugin_config.AddLine(f'header_rewrite.so {urtest.RunDirectory}/global.conf') + self.ts.Disk.remap_config.AddLine( + f'map http://test.example http://127.0.0.1:{self.server.Variables.Port} ' + f'@plugin=conf_remap.so @pparam=proxy.config.url_remap.pristine_host_hdr=1') + self.ts.Disk.remap_config.AddLine('map http://gen.example http://127.0.0.1/ @plugin=generator.so') + + self.ts.Disk.records_config.update({ + 'proxy.config.diags.debug.enabled': 0, + 'proxy.config.diags.debug.tags': 'plugin', + }) + + def driveTraffic(self): + # curl as a forward proxy sends an absolute-URI request so the named remap rule (conf_remap) + # matches; the 200 confirms it was served rather than 404'd. + tr = urtest.AddTestRun("Drive traffic through global + remap plugins") + tr.Processes.Default.StartBefore(self.server) + tr.Processes.Default.StartBefore(self.ts) + tr.MakeCurlCommand( + f'-s -D - -o /dev/null --proxy 127.0.0.1:{self.ts.Variables.port} "http://test.example/"', ts=self.ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.stdout = Testers.ContainsExpression( + '200 OK', 'request should match the remap rule and be served') + tr.StillRunningAfter = self.server + tr.StillRunningAfter = self.ts + + # Drive a generator request: it serves a 4096-byte body through a PluginVC intercept. + tr = urtest.AddTestRun("Drive traffic through a PluginVC intercept plugin") + tr.MakeCurlCommand( + f'-s -o /dev/null --proxy 127.0.0.1:{self.ts.Variables.port} "http://gen.example/nocache/4096"', ts=self.ts) + tr.Processes.Default.ReturnCode = 0 + tr.StillRunningAfter = self.ts + tr.StillRunningAfter = self.server + + def checkMetric(self, description, metric, message): + tr = urtest.AddTestRun(description) + tr.Processes.Default.Env = self.ts.Env + tr.Processes.Default.Command = f'traffic_ctl metric get {metric}' + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.stdout = Testers.ContainsExpression(f'{re.escape(metric)} [1-9]', message) + tr.StillRunningAfter = self.ts + tr.StillRunningAfter = self.server + + def checkMetrics(self): + self.checkMetric( + "Global plugin invocation counter is non-zero", 'proxy.process.plugin.header_rewrite.invocations', + 'global header_rewrite invocations should be counted') + self.checkMetric( + "Remap plugin invocation counter is non-zero", 'proxy.process.plugin.conf_remap.invocations', + 'remap conf_remap invocations should be counted') + self.checkMetric( + "PluginVC intercept bytes counter is non-zero", 'proxy.process.plugin.generator.bytes', + 'generator PluginVC transport bytes should be counted') + self.checkMetric( + "PluginVC intercept transfers counter is non-zero", 'proxy.process.plugin.generator.transfers', + 'generator PluginVC transfer events should be counted') + + def run(self): + self.driveTraffic() + self.checkMetrics() + + TestPerPluginWorkloadCounters().run() + urtest.execute() diff --git a/tests/uranium_tests/pluginTest/plugin_yaml/test_plugin_yaml.py b/tests/uranium_tests/pluginTest/plugin_yaml/test_plugin_yaml.py new file mode 100644 index 00000000000..97eaa7f0f77 --- /dev/null +++ b/tests/uranium_tests/pluginTest/plugin_yaml/test_plugin_yaml.py @@ -0,0 +1,113 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_plugin_yaml(urtest: UraniumTest) -> None: + ''' + Test plugin.yaml loading with inline config and enabled/disabled plugins. + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + urtest.Summary = ''' + Test that plugin.yaml is loaded instead of plugin.config, that inline config + works with header_rewrite.so, and that enabled: false skips a plugin. + ''' + + urtest.ContinueOnFail = True + + server = urtest.MakeOriginServer("server") + + request_header = {"headers": "GET /test HTTP/1.1\r\nHost: example.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + server.addResponse("sessionlog.json", request_header, response_header) + + ts = urtest.MakeATSProcess("ts", enable_cache=False) + + ts.Disk.records_config.update( + { + 'proxy.config.url_remap.remap_required': 0, + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'header_rewrite|plugin', + }) + + ts.Disk.remap_config.AddLine("map http://example.com http://127.0.0.1:{0}".format(server.Variables.Port)) + + # Write plugin.yaml into the config directory. ATS will prefer this over + # plugin.config. Uses inline config (scalar literal) with header_rewrite.so + # to set a custom response header. xdebug.so is listed but disabled. + ts.Disk.MakeConfigFile("plugin.yaml").update( + { + "plugins": + [ + { + "path": "header_rewrite.so", + "config": "cond %{SEND_RESPONSE_HDR_HOOK}\n set-header X-Plugin-YAML \"loaded-from-inline\"\n", + }, + { + "path": "xdebug.so", + "enabled": False, + "params": ["--enable=x-cache"], + }, + ] + }) + + # Test 1: Verify header_rewrite loaded via plugin.yaml sets the response header. + tr = urtest.AddTestRun("Verify inline config sets response header") + tr.Processes.Default.StartBefore(server, ready=When.PortOpen(server.Variables.Port)) + tr.Processes.Default.StartBefore(ts) + tr.MakeCurlCommand('-s -D- -o /dev/null -H "Host: example.com" http://127.0.0.1:{0}/test'.format(ts.Variables.port), ts=ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.stdout = Testers.ContainsExpression( + "X-Plugin-YAML: loaded-from-inline", "Response should contain X-Plugin-YAML header set by inline config") + tr.StillRunningAfter = server + tr.StillRunningAfter = ts + + # Test 2: Verify xdebug is NOT loaded (enabled: false). Send the X-Debug + # header and confirm the X-Cache header is absent from the response. + tr = urtest.AddTestRun("Verify disabled plugin is not loaded") + tr.MakeCurlCommand( + '-s -D- -o /dev/null -H "Host: example.com" -H "X-Debug: x-cache" http://127.0.0.1:{0}/test'.format(ts.Variables.port), + ts=ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.stdout = Testers.ExcludesExpression( + "X-Cache:", "Response should NOT contain X-Cache header since xdebug is disabled") + tr.StillRunningAfter = server + tr.StillRunningAfter = ts + + # Test 3: Verify the diags.log shows plugin.yaml was used. + tr = urtest.AddTestRun("Verify plugin.yaml loading logged") + tr.Processes.Default.Command = "echo check diags.log" + tr.Processes.Default.ReturnCode = 0 + ts.Disk.diags_log.Content += Testers.ContainsExpression( + "plugin.yaml loading", "diags.log should indicate plugin.yaml was loaded") + ts.Disk.diags_log.Content += Testers.ContainsExpression("skipped", "diags.log should indicate a plugin was skipped") + urtest.execute() diff --git a/tests/gold_tests/pluginTest/polite_hook_wait/CMakeLists.txt b/tests/uranium_tests/pluginTest/polite_hook_wait/CMakeLists.txt similarity index 93% rename from tests/gold_tests/pluginTest/polite_hook_wait/CMakeLists.txt rename to tests/uranium_tests/pluginTest/polite_hook_wait/CMakeLists.txt index 3d48fb431bb..db3944ffefc 100644 --- a/tests/gold_tests/pluginTest/polite_hook_wait/CMakeLists.txt +++ b/tests/uranium_tests/pluginTest/polite_hook_wait/CMakeLists.txt @@ -15,4 +15,4 @@ # ####################### -add_autest_plugin(polite_hook_wait polite_hook_wait.cc) +add_urtest_plugin(polite_hook_wait polite_hook_wait.cc) diff --git a/tests/gold_tests/pluginTest/polite_hook_wait/curl.gold b/tests/uranium_tests/pluginTest/polite_hook_wait/curl.gold similarity index 100% rename from tests/gold_tests/pluginTest/polite_hook_wait/curl.gold rename to tests/uranium_tests/pluginTest/polite_hook_wait/curl.gold diff --git a/tests/gold_tests/pluginTest/polite_hook_wait/polite_hook_wait.cc b/tests/uranium_tests/pluginTest/polite_hook_wait/polite_hook_wait.cc similarity index 100% rename from tests/gold_tests/pluginTest/polite_hook_wait/polite_hook_wait.cc rename to tests/uranium_tests/pluginTest/polite_hook_wait/polite_hook_wait.cc diff --git a/tests/uranium_tests/pluginTest/polite_hook_wait/test_polite_hook_wait.py b/tests/uranium_tests/pluginTest/polite_hook_wait/test_polite_hook_wait.py new file mode 100644 index 00000000000..fd53259d28e --- /dev/null +++ b/tests/uranium_tests/pluginTest/polite_hook_wait/test_polite_hook_wait.py @@ -0,0 +1,92 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_polite_hook_wait(urtest: UraniumTest) -> None: + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import os + + urtest.Summary = ''' + Test spawning a thread in a transaction hook continuation, and getting a result from it, without blocking event task. + ''' + + plugin_name = "polite_hook_wait" + + server = urtest.MakeOriginServer("server") + + request_header = {"headers": "GET / HTTP/1.1\r\nHost: doesnotmatter\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": "112233"} + server.addResponse("sessionlog.json", request_header, response_header) + + # Disable the cache to make sure each request is forwarded to the origin + # server. + ts = urtest.MakeATSProcess("ts", enable_cache=False, block_for_debug=False) + + ts.Disk.records_config.update( + { + 'proxy.config.proxy_name': 'Poxy_Proxy', # This will be the server name. + 'proxy.config.url_remap.remap_required': 1, + 'proxy.config.diags.debug.enabled': 3, + 'proxy.config.diags.debug.tags': f'{plugin_name}', + }) + + rp = os.path.join(urtest.Variables.AtsBuildUraniumTestsDir, 'pluginTest', 'polite_hook_wait', '.libs', f'{plugin_name}.so') + ts.Setup.Copy(rp, ts.Env['PROXY_CONFIG_PLUGIN_PLUGIN_DIR']) + + ts.Disk.plugin_config.AddLine(f"{plugin_name}.so") + + ts.Disk.remap_config.AddLine("map http://myhost.test http://127.0.0.1:{0}".format(server.Variables.Port)) + + ipv4flag = "" + if not Condition.CurlUsingUnixDomainSocket(): + ipv4flag = "--ipv4" + + tr = urtest.AddTestRun() + tr.Processes.Default.StartBefore(server) + tr.Processes.Default.StartBefore(ts) + tr.MakeCurlCommand( + '--verbose {0} --header "Host:myhost.test" http://localhost:{1}/ 2>curl.txt'.format(ipv4flag, ts.Variables.port), ts=ts) + tr.Processes.Default.ReturnCode = 0 + + tr = urtest.AddTestRun() + tr.MakeCurlCommand( + '--verbose {0} --header "Host:myhost.test" http://localhost:{1}/ 2>curl.txt'.format(ipv4flag, ts.Variables.port), ts=ts) + tr.Processes.Default.ReturnCode = 0 + + tr = urtest.AddTestRun() + # Later versions of curl add a "using HTTP" line to the output, so we filter it + # out to keep this test compatible with old and new versions. + tr.Processes.Default.Command = "grep -F HTTP/ curl.txt | grep -v 'using HTTP'" + tr.Processes.Default.Streams.stdout = "curl.gold" + tr.Processes.Default.ReturnCode = 0 + urtest.execute() diff --git a/tests/gold_tests/pluginTest/prefetch/header_rewrite.conf b/tests/uranium_tests/pluginTest/prefetch/header_rewrite.conf similarity index 100% rename from tests/gold_tests/pluginTest/prefetch/header_rewrite.conf rename to tests/uranium_tests/pluginTest/prefetch/header_rewrite.conf diff --git a/tests/gold_tests/pluginTest/prefetch/prefetch_bignum.gold b/tests/uranium_tests/pluginTest/prefetch/prefetch_bignum.gold similarity index 100% rename from tests/gold_tests/pluginTest/prefetch/prefetch_bignum.gold rename to tests/uranium_tests/pluginTest/prefetch/prefetch_bignum.gold diff --git a/tests/gold_tests/pluginTest/prefetch/prefetch_cmcd0.gold b/tests/uranium_tests/pluginTest/prefetch/prefetch_cmcd0.gold similarity index 100% rename from tests/gold_tests/pluginTest/prefetch/prefetch_cmcd0.gold rename to tests/uranium_tests/pluginTest/prefetch/prefetch_cmcd0.gold diff --git a/tests/gold_tests/pluginTest/prefetch/prefetch_cmcd1.gold b/tests/uranium_tests/pluginTest/prefetch/prefetch_cmcd1.gold similarity index 100% rename from tests/gold_tests/pluginTest/prefetch/prefetch_cmcd1.gold rename to tests/uranium_tests/pluginTest/prefetch/prefetch_cmcd1.gold diff --git a/tests/gold_tests/pluginTest/prefetch/prefetch_optional_group.gold b/tests/uranium_tests/pluginTest/prefetch/prefetch_optional_group.gold similarity index 100% rename from tests/gold_tests/pluginTest/prefetch/prefetch_optional_group.gold rename to tests/uranium_tests/pluginTest/prefetch/prefetch_optional_group.gold diff --git a/tests/gold_tests/pluginTest/prefetch/prefetch_overflow.gold b/tests/uranium_tests/pluginTest/prefetch/prefetch_overflow.gold similarity index 100% rename from tests/gold_tests/pluginTest/prefetch/prefetch_overflow.gold rename to tests/uranium_tests/pluginTest/prefetch/prefetch_overflow.gold diff --git a/tests/gold_tests/pluginTest/prefetch/prefetch_query_path_traversal.test.yaml b/tests/uranium_tests/pluginTest/prefetch/prefetch_query_path_traversal.test.yaml similarity index 99% rename from tests/gold_tests/pluginTest/prefetch/prefetch_query_path_traversal.test.yaml rename to tests/uranium_tests/pluginTest/prefetch/prefetch_query_path_traversal.test.yaml index 6634ff64ac6..3ef913209fb 100644 --- a/tests/gold_tests/pluginTest/prefetch/prefetch_query_path_traversal.test.yaml +++ b/tests/uranium_tests/pluginTest/prefetch/prefetch_query_path_traversal.test.yaml @@ -17,7 +17,7 @@ meta: version: "1.0" -autest: +urtest: description: "Verify query prefetch blocks path traversal" server: diff --git a/tests/gold_tests/pluginTest/prefetch/prefetch_simple.gold b/tests/uranium_tests/pluginTest/prefetch/prefetch_simple.gold similarity index 100% rename from tests/gold_tests/pluginTest/prefetch/prefetch_simple.gold rename to tests/uranium_tests/pluginTest/prefetch/prefetch_simple.gold diff --git a/tests/uranium_tests/pluginTest/prefetch/test_prefetch_bad_count_refused.py b/tests/uranium_tests/pluginTest/prefetch/test_prefetch_bad_count_refused.py new file mode 100644 index 00000000000..0df7168a30a --- /dev/null +++ b/tests/uranium_tests/pluginTest/prefetch/test_prefetch_bad_count_refused.py @@ -0,0 +1,74 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_prefetch_bad_count_refused(urtest: UraniumTest) -> None: + ''' + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + urtest.Summary = ''' + Test that prefetch.so treats a --fetch-count that does not fit in an unsigned as a configuration + error: ATS refuses to load the remap rule (and fails to start) instead of truncating the value. + + The value below is a string of decimal digits, so it passes a digits-only check, but it exceeds the + range of the unsigned the plugin stores it in. + ''' + + ts = urtest.MakeATSProcess("ts") + ts.Disk.records_config.update({ + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'prefetch', + }) + ts.Disk.remap_config.AddLine( + "map http://domain.in http://127.0.0.1:8080" + " @plugin=prefetch.so" + " @pparam=--front=true" + + " @pparam=--fetch-policy=simple" + " @pparam=--fetch-count=5000000000") + + ts.ReturnCode = 33 # Emergency exit: remap.config failed to load. + ts.Ready = 0 + # ATS is expected to log the rejection; this ContainsExpression both asserts it and replaces the + # default "diags.log must not contain ERROR:" check (the rejection is logged via TSError). + ts.Disk.diags_log.Content = Testers.ContainsExpression( + "invalid --fetch-count '5000000000'", "an out-of-range fetch-count must be rejected at config load") + + tr = urtest.AddTestRun("prefetch rejects an out-of-range fetch-count at load") + # Wait for the rejection message with a separate watcher: gating ts readiness on the log line directly + # can race the process exiting before autest observes the line. + watcher = urtest.Processes.Process("watcher") + watcher.Command = "sleep 30" + watcher.Ready = When.FileContains(ts.Disk.diags_log.Name, "invalid --fetch-count '5000000000'") + watcher.StartBefore(ts) + + tr.Processes.Default.Command = "echo done" + tr.TimeOut = 30 + tr.Processes.Default.StartBefore(watcher) + urtest.execute() diff --git a/tests/uranium_tests/pluginTest/prefetch/test_prefetch_bad_pattern_refused.py b/tests/uranium_tests/pluginTest/prefetch/test_prefetch_bad_pattern_refused.py new file mode 100644 index 00000000000..a4d6bfac8aa --- /dev/null +++ b/tests/uranium_tests/pluginTest/prefetch/test_prefetch_bad_pattern_refused.py @@ -0,0 +1,75 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_prefetch_bad_pattern_refused(urtest: UraniumTest) -> None: + ''' + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + urtest.Summary = ''' + Test that prefetch.so treats an unusable fetch-path-pattern as a configuration error: ATS refuses to + load the remap rule (and fails to start) instead of silently running with prefetch disabled. + + The pattern below defines 10 capture groups. The plugin's ovector (OVECOUNT = TOKENCOUNT*3) can hold + offsets for the whole match plus at most TOKENCOUNT-1 (9) groups, so the pattern is rejected at + config-load time; that fails the remap instance, and remap.config fails to load. + ''' + + ts = urtest.MakeATSProcess("ts") + ts.Disk.records_config.update({ + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'prefetch', + }) + ts.Disk.remap_config.AddLine( + "map http://domain.in http://127.0.0.1:8080" + " @plugin=prefetch.so" + " @pparam=--front=true" + + " @pparam=--fetch-policy=simple" + r" @pparam=--fetch-path-pattern=/(a)(b)(c)(d)(e)(f)(g)(h)(i)(j)/$1/") + + ts.ReturnCode = 33 # Emergency exit: remap.config failed to load. + ts.Ready = 0 + # ATS is expected to log the rejection; this ContainsExpression both asserts it and replaces the + # default "diags.log must not contain ERROR:" check (the rejection is logged via TSError). + ts.Disk.diags_log.Content = Testers.ContainsExpression( + "defines 10 capture groups", "over-limit fetch-path-pattern must be rejected at config load") + + tr = urtest.AddTestRun("prefetch rejects an over-limit capture-group pattern at load") + # Wait for the rejection message with a separate watcher: gating ts readiness on the log line directly + # can race the process exiting before autest observes the line. + watcher = urtest.Processes.Process("watcher") + watcher.Command = "sleep 30" + watcher.Ready = When.FileContains(ts.Disk.diags_log.Name, "defines 10 capture groups") + watcher.StartBefore(ts) + + tr.Processes.Default.Command = "echo done" + tr.TimeOut = 30 + tr.Processes.Default.StartBefore(watcher) + urtest.execute() diff --git a/tests/uranium_tests/pluginTest/prefetch/test_prefetch_bignum.py b/tests/uranium_tests/pluginTest/prefetch/test_prefetch_bignum.py new file mode 100644 index 00000000000..256057d3755 --- /dev/null +++ b/tests/uranium_tests/pluginTest/prefetch/test_prefetch_bignum.py @@ -0,0 +1,103 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_prefetch_bignum(urtest: UraniumTest) -> None: + ''' + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + urtest.Summary = ''' + Test prefetch.so plugin (simple overflow64 mode). + ''' + + server = urtest.MakeOriginServer("server") + vals = [ + "3842948374928374982374982374", "3842948374928374982374982375", "3842948374928374982374982376", + "3842948374928374982374982377" + ] + for i in vals: + request_header = { + "headers": + f"GET /texts/demo-{i}.txt HTTP/1.1\r\n" + "Host: does.not.matter\r\n" # But cannot be omitted. + "\r\n", + "timestamp": "1469733493.993", + "body": "" + } + response_header = { + "headers": "HTTP/1.1 200 OK\r\n" + "Connection: close\r\n" + "Cache-control: max-age=85000\r\n" + "\r\n", + "timestamp": "1469733493.993", + "body": f"This is the body for demo-{i}.txt.\n" + } + server.addResponse("sessionlog.json", request_header, response_header) + + dns = urtest.MakeDNServer("dns") + + ts = urtest.MakeATSProcess("ts") + ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'http|dns|prefetch', + 'proxy.config.dns.nameservers': f"127.0.0.1:{dns.Variables.Port}", + 'proxy.config.dns.resolv_conf': "NULL", + }) + ts.Disk.remap_config.AddLine( + f"map http://domain.in http://127.0.0.1:{server.Variables.Port}" + " @plugin=cachekey.so @pparam=--remove-all-params=true" + " @plugin=prefetch.so" + " @pparam=--front=true" + " @pparam=--fetch-policy=simple" + + r" @pparam=--fetch-path-pattern=/(.*-)(\d+)(.*)/$1{$2+1}$3/" + " @pparam=--fetch-count=3" + + " @pparam=--fetch-overflow=bignum") + ts.ReturnCode = Any(0, -2) + + tr = urtest.AddTestRun() + tr.Processes.Default.StartBefore(server) + tr.Processes.Default.StartBefore(dns) + tr.Processes.Default.StartBefore(ts) + tr.Processes.Default.Command = 'echo start TS, HTTP server and DNS.' + tr.Processes.Default.ReturnCode = 0 + + tr = urtest.AddTestRun() + tr.MakeCurlCommand( + f'--verbose --proxy 127.0.0.1:{ts.Variables.port} http://domain.in/texts/demo-3842948374928374982374982374.txt', ts=ts) + tr.Processes.Default.ReturnCode = 0 + + urtest.AddAwaitFileContainsTestRun( + 'Await transactions to finish logging.', ts.Disk.traffic_out.Name, '3842948374928374982374982377') + + tr = urtest.AddTestRun() + tr.Processes.Default.Command = (f"grep 'GET http://' {ts.Disk.traffic_out.Name} | grep -v '127.0.0.1' ") + tr.Streams.stdout = "prefetch_bignum.gold" + tr.Processes.Default.ReturnCode = 0 + urtest.execute() diff --git a/tests/uranium_tests/pluginTest/prefetch/test_prefetch_cmcd.py b/tests/uranium_tests/pluginTest/prefetch/test_prefetch_cmcd.py new file mode 100644 index 00000000000..aed07cfec62 --- /dev/null +++ b/tests/uranium_tests/pluginTest/prefetch/test_prefetch_cmcd.py @@ -0,0 +1,311 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_prefetch_cmcd(urtest: UraniumTest) -> None: + ''' + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import os + import urllib.parse + + urtest.Summary = ''' + Test prefetch.so plugin (simple mode). + ''' + + origin = urtest.MakeOriginServer("origin") + + asset_name = 'request.txt' + pf_name = 'prefetch.txt' + pf_header = f'Cmcd-Request: foo=12,nor="{pf_name}",bar=42' + + request_header = { + "headers": + f"GET /tests/{asset_name} HTTP/1.1\r\n" + "Host: does.not.matter\r\n" # But cant be omitted + f"{pf_header}\r\n" + "\r\n", + "timestamp": "1469733493.993", + "body": "" + } + response_header = { + "headers": "HTTP/1.1 200 OK\r\n" + "Connection: close\r\n" + "Cache-control: max-age=60\r\n" + "\r\n", + "timestamp": "1469733493.993", + "body": f"This is the body for {asset_name}\n" + } + origin.addResponse("sessionlog.json", request_header, response_header) + + # query string + query_name = 'query?this=foo&that' + query_pf_name = 'query?bar=baz' + query_pf_header = f'Cmcd-Request: nor="{query_pf_name}"' + + # nor field may be percent encoded + query_pf_perc_name = urllib.parse.quote(query_pf_name) + query_pf_perc_header = f'Cmcd-Request: nor="{query_pf_perc_name}"' + + request_header = { + "headers": + f"GET /tests/{query_name} HTTP/1.1\r\n" + "Host: does.not.matter\r\n" # But cant be omitted + f"{query_pf_perc_header}\r\n" + "\r\n", + "timestamp": "1469733493.993", + "body": "" + } + response_header = { + "headers": "HTTP/1.1 200 OK\r\n" + "Connection: close\r\n" + "Cache-control: max-age=60\r\n" + "\r\n", + "timestamp": "1469733493.993", + "body": f"This is the body for {query_name}\n" + } + origin.addResponse("sessionlog.json", request_header, response_header) + + # setup the prefetched assets + names = [pf_name, query_pf_name] + + for name in names: + request_header = { + "headers": + f"GET /tests/{name} HTTP/1.1\r\n" + "Host: does.not.matter\r\n" # But cant be omitted + "\r\n", + "timestamp": "1469733493.993", + "body": "" + } + response_header = { + "headers": "HTTP/1.1 200 OK\r\n" + "Connection: close\r\n" + "Cache-control: max-age=60\r\n" + "\r\n", + "timestamp": "1469733493.993", + "body": f"This is the body for {name}\n" + } + origin.addResponse("sessionlog.json", request_header, response_header) + + # prefetch from root + root_name = 'root.txt' + root_header = f'Cmcd-Request: nor="rooted"' + + request_header = { + "headers": + f"GET /{root_name} HTTP/1.1\r\n" + "Host: does.not.matter\r\n" # But cant be omitted + f"{root_header}\r\n" + "\r\n", + "timestamp": "1469733493.993", + "body": "" + } + response_header = { + "headers": "HTTP/1.1 200 OK\r\n" + "Connection: close\r\n" + "Cache-control: max-age=60\r\n" + "\r\n", + "timestamp": "1469733493.993", + "body": f"This is the body for {root_name}\n" + } + origin.addResponse("sessionlog.json", request_header, response_header) + + request_header = { + "headers": + f"GET /rooted HTTP/1.1\r\n" + "Host: does.not.matter\r\n" # But cant be omitted + "\r\n", + "timestamp": "1469733493.993", + "body": "" + } + response_header = { + "headers": "HTTP/1.1 200 OK\r\n" + "Connection: close\r\n" + "Cache-control: max-age=60\r\n" + "\r\n", + "timestamp": "1469733493.993", + "body": f"This is the body for rooted\n" + } + origin.addResponse("sessionlog.json", request_header, response_header) + + # ignore if cmcd-request nrr= found + crr_name = 'crr.txt' + crr_header = f'Cmcd-Request: foo=12,nor="{crr_name}",bar=42,nrr="0-"' + request_header = { + "headers": + f"GET /tests/{crr_name} HTTP/1.1\r\n" + "Host: does.not.matter\r\n" # But cant be omitted + f"{crr_header}\r\n" + "\r\n", + "timestamp": "1469733493.993", + "body": "" + } + response_header = { + "headers": "HTTP/1.1 200 OK\r\n" + "Connection: close\r\n" + "Cache-control: max-age=60\r\n" + "\r\n", + "timestamp": "1469733493.993", + "body": f"This is the body for {crr_name}\n" + } + origin.addResponse("sessionlog.json", request_header, response_header) + + # allows for multiple ats on localhost + dns = urtest.MakeDNServer("dns") + + # next hop trafficserver instance + ts1 = urtest.MakeATSProcess("ts1", enable_uds=False) + ts1.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'prefetch|http', + 'proxy.config.dns.nameservers': f"127.0.0.1:{dns.Variables.Port}", + 'proxy.config.dns.resolv_conf': "NULL", + 'proxy.config.http.parent_proxy.self_detect': 0, + }) + dns.addRecords(records={f"ts1": ["127.0.0.1"]}) + ts1.Disk.remap_config.AddLine( + f"map / http://127.0.0.1:{origin.Variables.Port}" + " @plugin=cachekey.so @pparam==--sort-params=true" + " @plugin=prefetch.so @pparam==--front=false") + + ts1.Disk.logging_yaml.AddLines( + ''' + logging: + formats: + - name: custom + format: '% % % % % %<{X-CDN-Prefetch}cqh>' + logs: + - filename: transaction + format: custom + '''.split("\n")) + + ts0 = urtest.MakeATSProcess("ts0") + ts0.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'prefetch|http', + 'proxy.config.dns.nameservers': f"127.0.0.1:{dns.Variables.Port}", + 'proxy.config.dns.resolv_conf': "NULL", + 'proxy.config.http.parent_proxy.self_detect': 0, + }) + + dns.addRecords(records={f"ts0": ["127.0.0.1"]}) + ts0.Disk.remap_config.AddLine( + f"map http://ts0 http://ts1:{ts1.Variables.port}" + " @plugin=cachekey.so @pparam=--sort-params=true" + " @plugin=prefetch.so" + " @pparam=--front=true" + " @pparam=--fetch-policy=simple" + " @pparam=--cmcd-nor=true") + + ts0.Disk.logging_yaml.AddLines( + ''' + logging: + formats: + - name: custom + format: '% % % % % %<{X-CDN-Prefetch}cqh>' + logs: + - filename: transaction + format: custom + '''.split("\n")) + + # start everything up + tr = urtest.AddTestRun() + tr.Processes.Default.StartBefore(origin) + tr.Processes.Default.StartBefore(dns) + tr.Processes.Default.StartBefore(ts0) + tr.Processes.Default.StartBefore(ts1) + tr.Processes.Default.Command = 'echo start TS, TSH_N, HTTP origin and DNS.' + tr.Processes.Default.ReturnCode = 0 + + # attempt to get normal asset + tr = urtest.AddTestRun() + tr.MakeCurlCommand(f"--verbose --proxy 127.0.0.1:{ts0.Variables.port} http://ts0/tests/{asset_name}", ts=ts0) + tr.Processes.Default.ReturnCode = 0 + + # issue curl form same asset, with prefetch + tr = urtest.AddTestRun() + tr.DelayStart = 1 + tr.MakeCurlCommand(f"--verbose --proxy 127.0.0.1:{ts0.Variables.port} http://ts0/tests/{asset_name} -H \'{pf_header}\'", ts=ts0) + tr.Processes.Default.ReturnCode = 0 + + # fetch the prefetched asset (only cached on ts1) + tr = urtest.AddTestRun() + tr.DelayStart = 1 + tr.MakeCurlCommand(f"--verbose --proxy 127.0.0.1:{ts0.Variables.port} http://ts0/tests/{pf_name}", ts=ts0) + tr.Processes.Default.ReturnCode = 0 + + # attempt to prefetch again + tr = urtest.AddTestRun() + tr.MakeCurlCommand(f"--verbose --proxy 127.0.0.1:{ts0.Variables.port} http://ts0/tests/{asset_name} -H \'{pf_header}\'", ts=ts0) + tr.Processes.Default.ReturnCode = 0 + + # request the prefetched asset + tr = urtest.AddTestRun() + tr.MakeCurlCommand(f"--verbose --proxy 127.0.0.1:{ts0.Variables.port} http://ts0/tests/{pf_name}", ts=ts0) + tr.Processes.Default.ReturnCode = 0 + + # prefetch using query params with query prefetch perc encoded + tr = urtest.AddTestRun() + tr.MakeCurlCommand( + f"--verbose --proxy 127.0.0.1:{ts0.Variables.port} \'http://ts0/tests/{query_name}\' -H \'{query_pf_perc_header}\'", ts=ts0) + tr.Processes.Default.ReturnCode = 0 + + # request the prefetched asset without perc encoding + tr = urtest.AddTestRun() + tr.MakeCurlCommand(f"--verbose --proxy 127.0.0.1:{ts0.Variables.port} \'http://ts0/tests/{query_pf_name}\'", ts=ts0) + tr.Processes.Default.ReturnCode = 0 + + # ensure root path prefetch works + tr = urtest.AddTestRun() + tr.MakeCurlCommand(f"--verbose --proxy 127.0.0.1:{ts0.Variables.port} \'http://ts0/{root_name}\' -H \'{root_header}\'", ts=ts0) + tr.Processes.Default.ReturnCode = 0 + + # ensure request with nrr= field is skipped + tr = urtest.AddTestRun() + tr.MakeCurlCommand(f"--verbose --proxy 127.0.0.1:{ts0.Variables.port} \'http://ts0/{crr_name}\' -H \'{crr_header}\'", ts=ts0) + tr.Processes.Default.ReturnCode = 0 + + ts0log = os.path.join(ts0.Variables.LOGDIR, 'transaction.log') + urtest.AddAwaitFileContainsTestRun('Await ts transactions to finish logging.', ts0log, 'crr.txt') + + ts1log = os.path.join(ts1.Variables.LOGDIR, 'transaction.log') + urtest.AddAwaitFileContainsTestRun('Await ts1 transactions to finish logging.', ts1log, 'crr.txt') + + tr = urtest.AddTestRun() + tr.Processes.Default.Command = (f"cat {ts0log}") + tr.Streams.stdout = "prefetch_cmcd0.gold" + tr.Processes.Default.ReturnCode = 0 + + tr = urtest.AddTestRun() + tr.Processes.Default.Command = (f"cat {ts1log}") + tr.Streams.stdout = "prefetch_cmcd1.gold" + tr.Processes.Default.ReturnCode = 0 + urtest.execute() diff --git a/tests/uranium_tests/pluginTest/prefetch/test_prefetch_empty_replacement.py b/tests/uranium_tests/pluginTest/prefetch/test_prefetch_empty_replacement.py new file mode 100644 index 00000000000..c2cd3cd5314 --- /dev/null +++ b/tests/uranium_tests/pluginTest/prefetch/test_prefetch_empty_replacement.py @@ -0,0 +1,111 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_prefetch_empty_replacement(urtest: UraniumTest) -> None: + ''' + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + urtest.Summary = ''' + Test prefetch.so does not self-prefetch when a valid pattern's replacement collapses to an empty path. + + The replacement here is just "$3", where group 3 ("(\\?.*)?") is optional. For a query-less request + that group is absent, so the replacement expands to the empty string. An empty expanded path would + otherwise be scheduled with zero length, which BgFetch skips -- leaving the pristine request path and + prefetching the URL itself. The plugin must instead log and stop; the client request is still served. + ''' + + server = urtest.MakeOriginServer("server") + for i in list(range(1, 1 + 2)): + request_header = { + "headers": + f"GET /texts/demo-{i} HTTP/1.1\r\n" + "Host: does.not.matter\r\n" # But cannot be omitted. + "\r\n", + "timestamp": "1469733493.993", + "body": "" + } + response_header = { + "headers": "HTTP/1.1 200 OK\r\n" + "Connection: close\r\n" + "Cache-control: max-age=85000\r\n" + "\r\n", + "timestamp": "1469733493.993", + "body": f"This is the body for demo-{i}.\n" + } + server.addResponse("sessionlog.json", request_header, response_header) + + dns = urtest.MakeDNServer("dns") + + ts = urtest.MakeATSProcess("ts") + ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'http|dns|prefetch', + 'proxy.config.dns.nameservers': f"127.0.0.1:{dns.Variables.Port}", + 'proxy.config.dns.resolv_conf': "NULL", + }) + # A valid pattern (3 defined groups, $3 in range) whose replacement is only the optional $3 group. + ts.Disk.remap_config.AddLine( + f"map http://domain.in http://127.0.0.1:{server.Variables.Port}" + " @plugin=cachekey.so @pparam=--remove-all-params=true" + " @plugin=prefetch.so" + " @pparam=--front=true" + " @pparam=--fetch-policy=simple" + + r" @pparam=--fetch-path-pattern=/(.*-)(\d+)(\?.*)?$/$3/" + " @pparam=--fetch-count=3") + ts.ReturnCode = Any(0, -2) + + # The empty replacement must be logged and skipped. This ContainsExpression both asserts the message + # and replaces the default "diags.log must not contain ERROR:" check (it is logged via TSError). + ts.Disk.diags_log.Content = Testers.ContainsExpression( + "produced an empty path", "an empty replacement must be logged and skipped, not self-prefetched") + + tr = urtest.AddTestRun() + tr.Processes.Default.StartBefore(server) + tr.Processes.Default.StartBefore(dns) + tr.Processes.Default.StartBefore(ts) + tr.Processes.Default.Command = 'echo start TS, HTTP server and DNS.' + tr.Processes.Default.ReturnCode = 0 + + # The client request is still served normally even though the prefetch is skipped. + tr = urtest.AddTestRun() + tr.MakeCurlCommand(f'--verbose --proxy 127.0.0.1:{ts.Variables.port} http://domain.in/texts/demo-1') + tr.Processes.Default.ReturnCode = 0 + + urtest.AddAwaitFileContainsTestRun('Await the empty-path skip to be logged.', ts.Disk.diags_log.Name, 'produced an empty path') + + # The self-prefetch that the old code issued would re-fetch the original path. With the fix the loop + # stops before scheduling, so "failed to process the pattern" (the old second-iteration symptom on the + # emptied working path) must never appear. + tr = urtest.AddTestRun() + tr.Processes.Default.Command = (f"grep -c 'failed to process the pattern' {ts.Disk.traffic_out.Name} || true") + tr.Streams.stdout = Testers.ContainsExpression("0", "no per-request pattern-processing failure") + tr.Processes.Default.ReturnCode = 0 + urtest.execute() diff --git a/tests/uranium_tests/pluginTest/prefetch/test_prefetch_optional_group.py b/tests/uranium_tests/pluginTest/prefetch/test_prefetch_optional_group.py new file mode 100644 index 00000000000..45c738f0226 --- /dev/null +++ b/tests/uranium_tests/pluginTest/prefetch/test_prefetch_optional_group.py @@ -0,0 +1,116 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_prefetch_optional_group(urtest: UraniumTest) -> None: + ''' + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + urtest.Summary = ''' + Test prefetch.so with an optional trailing capture group that does not participate in the match. + + The fetch-path-pattern below ends in an optional "(\\?.*)?" group that only participates when the + subject carries a query string. For a query-less request that group is absent, so pcre_exec() returns + 3 -- one past the highest *participating* group -- even though the pattern defines a 3rd group that the + replacement references as $3. The old code compared $3 against that return value and wrongly rejected + it; a non-participating group must instead substitute an empty string rather than fail the whole + replacement (which previously logged "invalid reference in replacement string: $3" and silently + dropped every prefetch). Mirrors the production hls/mvod remap pattern + "/(.*-)(\\.m3u8)(\\?.*)?$/$1-0.mp4$3/". + ''' + + server = urtest.MakeOriginServer("server") + for i in list(range(1, 1 + 4)): + request_header = { + "headers": + f"GET /texts/demo-{i} HTTP/1.1\r\n" + "Host: does.not.matter\r\n" # But cannot be omitted. + "\r\n", + "timestamp": "1469733493.993", + "body": "" + } + response_header = { + "headers": "HTTP/1.1 200 OK\r\n" + "Connection: close\r\n" + "Cache-control: max-age=85000\r\n" + "\r\n", + "timestamp": "1469733493.993", + "body": f"This is the body for demo-{i}.\n" + } + server.addResponse("sessionlog.json", request_header, response_header) + + dns = urtest.MakeDNServer("dns") + + ts = urtest.MakeATSProcess("ts") + ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'http|dns|prefetch', + 'proxy.config.dns.nameservers': f"127.0.0.1:{dns.Variables.Port}", + 'proxy.config.dns.resolv_conf': "NULL", + }) + ts.Disk.remap_config.AddLine( + f"map http://domain.in http://127.0.0.1:{server.Variables.Port}" + " @plugin=cachekey.so @pparam=--remove-all-params=true" + " @plugin=prefetch.so" + " @pparam=--front=true" + " @pparam=--fetch-policy=simple" + + r" @pparam=--fetch-path-pattern=/(.*-)(\d+)(\?.*)?$/$1{$2+1}$3/" + " @pparam=--fetch-count=3") + ts.ReturnCode = Any(0, -2) + + # Belt-and-suspenders next to the gold comparison (which is the primary guard): a regression that + # re-introduces a per-request rejection of the non-participating $3 logs an "invalid reference ..." + # error. This pattern is valid (3 defined groups, $3 in range) so the compile-time validator never + # fires either, hence no "invalid reference" text should ever reach the log. + ts.Disk.traffic_out.Content = Testers.ExcludesExpression( + "invalid reference", "optional non-participating group must not fail the replacement") + + tr = urtest.AddTestRun() + tr.Processes.Default.StartBefore(server) + tr.Processes.Default.StartBefore(dns) + tr.Processes.Default.StartBefore(ts) + tr.Processes.Default.Command = 'echo start TS, HTTP server and DNS.' + tr.Processes.Default.ReturnCode = 0 + + tr = urtest.AddTestRun() + tr.MakeCurlCommand(f'--verbose --proxy 127.0.0.1:{ts.Variables.port} http://domain.in/texts/demo-1') + tr.Processes.Default.ReturnCode = 0 + + # The original request and the three prefetches are logged independently and may finish out of + # order, so wait for every expected URL to be logged before comparing, and sort both sides so the + # comparison does not depend on completion order. + for tag in ['demo-1', 'demo-2', 'demo-3', 'demo-4']: + urtest.AddAwaitFileContainsTestRun(f'Await {tag} to be logged.', ts.Disk.traffic_out.Name, tag) + + tr = urtest.AddTestRun() + tr.Processes.Default.Command = (f"grep 'GET http://domain.in' {ts.Disk.traffic_out.Name} | sort") + tr.Streams.stdout = "prefetch_optional_group.gold" + tr.Processes.Default.ReturnCode = 0 + urtest.execute() diff --git a/tests/uranium_tests/pluginTest/prefetch/test_prefetch_overflow.py b/tests/uranium_tests/pluginTest/prefetch/test_prefetch_overflow.py new file mode 100644 index 00000000000..dfbd3bbc47f --- /dev/null +++ b/tests/uranium_tests/pluginTest/prefetch/test_prefetch_overflow.py @@ -0,0 +1,96 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_prefetch_overflow(urtest: UraniumTest) -> None: + ''' + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + urtest.Summary = ''' + Test prefetch.so plugin (simple overflow64 mode). + ''' + + server = urtest.MakeOriginServer("server") + for i in list(range(3594967639391, 3594967639391 + 3)): + request_header = { + "headers": + f"GET /texts/demo-{i}.txt HTTP/1.1\r\n" + "Host: does.not.matter\r\n" # But cannot be omitted. + "\r\n", + "timestamp": "1469733493.993", + "body": "" + } + response_header = { + "headers": "HTTP/1.1 200 OK\r\n" + "Connection: close\r\n" + "Cache-control: max-age=85000\r\n" + "\r\n", + "timestamp": "1469733493.993", + "body": f"This is the body for demo-{i}.txt.\n" + } + server.addResponse("sessionlog.json", request_header, response_header) + + dns = urtest.MakeDNServer("dns") + + ts = urtest.MakeATSProcess("ts") + ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'http|dns|prefetch', + 'proxy.config.dns.nameservers': f"127.0.0.1:{dns.Variables.Port}", + 'proxy.config.dns.resolv_conf': "NULL", + }) + ts.Disk.remap_config.AddLine( + f"map http://domain.in http://127.0.0.1:{server.Variables.Port}" + " @plugin=cachekey.so @pparam=--remove-all-params=true" + " @plugin=prefetch.so" + " @pparam=--front=true" + " @pparam=--fetch-policy=simple" + + r" @pparam=--fetch-path-pattern=/(.*-)(\d+)(.*)/$1{$2+1}$3/" + " @pparam=--fetch-count=3" + " @pparam=--fetch-overflow=64") + ts.ReturnCode = Any(0, -2) + + tr = urtest.AddTestRun() + tr.Processes.Default.StartBefore(server) + tr.Processes.Default.StartBefore(dns) + tr.Processes.Default.StartBefore(ts) + tr.Processes.Default.Command = 'echo start TS, HTTP server and DNS.' + tr.Processes.Default.ReturnCode = 0 + + tr = urtest.AddTestRun() + tr.MakeCurlCommand(f'--verbose --proxy 127.0.0.1:{ts.Variables.port} http://domain.in/texts/demo-3594967639391.txt', ts=ts) + tr.Processes.Default.ReturnCode = 0 + + urtest.AddAwaitFileContainsTestRun('Await transactions to finish logging.', ts.Disk.traffic_out.Name, '3594967639394') + + tr = urtest.AddTestRun() + tr.Processes.Default.Command = (f"grep 'GET http://' {ts.Disk.traffic_out.Name} | grep -v '127.0.0.1'") + tr.Streams.stdout = "prefetch_overflow.gold" + tr.Processes.Default.ReturnCode = 0 + urtest.execute() diff --git a/tests/uranium_tests/pluginTest/prefetch/test_prefetch_simple.py b/tests/uranium_tests/pluginTest/prefetch/test_prefetch_simple.py new file mode 100644 index 00000000000..bab369575da --- /dev/null +++ b/tests/uranium_tests/pluginTest/prefetch/test_prefetch_simple.py @@ -0,0 +1,96 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_prefetch_simple(urtest: UraniumTest) -> None: + ''' + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + urtest.Summary = ''' + Test prefetch.so plugin (simple mode). + ''' + + server = urtest.MakeOriginServer("server") + for i in list(range(1, 1 + 3)): + request_header = { + "headers": + f"GET /texts/demo-{i}.txt HTTP/1.1\r\n" + "Host: does.not.matter\r\n" # But cannot be omitted. + "\r\n", + "timestamp": "1469733493.993", + "body": "" + } + response_header = { + "headers": "HTTP/1.1 200 OK\r\n" + "Connection: close\r\n" + "Cache-control: max-age=85000\r\n" + "\r\n", + "timestamp": "1469733493.993", + "body": f"This is the body for demo-{i}.txt.\n" + } + server.addResponse("sessionlog.json", request_header, response_header) + + dns = urtest.MakeDNServer("dns") + + ts = urtest.MakeATSProcess("ts") + ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'http|dns|prefetch', + 'proxy.config.dns.nameservers': f"127.0.0.1:{dns.Variables.Port}", + 'proxy.config.dns.resolv_conf': "NULL", + }) + ts.Disk.remap_config.AddLine( + f"map http://domain.in http://127.0.0.1:{server.Variables.Port}" + " @plugin=cachekey.so @pparam=--remove-all-params=true" + " @plugin=prefetch.so" + " @pparam=--front=true" + " @pparam=--fetch-policy=simple" + + r" @pparam=--fetch-path-pattern=/(.*-)(\d+)(.*)/$1{$2+1}$3/" + " @pparam=--fetch-count=3") + ts.ReturnCode = Any(0, -2) + + tr = urtest.AddTestRun() + tr.Processes.Default.StartBefore(server) + tr.Processes.Default.StartBefore(dns) + tr.Processes.Default.StartBefore(ts) + tr.Processes.Default.Command = 'echo start TS, HTTP server and DNS.' + tr.Processes.Default.ReturnCode = 0 + + tr = urtest.AddTestRun() + tr.MakeCurlCommand(f'--verbose --proxy 127.0.0.1:{ts.Variables.port} http://domain.in/texts/demo-1.txt', ts=ts) + tr.Processes.Default.ReturnCode = 0 + + urtest.AddAwaitFileContainsTestRun('Await transactions to finish logging.', ts.Disk.traffic_out.Name, 'demo-4.txt') + + tr = urtest.AddTestRun() + tr.Processes.Default.Command = (f"grep 'GET http://' {ts.Disk.traffic_out.Name} | grep -v '127.0.0.1'") + tr.Streams.stdout = "prefetch_simple.gold" + tr.Processes.Default.ReturnCode = 0 + urtest.execute() diff --git a/tests/gold_tests/pluginTest/rate_limit/concurrent_reject.sh b/tests/uranium_tests/pluginTest/rate_limit/concurrent_reject.sh similarity index 100% rename from tests/gold_tests/pluginTest/rate_limit/concurrent_reject.sh rename to tests/uranium_tests/pluginTest/rate_limit/concurrent_reject.sh diff --git a/tests/gold_tests/pluginTest/rate_limit/independent_limiters.sh b/tests/uranium_tests/pluginTest/rate_limit/independent_limiters.sh similarity index 100% rename from tests/gold_tests/pluginTest/rate_limit/independent_limiters.sh rename to tests/uranium_tests/pluginTest/rate_limit/independent_limiters.sh diff --git a/tests/gold_tests/pluginTest/rate_limit/queue_bypass_regression.sh b/tests/uranium_tests/pluginTest/rate_limit/queue_bypass_regression.sh similarity index 100% rename from tests/gold_tests/pluginTest/rate_limit/queue_bypass_regression.sh rename to tests/uranium_tests/pluginTest/rate_limit/queue_bypass_regression.sh diff --git a/tests/gold_tests/pluginTest/rate_limit/queue_drain.sh b/tests/uranium_tests/pluginTest/rate_limit/queue_drain.sh similarity index 100% rename from tests/gold_tests/pluginTest/rate_limit/queue_drain.sh rename to tests/uranium_tests/pluginTest/rate_limit/queue_drain.sh diff --git a/tests/gold_tests/pluginTest/rate_limit/rate_limit_sni_expiry_client.sh b/tests/uranium_tests/pluginTest/rate_limit/rate_limit_sni_expiry_client.sh similarity index 100% rename from tests/gold_tests/pluginTest/rate_limit/rate_limit_sni_expiry_client.sh rename to tests/uranium_tests/pluginTest/rate_limit/rate_limit_sni_expiry_client.sh diff --git a/tests/gold_tests/pluginTest/rate_limit/rate_limit_sni_queue_client.sh b/tests/uranium_tests/pluginTest/rate_limit/rate_limit_sni_queue_client.sh similarity index 100% rename from tests/gold_tests/pluginTest/rate_limit/rate_limit_sni_queue_client.sh rename to tests/uranium_tests/pluginTest/rate_limit/rate_limit_sni_queue_client.sh diff --git a/tests/gold_tests/pluginTest/rate_limit/rate_limit_sni_reject_client.sh b/tests/uranium_tests/pluginTest/rate_limit/rate_limit_sni_reject_client.sh similarity index 100% rename from tests/gold_tests/pluginTest/rate_limit/rate_limit_sni_reject_client.sh rename to tests/uranium_tests/pluginTest/rate_limit/rate_limit_sni_reject_client.sh diff --git a/tests/gold_tests/pluginTest/rate_limit/retry_after.sh b/tests/uranium_tests/pluginTest/rate_limit/retry_after.sh similarity index 100% rename from tests/gold_tests/pluginTest/rate_limit/retry_after.sh rename to tests/uranium_tests/pluginTest/rate_limit/retry_after.sh diff --git a/tests/gold_tests/pluginTest/rate_limit/sequential_pass.sh b/tests/uranium_tests/pluginTest/rate_limit/sequential_pass.sh similarity index 100% rename from tests/gold_tests/pluginTest/rate_limit/sequential_pass.sh rename to tests/uranium_tests/pluginTest/rate_limit/sequential_pass.sh diff --git a/tests/uranium_tests/pluginTest/rate_limit/test_rate_limit.py b/tests/uranium_tests/pluginTest/rate_limit/test_rate_limit.py new file mode 100644 index 00000000000..2f860733a25 --- /dev/null +++ b/tests/uranium_tests/pluginTest/rate_limit/test_rate_limit.py @@ -0,0 +1,153 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_rate_limit(urtest: UraniumTest) -> None: + ''' + Test rate_limit plugin: connection limit enforcement, queue drain, and 429 rejection. + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + urtest.Summary = ''' + Test rate_limit plugin: concurrent limit enforcement, queue drain, and independent limiters. + ''' + + urtest.ContinueOnFail = True + + server = urtest.MakeOriginServer("server", delay=3) + ts = urtest.MakeATSProcess("ts") + + server.addResponse( + "sessionlog.json", { + "headers": "GET /slow HTTP/1.1\r\nHost: limit.example.com\r\n\r\n", + "timestamp": "1469733493.993", + "body": "" + }, { + "headers": "HTTP/1.1 200 OK\r\n" + "Content-Length: 4\r\n" + "Connection: close\r\n\r\n", + "timestamp": "1469733493.993", + "body": "SLOW" + }) + + server.addResponse( + "sessionlog.json", { + "headers": "GET /fast HTTP/1.1\r\nHost: limit.example.com\r\n\r\n", + "timestamp": "1469733493.993", + "body": "" + }, { + "headers": "HTTP/1.1 200 OK\r\n" + "Content-Length: 4\r\n" + "Connection: close\r\n\r\n", + "timestamp": "1469733493.993", + "body": "FAST" + }) + + ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'rate_limit', + 'proxy.config.http.insert_response_via_str': 0, + 'proxy.config.url_remap.remap_required': 1, + }) + + # Rule 1: limit=1, no queue — immediate rejection + ts.Disk.remap_config.AddLine( + f'map http://limit.example.com/ http://127.0.0.1:{server.Variables.Port}/' + f' @plugin=rate_limit.so @pparam=--limit @pparam=1 @pparam=--queue @pparam=0' + f' @pparam=--error @pparam=429 @pparam=--retry @pparam=1') + + # Rule 2: limit=1, queue=5 — queues excess, resumes when slot freed + ts.Disk.remap_config.AddLine( + f'map http://queued.example.com/ http://127.0.0.1:{server.Variables.Port}/' + f' @plugin=rate_limit.so @pparam=--limit @pparam=1 @pparam=--queue @pparam=5' + f' @pparam=--maxage @pparam=10000 @pparam=--error @pparam=429') + + # Rules 3 & 4: two independent limiters (limit=1 each) + ts.Disk.remap_config.AddLine( + f'map http://limit-a.example.com/ http://127.0.0.1:{server.Variables.Port}/' + f' @plugin=rate_limit.so @pparam=--limit @pparam=1 @pparam=--queue @pparam=0' + f' @pparam=--error @pparam=429') + + ts.Disk.remap_config.AddLine( + f'map http://limit-b.example.com/ http://127.0.0.1:{server.Variables.Port}/' + f' @plugin=rate_limit.so @pparam=--limit @pparam=1 @pparam=--queue @pparam=0' + f' @pparam=--error @pparam=429') + + # Test 1: Concurrent rejection — second request gets 429 + tr = urtest.AddTestRun("Concurrent requests: second gets 429") + tr.Processes.Default.StartBefore(server) + tr.Processes.Default.StartBefore(ts) + tr.Processes.Default.Command = f'sh {urtest.TestDirectory}/concurrent_reject.sh {ts.Variables.port}' + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.stdout.Content = Testers.ContainsExpression( + "fast=429", "Second concurrent request should be rejected with 429") + + # Test 2: Sequential requests both pass + tr2 = urtest.AddTestRun("Sequential requests: both get 200") + tr2.Processes.Default.Command = f'sh {urtest.TestDirectory}/sequential_pass.sh {ts.Variables.port}' + tr2.Processes.Default.ReturnCode = 0 + tr2.Processes.Default.Streams.stdout.Content = Testers.ContainsExpression("first=200", "First sequential request should pass") + tr2.Processes.Default.Streams.stdout.Content += Testers.ContainsExpression( + "second=200", "Second sequential request should also pass") + + # Test 3: Retry-After header on 429 rejection + tr3 = urtest.AddTestRun("429 response includes Retry-After header") + tr3.Processes.Default.Command = f'sh {urtest.TestDirectory}/retry_after.sh {ts.Variables.port}' + tr3.Processes.Default.ReturnCode = 0 + tr3.Processes.Default.Streams.stdout.Content = Testers.ContainsExpression( + "Retry-After: 1", "429 response should include Retry-After header") + + # Test 4: Queue drain — exercises the fixed reserve() loop + tr4 = urtest.AddTestRun("Queue drain: queued request resumes with 200") + tr4.Processes.Default.Command = f'sh {urtest.TestDirectory}/queue_drain.sh {ts.Variables.port}' + tr4.Processes.Default.ReturnCode = 0 + tr4.Processes.Default.Streams.stdout.Content = Testers.ContainsExpression( + "queued=200", "Queued request should eventually succeed after slot freed") + + # Test 5: Independent limiters — saturating one rule doesn't block the other + tr5 = urtest.AddTestRun("Independent limiters: rule B passes while rule A is full") + tr5.Processes.Default.Command = f'sh {urtest.TestDirectory}/independent_limiters.sh {ts.Variables.port}' + tr5.Processes.Default.ReturnCode = 0 + tr5.Processes.Default.Streams.stdout.Content = Testers.ContainsExpression( + "independent=200", "Request to rule B should pass despite rule A being full") + + # Test 6: Regression for Finding #106 — queue bypass via incorrect reserve() check. + # With the bug, queued requests are resumed without a valid slot reservation, + # allowing all 3 requests to run concurrently (~3s). With the fix, they serialize + # through the single slot (~9s). We check wall time >= 6s as the pass criterion. + tr6 = urtest.AddTestRun("Regression #106: queue does not bypass limit") + tr6.Processes.Default.Command = f'sh {urtest.TestDirectory}/queue_bypass_regression.sh {ts.Variables.port}' + tr6.Processes.Default.ReturnCode = 0 + tr6.Processes.Default.Streams.stdout.Content = Testers.ContainsExpression( + "timing=correct", "Queued requests must serialize through the limiter, not bypass it") + urtest.execute() diff --git a/tests/uranium_tests/pluginTest/rate_limit/test_rate_limit_iprep.py b/tests/uranium_tests/pluginTest/rate_limit/test_rate_limit_iprep.py new file mode 100644 index 00000000000..6ef666c64b6 --- /dev/null +++ b/tests/uranium_tests/pluginTest/rate_limit/test_rate_limit_iprep.py @@ -0,0 +1,141 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_rate_limit_iprep(urtest: UraniumTest) -> None: + ''' + Test rate_limit plugin: IP reputation initialization (Finding #108). + + Validates that ip-rep buckets are properly initialized. The bug used + vector::reserve() instead of resize(), causing UB on indexed writes. + With the fix, ATS starts cleanly and processes TLS connections through + the ip-rep logic without crashing. + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import os + + urtest.Summary = ''' + Test rate_limit ip-rep initialization: reserve() vs resize() regression (Finding #108). + ''' + + urtest.ContinueOnFail = True + + server = urtest.MakeOriginServer("server") + ts = urtest.MakeATSProcess("ts", enable_tls=True) + + server.addResponse( + "sessionlog.json", { + "headers": "GET /test HTTP/1.1\r\nHost: iprep.example.com\r\n\r\n", + "timestamp": "1469733493.993", + "body": "" + }, { + "headers": "HTTP/1.1 200 OK\r\n" + "Content-Length: 2\r\n" + "Connection: close\r\n\r\n", + "timestamp": "1469733493.993", + "body": "OK" + }) + + ts.addDefaultSSLFiles() + + # Write the rate_limit YAML config with ip-rep enabled + rate_limit_yaml = os.path.join(ts.Variables.CONFIGDIR, 'rate_limit.yaml') + ts.Disk.File( + rate_limit_yaml, typename="ats:config").AddLines( + [ + 'ip-rep:', + ' - name: test-iprep', + ' buckets: 5', + ' size: 10', + ' percentage: 90', + ' max_age: 300', + '', + 'selector:', + ' - sni: iprep.example.com', + ' limit: 100', + ' ip-rep: test-iprep', + '', + ]) + + ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'rate_limit', + 'proxy.config.http.insert_response_via_str': 0, + 'proxy.config.url_remap.remap_required': 0, + 'proxy.config.ssl.server.cert.path': ts.Variables.SSLDir, + 'proxy.config.ssl.server.private_key.path': ts.Variables.SSLDir, + }) + + ts.Disk.ssl_multicert_yaml.AddLines( + """ + ssl_multicert: + - dest_ip: "*" + ssl_cert_name: server.pem + ssl_key_name: server.key + """.split("\n")) + + ts.Disk.remap_config.AddLine(f'map / http://127.0.0.1:{server.Variables.Port}/') + + ts.Disk.plugin_config.AddLine(f'rate_limit.so {rate_limit_yaml}') + + # Test 1: ATS starts with ip-rep config and handles a TLS request. + # With the reserve() bug, this would crash or produce UB on startup. + tr = urtest.AddTestRun("IP reputation init: TLS request through ip-rep selector") + tr.Processes.Default.StartBefore(server) + tr.Processes.Default.StartBefore(ts) + tr.Processes.Default.Command = ( + f"curl -sk -o /dev/null -w '%{{http_code}}' " + f"'https://iprep.example.com:{ts.Variables.ssl_port}/test' " + f"--resolve 'iprep.example.com:{ts.Variables.ssl_port}:127.0.0.1'") + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.stdout.Content = Testers.ContainsExpression( + "200", "TLS request through ip-rep selector should succeed") + + # Test 2: Make multiple requests to exercise the ip-rep increment path. + # Each TLS handshake from the same IP increments the reputation counter. + # If buckets were not properly initialized, this triggers the crash. + tr2 = urtest.AddTestRun("IP reputation: multiple requests increment counters") + tr2.Processes.Default.Command = ( + f'for i in 1 2 3 4 5; do ' + f' curl -sk -o /dev/null -w "%{{http_code}} " ' + f' "https://iprep.example.com:{ts.Variables.ssl_port}/test" ' + f' --resolve "iprep.example.com:{ts.Variables.ssl_port}:127.0.0.1"; ' + f'done; echo ""') + tr2.Processes.Default.ReturnCode = 0 + tr2.Processes.Default.Streams.stdout.Content = Testers.ExcludesExpression( + "000", "No request should get a connection failure (code 000)") + + # Verify ATS didn't crash + ts.Disk.diags_log.Content = Testers.ExcludesExpression("FATAL", "ATS should not crash with ip-rep enabled") + urtest.execute() diff --git a/tests/uranium_tests/pluginTest/rate_limit/test_rate_limit_sni.py b/tests/uranium_tests/pluginTest/rate_limit/test_rate_limit_sni.py new file mode 100644 index 00000000000..75a242ca4de --- /dev/null +++ b/tests/uranium_tests/pluginTest/rate_limit/test_rate_limit_sni.py @@ -0,0 +1,142 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_rate_limit_sni(urtest: UraniumTest) -> None: + ''' + Test rate_limit plugin: SNI queue expiry does not underflow active counter (Finding #109). + + With the bug, when a queued SNI connection expires via max_age, free() is + called on VCONN_CLOSE even though reserve() never succeeded, underflowing + the active counter and crashing on the next reserve() assertion. + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import os + + urtest.Summary = ''' + Test rate_limit SNI queue expiry: active counter underflow regression (Finding #109). + ''' + + urtest.ContinueOnFail = True + + server = urtest.MakeOriginServer("server", delay=4) + ts = urtest.MakeATSProcess("ts", enable_tls=True) + + server.addResponse( + "sessionlog.json", { + "headers": "GET /slow HTTP/1.1\r\nHost: queue-expiry.example.com\r\n\r\n", + "timestamp": "1469733493.993", + "body": "" + }, { + "headers": "HTTP/1.1 200 OK\r\n" + "Content-Length: 4\r\n" + "Connection: close\r\n\r\n", + "timestamp": "1469733493.993", + "body": "SLOW" + }) + + server.addResponse( + "sessionlog.json", { + "headers": "GET /test HTTP/1.1\r\nHost: queue-expiry.example.com\r\n\r\n", + "timestamp": "1469733493.993", + "body": "" + }, { + "headers": "HTTP/1.1 200 OK\r\n" + "Content-Length: 2\r\n" + "Connection: close\r\n\r\n", + "timestamp": "1469733493.993", + "body": "OK" + }) + + ts.addDefaultSSLFiles() + + # SNI selector with limit=1, queue=5, max_age=1s (1000ms). + # The short max_age causes queued connections to expire quickly. + rate_limit_yaml = os.path.join(ts.Variables.CONFIGDIR, 'rate_limit.yaml') + ts.Disk.File( + rate_limit_yaml, typename="ats:config").AddLines( + [ + 'selector:', + ' - sni: queue-expiry.example.com', + ' limit: 1', + ' queue:', + ' size: 5', + ' max_age: 1', + '', + ]) + + ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'rate_limit', + 'proxy.config.http.insert_response_via_str': 0, + 'proxy.config.url_remap.remap_required': 0, + 'proxy.config.ssl.server.cert.path': ts.Variables.SSLDir, + 'proxy.config.ssl.server.private_key.path': ts.Variables.SSLDir, + }) + + ts.Disk.ssl_multicert_yaml.AddLines( + """ + ssl_multicert: + - dest_ip: "*" + ssl_cert_name: server.pem + ssl_key_name: server.key + """.split("\n")) + + ts.Disk.remap_config.AddLine(f'map / http://127.0.0.1:{server.Variables.Port}/') + + ts.Disk.plugin_config.AddLine(f'rate_limit.so {rate_limit_yaml}') + + RESOLVE = f"--resolve 'queue-expiry.example.com:{ts.Variables.ssl_port}:127.0.0.1'" + BASE_URL = f"https://queue-expiry.example.com:{ts.Variables.ssl_port}" + + # Test: Queue expiry regression. + # First request holds the slot (4s origin delay), second gets queued and + # expires after max_age (1s). Health check after proves ATS didn't crash. + tr = urtest.AddTestRun("Queue expiry: ATS survives without active counter underflow") + tr.Processes.Default.StartBefore(server) + tr.Processes.Default.StartBefore(ts) + tr.Processes.Default.Command = ( + f"curl -sk -o /dev/null '{BASE_URL}/slow' {RESOLVE} & " + f"sleep 0.5; " + f"curl -sk -o /dev/null '{BASE_URL}/queued' {RESOLVE} 2>/dev/null; " + f"wait; sleep 0.5; " + f"curl -sk -o /dev/null -w '%{{http_code}}' '{BASE_URL}/test' {RESOLVE}") + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.stdout.Content = Testers.ContainsExpression( + "200", "Health check after queue expiry should succeed (ATS still alive)") + + # Verify ATS didn't crash + ts.Disk.diags_log.Content = Testers.ExcludesExpression("FATAL", "ATS should not crash from active counter underflow") + ts.Disk.diags_log.Content += Testers.ExcludesExpression("ink_release_assert", "No assertion failure from _active underflow") + urtest.execute() diff --git a/tests/uranium_tests/pluginTest/rate_limit/test_rate_limit_sni_expiry.py b/tests/uranium_tests/pluginTest/rate_limit/test_rate_limit_sni_expiry.py new file mode 100644 index 00000000000..c69632f219b --- /dev/null +++ b/tests/uranium_tests/pluginTest/rate_limit/test_rate_limit_sni_expiry.py @@ -0,0 +1,107 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_rate_limit_sni_expiry(urtest: UraniumTest) -> None: + ''' + Regression test for the max_age expiry branch of the rate_limit SNI queue accounting. A + queued connection never reserves a slot, so when the sweep expires it the plugin must + detach it rather than release a slot it never held; otherwise the expiry underflows the + active-slot counter and the next reserve() trips a release assertion, aborting the server. + ATS must survive the expiry. + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import os + + urtest.Summary = __doc__ + + urtest.SkipUnless(Condition.PluginExists('rate_limit.so')) + + class RateLimitSniExpiryTest: + """Age a queued connection out via max_age and assert the active-slot counter stays balanced.""" + + def __init__(self) -> None: + tr = urtest.AddTestRun('rate_limit SNI queue max_age expiry') + self._configure_trafficserver() + self._configure_client(tr) + + def _configure_trafficserver(self) -> None: + ts = urtest.MakeATSProcess('ts', enable_tls=True, enable_cache=False) + self._ts = ts + ts.addDefaultSSLFiles() + for line in ['ssl_multicert:', ' - dest_ip: "*"', ' ssl_cert_name: server.pem', ' ssl_key_name: server.key']: + ts.Disk.ssl_multicert_yaml.AddLine(line) + + # One concurrent handshake for this SNI, a one-deep queue, and a 1s max age so the + # sweep expires the queued connection. Named .config (not .yaml) so autest treats it + # as a plain config file; the plugin parses it as YAML regardless. + ts.Disk.MakeConfigFile('rate_limit.config').AddLines( + [ + 'selector:', + ' - sni: rate.limited.com', + ' limit: 1', + ' queue:', + ' size: 1', + ' max_age: 1', + ]) + ts.Disk.plugin_config.AddLine(f'rate_limit.so {ts.Variables.CONFIGDIR}/rate_limit.config') + + # Disable the freelist / ProxyAllocator so allocation behavior is not a confound; the + # abort under test is a release-assertion, and this keeps the run representative of CI. + ts.Command += ' -f -F' + + ts.Disk.records_config.update( + { + 'proxy.config.ssl.server.cert.path': ts.Variables.SSLDir, + 'proxy.config.ssl.server.private_key.path': ts.Variables.SSLDir, + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'rate_limit', + }) + + # The expiry branch is actually reached... + ts.Disk.traffic_out.Content = Testers.ContainsExpression('too old', 'a queued connection was expired') + # ...and expiring it does not underflow the active-slot counter into the release assertion. + ts.Disk.traffic_out.Content += Testers.ExcludesExpression( + '_active <= _limit|received signal', 'expiring a queued connection must not underflow and abort ATS') + + def _configure_client(self, tr: 'TestRun') -> None: + ts = self._ts + client = os.path.join(urtest.TestDirectory, 'rate_limit_sni_expiry_client.sh') + tr.Processes.Default.Command = f'bash {client} 127.0.0.1 {ts.Variables.ssl_port} rate.limited.com' + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.StartBefore(ts) + tr.Processes.Default.Streams.stdout = Testers.ContainsExpression( + 'rate_limit-expiry-done', 'the client ran to completion') + + RateLimitSniExpiryTest() + urtest.execute() diff --git a/tests/uranium_tests/pluginTest/rate_limit/test_rate_limit_sni_queue.py b/tests/uranium_tests/pluginTest/rate_limit/test_rate_limit_sni_queue.py new file mode 100644 index 00000000000..36c49c7c44f --- /dev/null +++ b/tests/uranium_tests/pluginTest/rate_limit/test_rate_limit_sni_queue.py @@ -0,0 +1,108 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_rate_limit_sni_queue(urtest: UraniumTest) -> None: + ''' + Regression test for a queue-accounting balance bug in the rate_limit SNI limiter: a + queued connection never reserves a slot, but its VCONN_CLOSE unconditionally releases + one, so a queued connection that closes underflows the active-slot counter and the next + reserve() trips a release assertion, aborting the server. ATS must survive the queue + churn. + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import os + + urtest.Summary = __doc__ + + urtest.SkipUnless(Condition.PluginExists('rate_limit.so')) + + class RateLimitSniQueueTest: + """Churn the rate_limit SNI queue and assert the active-slot counter never underflows.""" + + def __init__(self) -> None: + tr = urtest.AddTestRun('rate_limit SNI queue accounting') + self._configure_trafficserver() + self._configure_client(tr) + + def _configure_trafficserver(self) -> None: + ts = urtest.MakeATSProcess('ts', enable_tls=True, enable_cache=False) + self._ts = ts + ts.addDefaultSSLFiles() + for line in ['ssl_multicert:', ' - dest_ip: "*"', ' ssl_cert_name: server.pem', ' ssl_key_name: server.key']: + ts.Disk.ssl_multicert_yaml.AddLine(line) + + # One concurrent handshake for this SNI and a queue that admits exactly one more. + # No rate and no max_age -- the sweep's resume path alone drives the scenario, with + # no rate-bucket or expiry timing to confound it. Named .config (not .yaml) so autest + # treats it as a plain config file; the plugin parses it as YAML regardless. + ts.Disk.MakeConfigFile('rate_limit.config').AddLines( + [ + 'selector:', + ' - sni: rate.limited.com', + ' limit: 1', + ' queue:', + ' size: 1', + ]) + ts.Disk.plugin_config.AddLine(f'rate_limit.so {ts.Variables.CONFIGDIR}/rate_limit.config') + + # Disable the freelist / ProxyAllocator so freed objects are really released rather + # than recycled, keeping allocation reuse from masking a stale access. + ts.Command += ' -f -F' + + ts.Disk.records_config.update( + { + 'proxy.config.ssl.server.cert.path': ts.Variables.SSLDir, + 'proxy.config.ssl.server.private_key.path': ts.Variables.SSLDir, + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'rate_limit', + }) + + # The queue path is reached... + ts.Disk.traffic_out.Content = Testers.ContainsExpression('Queueing the VC', 'a connection was queued') + # ...and the active-slot counter never underflows into the release assertion. Match + # both the specific assertion (pins the failure to this bug) and the generic abort. + ts.Disk.traffic_out.Content += Testers.ExcludesExpression( + '_active <= _limit|received signal', 'the active-slot counter must not underflow and abort ATS') + + def _configure_client(self, tr: 'TestRun') -> None: + ts = self._ts + client = os.path.join(urtest.TestDirectory, 'rate_limit_sni_queue_client.sh') + tr.Processes.Default.Command = f'bash {client} 127.0.0.1 {ts.Variables.ssl_port} rate.limited.com' + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.StartBefore(ts) + tr.Processes.Default.Streams.stdout = Testers.ContainsExpression( + 'rate_limit-queue-crash-done', 'the client ran to completion') + + RateLimitSniQueueTest() + urtest.execute() diff --git a/tests/uranium_tests/pluginTest/rate_limit/test_rate_limit_sni_reject.py b/tests/uranium_tests/pluginTest/rate_limit/test_rate_limit_sni_reject.py new file mode 100644 index 00000000000..5e948eefedd --- /dev/null +++ b/tests/uranium_tests/pluginTest/rate_limit/test_rate_limit_sni_reject.py @@ -0,0 +1,103 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_rate_limit_sni_reject(urtest: UraniumTest) -> None: + ''' + Exercise the rate_limit SNI limiter's reject path against a TLS listener, so the + consumer-driven SSLNetVConnection teardown frees every rejected handshake VC + cleanly (no use-after-free or crash). + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import os + + urtest.Summary = __doc__ + + urtest.SkipUnless(Condition.PluginExists('rate_limit.so')) + + class RateLimitSniRejectTest: + """Drive rate_limit's SNI reject path and assert ATS frees the VCs without a fault.""" + + def __init__(self) -> None: + tr = urtest.AddTestRun('rate_limit SNI reject teardown') + self._configure_trafficserver() + self._configure_client(tr) + + def _configure_trafficserver(self) -> None: + ts = urtest.MakeATSProcess('ts', enable_tls=True, enable_cache=False) + self._ts = ts + ts.addDefaultSSLFiles() + for line in ['ssl_multicert:', ' - dest_ip: "*"', ' ssl_cert_name: server.pem', ' ssl_key_name: server.key']: + ts.Disk.ssl_multicert_yaml.AddLine(line) + + # One concurrent handshake for this SNI and no queue, so every further concurrent + # handshake is rejected outright (TS_EVENT_ERROR) rather than queued. Named .config + # (not .yaml) so autest treats it as a plain config file; the plugin parses it as + # YAML regardless (YAML::LoadFile). + ts.Disk.MakeConfigFile('rate_limit.config').AddLines([ + 'selector:', + ' - sni: rate.limited.com', + ' limit: 1', + ]) + ts.Disk.plugin_config.AddLine(f'rate_limit.so {ts.Variables.CONFIGDIR}/rate_limit.config') + + # Disable the freelist / ProxyAllocator so a freed SSLNetVConnection is really + # free()'d rather than recycled; a stale-VC access then hits freed memory + # instead of a still-valid recycled object. + ts.Command += ' -f -F' + + ts.Disk.records_config.update( + { + 'proxy.config.ssl.server.cert.path': ts.Variables.SSLDir, + 'proxy.config.ssl.server.private_key.path': ts.Variables.SSLDir, + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'rate_limit', + }) + + # The reject disposition is reached... + ts.Disk.traffic_out.Content = Testers.ContainsExpression('Rejecting connection', 'over-limit handshakes were rejected') + # ...and ATS tears every rejected handshake VC down without a memory-safety fault. + ts.Disk.traffic_out.Content += Testers.ExcludesExpression( + 'use-after-free|attempting free|SEGV|received signal', 'ATS must survive the reject churn') + + def _configure_client(self, tr: 'TestRun') -> None: + ts = self._ts + client = os.path.join(urtest.TestDirectory, 'rate_limit_sni_reject_client.sh') + tr.Processes.Default.Command = f'bash {client} 127.0.0.1 {ts.Variables.ssl_port} rate.limited.com' + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.StartBefore(ts) + tr.Processes.Default.Streams.stdout = Testers.ContainsExpression( + 'rate_limit-reject-done', 'the client ran to completion') + + RateLimitSniRejectTest() + urtest.execute() diff --git a/tests/uranium_tests/pluginTest/redirect_limit/test_redirect_limit.py b/tests/uranium_tests/pluginTest/redirect_limit/test_redirect_limit.py new file mode 100644 index 00000000000..10f3821ca0d --- /dev/null +++ b/tests/uranium_tests/pluginTest/redirect_limit/test_redirect_limit.py @@ -0,0 +1,155 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_redirect_limit(urtest: UraniumTest) -> None: + ''' + Plugin-initiated redirects must honor proxy.config.http.number_of_redirections. + + A plugin that calls TSHttpTxnRedirectUrlSet on every response hook (the test + plugin redirect_rearm does exactly this) must not be able to follow more + redirects than the configured limit. The redirect counter is shared with the + core redirect follower, so a plugin that re-sets the redirect URL on each hop + is still capped at number_of_redirections. + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import os + + urtest.Summary = 'A plugin re-setting the redirect URL each hop must not bypass number_of_redirections' + + urtest.ContinueOnFail = True + + server = urtest.MakeOriginServer("server") + + # Chain of 5 hops on the same origin server. Use literal 127.0.0.1:{server.port} + # in the Location headers so ATS's redirect follower doesn't have to do DNS. + ORIGIN = "http://127.0.0.1:{0}".format(server.Variables.Port) + + for i in range(1, 5): + server.addResponse( + "sessionlog.json", { + "headers": "GET /r{i} HTTP/1.1\r\nHost: *\r\n\r\n".format(i=i), + "timestamp": "1", + "body": "" + }, { + "headers": "HTTP/1.1 302 Found\r\nLocation: {0}/r{nxt}\r\nContent-Length: 0\r\n\r\n".format(ORIGIN, nxt=i + 1), + "timestamp": "1", + "body": "" + }) + + server.addResponse( + "sessionlog.json", { + "headers": "GET /r5 HTTP/1.1\r\nHost: *\r\n\r\n", + "timestamp": "1", + "body": "" + }, { + "headers": "HTTP/1.1 200 OK\r\nContent-Length: 5\r\n\r\n", + "timestamp": "1", + "body": "final" + }) + + # A short chain (/s1 -> /s2 -> 200) that stays within number_of_redirections=2. + # This is the positive case: a legitimate plugin-initiated redirect within the + # limit must still be followed all the way to the terminal 200. + server.addResponse( + "sessionlog.json", { + "headers": "GET /s1 HTTP/1.1\r\nHost: *\r\n\r\n", + "timestamp": "1", + "body": "" + }, { + "headers": "HTTP/1.1 302 Found\r\nLocation: {0}/s2\r\nContent-Length: 0\r\n\r\n".format(ORIGIN), + "timestamp": "1", + "body": "" + }) + server.addResponse( + "sessionlog.json", { + "headers": "GET /s2 HTTP/1.1\r\nHost: *\r\n\r\n", + "timestamp": "1", + "body": "" + }, { + "headers": "HTTP/1.1 200 OK\r\nContent-Length: 10\r\n\r\n", + "timestamp": "1", + "body": "shortfinal" + }) + + ts = urtest.MakeATSProcess("ts", enable_cache=False) + + ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'redirect_rearm|http_redirect|http', + 'proxy.config.http.number_of_redirections': 2, + 'proxy.config.http.redirect.actions': 'self:follow,private:follow', + }) + + urtest.PrepareTestPlugin(os.path.join(urtest.Variables.AtsTestPluginsDir, 'redirect_rearm.so'), ts) + + ts.Disk.remap_config.AddLine('map http://127.0.0.1:{0}/ http://127.0.0.1:{0}/'.format(server.Variables.Port)) + + tr = urtest.AddTestRun() + tr.MakeCurlCommand( + '-sS -i -x 127.0.0.1:TSPORT http://127.0.0.1:OPORT/r1'.replace('TSPORT', str(ts.Variables.port)).replace( + 'OPORT', str(server.Variables.Port)), + ts=ts) + tr.Processes.Default.StartBefore(ts) + tr.Processes.Default.StartBefore(server) + tr.Processes.Default.ReturnCode = 0 + # With the fix the limit fires and the client sees a 302 returned (the last + # followed hop's response). Without the fix the plugin re-arms the counter on + # every hop and the client sees the final 200 with body "final". + tr.Processes.Default.Streams.stdout = Testers.ContainsExpression( + "HTTP/1.1 302 ", "Client's terminal response must be a 302 from the limit firing, not the final 200") + # Pin the boundary: with number_of_redirections=2 the follower advances r1 -> r2 + # -> r3 and returns r3's response, whose Location points at /r4. Asserting the + # terminal Location is /r4 proves exactly two hops were followed, so this case + # cannot pass if a regression instead followed zero hops (Location /r2) or the + # whole chain. + tr.Processes.Default.Streams.stdout += Testers.ContainsExpression( + "[Ll]ocation: .*/r4", "Terminal 302 must be the third hop's response, proving exactly two redirects were followed") + tr.Processes.Default.Streams.stdout += Testers.ExcludesExpression( + "final", "Client must NOT receive the final body (limit bypassed)") + + # Positive case: a redirect chain within the limit (one hop) must still be + # followed to completion. Guards against the fix over-correcting and refusing + # legitimate plugin-initiated redirects. + tr = urtest.AddTestRun() + tr.MakeCurlCommand( + '-sS -i -x 127.0.0.1:TSPORT http://127.0.0.1:OPORT/s1'.replace('TSPORT', str(ts.Variables.port)).replace( + 'OPORT', str(server.Variables.Port)), + ts=ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.stdout = Testers.ContainsExpression( + "HTTP/1.1 200 ", "A within-limit plugin-initiated redirect must reach the terminal 200") + tr.Processes.Default.Streams.stdout += Testers.ContainsExpression( + "shortfinal", "Client must receive the terminal body for a within-limit redirect") + urtest.execute() diff --git a/tests/gold_tests/pluginTest/regex_remap/gold/regex_remap_crash.gold b/tests/uranium_tests/pluginTest/regex_remap/gold/regex_remap_crash.gold similarity index 100% rename from tests/gold_tests/pluginTest/regex_remap/gold/regex_remap_crash.gold rename to tests/uranium_tests/pluginTest/regex_remap/gold/regex_remap_crash.gold diff --git a/tests/gold_tests/pluginTest/regex_remap/gold/regex_remap_redirect.gold b/tests/uranium_tests/pluginTest/regex_remap/gold/regex_remap_redirect.gold similarity index 100% rename from tests/gold_tests/pluginTest/regex_remap/gold/regex_remap_redirect.gold rename to tests/uranium_tests/pluginTest/regex_remap/gold/regex_remap_redirect.gold diff --git a/tests/gold_tests/pluginTest/regex_remap/gold/regex_remap_simple.gold b/tests/uranium_tests/pluginTest/regex_remap/gold/regex_remap_simple.gold similarity index 100% rename from tests/gold_tests/pluginTest/regex_remap/gold/regex_remap_simple.gold rename to tests/uranium_tests/pluginTest/regex_remap/gold/regex_remap_simple.gold diff --git a/tests/gold_tests/pluginTest/regex_remap/gold/regex_remap_smoke.gold b/tests/uranium_tests/pluginTest/regex_remap/gold/regex_remap_smoke.gold similarity index 100% rename from tests/gold_tests/pluginTest/regex_remap/gold/regex_remap_smoke.gold rename to tests/uranium_tests/pluginTest/regex_remap/gold/regex_remap_smoke.gold diff --git a/tests/gold_tests/pluginTest/regex_remap/replay/yts-2819.replay.json b/tests/uranium_tests/pluginTest/regex_remap/replay/yts-2819.replay.json similarity index 100% rename from tests/gold_tests/pluginTest/regex_remap/replay/yts-2819.replay.json rename to tests/uranium_tests/pluginTest/regex_remap/replay/yts-2819.replay.json diff --git a/tests/uranium_tests/pluginTest/regex_remap/test_regex_remap.py b/tests/uranium_tests/pluginTest/regex_remap/test_regex_remap.py new file mode 100644 index 00000000000..8ff2a6003e2 --- /dev/null +++ b/tests/uranium_tests/pluginTest/regex_remap/test_regex_remap.py @@ -0,0 +1,273 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_regex_remap(urtest: UraniumTest) -> None: + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import os + import json + + urtest.Summary = ''' + Test regex_remap + ''' + + # Test description: + # Exercise regex_remap rule matching, redirects, pristine-URL mapping, and the + # regex match limit. Then verify that two map rules naming the same rule file + # share one compiled rule set, and that rewriting that file and reloading + # remap.config compiles a new shared generation instead of reusing the live one. + # No rule here uses a $n / $h substitution, so that path is not covered. + + urtest.SkipUnless(Condition.PluginExists('regex_remap.so'),) + urtest.ContinueOnFail = False + + # configure origin server + server = urtest.MakeOriginServer("server", lookup_key="{%uuid}") + server.addSessionFromFiles("replay") + replay = {} + with open(os.path.join(urtest.TestDirectory, 'replay/yts-2819.replay.json')) as src: + replay = json.load(src) + + replay_txns = replay["sessions"][0]["transactions"] + + nameserver = urtest.MakeDNServer("dns", default='127.0.0.1') + + # Define ATS and configure + ts = urtest.MakeATSProcess("ts", enable_cache=False) + + testName = "regex_remap" + + regex_remap_conf_path = os.path.join(ts.Variables.CONFIGDIR, 'regex_remap.conf') + regex_remap2_conf_path = os.path.join(ts.Variables.CONFIGDIR, 'regex_remap2.conf') + curl_and_args = '-s -D - -v --proxy localhost:{} '.format(ts.Variables.port) + + regex_remap_lines = [ + "# regex_remap configuration\n", + "^/alpha/bravo/[?]((?!action=(newsfeed|calendar|contacts|notepad)).)*$ https://redirect.com/ @status=301\n", + "^/match_limit/(a+)+$ https://redirect.com/ @status=301\n", + ] + + ts.Disk.File(regex_remap_conf_path, typename="ats:config").AddLines(regex_remap_lines) + + ts.Disk.File( + regex_remap2_conf_path, typename="ats:config").AddLines( + [ + "# 2nd regex_remap configuration\n" + "^/alpha/bravo/[?]((?!action=(newsfeed|calendar|contacts|notepad)).)*$ " + + f"http://localhost:{server.Variables.Port}\n" + ]) + + ts.Disk.remap_config.AddLine( + "map http://example.one/ http://localhost:{}/ @plugin=regex_remap.so @pparam=regex_remap.conf\n".format( + server.Variables.Port)) + ts.Disk.remap_config.AddLine( + "map http://example.two/ http://localhost:{}/ ".format(server.Variables.Port) + + "@plugin=regex_remap.so @pparam=regex_remap.conf @pparam=pristine\n") + ts.Disk.remap_config.AddLine( + "map http://example.three/ http://wrong.com/ ".format(server.Variables.Port) + + "@plugin=regex_remap.so @pparam=regex_remap2.conf @pparam=pristine\n") + + # The cache assertions below depend on regex_remap remaining in the debug tags. + ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'http|regex_remap', + 'proxy.config.dns.nameservers': f"127.0.0.1:{nameserver.Variables.Port}", + 'proxy.config.dns.resolv_conf': 'NULL' + }) + + # 0 Test - Load cache (miss) (path1) + tr = urtest.AddTestRun("smoke test") + tr.Processes.Default.StartBefore(server) + tr.Processes.Default.StartBefore(nameserver) + tr.Processes.Default.StartBefore(urtest.Processes.ts) + creq = replay_txns[0]['client-request'] + tr.MakeCurlCommand(curl_and_args + '--header "uuid: {}" '.format(creq["headers"]["fields"][1][1]) + creq["url"], ts=ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.stdout = "gold/regex_remap_smoke.gold" + tr.StillRunningAfter = ts + + # 1 Test - Match and redirect + tr = urtest.AddTestRun("pristine test") + tr.MakeCurlCommand( + curl_and_args + "'http://example.two/alpha/bravo/?action=newsfed;param0001=00003E;param0002=00004E;param0003=00005E'" + + f" | grep -e '^HTTP/' -e '^Location' | sed 's/{server.Variables.Port}/SERVER_PORT/'", + ts=ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.stdout = "gold/regex_remap_redirect.gold" + tr.StillRunningAfter = ts + + # 2 Test - Match and remap + tr = urtest.AddTestRun("2nd pristine test") + tr.MakeCurlCommand( + curl_and_args + '--header "uuid: {}" '.format(creq["headers"]["fields"][1][1]) + + " 'http://example.three/alpha/bravo/?action=newsfed;param0001=00003E;param0002=00004E;param0003=00005E'" + + " | grep -e '^HTTP/' -e '^Content-Length'", + ts=ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.stdout = "gold/regex_remap_simple.gold" + tr.StillRunningAfter = ts + + # 3 Test - Match limit test 0 + tr = urtest.AddTestRun("match limit 0") + creq = replay_txns[1]['client-request'] + tr.MakeCurlCommand(curl_and_args + \ + '--header "uuid: {}" '.format(creq["headers"]["fields"][1][1]) + '"{}"'.format(creq["url"]), ts=ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.stdout = "gold/regex_remap_crash.gold" + ts.Disk.diags_log.Content = Testers.ContainsExpression( + 'ERROR: .regex_remap. Bad regular expression result -47', "Match limit exceeded") + tr.StillRunningAfter = ts + + # 4 Test - Match limit test 1 + tr = urtest.AddTestRun("match limit 1") + creq = replay_txns[2]['client-request'] + tr.MakeCurlCommand(curl_and_args + \ + '--header "uuid: {}" '.format(creq["headers"]["fields"][1][1]) + '"{}"'.format(creq["url"]), ts=ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.stdout = "gold/regex_remap_crash.gold" + ts.Disk.diags_log.Content = Testers.ContainsExpression( + 'ERROR: .regex_remap. Bad regular expression result -47', "Match limit exceeded") + tr.StillRunningAfter = ts + + class TestRegexRemapRuleCache: + '''Verify shared compiled rules across a remap.config reload.''' + + updated_rule = "^/cache-generation$ https://updated.example/ @status=302\n" + + def __init__(self, ts_process: 'Process', original_rules: str, curl_args: str): + '''Configure the cache and reload TestRuns.''' + self._ts = ts_process + self._original_rules = original_rules + self._curl_args = curl_args + self._regex_remap_path = os.path.join(ts_process.Variables.CONFIGDIR, 'regex_remap.conf') + self._remap_path = os.path.join(ts_process.Variables.CONFIGDIR, 'remap.config') + + self._add_rule_update_run() + self._add_reload_run() + self._add_new_generation_run() + self._add_shared_generation_run() + self._add_isolated_generation_run() + self._add_cache_verification_run() + + def _update_rules(self) -> None: + '''Write a new rule generation and mark remap.config as changed.''' + with open(self._regex_remap_path, 'w') as config_file: + config_file.write(self.updated_rule + self._original_rules) + os.utime(self._remap_path) + + def _add_rule_update_run(self) -> 'TestRun': + '''Change the shared rule file while its first generation is live.''' + tr = urtest.AddTestRun("change shared regex_remap rules") + tr.Processes.Default.Command = "echo 'Updating shared regex_remap rules'" + tr.Processes.Default.Setup.Lambda(self._update_rules) + tr.Processes.Default.ReturnCode = 0 + tr.StillRunningAfter = self._ts + return tr + + def _add_reload_run(self) -> 'TestRun': + '''Reload remap.config after the shared rule file changes.''' + tr = urtest.AddConfigReload( + self._ts, expect_tasks=["remap.config"], description="Reload changed shared regex_remap rules") + tr.StillRunningAfter = self._ts + return tr + + def _add_new_generation_run(self) -> 'TestRun': + '''Verify the new rule generation is active after the reload.''' + tr = urtest.AddTestRun("new shared rule generation") + tr.MakeCurlCommand( + self._curl_args + "'http://example.one/cache-generation' | grep -e '^HTTP/' -e '^Location'", ts=self._ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.stdout = Testers.ContainsExpression("HTTP/1.1 302", "New rule returns a redirect") + tr.Processes.Default.Streams.stdout += Testers.ContainsExpression( + "Location: https://updated.example/", "New rule generation is active") + tr.StillRunningAfter = self._ts + return tr + + def _add_shared_generation_run(self) -> 'TestRun': + '''Verify the other mapping on this file sees the same generation.''' + tr = urtest.AddTestRun("second mapping sees same rule generation") + tr.MakeCurlCommand( + self._curl_args + "'http://example.two/cache-generation' | grep -e '^HTTP/' -e '^Location'", ts=self._ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.stdout = Testers.ContainsExpression("HTTP/1.1 302", "Sharing mapping returns a redirect") + tr.Processes.Default.Streams.stdout += Testers.ContainsExpression( + "Location: https://updated.example/", "Sharing mapping is on the new generation") + tr.StillRunningAfter = self._ts + return tr + + def _add_isolated_generation_run(self) -> 'TestRun': + '''Verify a different rule file does not reuse the changed generation.''' + tr = urtest.AddTestRun("different rule file remains isolated") + tr.MakeCurlCommand( + self._curl_args + "'http://example.three/cache-generation' | grep -e '^HTTP/' -e '^Location'", ts=self._ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.stdout = Testers.ExcludesExpression("HTTP/1.1 302", "Different file does not redirect") + tr.Processes.Default.Streams.stdout += Testers.ExcludesExpression( + "Location: https://updated.example/", "Different file does not use the changed generation") + tr.StillRunningAfter = self._ts + return tr + + def _add_cache_verification_run(self) -> 'TestRun': + '''Verify each distinct generation is compiled only once.''' + await_tr = urtest.AddAwaitFileContainsTestRun( + "await rule cache debug output", self._ts.Disk.traffic_out.Name, "Reusing cached regular expressions from", 3) + await_tr.StillRunningAfter = self._ts + + # Compiles: regex_remap.conf gen1, regex_remap2.conf gen1, and + # regex_remap.conf gen2 == 3 generations and 6 regular expressions. + # Reuses: example.two shares regex_remap.conf on both loads, while + # example.three reuses regex_remap2.conf across the build-then-swap + # reload because the previous remap table is still holding it == 3. + tr = urtest.AddTestRun("verify compiled rule cache") + tr.Processes.Default.Command = ( + f"log={self._ts.Disk.traffic_out.Name}; " + "cached=$$(grep -c 'Cached regular expressions from' $$log); " + "reused=$$(grep -c 'Reusing cached regular expressions from' $$log); " + "compiled=$$(grep -c 'Compiling regex:' $$log); " + "cached_primary=$$(grep 'Cached regular expressions from' $$log | grep -F -c '/regex_remap.conf'); " + "cached_secondary=$$(grep 'Cached regular expressions from' $$log | grep -F -c '/regex_remap2.conf'); " + "reused_primary=$$(grep 'Reusing cached regular expressions from' $$log | grep -F -c '/regex_remap.conf'); " + "reused_secondary=$$(grep 'Reusing cached regular expressions from' $$log | grep -F -c '/regex_remap2.conf'); " + "echo cached=$$cached reused=$$reused compiled=$$compiled " + "cached_primary=$$cached_primary cached_secondary=$$cached_secondary " + "reused_primary=$$reused_primary reused_secondary=$$reused_secondary; " + "test $$cached -eq 3 -a $$reused -eq 3 -a $$compiled -eq 6 -a " + "$$cached_primary -eq 2 -a $$cached_secondary -eq 1 -a " + "$$reused_primary -eq 2 -a $$reused_secondary -eq 1") + tr.Processes.Default.ReturnCode = 0 + tr.StillRunningAfter = self._ts + return tr + + TestRegexRemapRuleCache(ts, ''.join(regex_remap_lines), curl_and_args) + urtest.execute() diff --git a/tests/gold_tests/pluginTest/regex_revalidate/gold/metrics.gold b/tests/uranium_tests/pluginTest/regex_revalidate/gold/metrics.gold similarity index 100% rename from tests/gold_tests/pluginTest/regex_revalidate/gold/metrics.gold rename to tests/uranium_tests/pluginTest/regex_revalidate/gold/metrics.gold diff --git a/tests/gold_tests/pluginTest/regex_revalidate/gold/metrics_miss.gold b/tests/uranium_tests/pluginTest/regex_revalidate/gold/metrics_miss.gold similarity index 100% rename from tests/gold_tests/pluginTest/regex_revalidate/gold/metrics_miss.gold rename to tests/uranium_tests/pluginTest/regex_revalidate/gold/metrics_miss.gold diff --git a/tests/gold_tests/pluginTest/regex_revalidate/metrics.sh b/tests/uranium_tests/pluginTest/regex_revalidate/metrics.sh similarity index 100% rename from tests/gold_tests/pluginTest/regex_revalidate/metrics.sh rename to tests/uranium_tests/pluginTest/regex_revalidate/metrics.sh diff --git a/tests/gold_tests/pluginTest/regex_revalidate/metrics_miss.sh b/tests/uranium_tests/pluginTest/regex_revalidate/metrics_miss.sh similarity index 100% rename from tests/gold_tests/pluginTest/regex_revalidate/metrics_miss.sh rename to tests/uranium_tests/pluginTest/regex_revalidate/metrics_miss.sh diff --git a/tests/uranium_tests/pluginTest/regex_revalidate/test_regex_revalidate.py b/tests/uranium_tests/pluginTest/regex_revalidate/test_regex_revalidate.py new file mode 100644 index 00000000000..a7e516fcb55 --- /dev/null +++ b/tests/uranium_tests/pluginTest/regex_revalidate/test_regex_revalidate.py @@ -0,0 +1,297 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_regex_revalidate(urtest: UraniumTest) -> None: + ''' + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import os + import time + from jsonrpc import Request + + urtest.Summary = ''' + Test a basic regex_revalidate + ''' + + # Test description: + # Load up cache, ensure fresh + # Create regex reval rule, config reload: + # ensure item is staled only once. + # Add a new rule, config reload: + # ensure item isn't restaled again, but rule still in effect. + # + # If the rule disappears from regex_revalidate.conf its still loaded!! + # A rule's expiry can't be changed after the fact! + + urtest.SkipUnless(Condition.PluginExists('regex_revalidate.so'), Condition.PluginExists('xdebug.so')) + urtest.ContinueOnFail = False + + # configure origin server + server = urtest.MakeOriginServer("server") + + # Define ATS and configure + ts = urtest.MakeATSProcess("ts") + + urtest.testName = "regex_revalidate" + urtest.Setup.Copy("metrics.sh") + + # default root + request_header_0 = { + "headers": "GET / HTTP/1.1\r\n" + "Host: www.example.com\r\n" + "\r\n", + "timestamp": "1469733493.993", + "body": "", + } + + response_header_0 = { + "headers": "HTTP/1.1 200 OK\r\n" + "Connection: close\r\n" + "Cache-Control: max-age=300\r\n" + "\r\n", + "timestamp": "1469733493.993", + "body": "xxx", + } + + # cache item path1 + request_header_1 = { + "headers": "GET /path1 HTTP/1.1\r\n" + "Host: www.example.com\r\n" + "\r\n", + "timestamp": "1469733493.993", + "body": "" + } + response_header_1 = { + "headers": + "HTTP/1.1 200 OK\r\n" + "Connection: close\r\n" + 'Etag: "path1"\r\n' + "Cache-Control: max-age=600,public\r\n" + + "\r\n", + "timestamp": "1469733493.993", + "body": "abc" + } + + # cache item path1a + request_header_2 = { + "headers": "GET /path1a HTTP/1.1\r\n" + "Host: www.example.com\r\n" + "\r\n", + "timestamp": "1469733493.993", + "body": "" + } + response_header_2 = { + "headers": + "HTTP/1.1 200 OK\r\n" + "Connection: close\r\n" + 'Etag: "path1a"\r\n' + "Cache-Control: max-age=600,public\r\n" + + "\r\n", + "timestamp": "1469733493.993", + "body": "cde" + } + + # cache item path2a + request_header_3 = { + "headers": "GET /path2a HTTP/1.1\r\n" + "Host: www.example.com\r\n" + "\r\n", + "timestamp": "1469733493.993", + "body": "" + } + response_header_3 = { + "headers": + "HTTP/1.1 200 OK\r\n" + "Connection: close\r\n" + 'Etag: "path2a"\r\n' + "Cache-Control: max-age=900,public\r\n" + + "\r\n", + "timestamp": "1469733493.993", + "body": "efg" + } + + server.addResponse("sessionlog.json", request_header_0, response_header_0) + server.addResponse("sessionlog.json", request_header_1, response_header_1) + server.addResponse("sessionlog.json", request_header_2, response_header_2) + server.addResponse("sessionlog.json", request_header_3, response_header_3) + + # Configure ATS server + ts.Disk.plugin_config.AddLine('xdebug.so --enable=x-cache') + ts.Disk.plugin_config.AddLine('regex_revalidate.so -d -c regex_revalidate.conf') + + regex_revalidate_conf_path = os.path.join(ts.Variables.CONFIGDIR, 'regex_revalidate.conf') + curl_and_args = '-s -D - -v -H "x-debug: x-cache" -H "Host: www.example.com"' + + path1_rule = 'path1 {}\n'.format(int(time.time()) + 600) + + # Define first revision for when trafficserver starts + ts.Disk.File( + regex_revalidate_conf_path, typename="ats:config").AddLines([ + "# Empty\n", + ]) + + ts.Disk.remap_config.AddLine('map / http://127.0.0.1:{}'.format(server.Variables.Port)) + + # minimal configuration + ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'regex_revalidate', + 'proxy.config.http.insert_age_in_response': 0, + 'proxy.config.http.response_via_str': 3, + }) + + # 0 Test - Load cache (miss) (path1) + tr = urtest.AddTestRun("Cache miss path1") + ps = tr.Processes.Default + ps.StartBefore(server, ready=When.PortOpen(server.Variables.Port)) + ps.StartBefore(urtest.Processes.ts) + ps.StartBefore(urtest.Processes.ts) + tr.MakeCurlCommand(curl_and_args + ' http://127.0.0.1:{}/path1'.format(ts.Variables.port), ts=ts) + ps.ReturnCode = 0 + ps.Streams.stdout.Content = Testers.ContainsExpression("X-Cache: miss", "expected cache miss response") + tr.StillRunningAfter = ts + + # 1 Test - Load cache (miss) for later test (path1a) + tr = urtest.AddTestRun("Cache miss path1a") + ps = tr.Processes.Default + tr.MakeCurlCommand(curl_and_args + ' http://127.0.0.1:{}/path1a'.format(ts.Variables.port), ts=ts) + ps.ReturnCode = 0 + ps.Streams.stdout.Content = Testers.ContainsExpression("X-Cache: miss", "expected cache miss response") + tr.StillRunningAfter = ts + + # 2 Test - Load cache (miss) for later test (path2a) + tr = urtest.AddTestRun("Cache miss path2a") + ps = tr.Processes.Default + tr.MakeCurlCommand(curl_and_args + ' http://127.0.0.1:{}/path2a'.format(ts.Variables.port), ts=ts) + ps.ReturnCode = 0 + ps.Streams.stdout.Content = Testers.ContainsExpression("X-Cache: miss", "expected cache miss response") + tr.StillRunningAfter = ts + + # 3 Test - Cache hit path1 + tr = urtest.AddTestRun("Cache hit fresh path1") + ps = tr.Processes.Default + tr.MakeCurlCommand(curl_and_args + ' http://127.0.0.1:{}/path1'.format(ts.Variables.port), ts=ts) + ps.ReturnCode = 0 + ps.Streams.stdout.Content = Testers.ContainsExpression("X-Cache: hit-fresh", "expected cache hit fresh response") + tr.StillRunningAfter = ts + + # 4 Stage - Reload new regex_revalidate + tr = urtest.AddTestRun("Reload config add path1") + # Need a sufficient delay so that the modification time difference of the new config file versus + # the old is greater than the granularity of the time stamp used. (The config file write + # happens after the delay.) + tr.DelayStart = 1 + tr.Disk.File( + regex_revalidate_conf_path, typename="ats:config").AddLines([ + path1_rule, + ]) + tr.StillRunningAfter = ts + tr.StillRunningAfter = server + tr.AddJsonRPCClientRequest(ts, Request.admin_config_reload()) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.TimeOut = 5 + tr.TimeOut = 5 + + # 5 Test - Revalidate path1 + tr = urtest.AddTestRun("Revalidate stale path1") + ps = tr.Processes.Default + tr.DelayStart = 5 + tr.MakeCurlCommand(curl_and_args + ' http://127.0.0.1:{}/path1'.format(ts.Variables.port), ts=ts) + ps.ReturnCode = 0 + ps.Streams.stdout.Content = Testers.ContainsExpression("X-Cache: hit-stale", "expected cache hit stale response") + tr.StillRunningAfter = ts + + # 6 Test - Cache hit (path1) + tr = urtest.AddTestRun("Cache hit fresh path1") + ps = tr.Processes.Default + tr.MakeCurlCommand(curl_and_args + ' http://127.0.0.1:{}/path1'.format(ts.Variables.port), ts=ts) + ps.ReturnCode = 0 + ps.Streams.stdout.Content = Testers.ContainsExpression("X-Cache: hit-fresh", "expected cache hit fresh response") + tr.StillRunningAfter = ts + + # 7 Stage - Reload new regex_revalidate + tr = urtest.AddTestRun("Reload config add path2") + # Need a sufficient delay so that the modification time difference of the new config file versus + # the old is greater than the granularity of the time stamp used. (The config file write + # happens after the delay.) + tr.DelayStart = 1 + tr.Disk.File( + regex_revalidate_conf_path, typename="ats:config").AddLines([ + path1_rule, + 'path2 {}\n'.format(int(time.time()) + 700), + ]) + tr.StillRunningAfter = ts + tr.StillRunningAfter = server + tr.AddJsonRPCClientRequest(ts, Request.admin_config_reload()) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.TimeOut = 5 + tr.TimeOut = 5 + + # 8 Test - Cache hit (path1) + tr = urtest.AddTestRun("Cache hit fresh path1") + tr.DelayStart = 5 + ps = tr.Processes.Default + tr.MakeCurlCommand(curl_and_args + ' http://127.0.0.1:{}/path1'.format(ts.Variables.port), ts=ts) + ps.ReturnCode = 0 + ps.Streams.stdout.Content = Testers.ContainsExpression("X-Cache: hit-fresh", "expected cache hit fresh response") + tr.StillRunningAfter = ts + + # 9 Test - Cache stale (check rule is still loaded) (path1a) + tr = urtest.AddTestRun("Revalidate stale path1a") + ps = tr.Processes.Default + tr.MakeCurlCommand(curl_and_args + ' http://127.0.0.1:{}/path1a'.format(ts.Variables.port), ts=ts) + ps.ReturnCode = 0 + ps.Streams.stdout.Content = Testers.ContainsExpression("X-Cache: hit-stale", "expected cache hit stale response") + tr.StillRunningAfter = ts + + # The C version of regex_revalidate doesn't allow an existing rule to + # be changed by a reload. + + # 10 Stage - regex_revalidate rewrite rule early expire + tr = urtest.AddTestRun("Reload config change path2") + # Need a sufficient delay so that the modification time difference of the new config file versus + # the old is greater than the granularity of the time stamp used. (The config file write + # happens after the delay.) + tr.DelayStart = 1 + tr.Disk.File( + regex_revalidate_conf_path, typename="ats:config").AddLines([ + path1_rule, + 'path2 {}\n'.format(int(time.time()) - 100), + ]) + tr.StillRunningAfter = ts + tr.StillRunningAfter = server + tr.AddJsonRPCClientRequest(ts, Request.admin_config_reload()) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.TimeOut = 5 + tr.TimeOut = 5 + + # 11 Test - Cache hit (path2a) + tr = urtest.AddTestRun("Cache hit stale path2a") + tr.DelayStart = 5 + ps = tr.Processes.Default + tr.MakeCurlCommand(curl_and_args + ' http://127.0.0.1:{}/path2a'.format(ts.Variables.port), ts=ts) + ps.ReturnCode = 0 + ps.Streams.stdout.Content = Testers.ContainsExpression("X-Cache: hit-stale", "expected cache hit stale response") + tr.StillRunningAfter = ts + + # 12 Stats check + tr = urtest.AddTestRun("Check stats") + tr.DelayStart = 5 + ps = tr.Processes.Default + ps.Command = "bash -c ./metrics.sh" + ps.Env = ts.Env + ps.ReturnCode = 0 + tr.StillRunningAfter = ts + urtest.execute() diff --git a/tests/uranium_tests/pluginTest/regex_revalidate/test_regex_revalidate_miss.py b/tests/uranium_tests/pluginTest/regex_revalidate/test_regex_revalidate_miss.py new file mode 100644 index 00000000000..e3aa457f151 --- /dev/null +++ b/tests/uranium_tests/pluginTest/regex_revalidate/test_regex_revalidate_miss.py @@ -0,0 +1,271 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_regex_revalidate_miss(urtest: UraniumTest) -> None: + ''' + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import os + import time + from jsonrpc import Request + + urtest.Summary = ''' + regex_revalidate plugin test, MISS (refetch) functionality + ''' + + # Test description: + # If MISS tag encountered, should load rule as refetch instead of IMS. + # If rule switched from MISS to IMS or vice versa, rule should reset. + + urtest.SkipUnless(Condition.PluginExists('regex_revalidate.so'), Condition.PluginExists('xdebug.so')) + urtest.ContinueOnFail = False + + # configure origin server + server = urtest.MakeOriginServer("server") + + # Define ATS and configure + ts = urtest.MakeATSProcess("ts") + + urtest.testName = "regex_revalidate_miss" + urtest.Setup.Copy("metrics_miss.sh") + + # default root + request_header_0 = { + "headers": "GET / HTTP/1.1\r\n" + "Host: www.example.com\r\n" + "\r\n", + "timestamp": "1469733493.993", + "body": "", + } + + response_header_0 = { + "headers": "HTTP/1.1 200 OK\r\n" + "Connection: close\r\n" + "Cache-Control: max-age=300\r\n" + "\r\n", + "timestamp": "1469733493.993", + "body": "xxx", + } + + # cache item path1 + request_header_1 = { + "headers": "GET /path1 HTTP/1.1\r\n" + "Host: www.example.com\r\n" + "\r\n", + "timestamp": "1469733493.993", + "body": "" + } + response_header_1 = { + "headers": + "HTTP/1.1 200 OK\r\n" + "Connection: close\r\n" + 'Etag: "path1"\r\n' + "Cache-Control: max-age=600,public\r\n" + + "\r\n", + "timestamp": "1469733493.993", + "body": "abc" + } + + server.addResponse("sessionlog.json", request_header_0, response_header_0) + server.addResponse("sessionlog.json", request_header_1, response_header_1) + + # Configure ATS server + ts.Disk.plugin_config.AddLine('xdebug.so --enable=x-cache') + ts.Disk.plugin_config.AddLine('regex_revalidate.so -d -c regex_revalidate.conf -l revalidate.log -m reval') + + regex_revalidate_conf_path = os.path.join(ts.Variables.CONFIGDIR, 'regex_revalidate.conf') + #curl_and_args = '-s -D - -v -H "x-debug: x-cache" -H "Host: www.example.com"' + + path1_rule = 'path1 {}'.format(int(time.time()) + 600) + + # Define first revision for when trafficserver starts + ts.Disk.File(regex_revalidate_conf_path, typename="ats:config").AddLine("# Empty") + + ts.Disk.remap_config.AddLine('map http://ats/ http://127.0.0.1:{}'.format(server.Variables.Port)) + + # minimal configuration + ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'regex_revalidate', + 'proxy.config.http.insert_age_in_response': 0, + 'proxy.config.http.response_via_str': 3, + 'proxy.config.http.cache.http': 1, + 'proxy.config.http.wait_for_cache': 1, + }) + + curl_and_args = '-s -D /dev/stdout -o /dev/stderr -x http://127.0.0.1:{}'.format(ts.Variables.port) + ' -H "x-debug: x-cache"' + + # 0 Request, cache miss expected + tr = urtest.AddTestRun("Cache miss path1") + ps = tr.Processes.Default + ps.StartBefore(server, ready=When.PortOpen(server.Variables.Port)) + ps.StartBefore(urtest.Processes.ts) + tr.MakeCurlCommand(curl_and_args + ' http://ats/path1', ts=ts) + ps.ReturnCode = 0 + ps.Streams.stdout.Content = Testers.ContainsExpression("X-Cache: miss", "expected cache miss response") + tr.StillRunningAfter = ts + + # 1 Request, cache hit expected + tr = urtest.AddTestRun("Cache hit fresh path1") + ps = tr.Processes.Default + tr.MakeCurlCommand(curl_and_args + ' http://ats/path1', ts=ts) + ps.ReturnCode = 0 + ps.Streams.stdout.Content = Testers.ContainsExpression("X-Cache: hit-fresh", "expected cache hit fresh response") + tr.StillRunningAfter = ts + + # 2 Reload, populated config + tr = urtest.AddTestRun("Reload config add path1") + ps = tr.Processes.Default + tr.Disk.File(regex_revalidate_conf_path, typename="ats:config").AddLine(path1_rule + ' MISS') + # keep this for debug + tr.Disk.File(regex_revalidate_conf_path + "_tr2", typename="ats:config").AddLine(path1_rule + ' MISS') + tr.StillRunningAfter = ts + tr.StillRunningAfter = server + tr.AddJsonRPCClientRequest(ts, Request.admin_config_reload()) + ps.ReturnCode = 0 + ps.TimeOut = 5 + tr.TimeOut = 5 + # Delay it so the reload can catch up the diff between config files timestamps. + tr.DelayStart = 1 + + # 3 Request, cache miss expected + tr = urtest.AddTestRun("Revalidate MISS path1") + ps = tr.Processes.Default + tr.DelayStart = 7 + tr.MakeCurlCommand(curl_and_args + ' http://ats/path1', ts=ts) + ps.ReturnCode = 0 + ps.Streams.stdout.Content = Testers.ContainsExpression("X-Cache: miss", "expected cache miss response") + tr.StillRunningAfter = ts + + # 4 Request, cache hit (path1) + tr = urtest.AddTestRun("Cache hit fresh path1") + ps = tr.Processes.Default + tr.MakeCurlCommand(curl_and_args + ' http://ats/path1', ts=ts) + ps.ReturnCode = 0 + ps.Streams.stdout.Content = Testers.ContainsExpression("X-Cache: hit-fresh", "expected cache hit fresh response") + tr.StillRunningAfter = ts + + # 5 Reload, change MISS to STALE (resets rule) + tr = urtest.AddTestRun("Reload config path1 STALE") + ps = tr.Processes.Default + tr.Disk.File(regex_revalidate_conf_path, typename="ats:config").AddLine(path1_rule + ' STALE') + tr.Disk.File(regex_revalidate_conf_path + "_tr5", typename="ats:config").AddLine(path1_rule + ' STALE') + tr.StillRunningAfter = ts + tr.StillRunningAfter = server + tr.AddJsonRPCClientRequest(ts, Request.admin_config_reload()) + ps.ReturnCode = 0 + ps.TimeOut = 5 + tr.TimeOut = 5 + # Delay it so the reload can catch up the diff between config files timestamps. + tr.DelayStart = 1 + + # 6 Request, cache stale expected + tr = urtest.AddTestRun("Cache stale path1") + ps = tr.Processes.Default + tr.DelayStart = 7 + tr.MakeCurlCommand(curl_and_args + ' http://ats/path1', ts=ts) + ps.ReturnCode = 0 + ps.Streams.stdout.Content = Testers.ContainsExpression("X-Cache: hit-stale", "expected cache hit stale response") + tr.StillRunningAfter = ts + + # 7 Reload, change STALE to MISS (resets rule) + tr = urtest.AddTestRun("Reload config path1 MISS") + ps = tr.Processes.Default + tr.Disk.File(regex_revalidate_conf_path, typename="ats:config").AddLine(path1_rule + ' MISS') + tr.Disk.File(regex_revalidate_conf_path + "_tr7", typename="ats:config").AddLine(path1_rule + ' MISS') + tr.StillRunningAfter = ts + tr.StillRunningAfter = server + tr.AddJsonRPCClientRequest(ts, Request.admin_config_reload()) + ps.ReturnCode = 0 + ps.TimeOut = 5 + tr.TimeOut = 5 + # Delay it so the reload can catch up the diff between config files timestamps. + tr.DelayStart = 1 + + # 8 Request, cache miss expected + tr = urtest.AddTestRun("Cache mis path1") + ps = tr.Processes.Default + tr.DelayStart = 7 + tr.MakeCurlCommand(curl_and_args + ' http://ats/path1', ts=ts) + ps.ReturnCode = 0 + ps.Streams.stdout.Content = Testers.ContainsExpression("X-Cache: miss", "expected cache miss response") + tr.StillRunningAfter = ts + + # 9 Request, cache hit expected + tr = urtest.AddTestRun("Cache hit path1") + ps = tr.Processes.Default + tr.DelayStart = 5 + tr.MakeCurlCommand(curl_and_args + ' http://ats/path1', ts=ts) + ps.ReturnCode = 0 + ps.Streams.stdout.Content = Testers.ContainsExpression("X-Cache: hit-fresh", "expected cache hit response") + tr.StillRunningAfter = ts + + # 10 Reload, no changes + tr = urtest.AddTestRun("Reload no changes") + ps = tr.Processes.Default + tr.StillRunningAfter = ts + tr.StillRunningAfter = server + tr.AddJsonRPCClientRequest(ts, Request.admin_config_reload()) + ps.ReturnCode = 0 + ps.TimeOut = 5 + tr.TimeOut = 5 + # Delay it so the reload can catch up the diff between config files timestamps. + tr.DelayStart = 1 + + # 11 Request again, cache hit expected + tr = urtest.AddTestRun("Cache hit path1") + ps = tr.Processes.Default + tr.DelayStart = 7 + tr.MakeCurlCommand(curl_and_args + ' http://ats/path1', ts=ts) + ps.ReturnCode = 0 + ps.Streams.stdout.Content = Testers.ContainsExpression("X-Cache: hit-fresh", "expected cache hit response") + tr.StillRunningAfter = ts + + # 12 Stage - Touch the rules file to trigger reload detection + tr = urtest.AddTestRun("Touch regex_revalidate config") + tr.Processes.Default.Command = f'touch {regex_revalidate_conf_path}' + tr.Processes.Default.ReturnCode = 0 + tr.StillRunningAfter = ts + tr.StillRunningAfter = server + + tr = urtest.AddConfigReload(ts, description="Reload same config") + tr.StillRunningAfter = server + + # 13 Request, Cache hit expected + tr = urtest.AddTestRun("Cache hit path1") + ps = tr.Processes.Default + tr.MakeCurlCommand(curl_and_args + ' http://ats/path1', ts=ts) + ps.ReturnCode = 0 + ps.Streams.stdout.Content = Testers.ContainsExpression("X-Cache: hit-fresh", "expected cache hit response") + tr.StillRunningAfter = ts + + # 14 Stats check + tr = urtest.AddTestRun("Check stats") + tr.DelayStart = 5 + tr.Processes.Default.Command = f"bash -c ./metrics_miss.sh" + tr.Processes.Default.Env = ts.Env + tr.Processes.Default.ReturnCode = 0 + tr.StillRunningAfter = ts + urtest.execute() diff --git a/tests/uranium_tests/pluginTest/regex_revalidate/test_regex_revalidate_state.py b/tests/uranium_tests/pluginTest/regex_revalidate/test_regex_revalidate_state.py new file mode 100644 index 00000000000..72a9d43b3d0 --- /dev/null +++ b/tests/uranium_tests/pluginTest/regex_revalidate/test_regex_revalidate_state.py @@ -0,0 +1,160 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_regex_revalidate_state(urtest: UraniumTest) -> None: + ''' + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import os + import time + + urtest.Summary = ''' + regex_revalidate plugin test, reload epoch state on ats start + ''' + + # Test description: + # Ensures that that the regex revalidate config file is loaded, + # then epoch times from the state file are properly merged. + + urtest.SkipUnless(Condition.PluginExists('regex_revalidate.so')) + urtest.ContinueOnFail = False + + # configure origin server + server = urtest.MakeOriginServer("server") + + # Define ATS and configure + ts = urtest.MakeATSProcess("ts") + + # **testname is required** + testName = "regex_revalidate_state" + + # default root + request_header_0 = { + "headers": "GET / HTTP/1.1\r\n" + "Host: www.example.com\r\n" + "\r\n", + "timestamp": "1469733493.993", + "body": "", + } + + response_header_0 = { + "headers": "HTTP/1.1 200 OK\r\n" + "Connection: close\r\n" + "Cache-Control: max-age=300\r\n" + "\r\n", + "timestamp": "1469733493.993", + "body": "xxx", + } + + server.addResponse("sessionlog.json", request_header_0, response_header_0) + + reval_conf_path = os.path.join(ts.Variables.CONFIGDIR, 'reval.conf') + reval_state_path = os.path.join(ts.Variables.RUNTIMEDIR, 'reval.state') + + # Configure ATS server + ts.Disk.plugin_config.AddLine(f"regex_revalidate.so -d -c reval.conf -l reval.log -f {reval_state_path}") + + sep = ' ' + + # rule with no initial state + path0_regex = "path0" + path0_expiry = str(time.time() + 90).split('.')[0] + path0_type = "STALE" + path0_rule = sep.join([path0_regex, path0_expiry, path0_type]) + + path1_regex = "path1" + path1_epoch = str(time.time() - 50).split('.')[0] + path1_expiry = str(time.time() + 600).split('.')[0] + path1_type = "MISS" + path1_rule = sep.join([path1_regex, path1_expiry, path1_type]) + + # Create gold files + gold_path_good = reval_state_path + ".good" + ts.Disk.File( + gold_path_good, typename="ats:config").AddLines( + [ + sep.join([path0_regex, "``", path0_expiry, path0_type]), + sep.join([path1_regex, path1_epoch, path1_expiry, path1_type]), + ]) + + # It seems there's no API for negative gold file matching + ''' + gold_path_bad = reval_state_path + ".bad" + ts.Disk.File(gold_path_bad, typename="ats:config").AddLines([ + sep.join([path0_regex, path1_epoch, path0_expiry, path0_type]), + sep.join([path1_regex, path1_epoch, path1_expiry, path1_type]), + ]) + ''' + + # Create a state file, second line will be discarded and not merged + ts.Disk.File( + reval_state_path, typename="ats:config").AddLines( + [ + sep.join([path1_regex, path1_epoch, path1_expiry, path1_type]), + sep.join(["dummy", path1_epoch, path1_expiry, path1_type]), + ]) + + # Write out reval.conf file + ts.Disk.File( + reval_conf_path, typename="ats:config").AddLines([ + path0_rule, + path1_rule, + ]) + + ts.chownForATSProcess(reval_state_path) + + ts.Disk.remap_config.AddLine(f"map http://ats/ http://127.0.0.1:{server.Variables.Port}") + + # minimal configuration + ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'regex_revalidate', + 'proxy.config.http.wait_for_cache': 1, + }) + + # This TestRun creates the state file so it exists when the ts process's Setup + # logic is run so that it can be chowned at that point. + tr = urtest.AddTestRun("Populate the regex_revalidate state file") + tr.Processes.Default.Command = f'touch {reval_state_path}' + + # Start ATS and evaluate the new state file + tr = urtest.AddTestRun("Initial load, state merged") + ps = tr.Processes.Default + ps.StartBefore(server, ready=When.PortOpen(server.Variables.Port)) + + # Note the ready condition: wait for ATS to modify the contents + # of the file from dummy to path1. + ps.StartBefore(urtest.Processes.ts, ready=When.FileContains(reval_state_path, "path0")) + + ps.Command = 'cat ' + reval_state_path + ps.ReturnCode = 0 + ps.Streams.stdout.Content = Testers.GoldFile(gold_path_good) + tr.StillRunningAfter = ts + urtest.execute() diff --git a/tests/gold_tests/pluginTest/remap_stats/gold/metrics.gold b/tests/uranium_tests/pluginTest/remap_stats/gold/metrics.gold similarity index 100% rename from tests/gold_tests/pluginTest/remap_stats/gold/metrics.gold rename to tests/uranium_tests/pluginTest/remap_stats/gold/metrics.gold diff --git a/tests/gold_tests/pluginTest/remap_stats/gold/metrics_post.gold b/tests/uranium_tests/pluginTest/remap_stats/gold/metrics_post.gold similarity index 100% rename from tests/gold_tests/pluginTest/remap_stats/gold/metrics_post.gold rename to tests/uranium_tests/pluginTest/remap_stats/gold/metrics_post.gold diff --git a/tests/gold_tests/pluginTest/remap_stats/metrics.sh b/tests/uranium_tests/pluginTest/remap_stats/metrics.sh similarity index 100% rename from tests/gold_tests/pluginTest/remap_stats/metrics.sh rename to tests/uranium_tests/pluginTest/remap_stats/metrics.sh diff --git a/tests/gold_tests/pluginTest/remap_stats/metrics_post.sh b/tests/uranium_tests/pluginTest/remap_stats/metrics_post.sh similarity index 100% rename from tests/gold_tests/pluginTest/remap_stats/metrics_post.sh rename to tests/uranium_tests/pluginTest/remap_stats/metrics_post.sh diff --git a/tests/uranium_tests/pluginTest/remap_stats/test_remap_stats.py b/tests/uranium_tests/pluginTest/remap_stats/test_remap_stats.py new file mode 100644 index 00000000000..ac23bb3c402 --- /dev/null +++ b/tests/uranium_tests/pluginTest/remap_stats/test_remap_stats.py @@ -0,0 +1,87 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_remap_stats(urtest: UraniumTest) -> None: + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + urtest.Summary = ''' + Test remap_stats plugin + ''' + # Skip if plugins not present. + urtest.SkipUnless(Condition.PluginExists('remap_stats.so')) + + server = urtest.MakeOriginServer("server") + + urtest.Setup.Copy("metrics.sh") + + request_header = {"headers": "GET /argh HTTP/1.1\r\nHost: one\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + server.addResponse("sessionlog.json", request_header, response_header) + + ts = urtest.MakeATSProcess("ts") + + ts.Disk.plugin_config.AddLine('remap_stats.so') + + ts.Disk.remap_config.AddLine("map http://one http://127.0.0.1:{0}".format(server.Variables.Port)) + ts.Disk.remap_config.AddLine("map http://two http://127.0.0.1:{0}".format(server.Variables.Port)) + + ts.Disk.records_config.update( + { + 'proxy.config.http.transaction_active_timeout_out': 2, + 'proxy.config.http.transaction_no_activity_timeout_out': 2, + 'proxy.config.http.connect_attempts_timeout': 2, + }) + + # 0 Test - Curl host One + tr = urtest.AddTestRun("curl host one") + tr.Processes.Default.StartBefore(server) + tr.Processes.Default.StartBefore(urtest.Processes.ts) + tr.MakeCurlCommand('-o /dev/null -H "Host: one"' + ' http://127.0.0.1:{}/argh'.format(ts.Variables.port), ts=ts) + tr.Processes.Default.ReturnCode = 0 + tr.StillRunningAfter = ts + tr.StillRunningAfter = server + + # 1 Test - Curl host Two + tr = urtest.AddTestRun("curl host two") + tr.MakeCurlCommand('-o /dev/null -H "Host: two"' + ' http://127.0.0.1:{}/badpath'.format(ts.Variables.port), ts=ts) + tr.Processes.Default.ReturnCode = 0 + tr.StillRunningAfter = ts + tr.StillRunningAfter = server + + # 2 Test - Gather output + tr = urtest.AddTestRun("analyze stats") + tr.Processes.Default.Command = 'bash -c ./metrics.sh' + tr.Processes.Default.Env = ts.Env + tr.Processes.Default.ReturnCode = 0 + tr.StillRunningAfter = ts + urtest.execute() diff --git a/tests/uranium_tests/pluginTest/remap_stats/test_remap_stats_post.py b/tests/uranium_tests/pluginTest/remap_stats/test_remap_stats_post.py new file mode 100644 index 00000000000..36ac6d0d2e7 --- /dev/null +++ b/tests/uranium_tests/pluginTest/remap_stats/test_remap_stats_post.py @@ -0,0 +1,87 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_remap_stats_post(urtest: UraniumTest) -> None: + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + urtest.Summary = ''' + Test remap_stats plugin + ''' + # Skip if plugins not present. + urtest.SkipUnless(Condition.PluginExists('remap_stats.so')) + + server = urtest.MakeOriginServer("server") + + urtest.Setup.Copy("metrics_post.sh") + + request_header = {"headers": "GET /argh HTTP/1.1\r\nHost: one\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + server.addResponse("sessionlog.json", request_header, response_header) + + ts = urtest.MakeATSProcess("ts") + + ts.Disk.plugin_config.AddLine('remap_stats.so --post-remap-host') + + ts.Disk.remap_config.AddLine("map http://one http://127.0.0.1:{0}".format(server.Variables.Port)) + ts.Disk.remap_config.AddLine("map http://two http://127.0.0.1:{0}".format(server.Variables.Port)) + + ts.Disk.records_config.update( + { + 'proxy.config.http.transaction_active_timeout_out': 2, + 'proxy.config.http.transaction_no_activity_timeout_out': 2, + 'proxy.config.http.connect_attempts_timeout': 2, + }) + + # 0 Test - Curl host One + tr = urtest.AddTestRun("curl host one") + tr.Processes.Default.StartBefore(server) + tr.Processes.Default.StartBefore(urtest.Processes.ts) + tr.MakeCurlCommand('-o /dev/null -H "Host: one"' + ' http://127.0.0.1:{}/argh'.format(ts.Variables.port), ts=ts) + tr.Processes.Default.ReturnCode = 0 + tr.StillRunningAfter = ts + tr.StillRunningAfter = server + + # 1 Test - Curl host Two + tr = urtest.AddTestRun("curl host two") + tr.MakeCurlCommand('-o /dev/null -H "Host: two"' + ' http://127.0.0.1:{}/badpath'.format(ts.Variables.port), ts=ts) + tr.Processes.Default.ReturnCode = 0 + tr.StillRunningAfter = ts + tr.StillRunningAfter = server + + # 2 Test - Gather output + tr = urtest.AddTestRun("analyze stats") + tr.Processes.Default.Command = 'bash -c ./metrics_post.sh' + tr.Processes.Default.Env = ts.Env + tr.Processes.Default.ReturnCode = 0 + tr.StillRunningAfter = ts + urtest.execute() diff --git a/tests/gold_tests/pluginTest/server_push_preload/gold/server_push_preload_0_stdout.gold b/tests/uranium_tests/pluginTest/server_push_preload/gold/server_push_preload_0_stdout.gold similarity index 100% rename from tests/gold_tests/pluginTest/server_push_preload/gold/server_push_preload_0_stdout.gold rename to tests/uranium_tests/pluginTest/server_push_preload/gold/server_push_preload_0_stdout.gold diff --git a/tests/uranium_tests/pluginTest/server_push_preload/test_server_push_preload.py b/tests/uranium_tests/pluginTest/server_push_preload/test_server_push_preload.py new file mode 100644 index 00000000000..a7363ac1951 --- /dev/null +++ b/tests/uranium_tests/pluginTest/server_push_preload/test_server_push_preload.py @@ -0,0 +1,125 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_server_push_preload(urtest: UraniumTest) -> None: + ''' + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + urtest.Summary = ''' + Test for server_push_preload plugin + ''' + + urtest.SkipUnless( + Condition.PluginExists('server_push_preload.so'), + Condition.HasProgram("nghttp", "Nghttp need to be installed on system for this test to work"), + ) + urtest.testName = "server_push_preload" + urtest.ContinueOnFail = True + + # ---- + # Setup Origin Server + # ---- + microserver = urtest.MakeOriginServer("microserver") + + # index.html + microserver.addResponse( + "sessionfile.log", { + "headers": "GET /index.html HTTP/1.1\r\nHost: www.example.com\r\n\r\n", + "body": "" + }, { + "headers": + "HTTP/1.1 200 OK\r\nConnection: close\r\nLink: ; rel=preload; as=style; nopush\r\nLink: ; rel=preload; as=script\r\n\r\n", + "body": + "\r\n\r\n\r\n\r\n\r\n\r\nServer Push Preload Test\r\n\r\n\r\n" + }) + + # /app/style.css + microserver.addResponse( + "sessionfile.log", { + "headers": "GET /app/style.css HTTP/1.1\r\nHost: www.example.com\r\n\r\n", + "body": "" + }, { + "headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", + "body": "body { font-weight: bold; }\r\n" + }) + + # /app/script.js + microserver.addResponse( + "sessionfile.log", { + "headers": "GET /app/script.js HTTP/1.1\r\nHost: www.example.com\r\n\r\n", + "body": "" + }, { + "headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", + "body": "function do_nothing() { return; }\r\n" + }) + + # ---- + # Setup ATS + # ---- + ts = urtest.MakeATSProcess("ts", enable_tls=True) + + ts.addDefaultSSLFiles() + + ts.Disk.ssl_multicert_yaml.AddLines( + """ + ssl_multicert: + - dest_ip: "*" + ssl_cert_name: server.pem + ssl_key_name: server.key + """.split("\n")) + + ts.Disk.remap_config.AddLine('map / http://127.0.0.1:{0}/ @plugin=server_push_preload.so'.format(microserver.Variables.Port)) + + ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'http2|server_push_preload', + 'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir), + 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts.Variables.SSLDir), + 'proxy.config.http2.active_timeout_in': 3, + }) + + # ---- + # Test Cases + # ---- + + # Test Case 0: Server Push by Link header + tr = urtest.AddTestRun() + tr.Processes.Default.Command = "nghttp -vs --no-dep 'https://127.0.0.1:{0}/index.html'".format(ts.Variables.ssl_port) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.StartBefore(microserver, ready=When.PortOpen(microserver.Variables.Port)) + tr.Processes.Default.StartBefore(urtest.Processes.ts) + tr.Processes.Default.Streams.stdout = "gold/server_push_preload_0_stdout.gold" + tr.StillRunningAfter = microserver + urtest.execute() diff --git a/tests/gold_tests/pluginTest/slice/gold/aa.gold b/tests/uranium_tests/pluginTest/slice/gold/aa.gold similarity index 100% rename from tests/gold_tests/pluginTest/slice/gold/aa.gold rename to tests/uranium_tests/pluginTest/slice/gold/aa.gold diff --git a/tests/gold_tests/pluginTest/slice/gold/aaa.gold b/tests/uranium_tests/pluginTest/slice/gold/aaa.gold similarity index 100% rename from tests/gold_tests/pluginTest/slice/gold/aaa.gold rename to tests/uranium_tests/pluginTest/slice/gold/aaa.gold diff --git a/tests/gold_tests/pluginTest/slice/gold/bb.gold b/tests/uranium_tests/pluginTest/slice/gold/bb.gold similarity index 100% rename from tests/gold_tests/pluginTest/slice/gold/bb.gold rename to tests/uranium_tests/pluginTest/slice/gold/bb.gold diff --git a/tests/gold_tests/pluginTest/slice/gold/bbb.gold b/tests/uranium_tests/pluginTest/slice/gold/bbb.gold similarity index 100% rename from tests/gold_tests/pluginTest/slice/gold/bbb.gold rename to tests/uranium_tests/pluginTest/slice/gold/bbb.gold diff --git a/tests/gold_tests/pluginTest/slice/gold/slice_200.stderr.gold b/tests/uranium_tests/pluginTest/slice/gold/slice_200.stderr.gold similarity index 100% rename from tests/gold_tests/pluginTest/slice/gold/slice_200.stderr.gold rename to tests/uranium_tests/pluginTest/slice/gold/slice_200.stderr.gold diff --git a/tests/gold_tests/pluginTest/slice/gold/slice_206.stderr.gold b/tests/uranium_tests/pluginTest/slice/gold/slice_206.stderr.gold similarity index 100% rename from tests/gold_tests/pluginTest/slice/gold/slice_206.stderr.gold rename to tests/uranium_tests/pluginTest/slice/gold/slice_206.stderr.gold diff --git a/tests/gold_tests/pluginTest/slice/gold/slice_crr_ident.gold b/tests/uranium_tests/pluginTest/slice/gold/slice_crr_ident.gold similarity index 100% rename from tests/gold_tests/pluginTest/slice/gold/slice_crr_ident.gold rename to tests/uranium_tests/pluginTest/slice/gold/slice_crr_ident.gold diff --git a/tests/gold_tests/pluginTest/slice/gold/slice_first.stderr.gold b/tests/uranium_tests/pluginTest/slice/gold/slice_first.stderr.gold similarity index 100% rename from tests/gold_tests/pluginTest/slice/gold/slice_first.stderr.gold rename to tests/uranium_tests/pluginTest/slice/gold/slice_first.stderr.gold diff --git a/tests/gold_tests/pluginTest/slice/gold/slice_ident.gold b/tests/uranium_tests/pluginTest/slice/gold/slice_ident.gold similarity index 100% rename from tests/gold_tests/pluginTest/slice/gold/slice_ident.gold rename to tests/uranium_tests/pluginTest/slice/gold/slice_ident.gold diff --git a/tests/gold_tests/pluginTest/slice/gold/slice_last.stderr.gold b/tests/uranium_tests/pluginTest/slice/gold/slice_last.stderr.gold similarity index 100% rename from tests/gold_tests/pluginTest/slice/gold/slice_last.stderr.gold rename to tests/uranium_tests/pluginTest/slice/gold/slice_last.stderr.gold diff --git a/tests/gold_tests/pluginTest/slice/gold/slice_mid.stderr.gold b/tests/uranium_tests/pluginTest/slice/gold/slice_mid.stderr.gold similarity index 100% rename from tests/gold_tests/pluginTest/slice/gold/slice_mid.stderr.gold rename to tests/uranium_tests/pluginTest/slice/gold/slice_mid.stderr.gold diff --git a/tests/gold_tests/pluginTest/slice/gold_error/contents.stderr.gold b/tests/uranium_tests/pluginTest/slice/gold_error/contents.stderr.gold similarity index 100% rename from tests/gold_tests/pluginTest/slice/gold_error/contents.stderr.gold rename to tests/uranium_tests/pluginTest/slice/gold_error/contents.stderr.gold diff --git a/tests/gold_tests/pluginTest/slice/replay/slice_long_etag.test.yaml b/tests/uranium_tests/pluginTest/slice/replay/slice_long_etag.test.yaml similarity index 99% rename from tests/gold_tests/pluginTest/slice/replay/slice_long_etag.test.yaml rename to tests/uranium_tests/pluginTest/slice/replay/slice_long_etag.test.yaml index 4db9da4a131..e9f9388f48a 100644 --- a/tests/gold_tests/pluginTest/slice/replay/slice_long_etag.test.yaml +++ b/tests/uranium_tests/pluginTest/slice/replay/slice_long_etag.test.yaml @@ -17,7 +17,7 @@ meta: version: "1.0" -autest: +urtest: description: 'Verify slice plugin handles long ETag values without crashing' requires: plugins: [slice.so] diff --git a/tests/gold_tests/pluginTest/slice/replay/slice_purge.replay.yaml b/tests/uranium_tests/pluginTest/slice/replay/slice_purge.replay.yaml similarity index 100% rename from tests/gold_tests/pluginTest/slice/replay/slice_purge.replay.yaml rename to tests/uranium_tests/pluginTest/slice/replay/slice_purge.replay.yaml diff --git a/tests/gold_tests/pluginTest/slice/replay/slice_purge_gaps_client.replay.yaml b/tests/uranium_tests/pluginTest/slice/replay/slice_purge_gaps_client.replay.yaml similarity index 100% rename from tests/gold_tests/pluginTest/slice/replay/slice_purge_gaps_client.replay.yaml rename to tests/uranium_tests/pluginTest/slice/replay/slice_purge_gaps_client.replay.yaml diff --git a/tests/gold_tests/pluginTest/slice/replay/slice_purge_gaps_server.replay.yaml b/tests/uranium_tests/pluginTest/slice/replay/slice_purge_gaps_server.replay.yaml similarity index 100% rename from tests/gold_tests/pluginTest/slice/replay/slice_purge_gaps_server.replay.yaml rename to tests/uranium_tests/pluginTest/slice/replay/slice_purge_gaps_server.replay.yaml diff --git a/tests/gold_tests/pluginTest/slice/replay/slice_purge_no_ref.replay.yaml b/tests/uranium_tests/pluginTest/slice/replay/slice_purge_no_ref.replay.yaml similarity index 100% rename from tests/gold_tests/pluginTest/slice/replay/slice_purge_no_ref.replay.yaml rename to tests/uranium_tests/pluginTest/slice/replay/slice_purge_no_ref.replay.yaml diff --git a/tests/gold_tests/pluginTest/slice/replay/slice_purge_ref.replay.yaml b/tests/uranium_tests/pluginTest/slice/replay/slice_purge_ref.replay.yaml similarity index 100% rename from tests/gold_tests/pluginTest/slice/replay/slice_purge_ref.replay.yaml rename to tests/uranium_tests/pluginTest/slice/replay/slice_purge_ref.replay.yaml diff --git a/tests/gold_tests/pluginTest/slice/replay/slice_range.replay.yaml b/tests/uranium_tests/pluginTest/slice/replay/slice_range.replay.yaml similarity index 100% rename from tests/gold_tests/pluginTest/slice/replay/slice_range.replay.yaml rename to tests/uranium_tests/pluginTest/slice/replay/slice_range.replay.yaml diff --git a/tests/gold_tests/pluginTest/slice/replay/slice_stale_generation_client.replay.yaml b/tests/uranium_tests/pluginTest/slice/replay/slice_stale_generation_client.replay.yaml similarity index 100% rename from tests/gold_tests/pluginTest/slice/replay/slice_stale_generation_client.replay.yaml rename to tests/uranium_tests/pluginTest/slice/replay/slice_stale_generation_client.replay.yaml diff --git a/tests/gold_tests/pluginTest/slice/replay/slice_stale_generation_server.replay.yaml b/tests/uranium_tests/pluginTest/slice/replay/slice_stale_generation_server.replay.yaml similarity index 100% rename from tests/gold_tests/pluginTest/slice/replay/slice_stale_generation_server.replay.yaml rename to tests/uranium_tests/pluginTest/slice/replay/slice_stale_generation_server.replay.yaml diff --git a/tests/gold_tests/pluginTest/slice/rules/purge_block_failure.conf b/tests/uranium_tests/pluginTest/slice/rules/purge_block_failure.conf similarity index 100% rename from tests/gold_tests/pluginTest/slice/rules/purge_block_failure.conf rename to tests/uranium_tests/pluginTest/slice/rules/purge_block_failure.conf diff --git a/tests/gold_tests/pluginTest/slice/shrink_origin.py b/tests/uranium_tests/pluginTest/slice/shrink_origin.py similarity index 100% rename from tests/gold_tests/pluginTest/slice/shrink_origin.py rename to tests/uranium_tests/pluginTest/slice/shrink_origin.py diff --git a/tests/uranium_tests/pluginTest/slice/test_slice.py b/tests/uranium_tests/pluginTest/slice/test_slice.py new file mode 100644 index 00000000000..9c025636a97 --- /dev/null +++ b/tests/uranium_tests/pluginTest/slice/test_slice.py @@ -0,0 +1,206 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_slice(urtest: UraniumTest) -> None: + ''' + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + urtest.Summary = ''' + Basic slice plugin test + ''' + + # Test description: + # Preload the cache with the entire asset to be range requested. + # Reload remap rule with slice plugin + # Request content through the slice plugin + + urtest.SkipUnless(Condition.PluginExists('slice.so'),) + urtest.ContinueOnFail = False + + # configure origin server + server = urtest.MakeOriginServer("server") + + # Define ATS and configure + ts = urtest.MakeATSProcess("ts") + + # default root + request_header_chk = { + "headers": "GET / HTTP/1.1\r\n" + "Host: ats\r\n" + "\r\n", + "timestamp": "1469733493.993", + "body": "", + } + + response_header_chk = { + "headers": "HTTP/1.1 200 OK\r\n" + "Connection: close\r\n" + "\r\n", + "timestamp": "1469733493.993", + "body": "", + } + + server.addResponse("sessionlog.json", request_header_chk, response_header_chk) + + block_bytes = 7 + body = "lets go surfin now" + + request_header = { + "headers": "GET /path HTTP/1.1\r\n" + "Host: origin\r\n" + "\r\n", + "timestamp": "1469733493.993", + "body": "", + } + + response_header = { + "headers": "HTTP/1.1 200 OK\r\n" + "Connection: close\r\n" + 'Etag: "path"\r\n' + "Cache-Control: max-age=500\r\n" + "\r\n", + "timestamp": "1469733493.993", + "body": body, + } + + server.addResponse("sessionlog.json", request_header, response_header) + + curl_and_args = '-s -D /dev/stdout -o /dev/stderr -x http://127.0.0.1:{}'.format(ts.Variables.port) + + # set up whole asset fetch into cache + ts.Disk.remap_config.AddLines( + [ + f'map http://preload/ http://127.0.0.1:{server.Variables.Port}', + f'map http://slice_only/ http://127.0.0.1:{server.Variables.Port}', + f'map http://slice/ http://127.0.0.1:{server.Variables.Port}' + + f' @plugin=slice.so @pparam=--blockbytes-test={block_bytes}', + f'map http://slicehdr/ http://127.0.0.1:{server.Variables.Port}' + + f' @plugin=slice.so @pparam=--blockbytes-test={block_bytes}' + ' @pparam=--skip-header=SkipSlice', + ]) + + ts.Disk.records_config.update({ + 'proxy.config.diags.debug.enabled': 0, + 'proxy.config.diags.debug.tags': 'slice', + }) + + # 0 Test - Prefetch entire asset into cache + tr = urtest.AddTestRun("Fetch first slice range") + ps = tr.Processes.Default + ps.StartBefore(server, ready=When.PortOpen(server.Variables.Port)) + ps.StartBefore(urtest.Processes.ts) + tr.MakeCurlCommand(curl_and_args + ' http://preload/path', ts=ts) + ps.ReturnCode = 0 + ps.Streams.stderr = "gold/slice_200.stderr.gold" + ps.Streams.stdout.Content = Testers.ContainsExpression("200 OK", "expected 200 OK response") + tr.StillRunningAfter = ts + + # 1 Test - First complete slice + tr = urtest.AddTestRun("Fetch first slice range") + ps = tr.Processes.Default + tr.MakeCurlCommand(curl_and_args + ' http://slice/path' + ' -r 0-6', ts=ts) + ps.ReturnCode = 0 + ps.Streams.stderr = "gold/slice_first.stderr.gold" + ps.Streams.stdout.Content = Testers.ContainsExpression("206 Partial Content", "expected 206 response") + ps.Streams.stdout.Content += Testers.ContainsExpression("Content-Range: bytes 0-6/18", "mismatch byte content response") + tr.StillRunningAfter = ts + + # 2 Test - Last slice auto + tr = urtest.AddTestRun("Last slice -- 14-") + ps = tr.Processes.Default + tr.MakeCurlCommand(curl_and_args + ' http://slice/path' + ' -r 14-', ts=ts) + ps.ReturnCode = 0 + ps.Streams.stderr = "gold/slice_last.stderr.gold" + ps.Streams.stdout.Content = Testers.ContainsExpression("206 Partial Content", "expected 206 response") + ps.Streams.stdout.Content += Testers.ContainsExpression("Content-Range: bytes 14-17/18", "mismatch byte content response") + tr.StillRunningAfter = ts + + # 3 Test - Last slice exact + tr = urtest.AddTestRun("Last slice 14-17") + ps = tr.Processes.Default + tr.MakeCurlCommand(curl_and_args + ' http://slice/path' + ' -r 14-17', ts=ts) + ps.ReturnCode = 0 + ps.Streams.stderr = "gold/slice_last.stderr.gold" + ps.Streams.stdout.Content = Testers.ContainsExpression("206 Partial Content", "expected 206 response") + ps.Streams.stdout.Content += Testers.ContainsExpression("Content-Range: bytes 14-17/18", "mismatch byte content response") + tr.StillRunningAfter = ts + + # 4 Test - Last slice truncated + tr = urtest.AddTestRun("Last truncated slice 14-20") + ps = tr.Processes.Default + tr.MakeCurlCommand(curl_and_args + ' http://slice/path' + ' -r 14-20', ts=ts) + ps.ReturnCode = 0 + ps.Streams.stderr = "gold/slice_last.stderr.gold" + ps.Streams.stdout.Content = Testers.ContainsExpression("206 Partial Content", "expected 206 response") + ps.Streams.stdout.Content += Testers.ContainsExpression("Content-Range: bytes 14-17/18", "mismatch byte content response") + tr.StillRunningAfter = ts + + # 5 Test - Whole asset via slices + tr = urtest.AddTestRun("Whole asset via slices") + ps = tr.Processes.Default + tr.MakeCurlCommand(curl_and_args + ' http://slice/path', ts=ts) + ps.ReturnCode = 0 + ps.Streams.stderr = "gold/slice_200.stderr.gold" + ps.Streams.stdout.Content = Testers.ContainsExpression("200 OK", "expected 200 OK response") + tr.StillRunningAfter = ts + + # 6 Test - Whole asset via range + tr = urtest.AddTestRun("Whole asset via range") + ps = tr.Processes.Default + tr.MakeCurlCommand(curl_and_args + ' http://slice/path' + ' -r 0-', ts=ts) + ps.ReturnCode = 0 + ps.Streams.stderr = "gold/slice_206.stderr.gold" + ps.Streams.stdout.Content = Testers.ContainsExpression("206 Partial Content", "expected 206 response") + ps.Streams.stdout.Content += Testers.ContainsExpression("Content-Range: bytes 0-17/18", "mismatch byte content response") + tr.StillRunningAfter = ts + + # 7 Test - Non aligned slice request + tr = urtest.AddTestRun("Non aligned slice request") + ps = tr.Processes.Default + tr.MakeCurlCommand(curl_and_args + ' http://slice/path' + ' -r 5-16', ts=ts) + ps.ReturnCode = 0 + ps.Streams.stderr = "gold/slice_mid.stderr.gold" + ps.Streams.stdout.Content = Testers.ContainsExpression("206 Partial Content", "expected 206 response") + ps.Streams.stdout.Content += Testers.ContainsExpression("Content-Range: bytes 5-16/18", "mismatch byte content response") + tr.StillRunningAfter = ts + + # 8 Test - special case, begin inside last slice block but outside asset len + tr = urtest.AddTestRun("Invalid end range request, 416") + beg = len(body) + 1 + end = beg + block_bytes + ps = tr.Processes.Default + tr.MakeCurlCommand(curl_and_args + ' http://slice/path' + ' -r {}-{}'.format(beg, end), ts=ts) + ps.Streams.stdout.Content = Testers.ContainsExpression("416 Requested Range Not Satisfiable", "expected 416 response") + tr.StillRunningAfter = ts + + # 9 Test - First complete slice using override header + # if this fails it will infinite loop + tr = urtest.AddTestRun("Fetch first slice range") + ps = tr.Processes.Default + tr.MakeCurlCommand(curl_and_args + ' http://slicehdr/path' + ' -r 0-6', ts=ts) + ps.ReturnCode = 0 + ps.Streams.stderr = "gold/slice_first.stderr.gold" + ps.Streams.stdout.Content = Testers.ContainsExpression("206 Partial Content", "expected 206 response") + ps.Streams.stdout.Content += Testers.ContainsExpression("Content-Range: bytes 0-6/18", "mismatch byte content response") + tr.StillRunningAfter = ts + urtest.execute() diff --git a/tests/uranium_tests/pluginTest/slice/test_slice_conditional.py b/tests/uranium_tests/pluginTest/slice/test_slice_conditional.py new file mode 100644 index 00000000000..0e9e49803ae --- /dev/null +++ b/tests/uranium_tests/pluginTest/slice/test_slice_conditional.py @@ -0,0 +1,179 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_slice_conditional(urtest: UraniumTest) -> None: + ''' + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + urtest.Summary = ''' + Conditional Slicing test + ''' + + # Test description: + # Preload the cache with the entire asset to be range requested. + # Reload remap rule with slice plugin + # Request content through the slice plugin + + urtest.SkipUnless( + Condition.PluginExists('slice.so'), + Condition.PluginExists('cache_range_requests.so'), + Condition.PluginExists('xdebug.so'), + ) + urtest.ContinueOnFail = False + + # configure origin server + server = urtest.MakeOriginServer("server", lookup_key="{PATH}{%Range}", options={'-v': None}) + + # Define ATS and configure + ts = urtest.MakeATSProcess("ts") + + # small object, should not be sliced + req_small = { + "headers": "GET /small HTTP/1.1\r\n" + "Host: www.example.com\r\n" + "\r\n", + "body": "", + } + res_small = { + "headers": "HTTP/1.1 200 OK\r\n" + "Connection: close\r\n" + "Cache-Control: max-age=10,public\r\n" + "\r\n", + "body": "smol", + } + server.addResponse("sessionlog.json", req_small, res_small) + + # large object, all in one slice + req_large = { + "headers": "GET /large HTTP/1.1\r\n" + "Host: www.example.com\r\n" + "\r\n", + "body": "", + } + res_large = { + "headers": "HTTP/1.1 200 OK\r\n" + "Connection: close\r\n" + "Cache-Control: max-age=10,public\r\n" + "\r\n", + "body": "unsliced large object!" + } + server.addResponse("sessionlog.json", req_large, res_large) + + # large object, this populates the individual slices in the server + + large_body = "large object sliced!" + body_len = len(large_body) + slice_begin = 0 + slice_block_size = 10 + while (slice_begin < body_len): + slice_end = slice_begin + slice_block_size + req_large_slice = { + "headers": + "GET /large HTTP/1.1\r\n" + "Host: www.example.com\r\n" + f"Range: bytes={slice_begin}-{slice_end - 1}" + "\r\n", + "body": "", + } + if slice_end > body_len: + slice_end = body_len + res_large_slice = { + "headers": + "HTTP/1.1 206 Partial Content\r\n" + "Connection: close\r\n" + "Accept-Ranges: bytes\r\n" + + f"Content-Range: bytes {slice_begin}-{slice_end - 1}/{body_len}\r\n" + "Cache-Control: max-age=10,public\r\n" + + "\r\n", + "body": large_body[slice_begin:slice_end] + } + server.addResponse("sessionlog.json", req_large_slice, res_large_slice) + slice_begin += slice_block_size + + # set up slice plugin with remap host into cache_range_requests + ts.Disk.remap_config.AddLines( + [ + f'map http://slice/ http://127.0.0.1:{server.Variables.Port}/' + + f' @plugin=slice.so @pparam=--blockbytes-test={slice_block_size} @pparam=--minimum-size=8 @pparam=--metadata-cache-size=4 @plugin=cache_range_requests.so' + ]) + + ts.Disk.plugin_config.AddLine('xdebug.so --enable=x-cache') + ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': '0', + 'proxy.config.diags.debug.tags': 'http|cache|slice|xdebug|cache_range_requests', + }) + + curl_and_args = '-s -D /dev/stdout -o /dev/stderr -x localhost:{}'.format(ts.Variables.port) + ' -H "x-debug: x-cache"' + + # Test case: first request of small object + tr = urtest.AddTestRun("Small request 1") + ps = tr.Processes.Default + ps.StartBefore(server, ready=When.PortOpen(server.Variables.Port)) + ps.StartBefore(urtest.Processes.ts) + tr.MakeCurlCommand(curl_and_args + ' http://slice/small', ts=ts) + ps.ReturnCode = 0 + ps.Streams.stderr.Content = Testers.ContainsExpression('smol', 'expected smol') + ps.Streams.stdout.Content = Testers.ContainsExpression('X-Cache: miss', 'expected cache miss') + tr.StillRunningAfter = ts + + # Test case: second request of small object - expect cache hit + tr = urtest.AddTestRun("Small request 2") + ps = tr.Processes.Default + tr.MakeCurlCommand(curl_and_args + ' http://slice/small', ts=ts) + ps.ReturnCode = 0 + ps.Streams.stderr.Content = Testers.ContainsExpression('smol', 'expected smol') + ps.Streams.stdout.Content = Testers.ContainsExpression('X-Cache: hit-fresh', 'expected cache hit-fresh') + tr.StillRunningAfter = ts + + # Test case: range request of small object - expect cache hit (proxy.config.http.cache.range.lookup = 1) + tr = urtest.AddTestRun("Small request - ranged") + ps = tr.Processes.Default + tr.MakeCurlCommand(curl_and_args + ' -r 1-2 http://slice/small', ts=ts) + ps.ReturnCode = 0 + ps.Streams.stderr.Content = Testers.ContainsExpression('mo', 'expected mo') + ps.Streams.stdout.Content = Testers.ContainsExpression('X-Cache: hit-fresh', 'expected cache hit-fresh') + tr.StillRunningAfter = ts + + # Test case: first request of large object - expect unsliced, cache write disabled + tr = urtest.AddTestRun("Large request 1") + ps = tr.Processes.Default + tr.MakeCurlCommand(curl_and_args + ' http://slice/large', ts=ts) + ps.ReturnCode = 0 + ps.Streams.stderr.Content = Testers.ContainsExpression('unsliced large object!', 'expected large object') + ps.Streams.stdout.Content = Testers.ContainsExpression('X-Cache: miss', 'expected cache miss') + tr.StillRunningAfter = ts + + # Test case: first request of large object - expect sliced, cache miss + tr = urtest.AddTestRun("Large request 2") + ps = tr.Processes.Default + tr.MakeCurlCommand(curl_and_args + ' http://slice/large', ts=ts) + ps.ReturnCode = 0 + ps.Streams.stderr.Content = Testers.ContainsExpression('large object sliced!', 'expected large object') + ps.Streams.stdout.Content = Testers.ContainsExpression('X-Cache: miss', 'expected cache miss') + tr.StillRunningAfter = ts + + ## Test case: first request of large object - expect cache hit + tr = urtest.AddTestRun("Large request 3") + ps = tr.Processes.Default + tr.MakeCurlCommand(curl_and_args + ' http://slice/large', ts=ts) + ps.ReturnCode = 0 + ps.Streams.stderr.Content = Testers.ContainsExpression('large object sliced!', 'expected large object') + ps.Streams.stdout.Content = Testers.ContainsExpression('X-Cache: hit-fresh', 'expected cache hit-fresh') + tr.StillRunningAfter = ts + urtest.execute() diff --git a/tests/uranium_tests/pluginTest/slice/test_slice_content_shrink.py b/tests/uranium_tests/pluginTest/slice/test_slice_content_shrink.py new file mode 100644 index 00000000000..b8ac4d3cf31 --- /dev/null +++ b/tests/uranium_tests/pluginTest/slice/test_slice_content_shrink.py @@ -0,0 +1,116 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_slice_content_shrink(urtest: UraniumTest) -> None: + ''' + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import sys + + from ports import get_port + + urtest.Summary = ''' + Slice plugin: verify no integer underflow when content shrinks below block position. + + When origin reports Content-Range with total length smaller than the requested + block position (content shrunk between fetches), the plugin must fail gracefully + rather than underflowing m_blockskip which would corrupt the response. + ''' + + urtest.SkipUnless(Condition.PluginExists('slice.so'),) + urtest.ContinueOnFail = False + + # Define ATS - no cache so every slice sub-request goes to origin + ts = urtest.MakeATSProcess("ts", enable_cache=False) + + # Test: Request bytes 14-20 via slice plugin (blockbytes=7) + # Slice will: + # 1. Fetch block 0 (reference): gets CL=21, etag "old" + # 2. Fetch block 2 (interior, skips block 1): gets CL=10, etag "new" (MISMATCH! m_contentlen=10) + # 3. Refetch block 0 (reference): gets CL=10, etag "new" (matches new m_contentlen) + # 4. Enters ActiveRef: blockpos=14, m_contentlen=10 => guard triggers, Fail state + # + # Client should get an error/empty response, NOT corrupted data. + + tr = urtest.AddTestRun("Request triggering content shrink underflow guard") + + # Copy and start custom origin server + tr.Setup.CopyAs("shrink_origin.py") + + origin = tr.Processes.Process("origin") + origin_port = get_port(origin, 'http_port') + origin.Command = f'{sys.executable} shrink_origin.py {origin_port}' + origin.Ready = When.PortOpenv4(origin_port) + + # Configure remap to point at our custom origin + ts.Disk.remap_config.AddLines( + [ + f'map http://slice/ http://127.0.0.1:{origin_port}/' + ' @plugin=slice.so @pparam=--blockbytes-test=7', + ]) + + ts.Disk.records_config.update({ + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'slice', + }) + + ps = tr.Processes.Default + ps.StartBefore(origin) + ps.StartBefore(urtest.Processes.ts) + ps.Command = ( + f'curl -s -D /dev/stdout -o /dev/stderr' + f' -x localhost:{ts.Variables.port}' + f' http://slice/shrink -r 14-20' + f' -w "\\nSIZE:%{{size_download}}"') + ps.Streams.stdout = Testers.ContainsExpression(r"SIZE:0\b", "expected zero client-visible body size") + ps.Streams.stderr = Testers.ExcludesExpression(r".", "expected no client-visible response body") + tr.StillRunningAfter = ts + + # Test 2: Non-block-aligned range. blockbytes=7, client requests bytes=16-20. + # firstblock=2, blockpos=14. Content shrinks to 15. + # m_contentlen(15) > blockpos(14) would pass a blockpos-only guard, but + # m_contentlen(15) <= m_req_range.m_beg(16) catches it. + tr = urtest.AddTestRun("Mid-block range: content shrinks above blockpos but below range start") + ps = tr.Processes.Default + ps.Command = ( + f'curl -s -D /dev/stdout -o /dev/stderr' + f' -x localhost:{ts.Variables.port}' + f' http://slice/shrink_mid -r 16-20' + f' -w "\\nSIZE:%{{size_download}}"') + tr.StillRunningAfter = ts + + # Verify the error was logged (our new guard message) + ts.Disk.diags_log.Content = Testers.ContainsExpression( + "shrunk below requested range start", "expected underflow guard error log") + urtest.execute() diff --git a/tests/uranium_tests/pluginTest/slice/test_slice_crr_ident.py b/tests/uranium_tests/pluginTest/slice/test_slice_crr_ident.py new file mode 100644 index 00000000000..1d22de080a5 --- /dev/null +++ b/tests/uranium_tests/pluginTest/slice/test_slice_crr_ident.py @@ -0,0 +1,241 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_slice_crr_ident(urtest: UraniumTest) -> None: + ''' + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + import os + + urtest.Summary = ''' + Combined Slice/crr ident test + ''' + + # Test description: + # Preload the cache with the entire asset to be range requested. + # Reload remap rule with slice plugin + # Request content through the slice plugin + + urtest.SkipUnless( + Condition.PluginExists('cache_range_requests.so'), + Condition.PluginExists('header_rewrite.so'), + Condition.PluginExists('slice.so'), + Condition.PluginExists('xdebug.so'), + ) + urtest.ContinueOnFail = False + + # configure origin server + server = urtest.MakeOriginServer("server", lookup_key="{%UID}") + + # default root + req_header_chk = { + "headers": "GET / HTTP/1.1\r\n" + "Host: www.example.com\r\n" + "UID: none\r\n" + "\r\n", + "timestamp": "1469733493.993", + "body": "", + } + + res_header_chk = { + "headers": "HTTP/1.1 200 OK\r\n" + "Connection: close\r\n" + "\r\n", + "timestamp": "1469733493.993", + "body": "", + } + + server.addResponse("sessionlog.json", req_header_chk, res_header_chk) + + # Define ATS and configure + ts = urtest.MakeATSProcess("ts") + + # set up slice plugin with remap host into cache_range_requests + ts.Disk.remap_config.AddLines( + [ + f'map http://slice/ http://127.0.0.1:{server.Variables.Port}/' + + ' @plugin=slice.so @pparam=--blockbytes-test=3 @pparam=--remap-host=crr', + f'map http://crr/ http://127.0.0.1:{server.Variables.Port}/' + + ' @plugin=cache_range_requests.so @pparam=--consider-ims @pparam=--consider-ident' + + ' @plugin=header_rewrite.so @pparam=hdr_rw.conf', + ]) + + ts.Disk.logging_yaml.AddLines( + ''' + logging: + formats: + - name: custom + format: 'cpuup=% sssc=% pssc=% phr=% range=::%<{Range}cqh>:: x-crr-ident=::%<{X-Crr-Ident}cqh>:: uid=::%<{UID}pqh>:: crc=%' + logs: + - filename: transaction + format: custom + '''.split("\n")) + + ts.Disk.plugin_config.AddLine('xdebug.so --enable=x-cache') + + ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'cache_range_requests|header_rewrite|slice|log', + }) + + ts.Disk.MakeConfigFile("hdr_rw.conf").AddLines( + [ + 'cond %{SEND_REQUEST_HDR_HOOK}', 'cond %{HEADER:Range} ="bytes=0-2" [AND]', 'set-header UID %{CLIENT-HEADER:UID} 0' + '', 'cond %{SEND_REQUEST_HDR_HOOK}', 'cond %{HEADER:Range} ="bytes=3-5" [AND]', 'set-header UID %{CLIENT-HEADER:UID} 1' + ]) + + # Test case: short lived asset. second slice should be HIT_FRESH + + req_header_plain_0 = { + "headers": "GET /plain HTTP/1.1\r\n" + "Host: www.example.com\r\n" + "UID: plain 0\r\n" + "Range: bytes=0-2\r\n" + "\r\n", + "timestamp": "1469733493.993", + "body": "", + } + + res_header_plain_0 = { + "headers": + "HTTP/1.1 206 Partial Content\r\n" + "Accept-Ranges: bytes\r\n" + "Cache-Control: max-age=1\r\n" + + "Connection: close\r\n" + "Content-Range: bytes 0-2/5\r\n" + 'Etag: "plain"\r\n' + "\r\n", + "timestamp": "1469733493.993", + "body": "aaa" + } + + server.addResponse("sessionlog.json", req_header_plain_0, res_header_plain_0) + + req_header_plain_1 = { + "headers": "GET /plain HTTP/1.1\r\n" + "Host: www.example.com\r\n" + "UID: plain 1\r\n" + "Range: bytes=3-5\r\n" + "\r\n", + "timestamp": "1469733493.993", + "body": "", + } + + res_header_plain_1 = { + "headers": + "HTTP/1.1 206 Partial Content\r\n" + "Accept-Ranges: bytes\r\n" + "Cache-Control: max-age=1\r\n" + + "Connection: close\r\n" + "Content-Range: bytes 3-4/5\r\n" + 'Etag: "plain"\r\n' + "\r\n", + "timestamp": "1469733493.993", + "body": "BB" + } + + server.addResponse("sessionlog.json", req_header_plain_1, res_header_plain_1) + + # curl helper + curl_and_args = '-s -D /dev/stdout -o /dev/stderr -x localhost:{}'.format(ts.Variables.port) + ' -H "x-debug: x-cache"' + + # 0 Test - Preload plain asset + tr = urtest.AddTestRun("Preload plain") + ps = tr.Processes.Default + ps.StartBefore(server, ready=When.PortOpen(server.Variables.Port)) + ps.StartBefore(urtest.Processes.ts) + tr.MakeCurlCommand(curl_and_args + ' http://slice/plain -H "UID: plain"', ts=ts) + ps.ReturnCode = 0 + ps.Streams.stderr.Content = Testers.ContainsExpression("aaaBB", "expected aaaBB") + ps.Streams.stdout.Content = Testers.ContainsExpression('Etag: "plain"', "expected etag plain") + tr.StillRunningAfter = ts + + # 2 Test - Request again, should result in stale asset + tr = urtest.AddTestRun("Request 2nd slice (expect slice1 to be fresh)") + tr.DelayStart = 2 # ensure its really stale + ps = tr.Processes.Default + tr.MakeCurlCommand(curl_and_args + ' http://slice/plain -H "UID: plain"', ts=ts) + ps.ReturnCode = 0 + ps.Streams.stderr = Testers.ContainsExpression("aaaBB", "expected aaaBB") + ps.Streams.stdout.Content = Testers.ContainsExpression('Etag: "plain"', "expected etag plain") + tr.StillRunningAfter = ts + + # change out the asset + + req_header_chg_0 = { + "headers": "GET /plain HTTP/1.1\r\n" + "Host: www.example.com\r\n" + "UID: chg 0\r\n" + "Range: bytes=0-2\r\n" + "\r\n", + "timestamp": "1469733493.993", + "body": "", + } + + res_header_chg_0 = { + "headers": + "HTTP/1.1 206 Partial Content\r\n" + "Accept-Ranges: bytes\r\n" + "Cache-Control: max-age=60\r\n" + + "Connection: close\r\n" + "Content-Range: bytes 0-2/5\r\n" + 'Etag: "chg"\r\n' + "\r\n", + "timestamp": "1469733493.993", + "body": "AAA" + } + + server.addResponse("sessionlog.json", req_header_chg_0, res_header_chg_0) + + req_header_chg_1 = { + "headers": "GET /plain HTTP/1.1\r\n" + "Host: www.example.com\r\n" + "UID: chg 1\r\n" + "Range: bytes=3-5\r\n" + "\r\n", + "timestamp": "1469733493.993", + "body": "", + } + + res_header_chg_1 = { + "headers": + "HTTP/1.1 206 Partial Content\r\n" + "Accept-Ranges: bytes\r\n" + "Cache-Control: max-age=60\r\n" + + "Connection: close\r\n" + "Content-Range: bytes 3-4/5\r\n" + 'Etag: "chg"\r\n' + "\r\n", + "timestamp": "1469733493.993", + "body": "bb" + } + + server.addResponse("sessionlog.json", req_header_chg_1, res_header_chg_1) + + # 3 Test - Request again, should result in new asset + tr = urtest.AddTestRun("Request again, asset replaced") + tr.DelayStart = 2 # ensure its really stale + ps = tr.Processes.Default + tr.MakeCurlCommand(curl_and_args + ' http://slice/plain -H "UID: chg"', ts=ts) + ps.ReturnCode = 0 + ps.Streams.stderr = Testers.ContainsExpression("AAAbb", "expected AAAbb") + ps.Streams.stdout.Content = Testers.ContainsExpression('Etag: "chg"', "expected etag chg") + tr.StillRunningAfter = ts + + # 4 Test - Request again, should all be hit + tr = urtest.AddTestRun("Request again, asset replaced but hit") + tr.DelayStart = 2 # ensure its really stale + ps = tr.Processes.Default + tr.MakeCurlCommand(curl_and_args + ' http://slice/plain -H "UID: chg"', ts=ts) + ps.ReturnCode = 0 + ps.Streams.stderr = Testers.ContainsExpression("AAAbb", "expected AAAbb") + ps.Streams.stdout.Content = Testers.ContainsExpression('Etag: "chg"', "expected etag chg") + tr.StillRunningAfter = ts + + # 5 Test - add token to transaction log + tr = urtest.AddTestRun("Fetch 404.txt asset") + ps = tr.Processes.Default + tr.MakeCurlCommand(curl_and_args + ' http://crr/404.txt', ts=ts) + ps.ReturnCode = 0 + ps.Streams.stdout.Content = Testers.ContainsExpression("404", "expected 404 Not Found response") + tr.StillRunningAfter = ts + + tslog = os.path.join(ts.Variables.LOGDIR, 'transaction.log') + urtest.AddAwaitFileContainsTestRun('Await ts transactions to finish logging.', tslog, '404.txt') + + # 6 Check logs + tr = urtest.AddTestRun() + tr.Processes.Default.Command = (f"cat {tslog}") + tr.Streams.stdout = "gold/slice_crr_ident.gold" + tr.Processes.Default.ReturnCode = 0 + urtest.execute() diff --git a/tests/uranium_tests/pluginTest/slice/test_slice_error.py b/tests/uranium_tests/pluginTest/slice/test_slice_error.py new file mode 100644 index 00000000000..046e4e7c841 --- /dev/null +++ b/tests/uranium_tests/pluginTest/slice/test_slice_error.py @@ -0,0 +1,341 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_slice_error(urtest: UraniumTest) -> None: + ''' + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + urtest.Summary = ''' + Slice plugin error.log test + ''' + + # Test description: + # Preload the cache with the entire asset to be range requested. + # Reload remap rule with slice plugin + # Request content through the slice plugin + + urtest.SkipUnless(Condition.PluginExists('slice.so'),) + urtest.ContinueOnFail = False + + # configure origin server + server = urtest.MakeOriginServer("server", lookup_key="{%Range}{PATH}") + + # Define ATS and configure + ts = urtest.MakeATSProcess("ts", enable_cache=False) + + body = "the quick brown fox" # len 19 + + # default root + request_header_chk = { + "headers": "GET / HTTP/1.1\r\n" + "Host: ats\r\n" + "Range: bytes=0-\r\n" + "\r\n", + "timestamp": "1469733493.993", + "body": "", + } + + response_header_chk = { + "headers": "HTTP/1.1 206 Partial Content\r\n" + "Connection: close\r\n" + "\r\n", + "timestamp": "1469733493.993", + "body": body, + } + + server.addResponse("sessionlog.json", request_header_chk, response_header_chk) + + blockbytes = 9 + + range0 = "{}-{}".format(0, blockbytes - 1) + range1 = "{}-{}".format(blockbytes, (2 * blockbytes) - 1) + + body0 = body[0:blockbytes] + body1 = body[blockbytes:2 * blockbytes] + + # Mismatch etag + + request_header_etag0 = { + "headers": + "GET /etag HTTP/1.1\r\n" + "Host: ats\r\n" + "Range: bytes={}\r\n".format(range0) + + "X-Slicer-Info: full content request\r\n" + "\r\n", + "timestamp": "1469733493.993", + "body": "", + } + + response_header_etag0 = { + "headers": + "HTTP/1.1 206 Partial Content\r\n" + "Connection: close\r\n" + 'Etag: "etag0"\r\n' + + "Content-Range: bytes {}/{}\r\n".format(range0, len(body)) + "Cache-Control: max-age=500\r\n" + "\r\n", + "timestamp": "1469733493.993", + "body": body0, + } + + server.addResponse("sessionlog.json", request_header_etag0, response_header_etag0) + + request_header_etag1 = { + "headers": + "GET /etag HTTP/1.1\r\n" + "Host: ats\r\n" + "Range: bytes={}\r\n".format(range1) + + "X-Slicer-Info: full content request\r\n" + "\r\n", + "timestamp": "1469733493.993", + "body": "", + } + + response_header_etag1 = { + "headers": + "HTTP/1.1 206 Partial Content\r\n" + "Connection: close\r\n" + 'Etag: "etag1"\r\n' + + "Content-Range: bytes {}/{}\r\n".format(range1, len(body)) + "Cache-Control: max-age=500\r\n" + "\r\n", + "timestamp": "1469733493.993", + "body": body1, + } + + server.addResponse("sessionlog.json", request_header_etag1, response_header_etag1) + + # mismatch Last-Modified + + request_header_lm0 = { + "headers": + "GET /lastmodified HTTP/1.1\r\n" + "Host: ats\r\n" + "Range: bytes={}\r\n".format(range0) + + "X-Slicer-Info: full content request\r\n" + "\r\n", + "timestamp": "1469733493.993", + "body": "", + } + + response_header_lm0 = { + "headers": + "HTTP/1.1 206 Partial Content\r\n" + "Connection: close\r\n" + "Last-Modified: Tue, 08 May 2018 15:49:41 GMT\r\n" + + "Content-Range: bytes {}/{}\r\n".format(range0, len(body)) + "Cache-Control: max-age=500\r\n" + "\r\n", + "timestamp": "1469733493.993", + "body": body0, + } + + server.addResponse("sessionlog.json", request_header_lm0, response_header_lm0) + + request_header_lm1 = { + "headers": + "GET /lastmodified HTTP/1.1\r\n" + "Host: ats\r\n" + "Range: bytes={}\r\n".format(range1) + + "X-Slicer-Info: full content request\r\n" + "\r\n", + "timestamp": "1469733493.993", + "body": "", + } + + response_header_lm1 = { + "headers": + "HTTP/1.1 206 Partial Content\r\n" + "Connection: close\r\n" + "Last-Modified: Tue, 08 Apr 2019 18:00:00 GMT\r\n" + + "Content-Range: bytes {}/{}\r\n".format(range1, len(body)) + "Cache-Control: max-age=500\r\n" + "\r\n", + "timestamp": "1469733493.993", + "body": body1, + } + + server.addResponse("sessionlog.json", request_header_lm1, response_header_lm1) + + # non 206 slice block + + request_header_n206_0 = { + "headers": + "GET /non206 HTTP/1.1\r\n" + "Host: ats\r\n" + "Range: bytes={}\r\n".format(range0) + + "X-Slicer-Info: full content request\r\n" + "\r\n", + "timestamp": "1469733493.993", + "body": "", + } + + response_header_n206_0 = { + "headers": + "HTTP/1.1 206 Partial Content\r\n" + "Connection: close\r\n" + 'Etag: "etag"\r\n' + + "Last-Modified: Tue, 08 May 2018 15:49:41 GMT\r\n" + "Content-Range: bytes {}/{}\r\n".format(range0, len(body)) + + "Cache-Control: max-age=500\r\n" + "\r\n", + "timestamp": "1469733493.993", + "body": body0, + } + + server.addResponse("sessionlog.json", request_header_n206_0, response_header_n206_0) + + request_header_n206_1 = { + "headers": + "GET /non206 HTTP/1.1\r\n" + "Host: ats\r\n" + "Range: bytes={}\r\n".format(range1) + + "X-Slicer-Info: full content request\r\n" + "\r\n", + "timestamp": "1469733493.993", + "body": "", + } + + response_header_n206_1 = { + "headers": "HTTP/1.1 502 Bad Gateway\r\n" + "Connection: close\r\n" + "\r\n", + "timestamp": "1469733493.993", + "body": body1, + } + + server.addResponse("sessionlog.json", request_header_n206_1, response_header_n206_1) + + # mismatch content-range + + request_header_crr0 = { + "headers": + "GET /crr HTTP/1.1\r\n" + "Host: ats\r\n" + "Range: bytes={}\r\n".format(range0) + + "X-Slicer-Info: full content request\r\n" + "\r\n", + "timestamp": "1469733493.993", + "body": "", + } + + response_header_crr0 = { + "headers": + "HTTP/1.1 206 Partial Content\r\n" + "Connection: close\r\n" + "Etag: crr\r\n" + + "Content-Range: bytes {}/{}\r\n".format(range0, len(body)) + "Cache-Control: max-age=500\r\n" + "\r\n", + "timestamp": "1469733493.993", + "body": body0, + } + + server.addResponse("sessionlog.json", request_header_crr0, response_header_crr0) + + request_header_crr1 = { + "headers": + "GET /crr HTTP/1.1\r\n" + "Host: ats\r\n" + "Range: bytes={}\r\n".format(range1) + + "X-Slicer-Info: full content request\r\n" + "\r\n", + "timestamp": "1469733493.993", + "body": "", + } + + response_header_crr1 = { + "headers": + "HTTP/1.1 206 Partial Content\r\n" + "Connection: close\r\n" + "Etag: crr\r\n" + + "Content-Range: bytes {}/{}\r\n".format(range1, + len(body) - 1) + "Cache-Control: max-age=500\r\n" + "\r\n", + "timestamp": "1469733493.993", + "body": body1, + } + + server.addResponse("sessionlog.json", request_header_crr1, response_header_crr1) + + # 404 internal block + + request_header_internal404_0 = { + "headers": + "GET /internal404 HTTP/1.1\r\n" + "Host: ats\r\n" + "Range: bytes={}\r\n".format(range0) + + "X-Slicer-Info: full content request\r\n" + "\r\n", + "timestamp": "1469733493.993", + "body": "", + } + + response_header_internal404_0 = { + "headers": + "HTTP/1.1 206 Partial Content\r\n" + "Connection: close\r\n" + 'Etag: "etag"\r\n' + + "Last-Modified: Tue, 08 May 2018 15:49:41 GMT\r\n" + "Content-Range: bytes {}/{}\r\n".format(range0, len(body)) + + "Cache-Control: max-age=500\r\n" + "\r\n", + "timestamp": "1469733493.993", + "body": body0, + } + + server.addResponse("sessionlog.json", request_header_internal404_0, response_header_internal404_0) + + curl_and_args = '-s -D /dev/stdout -o /dev/stderr -x http://127.0.0.1:{}'.format(ts.Variables.port) + + # set up whole asset fetch into cache + ts.Disk.remap_config.AddLine( + 'map / http://127.0.0.1:{}'.format(server.Variables.Port) + + ' @plugin=slice.so @pparam=--blockbytes-test={}'.format(blockbytes)) + + # minimal configuration + ts.Disk.records_config.update({ + # 'proxy.config.diags.debug.enabled': 1, + # 'proxy.config.diags.debug.tags': 'slice', + }) + + # Override builtin error check as these cases will fail + # taken from the slice plug code + ts.Disk.diags_log.Content = Testers.ContainsExpression('reason="Mismatch block Etag', "Mismatch block etag") + ts.Disk.diags_log.Content += Testers.ContainsExpression('reason="Mismatch block Last-Modified', "Mismatch block Last-Modified") + ts.Disk.diags_log.Content += Testers.ContainsExpression( + 'reason="Mismatch/Bad block Content-Range', "Mismatch/Bad block Content-Range") + ts.Disk.diags_log.Content += Testers.ContainsExpression('reason="404 internal block response', "404 internal block response") + + # 0 Test - Etag mismatch test + tr = urtest.AddTestRun("Etag test") + ps = tr.Processes.Default + ps.StartBefore(server, ready=When.PortOpen(server.Variables.Port)) + ps.StartBefore(urtest.Processes.ts) + tr.MakeCurlCommand(curl_and_args + ' http://ats/etag', ts=ts) + # ps.ReturnCode = 0 # curl will return fail status + ps.Streams.stdout.Content = Testers.ContainsExpression("200 OK", "expected 200 OK response") + ps.Streams.stderr = "gold_error/contents.stderr.gold" + tr.StillRunningAfter = ts + + # 1 Check - diags.log message + tr = urtest.AddTestRun("Etag error check") + ps = tr.Processes.Default + ps.Command = "grep 'Mismatch block Etag' {}".format(ts.Disk.diags_log.Name) + ps.ReturnCode = 0 + tr.StillRunningAfter = ts + + # 2 Test - Last Modified mismatch test + tr = urtest.AddTestRun("Last-Modified test") + ps = tr.Processes.Default + tr.MakeCurlCommand(curl_and_args + ' http://ats/lastmodified', ts=ts) + # ps.ReturnCode = 0 # curl will return fail status + ps.Streams.stderr = "gold_error/contents.stderr.gold" + ps.Streams.stdout.Content = Testers.ContainsExpression("200 OK", "expected 200 OK response") + tr.StillRunningAfter = ts + + # 3 Check - diags.log message + tr = urtest.AddTestRun("Last-Modified error check") + ps = tr.Processes.Default + ps.Command = "grep 'Mismatch block Last-Modified' {}".format(ts.Disk.diags_log.Name) + ps.ReturnCode = 0 + tr.StillRunningAfter = ts + + # 4 Test - Block content-range + tr = urtest.AddTestRun("Content-Range test") + ps = tr.Processes.Default + tr.MakeCurlCommand(curl_and_args + ' http://ats/crr', ts=ts) + # ps.ReturnCode = 0 # curl will return fail status + ps.Streams.stderr = "gold_error/contents.stderr.gold" + ps.Streams.stdout.Content = Testers.ContainsExpression("200 OK", "expected 200 OK response") + tr.StillRunningAfter = ts + + # 5 Check - diags.log message + tr = urtest.AddTestRun("Content-Range error check") + ps = tr.Processes.Default + ps.Command = "grep 'Mismatch/Bad block Content-Range' {}".format(ts.Disk.diags_log.Name) + ps.ReturnCode = 0 + tr.StillRunningAfter = ts + + # 6 Test - 404 internal test + tr = urtest.AddTestRun("Internal 404 test") + ps = tr.Processes.Default + tr.MakeCurlCommand(curl_and_args + ' http://ats/internal404', ts=ts) + # ps.ReturnCode = 0 # curl will return fail status + ps.Streams.stderr = "gold_error/contents.stderr.gold" + ps.Streams.stdout.Content = Testers.ContainsExpression("200 OK", "expected 200 OK response") + tr.StillRunningAfter = ts + + # 7 Check - diags.log message + tr = urtest.AddTestRun("Internal 404 check") + ps = tr.Processes.Default + ps.Command = "grep '404 internal block response' {}".format(ts.Disk.diags_log.Name) + ps.ReturnCode = 0 + tr.StillRunningAfter = ts + urtest.execute() diff --git a/tests/uranium_tests/pluginTest/slice/test_slice_ident.py b/tests/uranium_tests/pluginTest/slice/test_slice_ident.py new file mode 100644 index 00000000000..3e4e5589955 --- /dev/null +++ b/tests/uranium_tests/pluginTest/slice/test_slice_ident.py @@ -0,0 +1,208 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_slice_ident(urtest: UraniumTest) -> None: + ''' + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import os + + urtest.Summary = ''' + Slice plugin test for sending ident header + ''' + + # Test description: + # Preload the cache with the entire asset to be range requested. + # Reload remap rule with slice plugin + # Request content through the slice plugin + + urtest.SkipUnless(Condition.PluginExists('slice.so'),) + urtest.ContinueOnFail = False + + # configure origin server + server = urtest.MakeOriginServer("server") + + # default root + request_header_chk = { + "headers": "GET / HTTP/1.1\r\n" + "Host: ats\r\n" + "Range: none\r\n" + "\r\n", + "timestamp": "1469733493.993", + "body": "", + } + + response_header_chk = { + "headers": "HTTP/1.1 200 OK\r\n" + "Connection: close\r\n" + "\r\n", + "timestamp": "1469733493.993", + "body": "", + } + + server.addResponse("sessionlog.json", request_header_chk, response_header_chk) + + block_bytes = 11 + body = "lets go surfin now" + + etag = '"foo"' + last_modified = "Fri, 07 Mar 2025 18:06:58 GMT" + + request_etag = { + "headers": "GET /etag HTTP/1.1\r\n" + "Host: origin\r\n" + "\r\n", + "timestamp": "1469733493.993", + "body": "", + } + + response_etag = { + "headers": + "HTTP/1.1 200 OK\r\n" + "Connection: close\r\n" + f'Etag: {etag}\r\n' + f'Last-Modified: {last_modified}\r\n' + + "Cache-Control: max-age=500\r\n" + "\r\n", + "timestamp": "1469733493.993", + "body": body, + } + + server.addResponse("sessionlog.json", request_etag, response_etag) + + request_lm = { + "headers": "GET /lm HTTP/1.1\r\n" + "Host: origin\r\n" + "\r\n", + "timestamp": "1469733493.993", + "body": "", + } + + response_lm = { + "headers": + "HTTP/1.1 200 OK\r\n" + "Connection: close\r\n" + f'Last-Modified: {last_modified}\r\n' + + "Cache-Control: max-age=500\r\n" + "\r\n", + "timestamp": "1469733493.993", + "body": body, + } + + server.addResponse("sessionlog.json", request_lm, response_lm) + + # use this second ats instance to serve up the slices + + # Define ATS and configure + ts = urtest.MakeATSProcess("ts") + ts.Disk.records_config.update({ + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'slice', + }) + + ts.Disk.remap_config.AddLines( + [ + f"map http://preload/ http://127.0.0.1:{server.Variables.Port}", + f'map http://slice/ http://127.0.0.1:{server.Variables.Port}' + + f' @plugin=slice.so @pparam=--blockbytes-test={block_bytes}', + f'map http://slicecustom/ http://127.0.0.1:{server.Variables.Port}' + + f' @plugin=slice.so @pparam=--blockbytes-test={block_bytes}' + ' @pparam=--crr-ident-header=CrrIdent', + ]) + + ts.Disk.logging_yaml.AddLines( + ''' + logging: + formats: + - name: custom + format: '% % % range=::%<{Range}cqh>:: x-crr-ident=::%<{X-Crr-Ident}cqh>:: crrident=::%<{CrrIdent}cqh>::' + logs: + - filename: transaction + format: custom + '''.split("\n")) + + # helpers for curl + curl_and_args = f'-s -D /dev/stdout -o /dev/stderr -x localhost:{ts.Variables.port}' + + # 0 Test - Preload etag asset + tr = urtest.AddTestRun("Preload etag asset") + ps = tr.Processes.Default + ps.StartBefore(server, ready=When.PortOpen(server.Variables.Port)) + ps.StartBefore(urtest.Processes.ts) + tr.MakeCurlCommand(curl_and_args + ' http://preload/etag', ts=ts) + ps.ReturnCode = 0 + ps.Streams.stdout.Content = Testers.ContainsExpression("200 OK", "expected 200 OK response") + tr.StillRunningAfter = ts + + # 1 Test - Preload last-modified asset + tr = urtest.AddTestRun("Preload last-modified asset") + ps = tr.Processes.Default + tr.MakeCurlCommand(curl_and_args + ' http://preload/lm', ts=ts) + ps.ReturnCode = 0 + ps.Streams.stdout.Content = Testers.ContainsExpression("200 OK", "expected 200 OK response") + tr.StillRunningAfter = ts + + # 2 Test - Fetch etag asset + tr = urtest.AddTestRun("Fetch etag asset") + ps = tr.Processes.Default + tr.MakeCurlCommand(curl_and_args + ' http://slice/etag', ts=ts) + ps.ReturnCode = 0 + ps.Streams.stdout.Content = Testers.ContainsExpression("200 OK", "expected 200 OK response") + tr.StillRunningAfter = ts + + # 3 Test - Fetch etag asset + tr = urtest.AddTestRun("Fetch etag asset, custom") + ps = tr.Processes.Default + tr.MakeCurlCommand(curl_and_args + ' http://slicecustom/etag', ts=ts) + ps.ReturnCode = 0 + ps.Streams.stdout.Content = Testers.ContainsExpression("200 OK", "expected 200 OK response") + tr.StillRunningAfter = ts + + # 4 Test - Fetch last-modified asset + tr = urtest.AddTestRun("Fetch last-modified asset, custom") + ps = tr.Processes.Default + tr.MakeCurlCommand(curl_and_args + ' http://slice/lm', ts=ts) + ps.ReturnCode = 0 + ps.Streams.stdout.Content = Testers.ContainsExpression("200 OK", "expected 200 OK response") + tr.StillRunningAfter = ts + + # 5 Test - Fetch last-modified asset, custom + tr = urtest.AddTestRun("Fetch last-modified asset") + ps = tr.Processes.Default + tr.MakeCurlCommand(curl_and_args + ' http://slicecustom/lm', ts=ts) + ps.ReturnCode = 0 + ps.Streams.stdout.Content = Testers.ContainsExpression("200 OK", "expected 200 OK response") + tr.StillRunningAfter = ts + + # 6 Test - add token to transaction log + tr = urtest.AddTestRun("Add 404 token to transaction log") + ps = tr.Processes.Default + tr.MakeCurlCommand(curl_and_args + ' http://prefetch/404.txt', ts=ts) + ps.ReturnCode = 0 + ps.Streams.stdout.Content = Testers.ContainsExpression("404", "expected 404 Not Found response") + tr.StillRunningAfter = ts + + # 7 - wait for logs + tslog = os.path.join(ts.Variables.LOGDIR, 'transaction.log') + urtest.AddAwaitFileContainsTestRun('Await ts transactions to finish logging.', tslog, '404.txt') + + # 8 Check logs + tr = urtest.AddTestRun() + tr.Processes.Default.Command = (f"cat {tslog}") + tr.Streams.stdout = "gold/slice_ident.gold" + tr.Processes.Default.ReturnCode = 0 + urtest.execute() diff --git a/tests/uranium_tests/pluginTest/slice/test_slice_prefetch.py b/tests/uranium_tests/pluginTest/slice/test_slice_prefetch.py new file mode 100644 index 00000000000..2969486ec69 --- /dev/null +++ b/tests/uranium_tests/pluginTest/slice/test_slice_prefetch.py @@ -0,0 +1,236 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_slice_prefetch(urtest: UraniumTest) -> None: + ''' + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + import os + import re + + urtest.Summary = ''' + slice plugin prefetch feature test + ''' + + # Test description: + # Fill origin server with range requests + # Request content through slice plugin with varied prefetch counts + + urtest.SkipUnless( + Condition.PluginExists('slice.so'), + Condition.PluginExists('cache_range_requests.so'), + Condition.PluginExists('xdebug.so'), + ) + urtest.ContinueOnFail = False + + # configure origin server, lookup by Range header + server = urtest.MakeOriginServer("server", lookup_key="{%Range}") + + # Define ATS and configure + ts = urtest.MakeATSProcess("ts") + + block_bytes_1 = 7 + block_bytes_2 = 5 + body = "lets go surfin now" + bodylen = len(body) + + request_header = { + "headers": "GET /path HTTP/1.1\r\n" + "Host: origin\r\n" + "\r\n", + "timestamp": "1469733493.993", + "body": "", + } + + response_header = { + "headers": "HTTP/1.1 200 OK\r\n" + "Connection: close\r\n" + "Cache-Control: public, max-age=5\r\n" + "\r\n", + "timestamp": "1469733493.993", + "body": body, + } + + server.addResponse("sessionlog.json", request_header, response_header) + + # Autest OS doesn't support range request, must manually add requests/responses + for block_bytes in [block_bytes_1, block_bytes_2]: + for i in range(bodylen // block_bytes + 1): + b0 = i * block_bytes + b1 = b0 + block_bytes - 1 + req_header = { + "headers": "GET /path HTTP/1.1\r\n" + "Host: *\r\n" + "Accept: */*\r\n" + f"Range: bytes={b0}-{b1}\r\n" + "\r\n", + "timestamp": "1469733493.993", + "body": "" + } + if (b1 > bodylen - 1): + b1 = bodylen - 1 + resp_header = { + "headers": + "HTTP/1.1 206 Partial Content\r\n" + "Accept-Ranges: bytes\r\n" + "Cache-Control: public, max-age=5\r\n" + + f"Content-Range: bytes {b0}-{b1}/{bodylen}\r\n" + "Connection: close\r\n" + "\r\n", + "timestamp": "1469733493.993", + "body": body[b0:b1 + 1] + } + server.addResponse("sessionlog.json", req_header, resp_header) + + curl_and_args = '{{curl}} -s -D /dev/stdout -o /dev/stderr -x http://127.0.0.1:{} -H "x-debug: x-cache"'.format( + ts.Variables.port) + + ts.Disk.remap_config.AddLines( + [ + f'map http://sliceprefetchbytes1/ http://127.0.0.1:{server.Variables.Port}' + + f' @plugin=slice.so @pparam=--blockbytes-test={block_bytes_1} @pparam=--prefetch-count=1 \\' + + ' @plugin=cache_range_requests.so', + f'map http://sliceprefetchbytes2/ http://127.0.0.1:{server.Variables.Port}' + + f' @plugin=slice.so @pparam=--blockbytes-test={block_bytes_2} @pparam=--prefetch-count=3 \\' + + ' @plugin=cache_range_requests.so', + ]) + + ts.Disk.plugin_config.AddLine('xdebug.so --enable=x-cache') + ts.Disk.logging_yaml.AddLines( + [ + 'logging:', + ' formats:', + ' - name: cache', + ' format: "%<{Content-Range}psh> %<{X-Cache}psh>"', + ' logs:', + ' - filename: cache', + ' format: cache', + ' mode: ascii', + ]) + + ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'slice|cache_range_requests|xdebug', + }) + + # 0 Test - Full object slice (miss) with only block 14-20 prefetched in background, block bytes= 7 + tr = urtest.AddTestRun("Full object slice: first block is miss, only block 14-20 prefetched") + ps = tr.Processes.Default + ps.StartBefore(server, ready=When.PortOpen(server.Variables.Port)) + ps.StartBefore(urtest.Processes.ts) + tr.MakeCurlCommandMulti(curl_and_args + ' http://sliceprefetchbytes1/path', ts=ts) + ps.ReturnCode = 0 + ps.Streams.stderr = "gold/slice_200.stderr.gold" + ps.Streams.stdout.Content = Testers.ContainsExpression("200 OK", "expected 200 OK response") + ps.Streams.stdout.Content += Testers.ContainsExpression("X-Cache: miss", "expected cache miss") + tr.StillRunningAfter = ts + + # 1 Test - Full object slice (hit-fresh) with no prefetched blocks, block bytes= 7 + tr = urtest.AddTestRun("Full object slice: first block is hit-fresh, no blocks prefetched") + ps = tr.Processes.Default + tr.MakeCurlCommandMulti('sleep 1; ' + curl_and_args + ' http://sliceprefetchbytes1/path', ts=ts) + ps.ReturnCode = 0 + ps.Streams.stderr = "gold/slice_200.stderr.gold" + ps.Streams.stdout.Content = Testers.ContainsExpression("200 OK", "expected 200 OK response") + ps.Streams.stdout.Content += Testers.ContainsExpression("X-Cache: hit-fresh", "expected cache fresh") + tr.StillRunningAfter = ts + + # 2 Test - Full object slice with only next block prefetched in background, block bytes= 7 + tr = urtest.AddTestRun("Full object slice: first block is hit-stale, only block 14-20 prefetched") + ps = tr.Processes.Default + tr.MakeCurlCommandMulti('sleep 5; ' + curl_and_args + ' http://sliceprefetchbytes1/path', ts=ts) + ps.ReturnCode = 0 + ps.Streams.stderr = "gold/slice_200.stderr.gold" + ps.Streams.stdout.Content = Testers.ContainsExpression("200 OK", "expected 200 OK response") + ps.Streams.stdout.Content += Testers.ContainsExpression("X-Cache: hit-stale", "expected cache hit-stale") + tr.StillRunningAfter = ts + + # 3 Test - Full object slice (hit-fresh) with no prefetched blocks, block bytes= 7 + tr = urtest.AddTestRun("Full object slice: first block is hit-fresh with range 0-, no blocks prefetched") + ps = tr.Processes.Default + tr.MakeCurlCommandMulti(curl_and_args + ' http://sliceprefetchbytes1/path' + ' -r 0-', ts=ts) + ps.ReturnCode = 0 + ps.Streams.stderr = "gold/slice_200.stderr.gold" + ps.Streams.stdout.Content = Testers.ContainsExpression("206 Partial Content", "expected 206 response") + ps.Streams.stdout.Content += Testers.ContainsExpression("Content-Range: bytes 0-17/18", "mismatch byte content response") + ps.Streams.stdout.Content += Testers.ContainsExpression("X-Cache: hit-fresh", "expected cache hit-fresh") + tr.StillRunningAfter = ts + + # 4 Test - Client range request (hit-stale/miss) enables prefetching + tr = urtest.AddTestRun("Client range request") + ps = tr.Processes.Default + tr.MakeCurlCommandMulti(curl_and_args + ' http://sliceprefetchbytes2/path' + ' -r 5-16', ts=ts) + ps.ReturnCode = 0 + ps.Streams.stderr = "gold/slice_mid.stderr.gold" + ps.Streams.stdout.Content = Testers.ContainsExpression("206 Partial Content", "expected 206 response") + ps.Streams.stdout.Content += Testers.ContainsExpression("Content-Range: bytes 5-16/18", "mismatch byte content response") + ps.Streams.stdout.Content += Testers.ContainsExpression("X-Cache: miss", "expected cache miss") + tr.StillRunningAfter = ts + + # 5 Test - special case, begin inside last slice block but outside asset len + tr = urtest.AddTestRun("Invalid end range request, 416") + beg = len(body) + 1 + end = beg + block_bytes + ps = tr.Processes.Default + tr.MakeCurlCommandMulti(curl_and_args + f' http://sliceprefetchbytes1/path -r {beg}-{end}', ts=ts) + ps.Streams.stdout.Content = Testers.ContainsExpression("416 Requested Range Not Satisfiable", "expected 416 response") + tr.StillRunningAfter = ts + + # 6 Test - All requests (client & slice internal) logs to see background fetches + cache_file = os.path.join(ts.Variables.LOGDIR, 'cache.log') + # Wait for the final cache log line to be written. + test_run = urtest.AddAwaitFileContainsTestRun( + "Checking debug logs for background fetches", + cache_file, + r'\*/18 hit-fresh, none$', + ) + cache_log = ts.Disk.File(cache_file) + expected_cache_entries = [ + "bytes 0-6/18 miss", + "bytes 7-13/18 miss", + "bytes 14-17/18 miss", + "bytes 14-17/18 hit-fresh", + "- miss, none", + "bytes 0-6/18 hit-fresh", + "bytes 7-13/18 hit-fresh", + "bytes 14-17/18 hit-fresh", + "- hit-fresh, none", + "bytes 0-6/18 hit-stale", + "bytes 7-13/18 hit-stale", + "bytes 14-17/18 hit-stale", + "- hit-stale, none", + "bytes 0-17/18 hit-fresh, none", + "bytes 0-4/18 miss", + "bytes 5-9/18 miss", + "bytes 10-14/18 miss", + "bytes 15-17/18 miss", + "bytes 10-14/18 hit-fresh", + "bytes 15-17/18 hit-fresh", + "bytes 5-16/18 miss, none", + "*/18 hit-fresh, none", + ] + for index, entry in enumerate(expected_cache_entries): + tester = Testers.ContainsExpression(f'(?m)^{re.escape(entry)}$', f'Verify cache log contains: {entry}') + if index == 0: + cache_log.Content = tester + else: + cache_log.Content += tester + urtest.execute() diff --git a/tests/uranium_tests/pluginTest/slice/test_slice_purge.py b/tests/uranium_tests/pluginTest/slice/test_slice_purge.py new file mode 100644 index 00000000000..8a19f68f98b --- /dev/null +++ b/tests/uranium_tests/pluginTest/slice/test_slice_purge.py @@ -0,0 +1,115 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_slice_purge(urtest: UraniumTest) -> None: + ''' + Verify ATS slice plugin config accepts PURGE requests + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + urtest.Summary = ''' + Verify ATS slice plugin config accepts PURGE requests + ''' + urtest.SkipUnless( + Condition.PluginExists('slice.so'), + Condition.PluginExists('cache_range_requests.so'), + ) + + class SlicePurgeRequestTest: + replay_file = "replay/slice_purge.replay.yaml" + replay_ref_file = "replay/slice_purge_ref.replay.yaml" + replay_no_ref_file = "replay/slice_purge_no_ref.replay.yaml" + + def __init__(self, ref_block, num): + """Initialize the Test processes for the test runs.""" + self._ref_block = ref_block + self._num = num + self._server = urtest.MakeVerifierServerProcess(f"server_{self._num}", SlicePurgeRequestTest.replay_file) + self._configure_trafficserver() + + def _configure_trafficserver(self): + """Configure Traffic Server.""" + self._ts = urtest.MakeATSProcess(f"ts_{self._num}", enable_cache=True) + + if (self._ref_block): + self._ts.Disk.remap_config.AddLines( + [ + f"map /ref/block http://127.0.0.1:{self._server.Variables.http_port} \ + @plugin=slice.so @pparam=--blockbytes-test=10 \ + @plugin=cache_range_requests.so", + ]) + else: + self._ts.Disk.remap_config.AddLines( + [ + f"map /ref/block http://127.0.0.1:{self._server.Variables.http_port} \ + @plugin=slice.so @pparam=--blockbytes-test=10 @pparam=--ref-relative \ + @plugin=cache_range_requests.so", + ]) + + self._ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'http|slice|cache_range_requests', + }) + + def slice_purge(self): + tr = urtest.AddTestRun() + + tr.AddVerifierClientProcess( + f"client_{self._num}", SlicePurgeRequestTest.replay_file, http_ports=[self._ts.Variables.port]) + + tr.Processes.Default.StartBefore(self._server) + tr.Processes.Default.StartBefore(self._ts) + + def slice_purge_ref(self): + tr = urtest.AddTestRun() + + tr.AddVerifierClientProcess("client_ref", SlicePurgeRequestTest.replay_ref_file, http_ports=[self._ts.Variables.port]) + + tr.Processes.Default.StartBefore(self._server) + tr.Processes.Default.StartBefore(self._ts) + + def slice_purge_no_ref(self): + tr = urtest.AddTestRun() + + tr.AddVerifierClientProcess( + "client_no_ref", SlicePurgeRequestTest.replay_no_ref_file, http_ports=[self._ts.Variables.port]) + + tr.Processes.Default.StartBefore(self._server) + tr.Processes.Default.StartBefore(self._ts) + + SlicePurgeRequestTest(True, 0).slice_purge() + SlicePurgeRequestTest(True, 1).slice_purge_ref() + SlicePurgeRequestTest(False, 2).slice_purge() + SlicePurgeRequestTest(False, 3).slice_purge_no_ref() + urtest.execute() diff --git a/tests/uranium_tests/pluginTest/slice/test_slice_purge_gaps.py b/tests/uranium_tests/pluginTest/slice/test_slice_purge_gaps.py new file mode 100644 index 00000000000..aa139a1caf1 --- /dev/null +++ b/tests/uranium_tests/pluginTest/slice/test_slice_purge_gaps.py @@ -0,0 +1,366 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_slice_purge_gaps(urtest: UraniumTest) -> None: + """Verify a PURGE traverses every slice block, not just the ones before a gap.""" + + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + urtest.Summary = __doc__ + + urtest.SkipUnless( + Condition.PluginExists('slice.so'), + Condition.PluginExists('cache_range_requests.so'), + Condition.PluginExists('header_rewrite.so'), + ) + urtest.ContinueOnFail = True + + class SlicePurgeGapsTest: + """Verify a PURGE removes every block it was asked for. + + Slice turns one client PURGE into one PURGE per block. The walk used to stop + at the first block that was not cached, because it had no other end + condition: m_contentlen was only ever set from a 206's Content-Range, a PURGE + response carried none, and so m_req_range.blockIsInside() was true for every + block number. The 404 stood in for a length the plugin never learned. + + ATS now reports the removed object's extent as X-Purged-Content-Range, so the + walk learns where the object ends from the blocks it is already deleting, and + a 404 is merely noted and stepped over. The requested range bounds the walk + throughout; until some block reports an extent, an open ended one is bounded + only by a limit on consecutive misses. A block response that is neither 200 nor + 404 is a failure rather than an absence, and says nothing about the blocks + behind it, so the walk stops there and reports that status. + + Each object below exercises one thing, and each is purged exactly once, + because a purge consumes the state it is measured against. + + /hole blocks 0 and 2 cached, block 1 absent. A 404 mid-walk must not end + it, so block 2 goes too. + /nofirst only block 1 cached. The walk steps over block 0's 404, picks the + extent up from block 1, and the client must not be handed that 404. + /mixed blocks reporting a 30 byte and a 50 byte object, as happens when + the origin object is replaced in place. The walk must follow the + largest extent reported, not the first. + /ranged same gap as /hole, purged with a closed range. Already bounded, so + this isolates the 404 handling from the extent discovery. + /openend purged with "bytes=20-". The start is stated, so only block 2 may + go and block 0 must survive. + /outside only block 1 cached, purged for block 0 alone. No block is removed, + so no extent is ever reported and only the range can end the walk + before it reaches a block the client did not name. + /endbytes purged with "bytes=-10". A suffix range cannot know which block it + starts at, so it is widened to the whole object and every block + goes. + /sparse only block 4 cached, on a proxy whose miss bound is 2, so the + default walk cannot reach it. Covers the bound, its per-request + override, and the fallback when the override is malformed. + /badrange purged with an unparseable range, which must be refused rather + than silently applied to block 0. + /failblock blocks 0 and 3 cached, with a 500 injected for block 1's PURGE. Block + 0 is removed, the walk stops at block 1, block 3 is left alone, and + the client hears the 500 rather than the 200 block 0 earned. + /denied block 0 cached, with a 403 injected for every block PURGE, as + ip_allow refusing PURGE would. The walk stops at its first block, so + nothing is removed and the client must not be told 404. + + Whether a block was purged is measured on the origin, not on the response + body: the origin serves each check phase exactly what the matching fill phase + served, so a purged block and a surviving block are indistinguishable to the + client, and the only difference is whether the origin was asked again. + """ + + _client_replay: str = 'replay/slice_purge_gaps_client.replay.yaml' + _server_replay: str = 'replay/slice_purge_gaps_server.replay.yaml' + + # The core answers a block PURGE with nothing but 200 or 404, so a failure has to + # be injected to be tested at all. + _fail_rules: str = 'purge_block_failure.conf' + + _block_bytes: int = 10 + + # Low enough that the default walk cannot reach /sparse's block 4, which is + # what makes the per-request override observable. + _low_miss_bound: int = 2 + + # Keyed on the block range as well as the phase uuid, so one origin + # transaction answers one block of one phase. + _origin_key_format: str = '--format "{url}{field.range}{field.uuid}"' + + def __init__(self) -> None: + """Declare the origin and the two proxies.""" + self._started = False + self._configure_origin() + self._ts = self._make_ts('ts', fail_rules=True) + self._ts_bound = self._make_ts('ts-bound', miss_bound=self._low_miss_bound) + + self._ts.Disk.traffic_out.Content = Testers.ContainsExpression( + 'Purge suffix range widened to the whole object', + 'A suffix range purge should be widened rather than guessing at its start.') + + self._ts_bound.Disk.traffic_out.Content = Testers.ContainsExpression( + f'gave up after {self._low_miss_bound} consecutive uncached block', + 'The walk should stop at its configured miss bound rather than scanning the whole range.') + self._ts_bound.Disk.diags_log.Content = Testers.ContainsExpression( + 'Ignoring invalid X-Slice-Purge-Probe', 'A malformed override should be rejected, not acted on.') + + # A purge issues nothing but block PURGEs. request_block logs every request + # header it builds at debug, so an only-if-cached here would mean a + # read-only length probe had been reintroduced. + for ts in (self._ts, self._ts_bound): + ts.Disk.traffic_out.Content += Testers.ExcludesExpression( + 'only-if-cached', 'A purge should not issue a read-only length probe.') + + def _configure_origin(self) -> None: + """Configure the origin.""" + self._origin = urtest.MakeVerifierServerProcess('origin', self._server_replay, other_args=self._origin_key_format) + + # ATS answers PURGE itself and the plugin issues no other request kind, so + # neither may ever be seen upstream. + self._origin.Streams.stdout += Testers.ExcludesExpression( + 'PURGE', 'A PURGE should be answered by ATS and never forwarded to the origin.') + self._origin.Streams.stdout += Testers.ExcludesExpression('HEAD /', 'A purge should never issue a HEAD upstream.') + + def _make_ts(self, label: str, miss_bound: int = None, fail_rules: bool = False) -> 'Process': + """Create a proxy that slices in front of cache_range_requests. + + --ref-relative keeps a ranged GET from dragging block 0 in as a reference + block, which is what lets a fill phase leave a chosen block uncached. + + :param label: process name suffix. + :param miss_bound: --purge-probe-blocks value, or None for the default. + :param fail_rules: load the header_rewrite rules that fail a block PURGE. + """ + ts = urtest.MakeATSProcess(label, enable_cache=True) + + rules = '' + if fail_rules: + ts.Setup.CopyAs(f'rules/{self._fail_rules}', urtest.RunDirectory) + rules = f' @plugin=header_rewrite.so @pparam={urtest.RunDirectory}/{self._fail_rules}' + + bound = '' if miss_bound is None else f' @pparam=--purge-probe-blocks={miss_bound}' + ts.Disk.remap_config.AddLine( + f'map http://slice/ http://127.0.0.1:{self._origin.Variables.http_port}/' + f'{rules} @plugin=slice.so @pparam=--blockbytes-test={self._block_bytes} @pparam=--ref-relative{bound}' + ' @plugin=cache_range_requests.so') + ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'slice|cache_range_requests', + }) + return ts + + def _run(self, summary: str, phases: str, ts: 'Process' = None) -> None: + """Add a TestRun replaying one or more phases, in replay file order. + + :param summary: TestRun description. + :param phases: space separated verifier-client keys. + :param ts: proxy to replay against, defaulting to the ordinary one. + """ + ts = self._ts if ts is None else ts + tr = urtest.AddTestRun(summary) + if not self._started: + tr.Processes.Default.StartBefore(self._origin) + tr.Processes.Default.StartBefore(self._ts) + tr.Processes.Default.StartBefore(self._ts_bound) + self._started = True + tr.AddVerifierClientProcess( + f"client-{phases.replace(' ', '-')}", self._client_replay, http_ports=[ts.Variables.port], keys=phases) + tr.StillRunningAfter = ts + + def _origin_saw(self, phase: str, block: str, why: str) -> None: + """Assert the origin was asked for a block under a given phase.""" + self._origin.Streams.stdout += Testers.ContainsExpression(f'request with key /{block}{phase}', why) + + def _fill(self, summary: str, url: str, blocks: list, ts: 'Process' = None) -> None: + """Cache the given blocks of an object, and prove each fill happened. + + The fill assertions carry weight: a check phase only observes that the + origin was asked, which is equally true of a block that was purged and one + that was never cached at all. Asserting the fill reached the origin is what + makes the later check mean "removed" rather than merely "absent". + """ + self._run(summary, ' '.join(f'{url}-fill-{block}' for block in blocks), ts) + for block in blocks: + self._origin_saw( + f'{url}-fill-{block}', f'{url}bytes={block * 10}-{block * 10 + 9}', + f'Block {block} of /{url} should have been fetched and cached.') + + def _purged(self, phase: str, block: str, why: str) -> None: + """Assert a phase's block request reached the origin, so it was purged.""" + self._origin_saw(phase, block, why) + + def _survived(self, phase: str, block: str, why: str) -> None: + """Assert a phase's block request never reached the origin, so it survived. + + The origin has no transaction registered for such a phase either, so a + wrongly purged block fails the client's own expectation as well. + """ + self._origin.Streams.stdout += Testers.ExcludesExpression(f'request with key /{block}{phase}', why) + + def _gap_mid_walk(self) -> None: + """A 404 in the middle of the walk must not end it.""" + self._fill('Cache blocks 0 and 2 of /hole, leaving block 1 uncached', 'hole', [0, 2]) + self._run('PURGE the whole /hole object', 'hole-purge') + self._run('Both cached blocks of /hole were purged', 'hole-check-0 hole-check-2') + self._purged('hole-check-0', 'holebytes=0-9', 'Block 0 is in front of the gap, so it should be purged.') + self._purged('hole-check-2', 'holebytes=20-29', 'A PURGE should traverse blocks behind an uncached one.') + + def _uncached_first_block(self) -> None: + """A miss on the first block must not stop the purge before it starts.""" + self._fill('Cache only block 1 of /nofirst', 'nofirst', [1]) + self._run('PURGE /nofirst, whose block 0 is not cached', 'nofirst-purge') + self._run('Block 1 of /nofirst was purged', 'nofirst-check-1') + self._purged('nofirst-check-1', 'nofirstbytes=10-19', 'An uncached first block should not stop the purge.') + + def _largest_extent_wins(self) -> None: + """A block reporting a longer object widens the walk.""" + self._fill('Cache blocks 0, 1 and 4 of /mixed, disagreeing about its length', 'mixed', [0, 1, 4]) + self._run('PURGE /mixed', 'mixed-purge') + self._run('Block 4 of /mixed was purged, so the walk took the longer extent', 'mixed-check-4') + self._purged('mixed-check-4', 'mixedbytes=40-49', 'The walk should follow the largest extent any block reports.') + + def _closed_range(self) -> None: + """A closed range bounds the walk itself, isolating the 404 step-over.""" + self._fill('Cache blocks 0 and 2 of /ranged, leaving block 1 uncached', 'ranged', [0, 2]) + self._run('PURGE /ranged with a closed range spanning the whole object', 'ranged-purge') + self._run('Both cached blocks of /ranged were purged', 'ranged-check-0 ranged-check-2') + self._purged('ranged-check-0', 'rangedbytes=0-9', 'A closed range purge should remove the blocks it covers.') + self._purged('ranged-check-2', 'rangedbytes=20-29', 'A 404 should not end a closed range purge either.') + + def _open_ended_range(self) -> None: + """A "bytes=N-" purge states its start, so it purges only what it names.""" + self._fill('Cache blocks 0 and 2 of /openend', 'openend', [0, 2]) + self._run('PURGE /openend from byte 20 on', 'openend-purge') + self._run('Block 2 of /openend went and block 0 stayed', 'openend-check-2 openend-check-0') + self._purged('openend-check-2', 'openendbytes=20-29', 'The block covering the range should be purged.') + self._survived('openend-check-0', 'openendbytes=0-9', 'A purge should not remove blocks before its stated start.') + + def _range_bounds_the_walk(self) -> None: + """A range ends the walk even before any block has reported an extent. + + /outside has only block 1 cached and is purged for block 0 alone. Nothing + is ever removed, so no block reports an extent, and the miss bound would + otherwise carry the walk past the end of the named range and into a block + the client never asked to remove. + """ + self._fill('Cache only block 1 of /outside', 'outside', [1]) + self._run('PURGE /outside for block 0 only, which is not cached', 'outside-purge') + self._run('Block 1 of /outside survived a purge that did not name it', 'outside-check-1') + self._survived( + 'outside-check-1', 'outsidebytes=10-19', 'A purge should stop at the end of its range, not at the miss bound.') + + def _suffix_range(self) -> None: + """A "bytes=-N" purge is widened to the whole object.""" + self._fill('Cache blocks 0 and 2 of /endbytes', 'endbytes', [0, 2]) + self._run('PURGE the last 10 bytes of /endbytes', 'endbytes-purge') + self._run('Every cached block of /endbytes went, not just the named tail', 'endbytes-check-0 endbytes-check-2') + self._purged('endbytes-check-2', 'endbytesbytes=20-29', 'The block covering the suffix range must be purged.') + self._purged( + 'endbytes-check-0', 'endbytesbytes=0-9', + 'A widened suffix purge removes the whole object, which is a superset of what was named.') + + def _miss_bound_and_override(self) -> None: + """The miss bound stops a walk that has found nothing, and is overridable. + + /sparse has only block 4 of five cached, out of reach of this proxy's + configured bound of two. Nothing about the remap changes between the two + purges below; only the request header does. + """ + ts = self._ts_bound + self._fill('Cache only block 4 of /sparse, out of reach of the configured bound', 'sparse', [4], ts) + + self._run('A purge with a malformed override falls back to the configured bound', 'sparse-purge-narrow', ts) + self._run('Block 4 of /sparse survived the too-narrow purge', 'sparse-check-alive', ts) + self._survived( + 'sparse-check-alive', 'sparsebytes=40-49', 'A walk that gave up before block 4 should not have purged it.') + + self._run('PURGE /sparse with an override wide enough to reach block 4', 'sparse-purge-wide', ts) + self._run('Block 4 of /sparse was purged once the bound reached it', 'sparse-check-gone', ts) + self._purged('sparse-check-gone', 'sparsebytes=40-49', 'A request supplied bound should let the walk reach block 4.') + + def _unparseable_range(self) -> None: + """A purge whose range cannot be parsed is refused, not guessed at. + + An unparseable range leaves the plugin's range covering block 0 only, so + walking it would delete the head of the object and report success. A purge + is destructive, so it is rejected instead. + """ + self._fill('Cache block 0 of /badrange', 'badrange', [0]) + self._run('A PURGE with an unparseable range is refused', 'badrange-purge') + self._run('Block 0 of /badrange survived the refused purge', 'badrange-check-0') + self._survived('badrange-check-0', 'badrangebytes=0-9', 'A refused purge must not have removed anything.') + self._ts.Disk.diags_log.Content = Testers.ContainsExpression( + 'Refusing PURGE with an unparseable range', 'The refusal should be visible in the error log.') + + def _block_failure(self) -> None: + """A block that could not be purged is not a block that was absent. + + Only a 404 says the block was not cached. Any other status says the walk could + not tell, and says nothing about the blocks behind it either, so it stops there + and the client hears that status rather than the 200 the earlier blocks earned. + Both failures are injected with header_rewrite, since the core answers a block + PURGE with nothing but 200 or 404 on its own. + """ + self._fill('Cache blocks 0 and 3 of /failblock', 'failblock', [0, 3]) + self._run('PURGE /failblock, whose block 1 answers 500', 'failblock-purge') + self._run('/failblock was purged up to the failing block only', 'failblock-check-0 failblock-check-3') + self._purged('failblock-check-0', 'failblockbytes=0-9', 'A block removed before the failure should stay removed.') + self._survived( + 'failblock-check-3', 'failblockbytes=30-39', + 'The walk should stop at the failing block, leaving what is behind it cached.') + self._ts.Disk.diags_log.Content += Testers.ContainsExpression( + 'Purge of block 1 failed', 'The block that ended the walk should be logged.') + + self._fill('Cache block 0 of /denied', 'denied', [0]) + self._run('PURGE /denied, whose every block answers 403', 'denied-purge') + self._run('Block 0 of /denied survived the refused purge', 'denied-check-0') + self._survived('denied-check-0', 'deniedbytes=0-9', 'A purge refused at its first block must not remove anything.') + self._ts.Disk.diags_log.Content += Testers.ContainsExpression( + 'Purge of block 0 failed', 'A purge refused outright should be logged rather than reported as a 404.') + + def run(self) -> None: + """Configure the test runs.""" + self._gap_mid_walk() + self._uncached_first_block() + self._largest_extent_wins() + self._closed_range() + self._open_ended_range() + self._range_bounds_the_walk() + self._suffix_range() + self._unparseable_range() + self._miss_bound_and_override() + self._block_failure() + + SlicePurgeGapsTest().run() + urtest.execute() diff --git a/tests/uranium_tests/pluginTest/slice/test_slice_regex.py b/tests/uranium_tests/pluginTest/slice/test_slice_regex.py new file mode 100644 index 00000000000..9b778642140 --- /dev/null +++ b/tests/uranium_tests/pluginTest/slice/test_slice_regex.py @@ -0,0 +1,168 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_slice_regex(urtest: UraniumTest) -> None: + ''' + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + urtest.Summary = ''' + slice regex plugin test + ''' + + # Test description: + # Preload the cache with the entire asset to be range requested. + # Reload remap rule with slice plugin + # Request content through the slice plugin + + urtest.SkipUnless(Condition.PluginExists('slice.so'),) + urtest.ContinueOnFail = False + + # configure origin server + server = urtest.MakeOriginServer("server") + + # Define ATS and configure. + ts = urtest.MakeATSProcess("ts", enable_cache=False) + + # default root + request_header_chk = { + "headers": "GET / HTTP/1.1\r\n" + "Host: www.example.com\r\n" + "\r\n", + "timestamp": "1469733493.993", + "body": "", + } + + response_header_chk = { + "headers": "HTTP/1.1 200 OK\r\n" + "Connection: close\r\n" + "\r\n", + "timestamp": "1469733493.993", + "body": "", + } + + server.addResponse("sessionlog.json", request_header_chk, response_header_chk) + + body = "lets go surfin now" + + request_header_txt = { + "headers": "GET /slice.txt HTTP/1.1\r\n" + "Host: slice\r\n" + "\r\n", + "timestamp": "1469733493.993", + "body": "", + } + + response_header_txt = { + "headers": + "HTTP/1.1 200 OK\r\n" + "Connection: close\r\n" + 'Etag: "path"\r\n' + "Cache-Control: max-age=500\r\n" + + "X-Info: notsliced\r\n" + "\r\n", + "timestamp": "1469733493.993", + "body": body, + } + + server.addResponse("sessionlog.json", request_header_txt, response_header_txt) + + request_header_mp4 = { + "headers": "GET /slice.mp4 HTTP/1.1\r\n" + "Host: sliced\r\n" + "Range: bytes=0-99\r\n" + "\r\n", + "timestamp": "1469733493.993", + "body": "", + } + + response_header_mp4 = { + "headers": + "HTTP/1.1 206 Partial Content\r\n" + "Connection: close\r\n" + 'Etag: "path"\r\n' + + "Content-Range: bytes 0-{}/{}\r\n".format(len(body) - 1, len(body)) + "Cache-Control: max-age=500\r\n" + + "X-Info: sliced\r\n" + "\r\n", + "timestamp": "1469733493.993", + "body": body, + } + + server.addResponse("sessionlog.json", request_header_mp4, response_header_mp4) + + curl_and_args = '-s -D /dev/stdout -o /dev/stderr -x localhost:{} -H "x-debug: x-cache"'.format(ts.Variables.port) + + block_bytes = 100 + + # set up whole asset fetch into cache + ts.Disk.remap_config.AddLines( + [ + 'map http://exclude/ http://127.0.0.1:{}/'.format(server.Variables.Port) + ' @plugin=slice.so' + + ' @pparam=--blockbytes-test={}'.format(block_bytes) + ' @pparam=--exclude-regex=\\.txt' + ' @pparam=--remap-host=sliced', + 'map http://include/ http://127.0.0.1:{}/'.format(server.Variables.Port) + ' @plugin=slice.so' + + ' @pparam=--blockbytes-test={}'.format(block_bytes) + ' @pparam=--include-regex=\\.mp4' + ' @pparam=--remap-host=sliced', + 'map http://sliced/ http://127.0.0.1:{}/'.format(server.Variables.Port), + ]) + + # minimal configuration + ts.Disk.records_config.update( + { + # 'proxy.config.diags.debug.enabled': 1, + # 'proxy.config.diags.debug.tags': 'slice', + 'proxy.config.http.insert_age_in_response': 0, + 'proxy.config.http.response_via_str': 0, + }) + + # 0 Test - Exclude: ensure txt passes through + tr = urtest.AddTestRun("Exclude - asset passed through") + ps = tr.Processes.Default + ps.StartBefore(server, ready=When.PortOpen(server.Variables.Port)) + ps.StartBefore(urtest.Processes.ts) + tr.MakeCurlCommand(curl_and_args + ' http://exclude/slice.txt', ts=ts) + ps.ReturnCode = 0 + ps.Streams.stdout.Content = Testers.ContainsExpression("X-Info: notsliced", "expected not sliced header") + tr.StillRunningAfter = ts + + # 1 Test - Exclude mp4 gets sliced + tr = urtest.AddTestRun("Exclude - asset is sliced") + ps = tr.Processes.Default + tr.MakeCurlCommand(curl_and_args + ' http://exclude/slice.mp4', ts=ts) + ps.ReturnCode = 0 + ps.Streams.stdout.Content = Testers.ContainsExpression("X-Info: sliced", "expected sliced header") + tr.StillRunningAfter = ts + tr.StillRunningAfter = ts + + # 2 Test - Exclude: ensure txt passes through + tr = urtest.AddTestRun("Include - asset passed through") + ps = tr.Processes.Default + tr.MakeCurlCommand(curl_and_args + ' http://include/slice.txt', ts=ts) + ps.ReturnCode = 0 + ps.Streams.stdout.Content = Testers.ContainsExpression("X-Info: notsliced", "expected not sliced header") + tr.StillRunningAfter = ts + + # 3 Test - Exclude mp4 gets sliced + tr = urtest.AddTestRun("Include - asset is sliced") + ps = tr.Processes.Default + tr.MakeCurlCommand(curl_and_args + ' http://include/slice.mp4', ts=ts) + ps.ReturnCode = 0 + ps.Streams.stdout.Content = Testers.ContainsExpression("X-Info: sliced", "expected sliced header") + tr.StillRunningAfter = ts + tr.StillRunningAfter = ts + urtest.execute() diff --git a/tests/uranium_tests/pluginTest/slice/test_slice_rm_range.py b/tests/uranium_tests/pluginTest/slice/test_slice_rm_range.py new file mode 100644 index 00000000000..db34db2c8ea --- /dev/null +++ b/tests/uranium_tests/pluginTest/slice/test_slice_rm_range.py @@ -0,0 +1,89 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_slice_rm_range(urtest: UraniumTest) -> None: + ''' + Verify ATS slice plugin config: @pparam=--strip-range-for-head + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + urtest.Summary = ''' + Verify ATS slice plugin config: @pparam=--strip-range-for-head + ''' + urtest.SkipUnless( + Condition.PluginExists('slice.so'), + Condition.PluginExists('cache_range_requests.so'), + ) + + class SliceStripRangeForHeadRequestTest: + replay_file = "replay/slice_range.replay.yaml" + + def __init__(self): + """Initialize the Test processes for the test runs.""" + self._server = urtest.MakeVerifierServerProcess("server", SliceStripRangeForHeadRequestTest.replay_file) + self._configure_trafficserver() + + def _configure_trafficserver(self): + """Configure Traffic Server.""" + self._ts = urtest.MakeATSProcess("ts", enable_cache=False) + + self._ts.Disk.remap_config.AddLines( + [ + f"map /no/range http://127.0.0.1:{self._server.Variables.http_port} \ + @plugin=slice.so @pparam=--blockbytes-test=10 @pparam=--strip-range-for-head \ + @plugin=cache_range_requests.so", + f"map /with/range http://127.0.0.1:{self._server.Variables.http_port} \ + @plugin=slice.so @pparam=--blockbytes-test=10 \ + @plugin=cache_range_requests.so", + ]) + + self._ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'http|slice|cache_range_requests', + }) + + def _test_head_request_range_header(self): + tr = urtest.AddTestRun() + + tr.AddVerifierClientProcess( + "client", SliceStripRangeForHeadRequestTest.replay_file, http_ports=[self._ts.Variables.port]) + + tr.Processes.Default.StartBefore(self._server) + tr.Processes.Default.StartBefore(self._ts) + + def run(self): + self._test_head_request_range_header() + + SliceStripRangeForHeadRequestTest().run() + urtest.execute() diff --git a/tests/uranium_tests/pluginTest/slice/test_slice_selfhealing.py b/tests/uranium_tests/pluginTest/slice/test_slice_selfhealing.py new file mode 100644 index 00000000000..43e7f81cfa1 --- /dev/null +++ b/tests/uranium_tests/pluginTest/slice/test_slice_selfhealing.py @@ -0,0 +1,449 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_slice_selfhealing(urtest: UraniumTest) -> None: + ''' + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + import time + import datetime + + urtest.Summary = ''' + Slice selfhealing test + ''' + + def to_httpdate(dt): + # string representation of a date according to RFC 1123 (HTTP/1.1). + weekday = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"][dt.weekday()] + month = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"][dt.month - 1] + return "%s, %02d %s %04d %02d:%02d:%02d GMT" % (weekday, dt.day, month, dt.year, dt.hour, dt.minute, dt.second) + + # Test description: + # Preload the cache with the entire asset to be range requested. + # Reload remap rule with slice plugin + # Request content through the slice plugin + + urtest.SkipUnless( + Condition.PluginExists('slice.so'), + Condition.PluginExists('cache_range_requests.so'), + Condition.PluginExists('xdebug.so'), + ) + urtest.ContinueOnFail = False + + # configure origin server + server = urtest.MakeOriginServer("server", lookup_key="{%uuid}") + + # Define ATS and configure + ts = urtest.MakeATSProcess("ts") + + # default root + req_header_chk = { + "headers": "GET / HTTP/1.1\r\n" + "Host: www.example.com\r\n" + "uuid: none\r\n" + "\r\n", + "timestamp": "1469733493.993", + "body": "", + } + + res_header_chk = { + "headers": "HTTP/1.1 200 OK\r\n" + "Connection: close\r\n" + "\r\n", + "timestamp": "1469733493.993", + "body": "", + } + + server.addResponse("sessionlog.json", req_header_chk, res_header_chk) + + # set up slice plugin with remap host into cache_range_requests + ts.Disk.remap_config.AddLines( + [ + f'map http://slice/ http://127.0.0.1:{server.Variables.Port}/' + + ' @plugin=slice.so @pparam=--blockbytes-test=3 @pparam=--remap-host=crr', + f'map http://crr/ http://127.0.0.1:{server.Variables.Port}/' + + ' @plugin=cache_range_requests.so @pparam=--consider-ident', + f'map http://slicehdr/ http://127.0.0.1:{server.Variables.Port}/' + ' @plugin=slice.so @pparam=--blockbytes-test=3' + + ' @pparam=--remap-host=crrhdr @pparam=--crr-ident-header=crr-foo', + f'map http://crrhdr/ http://127.0.0.1:{server.Variables.Port}/' + ' @plugin=cache_range_requests.so @pparam=--ident-header=crr-foo', + ]) + + ts.Disk.plugin_config.AddLine('xdebug.so --enable=x-cache') + + ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'cache_range_requests|slice', + }) + + curl_and_args = '-s -D /dev/stdout -o /dev/stderr -x localhost:{}'.format(ts.Variables.port) + ' -H "x-debug: x-cache"' + + # Test case: 2nd slice out of date (refetch and continue) + + req_header_2ndold1 = { + "headers": "GET /second HTTP/1.1\r\n" + "Host: www.example.com\r\n" + "uuid: etagold-1\r\n" + "Range: bytes=3-5\r\n" + "\r\n", + "timestamp": "1469733493.993", + "body": "", + } + + res_header_2ndold1 = { + "headers": + "HTTP/1.1 206 Partial Content\r\n" + "Accept-Ranges: bytes\r\n" + "Cache-Control: max-age=5000\r\n" + + "Connection: close\r\n" + "Content-Range: bytes 3-4/5\r\n" + 'Etag: "etagold"\r\n' + "\r\n", + "timestamp": "1469733493.993", + "body": "aa" + } + + server.addResponse("sessionlog.json", req_header_2ndold1, res_header_2ndold1) + + req_header_2ndnew0 = { + "headers": "GET /second HTTP/1.1\r\n" + "Host: www.example.com\r\n" + "uuid: etagnew-0\r\n" + "Range: bytes=0-2\r\n" + "\r\n", + "timestamp": "1469733493.993", + "body": "", + } + + res_header_2ndnew0 = { + "headers": + "HTTP/1.1 206 Partial Content\r\n" + "Accept-Ranges: bytes\r\n" + "Cache-Control: max-age=5000\r\n" + + "Connection: close\r\n" + "Content-Range: bytes 0-2/5\r\n" + 'Etag: "etagnew"\r\n' + "\r\n", + "timestamp": "1469733493.993", + "body": "bbb" + } + + server.addResponse("sessionlog.json", req_header_2ndnew0, res_header_2ndnew0) + + req_header_2ndnew1 = { + "headers": "GET /second HTTP/1.1\r\n" + "Host: www.example.com\r\n" + "uuid: etagnew-1\r\n" + "Range: bytes=3-5\r\n" + "\r\n", + "timestamp": "1469733493.993", + "body": "", + } + + res_header_2ndnew1 = { + "headers": + "HTTP/1.1 206 Partial Content\r\n" + "Accept-Ranges: bytes\r\n" + "Cache-Control: max-age=5000\r\n" + + "Connection: close\r\n" + "Content-Range: bytes 3-4/5\r\n" + 'Etag: "etagnew"\r\n' + "\r\n", + "timestamp": "1469733493.993", + "body": "bb" + } + + server.addResponse("sessionlog.json", req_header_2ndnew1, res_header_2ndnew1) + + # 0 Test - Preload reference etagnew-0 + tr = urtest.AddTestRun("Preload reference etagnew-0") + ps = tr.Processes.Default + ps.StartBefore(server, ready=When.PortOpen(server.Variables.Port)) + ps.StartBefore(urtest.Processes.ts) + tr.MakeCurlCommand(curl_and_args + ' http://crr/second -r 0-2 -H "uuid: etagnew-0"', ts=ts) + ps.ReturnCode = 0 + ps.Streams.stderr = "gold/bbb.gold" + ps.Streams.stdout.Content = Testers.ContainsExpression("etagnew", "expected etagnew") + tr.StillRunningAfter = ts + + # 1 Test - Preload slice etagold-1 + tr = urtest.AddTestRun("Preload slice etagold-1") + ps = tr.Processes.Default + tr.MakeCurlCommand(curl_and_args + ' http://crr/second -r 3-5 -H "uuid: etagold-1"', ts=ts) + ps.ReturnCode = 0 + ps.Streams.stderr = "gold/aa.gold" + ps.Streams.stdout.Content = Testers.ContainsExpression("etagold", "expected etagold") + tr.StillRunningAfter = ts + + # 2 Test - Request second slice via slice plugin, with instructions to fetch new 2nd slice + tr = urtest.AddTestRun("Request 2nd slice (expect refetch)") + ps = tr.Processes.Default + tr.MakeCurlCommand(curl_and_args + ' http://slice/second -r 3- -H "uuid: etagnew-1"', ts=ts) + ps.ReturnCode = 0 + ps.Streams.stderr = "gold/bb.gold" + ps.Streams.stdout.Content = Testers.ContainsExpression("etagnew", "expected etagnew") + tr.StillRunningAfter = ts + + # 3 Test - Request fully healed asset via slice plugin + tr = urtest.AddTestRun("Request full healed slice") + ps = tr.Processes.Default + tr.MakeCurlCommand(curl_and_args + ' http://slice/second', ts=ts) + ps.ReturnCode = 0 + ps.Streams.stderr.Content = Testers.ContainsExpression("bbbbb", "expected bbbbb content") + ps.Streams.stdout.Content = Testers.ContainsExpression("etagnew", "expected etagnew") + tr.StillRunningAfter = ts + + # Test case: reference slice out of date (abort connection, heal reference) + + req_header_refold0 = { + "headers": + "GET /reference HTTP/1.1\r\n" + "Host: www.example.com\r\n" + "uuid: etagold-0\r\n" + "Range: bytes=0-2\r\n" + "\r\n", + "timestamp": "1469733493.993", + "body": "", + } + + res_header_refold0 = { + "headers": + "HTTP/1.1 206 Partial Content\r\n" + "Accept-Ranges: bytes\r\n" + "Cache-Control: max-age=5000\r\n" + + "Connection: close\r\n" + "Content-Range: bytes 0-2/5\r\n" + 'Etag: "etagold"\r\n' + "\r\n", + "timestamp": "1469733493.993", + "body": "aaa" + } + + server.addResponse("sessionlog.json", req_header_refold0, res_header_refold0) + + req_header_refnew0 = { + "headers": + "GET /reference HTTP/1.1\r\n" + "Host: www.example.com\r\n" + "uuid: etagnew-0\r\n" + "Range: bytes=0-2\r\n" + "\r\n", + "timestamp": "1469733493.993", + "body": "", + } + + res_header_refnew0 = { + "headers": + "HTTP/1.1 206 Partial Content\r\n" + "Accept-Ranges: bytes\r\n" + "Cache-Control: max-age=5000\r\n" + + "Connection: close\r\n" + "Content-Range: bytes 0-2/5\r\n" + 'Etag: "etagnew"\r\n' + "\r\n", + "timestamp": "1469733493.993", + "body": "bbb" + } + + server.addResponse("sessionlog.json", req_header_refnew0, res_header_refnew0) + + req_header_refnew1 = { + "headers": + "GET /reference HTTP/1.1\r\n" + "Host: www.example.com\r\n" + "uuid: etagnew-1\r\n" + "Range: bytes=3-5\r\n" + "\r\n", + "timestamp": "1469733493.993", + "body": "", + } + + res_header_refnew1 = { + "headers": + "HTTP/1.1 206 Partial Content\r\n" + "Accept-Ranges: bytes\r\n" + "Cache-Control: max-age=5000\r\n" + + "Connection: close\r\n" + "Content-Range: bytes 3-4/5\r\n" + 'Etag: "etagnew"\r\n' + "\r\n", + "timestamp": "1469733493.993", + "body": "bb" + } + + server.addResponse("sessionlog.json", req_header_refnew1, res_header_refnew1) + + # 4 Test - Preload reference etagold-0 + tr = urtest.AddTestRun("Preload reference etagold-0") + ps = tr.Processes.Default + tr.MakeCurlCommand(curl_and_args + ' http://crr/reference -r 0-2 -H "uuid: etagold-0"', ts=ts) + ps.ReturnCode = 0 + ps.Streams.stderr = "gold/aaa.gold" + ps.Streams.stdout.Content = Testers.ContainsExpression("etagold", "expected etagold") + tr.StillRunningAfter = ts + + # 5 Test - Preload slice 1 etagnew-1 + tr = urtest.AddTestRun("Preload slice etagnew-1") + ps = tr.Processes.Default + tr.MakeCurlCommand(curl_and_args + ' http://crr/reference -r 3-5 -H "uuid: etagnew-1"', ts=ts) + ps.ReturnCode = 0 + ps.Streams.stderr = "gold/bb.gold" + ps.Streams.stdout.Content = Testers.ContainsExpression("etagnew", "expected etagnew") + tr.StillRunningAfter = ts + + # 6 Test - Request reference slice via slice plugin, with instructions to + # fetch new 2nd slice -- this will send the old header, but abort and + # refetch it + tr = urtest.AddTestRun("Request 2nd slice (expect abort)") + ps = tr.Processes.Default + tr.MakeCurlCommand(curl_and_args + ' http://slice/reference -r 3- -H "uuid: etagnew-0" -w "SENT: \'%{size_download}\'"', ts=ts) + # ps.ReturnCode = 0 # curl will fail here + ps.Streams.stdout.Content = Testers.ContainsExpression("etagold", "expected etagold") + ps.Streams.stdout.Content += Testers.ContainsExpression("SENT: '0'", "expected empty payload") + tr.StillRunningAfter = ts + + # 7 Test - Request full healed asset via slice plugin + tr = urtest.AddTestRun("Request full healed slice") + ps = tr.Processes.Default + tr.MakeCurlCommand(curl_and_args + ' http://slice/reference', ts=ts) + ps.ReturnCode = 0 + ps.Streams.stderr.Content = Testers.ContainsExpression("bbbbb", "expected bbbbb content") + ps.Streams.stdout.Content = Testers.ContainsExpression("etagnew", "expected etagnew") + tr.StillRunningAfter = ts + + # Request results in 200, not 206 (server not support range requests) + + req_header_200 = { + "headers": "GET /code200 HTTP/1.1\r\n" + "Host: www.example.com\r\n" + "uuid: code200\r\n" + "Range: bytes=3-5\r\n" + "\r\n", + "timestamp": "1469733493.993", + "body": "", + } + + res_header_200 = { + "headers": "HTTP/1.1 200 OK\r\n" + "Cache-Control: max-age=5000\r\n" + "Connection: close\r\n" + 'Etag: "etag"\r\n' + + "\r\n", + "timestamp": "1469733493.993", + "body": "ccccc" + } + + server.addResponse("sessionlog.json", req_header_200, res_header_200) + + # 8 test - Request through slice but get a 200 back + tr = urtest.AddTestRun("Request gets a 200") + ps = tr.Processes.Default + tr.MakeCurlCommand(curl_and_args + ' http://slice/code200 -r 3-5 -H "uuid: code200"', ts=ts) + ps.ReturnCode = 0 + ps.Streams.stderr.Content = Testers.ContainsExpression("ccccc", "expected full ccccc content") + ps.Streams.stdout.Content = Testers.ContainsExpression("200 OK", "expected 200") + tr.StillRunningAfter = ts + + # Test for asset gone + + # Preload + req_header_assetgone0 = { + "headers": + "GET /assetgone HTTP/1.1\r\n" + "Host: www.example.com\r\n" + "uuid: assetgone-0\r\n" + "Range: bytes=0-2\r\n" + "\r\n", + "timestamp": "1469733493.993", + "body": "", + } + + res_header_assetgone0 = { + "headers": + "HTTP/1.1 206 Partial Content\r\n" + "Accept-Ranges: bytes\r\n" + "Cache-Control: max-age=5000\r\n" + + "Connection: close\r\n" + "Content-Range: bytes 0-2/5\r\n" + 'Etag: "etag"\r\n' + "\r\n", + "timestamp": "1469733493.993", + "body": "aaa" + } + + server.addResponse("sessionlog.json", req_header_assetgone0, res_header_assetgone0) + + # 9 test - Preload reference slice + tr = urtest.AddTestRun("Preload reference assetgone-0") + ps = tr.Processes.Default + tr.MakeCurlCommand(curl_and_args + ' http://slice/assetgone -r 0-2 -H "uuid: assetgone-0"', ts=ts) + ps.ReturnCode = 0 + ps.Streams.stderr = "gold/aaa.gold" + ps.Streams.stdout.Content = Testers.ContainsExpression("etag", "expected etag") + tr.StillRunningAfter = ts + + # 10 test - Fetch full asset, 2nd slice should trigger 404 response + tr = urtest.AddTestRun("Fetch full asset") + ps = tr.Processes.Default + tr.MakeCurlCommand(curl_and_args + ' http://slice/assetgone', ts=ts) + # ps.ReturnCode = 0 # curl will return non zero + ps.Streams.stderr = "gold/aaa.gold" + ps.Streams.stdout.Content = Testers.ContainsExpression("etag", "expected etag") + ps.Streams.stdout.Content += Testers.ContainsExpression("Content-Length: 5", "expected header of content-length 5") + tr.StillRunningAfter = ts + + # 11 test - Fetch full asset again, full blown 404 + tr = urtest.AddTestRun("Fetch full asset, 404") + ps = tr.Processes.Default + tr.MakeCurlCommand(curl_and_args + ' http://slice/assetgone', ts=ts) + # ps.ReturnCode = 0 # curl will return non zero + ps.Streams.stdout.Content = Testers.ContainsExpression("404 Not Found", "Expected 404") + tr.StillRunningAfter = ts + + ## custom headers + + # Test case: 2nd slice out of date (refetch and continue) + + req_header_custom_2ndold1 = { + "headers": + "GET /second-custom HTTP/1.1\r\n" + "Host: www.example.com\r\n" + "uuid: etagold-custom-1\r\n" + "Range: bytes=3-5\r\n" + "\r\n", + "timestamp": "1469733493.993", + "body": "", + } + + res_header_custom_2ndold1 = { + "headers": + "HTTP/1.1 206 Partial Content\r\n" + "Accept-Ranges: bytes\r\n" + "Cache-Control: max-age=5000\r\n" + + "Connection: close\r\n" + "Content-Range: bytes 3-4/5\r\n" + 'Etag: "etagold-custom"\r\n' + "\r\n", + "timestamp": "1469733493.993", + "body": "aa" + } + + server.addResponse("sessionlog.json", req_header_custom_2ndold1, res_header_custom_2ndold1) + + req_header_custom_2ndnew0 = { + "headers": + "GET /second-custom HTTP/1.1\r\n" + "Host: www.example.com\r\n" + "uuid: etagnew-custom-0\r\n" + "Range: bytes=0-2\r\n" + "\r\n", + "timestamp": "1469733493.993", + "body": "", + } + + res_header_custom_2ndnew0 = { + "headers": + "HTTP/1.1 206 Partial Content\r\n" + "Accept-Ranges: bytes\r\n" + "Cache-Control: max-age=5000\r\n" + + "Connection: close\r\n" + "Content-Range: bytes 0-2/5\r\n" + 'Etag: "etagnew-custom"\r\n' + "\r\n", + "timestamp": "1469733493.993", + "body": "bbb" + } + + server.addResponse("sessionlog.json", req_header_custom_2ndnew0, res_header_custom_2ndnew0) + + req_header_custom_2ndnew1 = { + "headers": + "GET /second-custom HTTP/1.1\r\n" + "Host: www.example.com\r\n" + "uuid: etagnew-custom-1\r\n" + "Range: bytes=3-5\r\n" + "\r\n", + "timestamp": "1469733493.993", + "body": "", + } + + res_header_custom_2ndnew1 = { + "headers": + "HTTP/1.1 206 Partial Content\r\n" + "Accept-Ranges: bytes\r\n" + "Cache-Control: max-age=5000\r\n" + + "Connection: close\r\n" + "Content-Range: bytes 3-4/5\r\n" + 'Etag: "etagnew-custom"\r\n' + "\r\n", + "timestamp": "1469733493.993", + "body": "bb" + } + + server.addResponse("sessionlog.json", req_header_custom_2ndnew1, res_header_custom_2ndnew1) + + edt = datetime.datetime.fromtimestamp(time.time() + 100) + edate = to_httpdate(edt) + + # 12 Test - Preload reference etagold-1 + tr = urtest.AddTestRun("Preload slice etagold-custom-1") + ps = tr.Processes.Default + tr.MakeCurlCommand( + curl_and_args + f' http://crrhdr/second-custom -r 3-5 -H "uuid: etagold-custom-1" -H "crr-foo: {edate}"', ts=ts) + ps.ReturnCode = 0 + ps.Streams.stderr = "gold/aa.gold" + ps.Streams.stdout.Content = Testers.ContainsExpression("etagold-custom", "expected etagold-custom") + tr.StillRunningAfter = ts + + # 13 Test - Request second slice via slice plugin, with instructions to fetch new 2nd slice + tr = urtest.AddTestRun("Request 2nd slice (expect refetch)") + ps = tr.Processes.Default + tr.MakeCurlCommand(curl_and_args + ' http://slicehdr/second-custom -r 3- -H "uuid: etagnew-custom-1"', ts=ts) + ps.ReturnCode = 0 + ps.Streams.stderr = "gold/bb.gold" + ps.Streams.stdout.Content = Testers.ContainsExpression("etagnew-custom", "expected etagnew-custom") + tr.StillRunningAfter = ts + + # Over riding the built in ERROR check since we expect to see logSliceErrors + ts.Disk.diags_log.Content = Testers.ContainsExpression("logSliceError", "logSliceErrors generated") + urtest.execute() diff --git a/tests/uranium_tests/pluginTest/slice/test_slice_stale_generation.py b/tests/uranium_tests/pluginTest/slice/test_slice_stale_generation.py new file mode 100644 index 00000000000..264b959474b --- /dev/null +++ b/tests/uranium_tests/pluginTest/slice/test_slice_stale_generation.py @@ -0,0 +1,345 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_slice_stale_generation(urtest: UraniumTest) -> None: + """Verify slice serves a stale object identity after the origin object changes.""" + + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + urtest.Summary = __doc__ + + urtest.SkipUnless( + Condition.PluginExists('slice.so'), + Condition.PluginExists('cache_range_requests.so'), + Condition.PluginExists('xdebug.so'), + ) + urtest.ContinueOnFail = False + + class SliceHierarchyTest: + """Build the child/parent hierarchy the incident ran on. + + Both tiers load slice with the same block size, and both put + cache_range_requests behind it, as the affected property does. + + The parent only behaves as a slicing proxy for requests that arrive without + slice's skip header. The child stamps that header onto every block request it + issues (client.cc:66), so slice returns immediately on the parent + (slice.cc:48) and a child block request is handled there by + cache_range_requests alone: look up this exact Range, forward it on a miss, + store whatever 206 comes back. The parent therefore holds N independent + per-Range objects with no shared identity, and cannot notice, refuse or + reconcile a version mix. That is where the mixed set lived in the incident. + + A client hitting the parent directly is a different path: the parent does + slice that request, forms its own reference block and clamps against it. + """ + + _server_replay: str = 'replay/slice_stale_generation_server.replay.yaml' + _client_replay: str = 'replay/slice_stale_generation_client.replay.yaml' + + _block_bytes: int = 16 + + _origin_key_format: str = '--format "{url}{field.range}{field.uuid}"' + + def __init__(self, name: str) -> None: + """Declare the origin, the parent and the child. + + :param name: suffix distinguishing this hierarchy's processes. + """ + self._name = name + self._configure_dns() + self._configure_origin() + self._configure_parent() + self._configure_child() + + def _configure_dns(self) -> None: + """Configure a DNS server so neither tier consults resolv.conf.""" + self._dns = urtest.MakeDNServer(f'dns-{self._name}', default='127.0.0.1') + + def _configure_origin(self) -> None: + """Configure the origin. + + The server is keyed on the block's byte range and on the phase uuid that + slice propagates from the client request, so one replay file answers + every block request of every phase and can replace the object between + phases without holding any state. + """ + self._origin = urtest.MakeVerifierServerProcess( + f'origin-{self._name}', self._server_replay, other_args=self._origin_key_format) + + def _slice_remap(self, source: str, upstream: str) -> str: + """Build a remap rule carrying slice in front of cache_range_requests.""" + return ( + f'map {source} {upstream}' + f' @plugin=slice.so @pparam=--blockbytes-test={self._block_bytes}' + ' @plugin=cache_range_requests.so') + + def _records(self, ts: 'Process', debug: int) -> None: + """Apply the records.yaml settings common to both tiers.""" + ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': debug, + 'proxy.config.diags.debug.tags': 'slice|cache_range_requests', + 'proxy.config.dns.nameservers': f'127.0.0.1:{self._dns.Variables.Port}', + 'proxy.config.dns.resolv_conf': 'NULL', + 'proxy.config.http.parent_proxy.self_detect': 0, + }) + + def _configure_parent(self) -> None: + """Configure the parent, which the child's block requests reach.""" + self._parent = urtest.MakeATSProcess(f'ts-parent-{self._name}') + self._parent.Disk.remap_config.AddLine( + self._slice_remap('http://origin.test/', f'http://127.0.0.1:{self._origin.Variables.http_port}/')) + self._parent.Disk.plugin_config.AddLine('xdebug.so --enable=x-cache') + self._records(self._parent, debug=1) + + # slice is loaded on the parent, but every block request the child sends + # carries the skip header, so slice returns immediately and the request + # is handled by cache_range_requests alone. Plugin debug output lands in + # traffic.out, not diags.log. + self._parent.Disk.traffic_out.Content = Testers.ContainsExpression( + 'slice passing GET or HEAD request through to next plugin', + "The child's block requests should bypass the parent's slice.") + self._parent.Disk.traffic_out.Content += Testers.ExcludesExpression( + 'slice accepting and slicing', 'The parent should never slice a child block request.') + + def _make_child(self, label: str) -> 'Process': + """Create a child tier that slices and forwards to the parent.""" + ts = urtest.MakeATSProcess(f'ts-{label}-{self._name}') + ts.Disk.remap_config.AddLine(self._slice_remap('http://slice/', 'http://origin.test/')) + ts.Disk.parent_config.AddLine( + f'dest_domain=. parent=127.0.0.1:{self._parent.Variables.port}' + ' round_robin=consistent_hash go_direct=false') + ts.Disk.plugin_config.AddLine('xdebug.so --enable=x-cache') + self._records(ts, debug=1) + return ts + + def _configure_child(self) -> None: + """Configure the child, which slices and forwards to the parent.""" + self._child = self._make_child('child') + + def _start_hierarchy(self, tr: 'TestRun') -> None: + """Bring up origin, parent and child for the first TestRun.""" + tr.Processes.Default.StartBefore(self._dns) + tr.Processes.Default.StartBefore(self._origin) + tr.Processes.Default.StartBefore(self._parent) + tr.Processes.Default.StartBefore(self._child) + + def _replay_phase(self, tr: 'TestRun', phase: str, ts: 'Process' = None) -> 'Process': + """Replay the client transaction for one phase against a child.""" + ts = self._child if ts is None else ts + return tr.AddVerifierClientProcess( + f'client-{phase}-{self._name}', self._client_replay, http_ports=[ts.Variables.port], keys=phase) + + def _still_running(self, tr: 'TestRun') -> None: + """Assert both tiers survive the TestRun.""" + tr.StillRunningAfter = self._child + tr.StillRunningAfter = self._parent + + class SliceStaleGenerationTest(SliceHierarchyTest): + """Verify a cached reference block pins a stale object identity. + + The plugin takes the whole object length from the reference block's + Content-Range and clips the client range to it, so the cached reference + block, not the origin, defines the object's identity for every request:: + + server.cc handleFirstServerHeader: + data->m_contentlen = blockcr.m_length; + data->m_req_range.m_end = std::min(data->m_contentlen, data->m_req_range.m_end); + + Replacing the object under the same URL therefore forks every cache into one + that filled before the replacement and one that filled after, for as long as + the reference block stays fresh. On the stale side, ranges that exist in the + current object are answered against the stale length with the stale ETag: + clipped short, or refused with a 416. Neither path logs a block stitch error, + because handleNextServerHeader only complains when blocks disagree with each + other and here they are uniformly stale. + + Modelled on an incident where a versioned, year-cacheable object was replaced + in place. Two edges seven hours apart on either side of the replacement served + object lengths 4043309056 and 7031250004 for the same URL, the stale one + clipping a 64 MiB range request down to 16 MiB. + """ + + # The reference block the origin holds at the new generation for each phase + # that reads the cached object. The test asserts the origin never gets these. + _unreachable_keys = ('/objbytes=0-15clipped', '/objbytes=0-15unsatisfiable') + + def __init__(self) -> None: + """Declare the hierarchy and its assertions.""" + super().__init__('stale') + + for key in self._unreachable_keys: + self._origin.Streams.stdout += Testers.ExcludesExpression( + f'request with key {key}', 'The stale object should never be refetched after the origin object changed.') + + # Debug is enabled on the child, so Config::canLogError cannot suppress a + # block stitch error by pacing. The stale response is served with none. + self._child.Disk.diags_log.Content = Testers.ExcludesExpression( + 'logSliceError', 'The stale response should be served with no block stitch error.') + self._child.Disk.diags_log.Content += Testers.ExcludesExpression( + 'Mismatch/Bad block Content-Range', 'The stale blocks agree with each other, so nothing should mismatch.') + + def _fill_cache(self) -> None: + """Cache the whole object while the origin holds the first generation.""" + tr = urtest.AddTestRun('Cache the object while the origin holds the first generation') + self._start_hierarchy(tr) + self._replay_phase(tr, 'fill') + self._still_running(tr) + + def _verify_clipped_range(self) -> None: + """A range inside the current object is clipped to the stale length.""" + tr = urtest.AddTestRun('A range inside the current object is clipped to the stale length') + self._replay_phase(tr, 'clipped') + self._still_running(tr) + + def _verify_unsatisfiable_range(self) -> None: + """A range past the stale length is refused with a 416.""" + tr = urtest.AddTestRun('A range past the stale length is refused with a 416') + self._replay_phase(tr, 'unsatisfiable') + self._still_running(tr) + + def _verify_uncached_object(self) -> None: + """An object first fetched after the replacement is served correctly.""" + tr = urtest.AddTestRun('An object first fetched after the replacement is served correctly') + self._replay_phase(tr, 'control') + self._still_running(tr) + + def run(self) -> None: + """Configure the test runs.""" + self._fill_cache() + self._verify_clipped_range() + self._verify_unsatisfiable_range() + self._verify_uncached_object() + + class SliceMixedGenerationTest(SliceHierarchyTest): + """Verify the parent stores a version mix and the child cannot recover. + + The other failure mode from the same origin object replacement, and the one + the parent's per-Range cache makes possible. Only the reference block is + refetched after the replacement, so the parent ends up holding two blocks of + one object at two different generations, both fresh, with nothing to relate + them. It serves each on request without complaint. + + The child is the only tier that compares blocks, and only against its own + reference block. It forms the client response header from the reference block, + which is correct for the current object, and only then discovers that the + interior block belongs to the previous one:: + + server.cc handleNextServerHeader: + if (!blockcr.isValid() || blockcr.m_length != data->m_contentlen) { + logSliceError("Mismatch/Bad block Content-Range", data, header); + + The self heal refetches the reference block, which is already the newest one + the parent holds, so the same block comes back and the interior block still + disagrees with it. The second mismatch is where slice gives up. + + The upstream is aborted. Slice can abort but cannot evict, so the mixed pair + on the parent survives. The final TestRun proves where the damage actually + lives: a second child with a completely cold cache, pointed at the same + parent, fails identically. It never saw the previous generation; it simply + inherits the mix from the one place that holds it. That is the incident's + shape, where all 105 blocks hashed to a single parent and every one of the 32 + child nodes served the same broken object. + """ + + # The reference block is cached with a one second lifetime, so let it expire. + _expiry_wait: int = 2 + + def __init__(self) -> None: + """Declare the hierarchy and its assertions.""" + super().__init__('mixed') + self._cold_child = self._make_child('cold-child') + + # Unlike the stale case, the child does report this one: first the interior + # block against the reference block, then the refetch against the interior. + self._child.Disk.diags_log.Content = Testers.ContainsExpression( + 'Mismatch/Bad block Content-Range.*blk_range="16-31".*etag_got="%22v1%22"', + 'The interior block should disagree with the reference block.') + self._child.Disk.diags_log.Content += Testers.ContainsExpression( + 'Mismatch/Bad block Content-Range.*blk_range="0-15".*etag_got="%22v2%22"', + 'The refetched reference block should disagree in turn, leaving no way out.') + + # The parent never compares blocks, so it never complains about the mix + # it is storing and serving to the child. + self._parent.Disk.diags_log.Content += Testers.ExcludesExpression( + 'logSliceError', 'The parent should not notice the version mix it holds.') + self._parent.Disk.diags_log.Content += Testers.ExcludesExpression( + 'Mismatch/Bad block Content-Range', 'The parent should not compare blocks at all.') + + def _fill_interior_block(self) -> None: + """Cache an interior block at the first generation, on both tiers.""" + tr = urtest.AddTestRun('Cache an interior block at the first generation') + self._start_hierarchy(tr) + self._replay_phase(tr, 'fill-interior') + self._still_running(tr) + + def _verify_aborted_response(self) -> None: + """The reference block moves on and the transaction cannot be completed.""" + tr = urtest.AddTestRun('A mixed generation object cannot be delivered and the request fails') + client = self._replay_phase(tr, 'mixed') + # Let the reference block go stale so that only it is revalidated. + tr.Processes.Default.Command = f'sleep {self._expiry_wait}; ' + tr.Processes.Default.Command + # Slice aborts the transaction, so the client never reads a response at + # all: not a short body, no response header. verifier-client exits 1. + client.ReturnCode = 1 + client.Streams.stdout += Testers.ContainsExpression( + 'Failed to find a well-formed, completed HTTP response: PARSE_INCOMPLETE', + 'The client should not receive a parsable response.') + client.Streams.stdout += Testers.ContainsExpression( + 'Failed HTTP/1 transaction with key: mixed', 'The transaction should fail.') + self._still_running(tr) + + def _verify_mix_is_on_the_parent(self) -> None: + """A cold child fails identically, because the mix lives on the parent.""" + tr = urtest.AddTestRun('A second child with a cold cache inherits the mix from the parent') + tr.Processes.Default.StartBefore(self._cold_child) + client = self._replay_phase(tr, 'cold-child', ts=self._cold_child) + client.ReturnCode = 1 + client.Streams.stdout += Testers.ContainsExpression( + 'Failed HTTP/1 transaction with key: cold-child', + 'A node that never saw the old generation should fail the same way.') + self._cold_child.Disk.diags_log.Content = Testers.ContainsExpression( + 'Mismatch/Bad block Content-Range', 'The cold child should hit the same mismatch.') + tr.StillRunningAfter = self._cold_child + self._still_running(tr) + + def run(self) -> None: + """Configure the test runs.""" + self._fill_interior_block() + self._verify_aborted_response() + self._verify_mix_is_on_the_parent() + + SliceStaleGenerationTest().run() + SliceMixedGenerationTest().run() + urtest.execute() diff --git a/tests/gold_tests/pluginTest/sslheaders/observer.py b/tests/uranium_tests/pluginTest/sslheaders/observer.py similarity index 100% rename from tests/gold_tests/pluginTest/sslheaders/observer.py rename to tests/uranium_tests/pluginTest/sslheaders/observer.py diff --git a/tests/gold_tests/pluginTest/sslheaders/sslheaders.gold b/tests/uranium_tests/pluginTest/sslheaders/sslheaders.gold similarity index 100% rename from tests/gold_tests/pluginTest/sslheaders/sslheaders.gold rename to tests/uranium_tests/pluginTest/sslheaders/sslheaders.gold diff --git a/tests/uranium_tests/pluginTest/sslheaders/test_sslheaders.py b/tests/uranium_tests/pluginTest/sslheaders/test_sslheaders.py new file mode 100644 index 00000000000..cbfb4e19989 --- /dev/null +++ b/tests/uranium_tests/pluginTest/sslheaders/test_sslheaders.py @@ -0,0 +1,99 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_sslheaders(urtest: UraniumTest) -> None: + ''' + Test the sslheaders plugin. + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + urtest.Summary = ''' + Test sslheaders plugin. + ''' + + urtest.SkipIf(Condition.CurlUsingUnixDomainSocket()) + urtest.SkipUnless( + Condition.PluginExists('sslheaders.so'), + Condition.HasCurlFeature('http2'), + ) + + urtest.Disk.File('sslheaders.log').Content = 'sslheaders.gold' + + server = urtest.MakeOriginServer("server", options={'--load': urtest.TestDirectory + '/observer.py'}) + + request_header = {"headers": "GET / HTTP/1.1\r\nHost: doesnotmatter\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + server.addResponse("sessionlog.json", request_header, response_header) + + # Disable the cache to make sure each request is forwarded to the origin + # server. + ts = urtest.MakeATSProcess("ts", enable_tls=True, enable_cache=False) + + ts.addDefaultSSLFiles() + # ts.addSSLfile("ssl/signer.pem") + + ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 0, + 'proxy.config.diags.debug.tags': 'http', + 'proxy.config.proxy_name': 'Poxy_Proxy', # This will be the server name. + 'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir), + 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts.Variables.SSLDir), + 'proxy.config.http.server_ports': + ( + 'ipv4:{0} ipv4:{1}:proto=http2;http:ssl ipv6:{0} ipv6:{1}:proto=http2;http:ssl'.format( + ts.Variables.port, ts.Variables.ssl_port)), + }) + + ts.Disk.ssl_multicert_yaml.AddLines( + """ + ssl_multicert: + - dest_ip: "*" + ssl_cert_name: server.pem + ssl_key_name: server.key + """.split("\n")) + + ts.Disk.remap_config.AddLine('map http://bar.com http://127.0.0.1:{0}'.format(server.Variables.Port)) + ts.Disk.remap_config.AddLine('map https://bar.com http://127.0.0.1:{0}'.format(server.Variables.Port)) + + ts.Disk.plugin_config.AddLine('sslheaders.so SSL-Client-ID=client.subject') + + tr = urtest.AddTestRun() + tr.Processes.Default.StartBefore(server) + tr.Processes.Default.StartBefore(urtest.Processes.ts) + tr.MakeCurlCommand( + '-H "SSL-Client-ID: My Fake Client ID" --verbose --ipv4 --insecure --header "Host: bar.com"' + + ' https://localhost:{}'.format(ts.Variables.ssl_port), + ts=ts) + tr.Processes.Default.ReturnCode = 0 + urtest.execute() diff --git a/tests/gold_tests/pluginTest/stale_response/stale_response_max_memory.replay.yaml b/tests/uranium_tests/pluginTest/stale_response/stale_response_max_memory.replay.yaml similarity index 100% rename from tests/gold_tests/pluginTest/stale_response/stale_response_max_memory.replay.yaml rename to tests/uranium_tests/pluginTest/stale_response/stale_response_max_memory.replay.yaml diff --git a/tests/gold_tests/pluginTest/stale_response/stale_response_no_default.replay.yaml b/tests/uranium_tests/pluginTest/stale_response/stale_response_no_default.replay.yaml similarity index 100% rename from tests/gold_tests/pluginTest/stale_response/stale_response_no_default.replay.yaml rename to tests/uranium_tests/pluginTest/stale_response/stale_response_no_default.replay.yaml diff --git a/tests/gold_tests/pluginTest/stale_response/stale_response_with_defaults.replay.yaml b/tests/uranium_tests/pluginTest/stale_response/stale_response_with_defaults.replay.yaml similarity index 100% rename from tests/gold_tests/pluginTest/stale_response/stale_response_with_defaults.replay.yaml rename to tests/uranium_tests/pluginTest/stale_response/stale_response_with_defaults.replay.yaml diff --git a/tests/gold_tests/pluginTest/stale_response/stale_response_with_force_sie.replay.yaml b/tests/uranium_tests/pluginTest/stale_response/stale_response_with_force_sie.replay.yaml similarity index 100% rename from tests/gold_tests/pluginTest/stale_response/stale_response_with_force_sie.replay.yaml rename to tests/uranium_tests/pluginTest/stale_response/stale_response_with_force_sie.replay.yaml diff --git a/tests/gold_tests/pluginTest/stale_response/stale_response_with_force_swr.replay.yaml b/tests/uranium_tests/pluginTest/stale_response/stale_response_with_force_swr.replay.yaml similarity index 100% rename from tests/gold_tests/pluginTest/stale_response/stale_response_with_force_swr.replay.yaml rename to tests/uranium_tests/pluginTest/stale_response/stale_response_with_force_swr.replay.yaml diff --git a/tests/uranium_tests/pluginTest/stale_response/test_stale_response.py b/tests/uranium_tests/pluginTest/stale_response/test_stale_response.py new file mode 100644 index 00000000000..4b1396d07ba --- /dev/null +++ b/tests/uranium_tests/pluginTest/stale_response/test_stale_response.py @@ -0,0 +1,206 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_stale_response(urtest: UraniumTest) -> None: + ''' + Verify correct stale_response plugin behavior + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + from enum import Enum + import os + + urtest.Summary = ''' + Verify correct stale_response plugin behavior + ''' + + urtest.SkipUnless(Condition.PluginExists('stale_response.so'),) + + class OptionType(Enum): + """Describe what options to pass to the plugin.""" + + NONE = 0 + DEFAULT_DIRECTIVES = 1 + FORCE_SWR = 2 + FORCE_SIE = 3 + MAX_MEMORY_USAGE = 4 + + assert len({option.value for option in OptionType.__members__.values()}) == len(OptionType.__members__) + + class TestStaleResponse: + """Verify correct stale_response.so plugin behavior.""" + + _replay_file: str + _ts_counter: int = 0 + _server_counter: int = 0 + _client_counter: int = 0 + + def __init__(self, option_type: OptionType, is_global: bool) -> None: + """Initialize the test. + + :param option_type: The type of options to pass to the stale_response plugin. + """ + self._option_type = option_type + self._is_global = is_global + + plugin_type_description = "global" if is_global else "per-remap" + if option_type == OptionType.NONE: + self._replay_file = "stale_response_no_default.replay.yaml" + option_description = f"no stale_response plugin options: {plugin_type_description}" + elif option_type == OptionType.DEFAULT_DIRECTIVES: + self._replay_file = "stale_response_with_defaults.replay.yaml" + option_description = f"--stale-while-revalidate-default 30 --stale-if-error-default 30: {plugin_type_description}" + elif option_type == OptionType.FORCE_SWR: + self._replay_file = "stale_response_with_force_swr.replay.yaml" + option_description = f"--force-stale-while-revalidate 30: {plugin_type_description}" + elif option_type == OptionType.FORCE_SIE: + self._replay_file = "stale_response_with_force_sie.replay.yaml" + option_description = f"--force-stale-if-error 30: {plugin_type_description}" + elif option_type == OptionType.MAX_MEMORY_USAGE: + self._replay_file = "stale_response_max_memory.replay.yaml" + option_description = f"--max-memory-usage 256: {plugin_type_description}" + + tr = urtest.AddTestRun(f"stale_response.so Options: {option_description}") + + self.setupOriginServer(tr) + self.setupTS() + self.setupClient(tr) + self.verify_plugin_log() + + def setupOriginServer(self, tr: 'TestRun') -> None: + """ Configure the server. + + :param tr: The test run to add the server to. + """ + name = f'server_{TestStaleResponse._server_counter}' + TestStaleResponse._server_counter += 1 + self._server = tr.AddVerifierServerProcess(name, self._replay_file) + + def setupTS(self) -> None: + """Configure the traffic server. + + ATS is not configured for a TestRun because we need it to last longer to + ensure that the plugin's log is created. + """ + name = f'ts_{TestStaleResponse._ts_counter}' + TestStaleResponse._ts_counter += 1 + ts = urtest.MakeATSProcess(name) + self._ts = ts + + log_path = os.path.join(ts.Variables.LOGDIR, 'stale_responses.log') + ts.Disk.File(log_path, id='stale_responses_log') + + remap_plugin_config = "" + if self._is_global: + plugin_command = 'stale_response.so --log-all --log-filename stale_responses' + if self._option_type == OptionType.DEFAULT_DIRECTIVES: + plugin_command += ' --stale-while-revalidate-default 30 --stale-if-error-default 30' + elif self._option_type == OptionType.FORCE_SWR: + plugin_command += ' --force-stale-while-revalidate 30' + elif self._option_type == OptionType.FORCE_SIE: + plugin_command += ' --force-stale-if-error 30' + elif self._option_type == OptionType.MAX_MEMORY_USAGE: + plugin_command += ' --max-memory-usage 256' + ts.Disk.plugin_config.AddLine(plugin_command) + else: + # Configure the stale_response plugin for the remap rule. + remap_plugin_config = "@plugin=stale_response.so @pparam=--log-all @pparam=--log-filename @pparam=stale_responses" + if self._option_type == OptionType.DEFAULT_DIRECTIVES: + remap_plugin_config += ' @pparam=--stale-while-revalidate-default @pparam=30 @pparam=--stale-if-error-default @pparam=30' + elif self._option_type == OptionType.FORCE_SWR: + remap_plugin_config += ' @pparam=--force-stale-while-revalidate @pparam=30' + elif self._option_type == OptionType.FORCE_SIE: + remap_plugin_config += ' @pparam=--force-stale-if-error @pparam=30' + elif self._option_type == OptionType.MAX_MEMORY_USAGE: + remap_plugin_config += ' @pparam=--max-memory-usage @pparam=256' + + ts.Disk.records_config.update( + { + "proxy.config.diags.debug.enabled": 1, + "proxy.config.diags.debug.tags": "http|stale_response", + "proxy.config.http.server_session_sharing.pool": "global", + # Turn off negative revalidating so that we can test stale-if-error. + "proxy.config.http.negative_revalidating_enabled": 0, + # Keep the active log filename available for the final content check if the test spans UTC midnight. + "proxy.config.log.rolling_enabled": 0, + }) + ts.Disk.remap_config.AddLine(f"map / http://127.0.0.1:{self._server.Variables.http_port}/ {remap_plugin_config}") + + def setupClient(self, tr: 'TestRun') -> None: + + name = f'client_{TestStaleResponse._client_counter}' + TestStaleResponse._client_counter += 1 + p = tr.AddVerifierClientProcess(name, self._replay_file, http_ports=[self._ts.Variables.port]) + p.StartBefore(self._server) + p.StartBefore(self._ts) + p.StillRunningAfter = self._ts + + def verify_plugin_log(self) -> None: + """Verify the contents of the stale_response plugin log.""" + if self._option_type == OptionType.MAX_MEMORY_USAGE: + diagnostic = "response exceeded memory limit; sending stale data" + urtest.AddAwaitFileContainsTestRun( + "Verify stale_response max-memory diagnostic", self._ts.Disk.traffic_out.Name, diagnostic) + self._ts.Disk.traffic_out.Content += Testers.ContainsExpression( + diagnostic, "Verify max-memory stale-if-error fallback is logged") + return + + swr_log_pattern = "stale-while-revalidate:.*stale.jpeg" + sie_log_pattern = "stale-if-error:.*error.jpeg" + + def expect_log_entry(pattern: str, description: str) -> None: + tr = urtest.AddAwaitFileContainsTestRun(f"Await {description}", self._ts.Disk.stale_responses_log.AbsPath, pattern) + tr.StillRunningBefore = self._ts + tr.StillRunningAfter = self._ts + self._ts.Disk.stale_responses_log.Content += Testers.ContainsExpression(pattern, f"Verify {description}") + + if self._option_type == OptionType.FORCE_SWR: + expect_log_entry(swr_log_pattern, "stale-while-revalidate directive is logged") + elif self._option_type == OptionType.FORCE_SIE: + expect_log_entry(sie_log_pattern, "stale-if-error directive is logged") + else: + expect_log_entry(swr_log_pattern, "stale-while-revalidate directive is logged") + expect_log_entry(sie_log_pattern, "stale-if-error directive is logged") + + TestStaleResponse(OptionType.NONE, is_global=True) + TestStaleResponse(OptionType.DEFAULT_DIRECTIVES, is_global=True) + TestStaleResponse(OptionType.FORCE_SWR, is_global=True) + TestStaleResponse(OptionType.FORCE_SIE, is_global=True) + TestStaleResponse(OptionType.MAX_MEMORY_USAGE, is_global=True) + + TestStaleResponse(OptionType.NONE, is_global=False) + TestStaleResponse(OptionType.DEFAULT_DIRECTIVES, is_global=False) + TestStaleResponse(OptionType.FORCE_SWR, is_global=False) + TestStaleResponse(OptionType.FORCE_SIE, is_global=False) + TestStaleResponse(OptionType.MAX_MEMORY_USAGE, is_global=False) + urtest.execute() diff --git a/tests/gold_tests/pluginTest/statichit/empty.txt b/tests/uranium_tests/pluginTest/statichit/empty.txt similarity index 100% rename from tests/gold_tests/pluginTest/statichit/empty.txt rename to tests/uranium_tests/pluginTest/statichit/empty.txt diff --git a/tests/gold_tests/pluginTest/statichit/small_body.txt b/tests/uranium_tests/pluginTest/statichit/small_body.txt similarity index 100% rename from tests/gold_tests/pluginTest/statichit/small_body.txt rename to tests/uranium_tests/pluginTest/statichit/small_body.txt diff --git a/tests/gold_tests/pluginTest/statichit/statichit.replay.yaml b/tests/uranium_tests/pluginTest/statichit/statichit.replay.yaml similarity index 100% rename from tests/gold_tests/pluginTest/statichit/statichit.replay.yaml rename to tests/uranium_tests/pluginTest/statichit/statichit.replay.yaml diff --git a/tests/gold_tests/pluginTest/statichit/story_16.json b/tests/uranium_tests/pluginTest/statichit/story_16.json similarity index 100% rename from tests/gold_tests/pluginTest/statichit/story_16.json rename to tests/uranium_tests/pluginTest/statichit/story_16.json diff --git a/tests/uranium_tests/pluginTest/statichit/test_statichit.py b/tests/uranium_tests/pluginTest/statichit/test_statichit.py new file mode 100644 index 00000000000..5a2c0867b07 --- /dev/null +++ b/tests/uranium_tests/pluginTest/statichit/test_statichit.py @@ -0,0 +1,133 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_statichit(urtest: UraniumTest) -> None: + ''' + Verify correct statichit plugin behavior + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import os + + urtest.Summary = ''' + Verify correct statichit plugin behavior + ''' + + urtest.SkipUnless(Condition.PluginExists('statichit.so'),) + + class RemapData: + """Data for each remap rule""" + + @staticmethod + def setup(): + # Path relative to config directory. The statichit directory will be created, and small_body.txt copied to it, + # by the 2nd (dummy) test run. + RemapData.small_body_path = os.path.join('statichit', 'small_body.txt') + + urtest.Setup.Copy('story_16.json', urtest.RunDirectory) + RemapData.json_body_path = os.path.join(urtest.RunDirectory, 'story_16.json') + + urtest.Setup.Copy('empty.txt', urtest.RunDirectory) + RemapData.empty_body_path = os.path.join(urtest.RunDirectory, 'empty.txt') + + # This directory will be created, and a file copied to it, by the 1st (dummy) test run. + RemapData.dir_path = os.path.join(urtest.RunDirectory, 'body_dir') + + def __init__(self, from_url, *args) -> None: + self.remap_from = from_url + self.plugin_args = args + self.body = RemapData.small_body_path + + def bad_body(self): + self.body = 'invalid' + return self + + def json_body(self): + self.body = RemapData.json_body_path + return self + + def empty_body(self): + self.body = RemapData.empty_body_path + return self + + def dir(self): + self.body = RemapData.dir_path + return self + + RemapData.setup() + + remap_data = [ + RemapData('http://fqdn1'), + RemapData('http://fqdn2', '--max-age=123'), + RemapData('http://fqdn3', '--success-code=200', '--disable-exact', '--failure-code=222'), + RemapData('http://fqdn4', '--success-code=200', '--failure-code=412').bad_body(), + RemapData('http://fqdn5', '--success-code=200', '--failure-code=412', "--mime-type=application/json").json_body(), + RemapData('http://fqdn6', '--success-code=200', '--failure-code=222').empty_body(), + RemapData('http://fqdn7', '--success-code', '200', '--disable-exact', '--failure-code', '222'), + RemapData('http://fqdn8', '-s', '200', '-d', '-c', '222'), + RemapData('http://fqdn9').dir(), + ] + + ts = urtest.MakeATSProcess('ts') + + ts.Disk.records_config.update({ + "proxy.config.diags.debug.enabled": 1, + "proxy.config.diags.debug.tags": "http|statichit", + }) + + for d in remap_data: + arg_str = '' + for arg in d.plugin_args: + arg_str += ' @pparam=' + arg + ts.Disk.remap_config.AddLine( + f'map {d.remap_from} http://127.0.0.1/ @plugin=statichit.so @pparam=--file-path={d.body}{arg_str}') + + # Dummy test run for copying a file. + tr = urtest.AddTestRun() + p = tr.Processes.Default + p.Command = ( + f'mkdir {urtest.RunDirectory}/body_dir ; ' + f'cp {urtest.TestDirectory}/small_body.txt {urtest.RunDirectory}/body_dir/.') + + # Dummy test run for copying a file. + tr = urtest.AddTestRun() + p = tr.Processes.Default + p.Command = ( + f'mkdir {ts.Variables.CONFIGDIR}/statichit ; ' + + f'cp {urtest.TestDirectory}/small_body.txt {ts.Variables.CONFIGDIR}/statichit/.') + p.StartBefore(ts) + p.StillRunningAfter = ts + + tr = urtest.AddTestRun() + p = tr.AddVerifierClientProcess('client', 'statichit.replay.yaml', http_ports=[ts.Variables.port]) + p.StillRunningAfter = ts + urtest.execute() diff --git a/tests/gold_tests/pluginTest/stats_over_http/gold/stats_over_http_csv_stderr.gold b/tests/uranium_tests/pluginTest/stats_over_http/gold/stats_over_http_csv_stderr.gold similarity index 100% rename from tests/gold_tests/pluginTest/stats_over_http/gold/stats_over_http_csv_stderr.gold rename to tests/uranium_tests/pluginTest/stats_over_http/gold/stats_over_http_csv_stderr.gold diff --git a/tests/gold_tests/pluginTest/stats_over_http/gold/stats_over_http_json_stderr.gold b/tests/uranium_tests/pluginTest/stats_over_http/gold/stats_over_http_json_stderr.gold similarity index 100% rename from tests/gold_tests/pluginTest/stats_over_http/gold/stats_over_http_json_stderr.gold rename to tests/uranium_tests/pluginTest/stats_over_http/gold/stats_over_http_json_stderr.gold diff --git a/tests/gold_tests/pluginTest/stats_over_http/gold/stats_over_http_prometheus_stderr.gold b/tests/uranium_tests/pluginTest/stats_over_http/gold/stats_over_http_prometheus_stderr.gold similarity index 100% rename from tests/gold_tests/pluginTest/stats_over_http/gold/stats_over_http_prometheus_stderr.gold rename to tests/uranium_tests/pluginTest/stats_over_http/gold/stats_over_http_prometheus_stderr.gold diff --git a/tests/gold_tests/pluginTest/stats_over_http/gold/stats_over_http_prometheus_v2_accept_stderr.gold b/tests/uranium_tests/pluginTest/stats_over_http/gold/stats_over_http_prometheus_v2_accept_stderr.gold similarity index 100% rename from tests/gold_tests/pluginTest/stats_over_http/gold/stats_over_http_prometheus_v2_accept_stderr.gold rename to tests/uranium_tests/pluginTest/stats_over_http/gold/stats_over_http_prometheus_v2_accept_stderr.gold diff --git a/tests/gold_tests/pluginTest/stats_over_http/gold/stats_over_http_prometheus_v2_stderr.gold b/tests/uranium_tests/pluginTest/stats_over_http/gold/stats_over_http_prometheus_v2_stderr.gold similarity index 100% rename from tests/gold_tests/pluginTest/stats_over_http/gold/stats_over_http_prometheus_v2_stderr.gold rename to tests/uranium_tests/pluginTest/stats_over_http/gold/stats_over_http_prometheus_v2_stderr.gold diff --git a/tests/gold_tests/pluginTest/stats_over_http/prometheus_stats_ingester.py b/tests/uranium_tests/pluginTest/stats_over_http/prometheus_stats_ingester.py similarity index 100% rename from tests/gold_tests/pluginTest/stats_over_http/prometheus_stats_ingester.py rename to tests/uranium_tests/pluginTest/stats_over_http/prometheus_stats_ingester.py diff --git a/tests/uranium_tests/pluginTest/stats_over_http/test_stats_over_http.py b/tests/uranium_tests/pluginTest/stats_over_http/test_stats_over_http.py new file mode 100644 index 00000000000..276f32ae64d --- /dev/null +++ b/tests/uranium_tests/pluginTest/stats_over_http/test_stats_over_http.py @@ -0,0 +1,422 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_stats_over_http(urtest: UraniumTest) -> None: + ''' + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + from enum import Enum + import re + import sys + + urtest.Summary = 'Exercise stats-over-http plugin' + urtest.SkipUnless(Condition.PluginExists('stats_over_http.so')) + urtest.ContinueOnFail = True + + # Skip until plugin supports pp or uds path + urtest.SkipIf(Condition.CurlUsingUnixDomainSocket()) + + class StatsOverHttpPluginTest: + """ + https://docs.trafficserver.apache.org/en/latest/admin-guide/plugins/stats_over_http.en.html + """ + + class State(Enum): + """ + State of process + """ + INIT = 0 + RUNNING = 1 + + def __init__(self): + self.state = self.State.INIT + self.__setupTS() + + def __setupTS(self): + self.ts = urtest.MakeATSProcess("ts") + + self.ts.Disk.plugin_config.AddLine('stats_over_http.so _stats') + + self.ts.Disk.records_config.update( + { + "proxy.config.http.server_ports": f"{self.ts.Variables.port} {self.ts.Variables.uds_path}", + "proxy.config.diags.debug.enabled": 1, + "proxy.config.diags.debug.tags": "stats_over_http" + }) + + def __checkProcessBefore(self, tr): + if self.state == self.State.RUNNING: + tr.StillRunningBefore = self.ts + else: + tr.Processes.Default.StartBefore(self.ts) + self.state = self.State.RUNNING + + def __checkProcessAfter(self, tr): + assert (self.state == self.State.RUNNING) + tr.StillRunningAfter = self.ts + + def __containsLiteral(self, p: "Test.Process", expression: str, description: str): + p.Streams.stdout += Testers.ContainsExpression(re.escape(expression), description) + + def __excludesLiteral(self, p: "Test.Process", expression: str, description: str): + p.Streams.stdout += Testers.ExcludesExpression(re.escape(expression), description) + + def __checkPrometheusMetrics(self, p: 'Test.Process', from_prometheus: bool): + '''Check the Prometheus metrics output. + :param p: The process whose output to check. + :param from_prometheus: Whether the output is from Prometheus. Otherwise it's from ATS. + ''' + p.Streams.stdout += Testers.ContainsExpression( + 'HELP proxy_process_http2_current_client_connections proxy.process.http2.current_client_connections', + 'Output should have a help line for a gauge.') + p.Streams.stdout += Testers.ContainsExpression( + 'TYPE proxy_process_http2_current_client_connections gauge', 'Output should have a type line for a gauge.') + p.Streams.stdout += Testers.ContainsExpression( + 'proxy_process_http2_current_client_connections 0', + 'Verify the successful parsing of Prometheus metrics for a gauge.') + + p.Streams.stdout += Testers.ContainsExpression( + 'HELP proxy_process_http_delete_requests proxy.process.http.delete_requests', + 'Output should have a help line for a counter.') + p.Streams.stdout += Testers.ContainsExpression( + 'TYPE proxy_process_http_delete_requests counter', 'Output should have a type line for a counter.') + + # Curiosly, Prometheus appaneds _total to counter metrics. + if from_prometheus: + p.Streams.stdout += Testers.ContainsExpression( + 'proxy_process_http_delete_requests_total 0', + 'Verify the successful parsing of Prometheus metrics for a counter.') + else: + p.Streams.stdout += Testers.ContainsExpression( + 'proxy_process_http_delete_requests 0', 'Verify the successful parsing of Prometheus metrics for a counter.') + + def __checkPrometheusV2Metrics(self, p: "Test.Process"): + """Check the Prometheus v2 metrics output. + :param p: The process whose output to check. + """ + p.Streams.stdout += Testers.ContainsExpression( + "# HELP proxy_process_http_requests", + "Output should have a help line for the base metric name.", + ) + p.Streams.stdout += Testers.ContainsExpression( + "# TYPE proxy_process_http_requests counter", + "Output should have a type line for the base metric name.", + ) + + self.__containsLiteral( + p, + 'proxy_process_http_requests{method="delete"}', + "Verify that HTTP method labels (GET, POST, DELETE, etc.) are extracted correctly.", + ) + + self.__containsLiteral( + p, + 'proxy_process_http_requests{method="extension_method"}', + "Verify that multi-token HTTP method labels are extracted correctly.", + ) + + self.__containsLiteral( + p, + 'proxy_process_http_requests{method="invalid_client"}', + "Verify that invalid client request labels are extracted correctly.", + ) + + self.__containsLiteral( + p, + 'proxy_process_http_requests{direction="incoming"}', + "Verify that direction labels (incoming / outgoing) are extracted correctly.", + ) + + p.Streams.stdout += Testers.ContainsExpression( + "proxy_process_http_completed_requests", + "Verify that completed_requests remains its own lifecycle counter.", + ) + self.__excludesLiteral( + p, + 'method="completed"', + "completed is not an HTTP method label.", + ) + + self.__containsLiteral( + p, + 'proxy_process_http_cache_fresh{result="hit"}', + "Verify that result labels are extracted correctly.", + ) + + self.__containsLiteral( + p, + 'proxy_process_http_disallowed_continue{method="post", status="100"}', + "Verify that status code labels are extracted correctly.", + ) + + self.__containsLiteral( + p, + 'proxy_process_cache_volume_lookup_active{volume="0"}', + "Verify that volume labels are extracted from volume_N patterns.", + ) + + self.__containsLiteral( + p, + 'proxy_process_eventloop_count{le="', + "Verify that time buckets are correctly transformed into le labels.", + ) + + def __checkParsedPrometheusV2Metrics(self, p: "Test.Process"): + """Check the Prometheus parser's view of the v2 metrics output. + :param p: The process whose output to check. + """ + p.Streams.stdout += Testers.ContainsExpression( + "# TYPE proxy_process_http_requests counter", + "Prometheus parser should recognize HTTP request metrics as one counter family.", + ) + self.__containsLiteral( + p, + 'proxy_process_http_requests_total{method="delete"}', + "Parsed output should retain HTTP method labels.", + ) + self.__containsLiteral( + p, + 'proxy_process_http_requests_total{method="extension_method"}', + "Parsed output should retain multi-token HTTP method labels.", + ) + self.__containsLiteral( + p, + 'proxy_process_http_requests_total{direction="incoming"}', + "Parsed output should retain direction labels.", + ) + p.Streams.stdout += Testers.ContainsExpression( + "proxy_process_http_completed_requests_total", + "Parsed output should keep completed_requests as its own counter family.", + ) + self.__excludesLiteral( + p, + 'method="completed"', + "completed should not be parsed as an HTTP method label.", + ) + self.__containsLiteral( + p, + 'proxy_process_http_disallowed_continue_total{method="post",status="100"}', + "Parsed output should preserve multiple labels.", + ) + p.Streams.stdout += Testers.ContainsExpression( + "# TYPE proxy_process_http_responses counter", + "Prometheus parser should recognize HTTP response metrics as one counter family.", + ) + self.__containsLiteral( + p, + 'proxy_process_http_responses_total{direction="incoming"}', + "Parsed output should retain direction labels on response metrics.", + ) + self.__containsLiteral( + p, + 'proxy_process_http_responses_total{status="2xx"}', + "Parsed output should retain status code class labels.", + ) + self.__containsLiteral( + p, + 'proxy_process_http_cache_ims_total{result="miss"}', + "Parsed output should retain cache result labels.", + ) + self.__containsLiteral( + p, + 'proxy_process_http_transaction_counts_failed_total{result="errors",method="connect"}', + "Parsed output should preserve combined result and method labels.", + ) + self.__containsLiteral( + p, + 'proxy_process_eventloop_count{le="100s"}', + "Parsed output should retain bucket labels.", + ) + self.__containsLiteral( + p, + 'proxy_process_cache_volume_lookup_active{volume="0"}', + "Parsed output should preserve gauge labels.", + ) + self.__containsLiteral( + p, + 'proxy_process_cache_volume_lookup_success_total{volume="0"}', + "Parsed output should preserve counter labels for volume metrics.", + ) + + def __testCaseNoAccept(self): + tr = urtest.AddTestRun('Fetch stats over HTTP in JSON format: no Accept and default path') + self.__checkProcessBefore(tr) + tr.MakeCurlCommand(f"-vs --http1.1 http://127.0.0.1:{self.ts.Variables.port}/_stats", ts=self.ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.stdout += Testers.ContainsExpression( + '{ "global": {', 'Output should have the JSON header.') + tr.Processes.Default.Streams.stdout += Testers.ContainsExpression( + '"proxy.process.http.delete_requests": "0",', 'Output should be JSON formatted.') + tr.Processes.Default.Streams.stderr = "gold/stats_over_http_json_stderr.gold" + tr.Processes.Default.TimeOut = 3 + self.__checkProcessAfter(tr) + + def __testCaseAcceptCSV(self): + tr = urtest.AddTestRun('Fetch stats over HTTP in CSV format') + self.__checkProcessBefore(tr) + tr.MakeCurlCommand(f"-vs -H'Accept: text/csv' --http1.1 http://127.0.0.1:{self.ts.Variables.port}/_stats", ts=self.ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.stdout += Testers.ContainsExpression( + 'proxy.process.http.delete_requests,0', 'Output should be CSV formatted.') + tr.Processes.Default.Streams.stderr = "gold/stats_over_http_csv_stderr.gold" + tr.Processes.Default.TimeOut = 3 + self.__checkProcessAfter(tr) + + def __testCaseAcceptPrometheus(self): + tr = urtest.AddTestRun('Fetch stats over HTTP in Prometheus format') + self.__checkProcessBefore(tr) + tr.MakeCurlCommand( + f"-vs -H'Accept: text/plain; version=0.0.4' --http1.1 http://127.0.0.1:{self.ts.Variables.port}/_stats", ts=self.ts) + tr.Processes.Default.ReturnCode = 0 + self.__checkPrometheusMetrics(tr.Processes.Default, from_prometheus=False) + tr.Processes.Default.Streams.stderr = "gold/stats_over_http_prometheus_stderr.gold" + tr.Processes.Default.TimeOut = 3 + self.__checkProcessAfter(tr) + + def __testCaseAcceptPrometheusV2(self): + tr = urtest.AddTestRun("Fetch stats over HTTP in Prometheus v2 format via Accept header") + self.__checkProcessBefore(tr) + tr.MakeCurlCommand( + f"-vs -H'Accept: text/plain; version=2.0.0' --http1.1 http://127.0.0.1:{self.ts.Variables.port}/_stats", + ts=self.ts, + ) + tr.Processes.Default.ReturnCode = 0 + self.__checkPrometheusV2Metrics(tr.Processes.Default) + tr.Processes.Default.Streams.stderr = ("gold/stats_over_http_prometheus_v2_accept_stderr.gold") + tr.Processes.Default.TimeOut = 3 + self.__checkProcessAfter(tr) + + def __testCasePathJSON(self): + tr = urtest.AddTestRun('Fetch stats over HTTP in JSON format via /_stats/json') + self.__checkProcessBefore(tr) + tr.MakeCurlCommand(f"-vs --http1.1 http://127.0.0.1:{self.ts.Variables.port}/_stats/json", ts=self.ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.stdout += Testers.ContainsExpression('{ "global": {', 'JSON header expected.') + tr.Processes.Default.Streams.stdout += Testers.ContainsExpression( + '"proxy.process.http.delete_requests": "0",', 'JSON field expected.') + tr.Processes.Default.Streams.stderr = "gold/stats_over_http_json_stderr.gold" + tr.Processes.Default.TimeOut = 3 + self.__checkProcessAfter(tr) + + def __testCasePathCSV(self): + tr = urtest.AddTestRun('Fetch stats over HTTP in CSV format via /_stats/csv') + self.__checkProcessBefore(tr) + tr.MakeCurlCommand(f"-vs --http1.1 http://127.0.0.1:{self.ts.Variables.port}/_stats/csv", ts=self.ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.stdout += Testers.ContainsExpression( + 'proxy.process.http.delete_requests,0', 'CSV output expected.') + tr.Processes.Default.Streams.stderr = "gold/stats_over_http_csv_stderr.gold" + tr.Processes.Default.TimeOut = 3 + self.__checkProcessAfter(tr) + + def __testCasePathPrometheus(self): + tr = urtest.AddTestRun('Fetch stats over HTTP in Prometheus format via /_stats/prometheus') + self.__checkProcessBefore(tr) + tr.MakeCurlCommand(f"-vs --http1.1 http://127.0.0.1:{self.ts.Variables.port}/_stats/prometheus", ts=self.ts) + tr.Processes.Default.ReturnCode = 0 + self.__checkPrometheusMetrics(tr.Processes.Default, from_prometheus=False) + tr.Processes.Default.Streams.stderr = "gold/stats_over_http_prometheus_stderr.gold" + tr.Processes.Default.TimeOut = 3 + self.__checkProcessAfter(tr) + + def __testCasePathPrometheusV2(self): + tr = urtest.AddTestRun("Fetch stats over HTTP in Prometheus v2 format via /_stats/prometheus_v2") + self.__checkProcessBefore(tr) + tr.MakeCurlCommand( + f"-vs --http1.1 http://127.0.0.1:{self.ts.Variables.port}/_stats/prometheus_v2", + ts=self.ts, + ) + tr.Processes.Default.ReturnCode = 0 + self.__checkPrometheusV2Metrics(tr.Processes.Default) + tr.Processes.Default.Streams.stderr = ("gold/stats_over_http_prometheus_v2_stderr.gold") + tr.Processes.Default.TimeOut = 3 + self.__checkProcessAfter(tr) + + def __testCaseAcceptIgnoredIfPathExplicit(self): + tr = urtest.AddTestRun('Fetch stats over HTTP in Prometheus format with Accept csv header') + self.__checkProcessBefore(tr) + tr.MakeCurlCommand( + f"-vs -H'Accept: text/csv' --http1.1 http://127.0.0.1:{self.ts.Variables.port}/_stats/prometheus", ts=self.ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.stdout += Testers.ContainsExpression( + 'proxy_process_http_delete_requests 0', 'Prometheus output expected.') + tr.Processes.Default.Streams.stderr = "gold/stats_over_http_prometheus_stderr.gold" + tr.Processes.Default.TimeOut = 3 + self.__checkProcessAfter(tr) + + def __queryAndParsePrometheusMetrics(self): + """ + Query the ATS stats over HTTP in Prometheus format and parse the output. + """ + tr = urtest.AddTestRun('Query and parse Prometheus metrics') + ingester = 'prometheus_stats_ingester.py' + tr.Setup.CopyAs(ingester) + self.__checkProcessBefore(tr) + p = tr.Processes.Default + p.Command = f'{sys.executable} {ingester} http://127.0.0.1:{self.ts.Variables.port}/_stats/prometheus' + p.ReturnCode = 0 + self.__checkPrometheusMetrics(p, from_prometheus=True) + self.__checkProcessAfter(tr) + + def __queryAndParsePrometheusV2Metrics(self): + """ + Query the ATS stats over HTTP in Prometheus v2 format and parse the output. + """ + tr = urtest.AddTestRun('Query and parse Prometheus v2 metrics') + ingester = 'prometheus_stats_ingester.py' + tr.Setup.CopyAs(ingester) + self.__checkProcessBefore(tr) + p = tr.Processes.Default + p.Command = ( + f'{sys.executable} {ingester} --validate-v2-format --strict-family-metadata ' + f'http://127.0.0.1:{self.ts.Variables.port}/_stats/prometheus_v2') + p.ReturnCode = 0 + self.__checkParsedPrometheusV2Metrics(p) + self.__checkProcessAfter(tr) + + def run(self): + self.__testCaseNoAccept() + self.__testCaseAcceptCSV() + self.__testCaseAcceptPrometheus() + self.__testCaseAcceptPrometheusV2() + self.__testCasePathJSON() + self.__testCasePathCSV() + self.__testCasePathPrometheus() + self.__testCasePathPrometheusV2() + self.__testCaseAcceptIgnoredIfPathExplicit() + self.__queryAndParsePrometheusMetrics() + self.__queryAndParsePrometheusV2Metrics() + + StatsOverHttpPluginTest().run() + urtest.execute() diff --git a/tests/gold_tests/pluginTest/stek_share/server_list.yaml b/tests/uranium_tests/pluginTest/stek_share/server_list.yaml similarity index 100% rename from tests/gold_tests/pluginTest/stek_share/server_list.yaml rename to tests/uranium_tests/pluginTest/stek_share/server_list.yaml diff --git a/tests/gold_tests/pluginTest/stek_share/ssl/self_signed.crt b/tests/uranium_tests/pluginTest/stek_share/ssl/self_signed.crt similarity index 100% rename from tests/gold_tests/pluginTest/stek_share/ssl/self_signed.crt rename to tests/uranium_tests/pluginTest/stek_share/ssl/self_signed.crt diff --git a/tests/gold_tests/pluginTest/stek_share/ssl/self_signed.key b/tests/uranium_tests/pluginTest/stek_share/ssl/self_signed.key similarity index 100% rename from tests/gold_tests/pluginTest/stek_share/ssl/self_signed.key rename to tests/uranium_tests/pluginTest/stek_share/ssl/self_signed.key diff --git a/tests/uranium_tests/pluginTest/stek_share/test_stek_share.py b/tests/uranium_tests/pluginTest/stek_share/test_stek_share.py new file mode 100644 index 00000000000..a52d5b5657d --- /dev/null +++ b/tests/uranium_tests/pluginTest/stek_share/test_stek_share.py @@ -0,0 +1,354 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_stek_share(urtest: UraniumTest) -> None: + ''' + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import os + import re + + urtest.Summary = 'Test the STEK Share plugin' + urtest.testName = "stek_share" + + urtest.SkipUnless(Condition.PluginExists('stek_share.so')) + + server = urtest.MakeOriginServer('server') + + ts1 = urtest.MakeATSProcess("ts1", enable_tls=True) + ts2 = urtest.MakeATSProcess("ts2", enable_tls=True) + ts3 = urtest.MakeATSProcess("ts3", enable_tls=True) + ts4 = urtest.MakeATSProcess("ts4", enable_tls=True) + ts5 = urtest.MakeATSProcess("ts5", enable_tls=True) + + urtest.Setup.Copy('ssl/self_signed.crt') + urtest.Setup.Copy('ssl/self_signed.key') + urtest.Setup.Copy('server_list.yaml') + + cert_path = os.path.join(urtest.RunDirectory, 'self_signed.crt') + key_path = os.path.join(urtest.RunDirectory, 'self_signed.key') + server_list_path = os.path.join(urtest.RunDirectory, 'server_list.yaml') + + request_header1 = {'headers': 'GET / HTTP/1.1\r\nHost: www.example.com\r\n\r\n', 'timestamp': '1469733493.993', 'body': ''} + response_header1 = { + 'headers': 'HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n', + 'timestamp': '1469733493.993', + 'body': 'curl test' + } + server.addResponse('sessionlog.json', request_header1, response_header1) + + stek_share_conf_path_1 = os.path.join(ts1.Variables.CONFIGDIR, 'stek_share_conf.yaml') + stek_share_conf_path_2 = os.path.join(ts2.Variables.CONFIGDIR, 'stek_share_conf.yaml') + stek_share_conf_path_3 = os.path.join(ts3.Variables.CONFIGDIR, 'stek_share_conf.yaml') + stek_share_conf_path_4 = os.path.join(ts4.Variables.CONFIGDIR, 'stek_share_conf.yaml') + stek_share_conf_path_5 = os.path.join(ts5.Variables.CONFIGDIR, 'stek_share_conf.yaml') + + ts1.Disk.File(stek_share_conf_path_1, id="stek_share_conf_1", typename="ats:config") + ts2.Disk.File(stek_share_conf_path_2, id="stek_share_conf_2", typename="ats:config") + ts3.Disk.File(stek_share_conf_path_3, id="stek_share_conf_3", typename="ats:config") + ts4.Disk.File(stek_share_conf_path_4, id="stek_share_conf_4", typename="ats:config") + ts5.Disk.File(stek_share_conf_path_5, id="stek_share_conf_5", typename="ats:config") + + ts1.Disk.stek_share_conf_1.AddLines( + [ + 'server_id: 1', + 'address: 127.0.0.1', + 'port: 10001', + 'asio_thread_pool_size: 4', + 'heart_beat_interval: 100', + 'election_timeout_lower_bound: 200', + 'election_timeout_upper_bound: 400', + 'reserved_log_items: 5', + 'snapshot_distance: 5', + 'client_req_timeout: 3000', # this is in milliseconds + 'key_update_interval: 3600', # this is in seconds + 'server_list_file: {0}'.format(server_list_path), + 'root_cert_file: {0}'.format(cert_path), + 'server_cert_file: {0}'.format(cert_path), + 'server_key_file: {0}'.format(key_path), + 'cert_verify_str: /C=US/ST=IL/O=Yahoo/OU=Edge/CN=stek-share', + ]) + + ts2.Disk.stek_share_conf_2.AddLines( + [ + 'server_id: 2', + 'address: 127.0.0.1', + 'port: 10002', + 'asio_thread_pool_size: 4', + 'heart_beat_interval: 100', + 'election_timeout_lower_bound: 200', + 'election_timeout_upper_bound: 400', + 'reserved_log_items: 5', + 'snapshot_distance: 5', + 'client_req_timeout: 3000', # this is in milliseconds + 'key_update_interval: 3600', # this is in seconds + 'server_list_file: {0}'.format(server_list_path), + 'root_cert_file: {0}'.format(cert_path), + 'server_cert_file: {0}'.format(cert_path), + 'server_key_file: {0}'.format(key_path), + 'cert_verify_str: /C=US/ST=IL/O=Yahoo/OU=Edge/CN=stek-share', + ]) + + ts3.Disk.stek_share_conf_3.AddLines( + [ + 'server_id: 3', + 'address: 127.0.0.1', + 'port: 10003', + 'asio_thread_pool_size: 4', + 'heart_beat_interval: 100', + 'election_timeout_lower_bound: 200', + 'election_timeout_upper_bound: 400', + 'reserved_log_items: 5', + 'snapshot_distance: 5', + 'client_req_timeout: 3000', # this is in milliseconds + 'key_update_interval: 3600', # this is in seconds + 'server_list_file: {0}'.format(server_list_path), + 'root_cert_file: {0}'.format(cert_path), + 'server_cert_file: {0}'.format(cert_path), + 'server_key_file: {0}'.format(key_path), + 'cert_verify_str: /C=US/ST=IL/O=Yahoo/OU=Edge/CN=stek-share', + ]) + + ts4.Disk.stek_share_conf_4.AddLines( + [ + 'server_id: 4', + 'address: 127.0.0.1', + 'port: 10004', + 'asio_thread_pool_size: 4', + 'heart_beat_interval: 100', + 'election_timeout_lower_bound: 200', + 'election_timeout_upper_bound: 400', + 'reserved_log_items: 5', + 'snapshot_distance: 5', + 'client_req_timeout: 3000', # this is in milliseconds + 'key_update_interval: 3600', # this is in seconds + 'server_list_file: {0}'.format(server_list_path), + 'root_cert_file: {0}'.format(cert_path), + 'server_cert_file: {0}'.format(cert_path), + 'server_key_file: {0}'.format(key_path), + 'cert_verify_str: /C=US/ST=IL/O=Yahoo/OU=Edge/CN=stek-share', + ]) + + ts5.Disk.stek_share_conf_5.AddLines( + [ + 'server_id: 5', + 'address: 127.0.0.1', + 'port: 10005', + 'asio_thread_pool_size: 4', + 'heart_beat_interval: 100', + 'election_timeout_lower_bound: 200', + 'election_timeout_upper_bound: 400', + 'reserved_log_items: 5', + 'snapshot_distance: 5', + 'client_req_timeout: 3000', # this is in milliseconds + 'key_update_interval: 3600', # this is in seconds + 'server_list_file: {0}'.format(server_list_path), + 'root_cert_file: {0}'.format(cert_path), + 'server_cert_file: {0}'.format(cert_path), + 'server_key_file: {0}'.format(key_path), + 'cert_verify_str: /C=US/ST=IL/O=Yahoo/OU=Edge/CN=stek-share', + ]) + + ts1.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'stek_share', + 'proxy.config.exec_thread.autoconfig.enabled': 0, + 'proxy.config.exec_thread.limit': 4, + 'proxy.config.ssl.server.cert.path': '{0}'.format(urtest.RunDirectory), + 'proxy.config.ssl.server.private_key.path': '{0}'.format(urtest.RunDirectory), + 'proxy.config.ssl.server.session_ticket.enable': 1, + 'proxy.config.ssl.server.cipher_suite': + 'ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-DSS-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-DSS-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA256:DHE-RSA-AES128-SHA256:DHE-DSS-AES128-SHA256:DHE-RSA-AES256-SHA:DHE-DSS-AES256-SHA:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!aECDH:!EDH-DSS-DES-CBC3-SHA:!EDH-RSA-DES-CBC3-SHA:!KRB5-DES-CBC3-SHA' + }) + ts1.Disk.plugin_config.AddLine('stek_share.so {0}'.format(stek_share_conf_path_1)) + ts1.Disk.ssl_multicert_yaml.AddLines( + """ + ssl_multicert: + - dest_ip: "*" + ssl_cert_name: self_signed.crt + ssl_key_name: self_signed.key + """.split("\n")) + ts1.Disk.remap_config.AddLine('map / http://127.0.0.1:{0}'.format(server.Variables.Port)) + + ts2.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'stek_share', + 'proxy.config.exec_thread.autoconfig.enabled': 0, + 'proxy.config.exec_thread.limit': 4, + 'proxy.config.ssl.server.cert.path': '{0}'.format(urtest.RunDirectory), + 'proxy.config.ssl.server.private_key.path': '{0}'.format(urtest.RunDirectory), + 'proxy.config.ssl.server.session_ticket.enable': 1, + 'proxy.config.ssl.server.cipher_suite': + 'ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-DSS-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-DSS-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA256:DHE-RSA-AES128-SHA256:DHE-DSS-AES128-SHA256:DHE-RSA-AES256-SHA:DHE-DSS-AES256-SHA:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!aECDH:!EDH-DSS-DES-CBC3-SHA:!EDH-RSA-DES-CBC3-SHA:!KRB5-DES-CBC3-SHA' + }) + ts2.Disk.plugin_config.AddLine('stek_share.so {0}'.format(stek_share_conf_path_2)) + ts2.Disk.ssl_multicert_yaml.AddLines( + """ + ssl_multicert: + - dest_ip: "*" + ssl_cert_name: self_signed.crt + ssl_key_name: self_signed.key + """.split("\n")) + ts2.Disk.remap_config.AddLine('map / http://127.0.0.1:{0}'.format(server.Variables.Port)) + + ts3.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'stek_share', + 'proxy.config.exec_thread.autoconfig.enabled': 0, + 'proxy.config.exec_thread.limit': 4, + 'proxy.config.ssl.server.cert.path': '{0}'.format(urtest.RunDirectory), + 'proxy.config.ssl.server.private_key.path': '{0}'.format(urtest.RunDirectory), + 'proxy.config.ssl.server.session_ticket.enable': 1, + 'proxy.config.ssl.server.cipher_suite': + 'ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-DSS-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-DSS-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA256:DHE-RSA-AES128-SHA256:DHE-DSS-AES128-SHA256:DHE-RSA-AES256-SHA:DHE-DSS-AES256-SHA:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!aECDH:!EDH-DSS-DES-CBC3-SHA:!EDH-RSA-DES-CBC3-SHA:!KRB5-DES-CBC3-SHA' + }) + ts3.Disk.plugin_config.AddLine('stek_share.so {0}'.format(stek_share_conf_path_3)) + ts3.Disk.ssl_multicert_yaml.AddLines( + """ + ssl_multicert: + - dest_ip: "*" + ssl_cert_name: self_signed.crt + ssl_key_name: self_signed.key + """.split("\n")) + ts3.Disk.remap_config.AddLine('map / http://127.0.0.1:{0}'.format(server.Variables.Port)) + + ts4.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'stek_share', + 'proxy.config.exec_thread.autoconfig.enabled': 0, + 'proxy.config.exec_thread.limit': 4, + 'proxy.config.ssl.server.cert.path': '{0}'.format(urtest.RunDirectory), + 'proxy.config.ssl.server.private_key.path': '{0}'.format(urtest.RunDirectory), + 'proxy.config.ssl.server.session_ticket.enable': 1, + 'proxy.config.ssl.server.cipher_suite': + 'ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-DSS-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-DSS-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA256:DHE-RSA-AES128-SHA256:DHE-DSS-AES128-SHA256:DHE-RSA-AES256-SHA:DHE-DSS-AES256-SHA:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!aECDH:!EDH-DSS-DES-CBC3-SHA:!EDH-RSA-DES-CBC3-SHA:!KRB5-DES-CBC3-SHA' + }) + ts4.Disk.plugin_config.AddLine('stek_share.so {0}'.format(stek_share_conf_path_4)) + ts4.Disk.ssl_multicert_yaml.AddLines( + """ + ssl_multicert: + - dest_ip: "*" + ssl_cert_name: self_signed.crt + ssl_key_name: self_signed.key + """.split("\n")) + ts4.Disk.remap_config.AddLine('map / http://127.0.0.1:{0}'.format(server.Variables.Port)) + + ts5.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'stek_share', + 'proxy.config.exec_thread.autoconfig.enabled': 0, + 'proxy.config.exec_thread.limit': 4, + 'proxy.config.ssl.server.cert.path': '{0}'.format(urtest.RunDirectory), + 'proxy.config.ssl.server.private_key.path': '{0}'.format(urtest.RunDirectory), + 'proxy.config.ssl.server.session_ticket.enable': 1, + 'proxy.config.ssl.server.cipher_suite': + 'ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-DSS-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-DSS-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA256:DHE-RSA-AES128-SHA256:DHE-DSS-AES128-SHA256:DHE-RSA-AES256-SHA:DHE-DSS-AES256-SHA:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!aECDH:!EDH-DSS-DES-CBC3-SHA:!EDH-RSA-DES-CBC3-SHA:!KRB5-DES-CBC3-SHA' + }) + ts5.Disk.plugin_config.AddLine('stek_share.so {0}'.format(stek_share_conf_path_5)) + ts5.Disk.ssl_multicert_yaml.AddLines( + """ + ssl_multicert: + - dest_ip: "*" + ssl_cert_name: self_signed.crt + ssl_key_name: self_signed.key + """.split("\n")) + ts5.Disk.remap_config.AddLine('map / http://127.0.0.1:{0}'.format(server.Variables.Port)) + + def check_session(ev, test): + retval = False + f = open(test.GetContent(ev), 'r') + err = "Session ids match" + if not f: + err = "Failed to open {0}".format(openssl_output) + return (retval, "Check that session ids match", err) + + content = f.read() + match = re.findall('Session-ID: ([0-9A-F]+)', content) + + if match: + if all(i == j for i, j in zip(match, match[1:])): + err = "{0} reused successfully {1} times".format(match[0], len(match) - 1) + retval = True + else: + err = "Session is not being reused as expected" + else: + err = "Didn't find session id" + return (retval, "Check that session ids match", err) + + tr1 = urtest.AddTestRun('Basic Curl test, and give it enough time for all ATS to start up and sync STEK') + tr1.Processes.Default.Command = 'sleep 10 && curl https://127.0.0.1:{0} -k'.format(ts1.Variables.ssl_port) + tr1.Processes.Default.ReturnCode = 0 + tr1.Processes.Default.StartBefore(server) + tr1.Processes.Default.StartBefore(ts1) + tr1.Processes.Default.StartBefore(ts2) + tr1.Processes.Default.StartBefore(ts3) + tr1.Processes.Default.StartBefore(ts4) + tr1.Processes.Default.StartBefore(ts5) + tr1.Processes.Default.Streams.All = Testers.ContainsExpression('curl test', 'Making sure the basics still work') + ts1.Disk.traffic_out.Content = Testers.ContainsExpression('Generate initial STEK succeeded', 'should succeed') + ts2.Disk.traffic_out.Content = Testers.ContainsExpression('Generate initial STEK succeeded', 'should succeed') + ts3.Disk.traffic_out.Content = Testers.ContainsExpression('Generate initial STEK succeeded', 'should succeed') + ts4.Disk.traffic_out.Content = Testers.ContainsExpression('Generate initial STEK succeeded', 'should succeed') + ts5.Disk.traffic_out.Content = Testers.ContainsExpression('Generate initial STEK succeeded', 'should succeed') + tr1.StillRunningAfter = server + tr1.StillRunningAfter += ts1 + tr1.StillRunningAfter += ts2 + tr1.StillRunningAfter += ts3 + tr1.StillRunningAfter += ts4 + tr1.StillRunningAfter += ts5 + + tr2 = urtest.AddTestRun("TLSv1.2 Session Ticket") + tr2.Command = \ + 'printf "GET / HTTP/1.1\r\n" | openssl s_client -tls1_2 -connect 127.0.0.1:{0} -sess_out {5} && ' \ + 'printf "GET / HTTP/1.1\r\n" | openssl s_client -tls1_2 -connect 127.0.0.1:{0} -sess_in {5} && ' \ + 'printf "GET / HTTP/1.1\r\n" | openssl s_client -tls1_2 -connect 127.0.0.1:{1} -sess_in {5} && ' \ + 'printf "GET / HTTP/1.1\r\n" | openssl s_client -tls1_2 -connect 127.0.0.1:{2} -sess_in {5} && ' \ + 'printf "GET / HTTP/1.1\r\n" | openssl s_client -tls1_2 -connect 127.0.0.1:{3} -sess_in {5} && ' \ + 'printf "GET / HTTP/1.1\r\n" | openssl s_client -tls1_2 -connect 127.0.0.1:{4} -sess_in {5}' \ + .format( + ts1.Variables.ssl_port, + ts2.Variables.ssl_port, + ts3.Variables.ssl_port, + ts4.Variables.ssl_port, + ts5.Variables.ssl_port, + os.path.join(urtest.RunDirectory, 'sess.dat') + ) + tr2.ReturnCode = 0 + tr2.Processes.Default.Streams.All.Content = Testers.Lambda(check_session) + urtest.execute() diff --git a/tests/uranium_tests/pluginTest/strategies/test_strategies_plugins.py b/tests/uranium_tests/pluginTest/strategies/test_strategies_plugins.py new file mode 100644 index 00000000000..700819cbe22 --- /dev/null +++ b/tests/uranium_tests/pluginTest/strategies/test_strategies_plugins.py @@ -0,0 +1,432 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_strategies_plugins(urtest: UraniumTest) -> None: + ''' + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + import os + + urtest.Summary = ''' + Combined header_rewrite/regex_remap/tslua strategies tests + ''' + + # Test description: + # Preload the cache with the entire asset to be range requested. + # Reload remap rule with slice plugin + # Request content through the slice plugin + + urtest.SkipUnless( + Condition.PluginExists('header_rewrite.so'), + Condition.PluginExists('regex_remap.so'), + Condition.PluginExists('tslua.so'), + ) + urtest.ContinueOnFail = False + + dns = urtest.MakeDNServer("dns") + + origins = [] + + chars = ['0', '1', '2', 'p', 's'] + for char in chars: + name = f"nh{char}" + origin = urtest.MakeOriginServer(name, options={"--verbose": ""}) + request_header = { + "headers": f"GET / HTTP/1.1\r\nHost: origin\r\n\r\n", + "timestamp": "1469733493.993", + "body": "", + } + response_header = { + "headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", + "timestamp": "1469733493.993", + "body": "", + } + origin.addResponse("sessionfile.log", request_header, response_header) + request_header = { + "headers": "GET /path HTTP/1.1\r\nHost: origin\r\n\r\n", + "timestamp": "1469733493.993", + "body": "", + } + response_header = { + "headers": f"HTTP/1.1 200 OK\r\nConnection: close\r\nOrigin: {name}\r\n\r\n", + "timestamp": "1469733493.993", + "body": name, + } + origin.addResponse("sessionfile.log", request_header, response_header) + request_header = { + "headers": f"GET /path/{name} HTTP/1.1\r\nHost: origin\r\n\r\n", + "timestamp": "1469733493.993", + "body": "", + } + response_header = { + "headers": f"HTTP/1.1 200 OK\r\nConnection: close\r\nOrigin: {name}\r\n\r\n", + "timestamp": "1469733493.993", + "body": name, + } + origin.addResponse("sessionfile.log", request_header, response_header) + origin.ReturnCode = 0 + origins.append(origin) + dns.addRecords(records={name: ["127.0.0.1"]}) + + # Define ATS and configure + ts = urtest.MakeATSProcess("ts", enable_cache=False) + ts.ReturnCode = 0 + ts.Disk.records_config.update( + { + 'proxy.config.dns.nameservers': f"127.0.0.1:{dns.Variables.Port}", + 'proxy.config.dns.resolv_conf': "NULL", + 'proxy.config.http.cache.http': 0, + "proxy.config.http.insert_response_via_str": 1, + 'proxy.config.http.uncacheable_requests_bypass_parent': 0, + 'proxy.config.http.no_dns_just_forward_to_parent': 1, + 'proxy.config.http.parent_proxy.mark_down_hostdb': 0, + 'proxy.config.http.parent_proxy.self_detect': 0, + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': "next_hop|dns|http|parent|regex_remap|header_rewrite|tslua", + }) + + ts.Disk.MakeConfigFile("hdr_rw.config").AddLines( + [ + 'cond %{CLIENT-HEADER:Strategy} ="nemo"', + "set-next-hop-strategy nemo", + 'cond %{CLIENT-HEADER:Strategy} ="nh0"', + "set-next-hop-strategy nh0", + 'cond %{CLIENT-HEADER:Strategy} ="nh1"', + "set-next-hop-strategy nh1", + 'cond %{CLIENT-HEADER:Strategy} ="null"', + "set-next-hop-strategy null", + 'cond %{CLIENT-HEADER:Strategy} ="clear"', + 'set-next-hop-strategy ""', + ]) + ts.Disk.MakeConfigFile("regex_remap.config").AddLines( + [ + "/nh0 http://origin/path @strategy=nh1", + '/nh1 http://origin/path @strategy=', + "/nh2 http://origin/path @strategy=nh0", + '/null http://origin/path @strategy=null', + "/nemo http://origin/path @strategy=nemo", + "# fallthrough", + "/ http://origin/path", + ]) + ts.Disk.MakeConfigFile("strategies.lua").AddLines( + [ + 'function do_remap()', + ' local uri = ts.client_request.get_uri()', + ' if uri:find("nh0") then', + ' ts.http.set_next_hop_strategy("nh1")', + ' elseif uri:find("nh1") then', + ' ts.http.set_next_hop_strategy("")', + ' elseif uri:find("nh2") then', + ' ts.http.set_next_hop_strategy("nh0")', + ' elseif uri:find("null") then', + ' ts.http.set_next_hop_strategy("null")', + ' elseif uri:find("nemo") then', + ' ts.http.set_next_hop_strategy("nemo")', + ' end', + ' ts.client_request.set_uri("path")', + ' return 0', + 'end', + ]) + + # parent.config + ts.Disk.parent_config.AddLines( + [f'dest_domain=. parent="nh2:{origins[2].Variables.Port}" round_robin=false go_direct=false parent_is_proxy=false']) + + # build strategies.yaml file + ts.Disk.File(ts.Variables.CONFIGDIR + "/strategies.yaml", id="strategies", typename="ats:config") + + s = ts.Disk.strategies + s.AddLine("groups:") + for ind in range(len(origins)): + char = chars[ind] + org = origins[ind] + name = f"nh{chars[ind]}" + s.AddLines( + [ + f" - &g{char}", + f" - host: {name}", + f" protocol:", + f" - scheme: http", + f" port: {org.Variables.Port}", + f" weight: 1.0", + ]) + + s.AddLine("strategies:") + + # third ts_nh + for char in chars: + s.AddLines( + [ + f" - strategy: nh{char}", + f" policy: consistent_hash", + f" hash_key: path", + f" go_direct: false", + f" parent_is_proxy: false", + f" ignore_self_detect: true", + f" groups:", + f" - *g{char}", + f" scheme: http", + ]) + + ts.Disk.remap_config.AddLines( + [ + "# header rewrite", + "map http://nhp_hr http://origin @plugin=header_rewrite.so @pparam=hdr_rw.config", + "map http://nhs_hr http://origin @strategy=nh0 @plugin=header_rewrite.so @pparam=hdr_rw.config", + "# modify strategy/parent", + "map http://nh0_hr http://origin @strategy=nh0 @plugin=header_rewrite.so @pparam=hdr_rw.config", + "map http://nh1_hr http://origin @strategy=nh1 @plugin=header_rewrite.so @pparam=hdr_rw.config", + "map http://nh2_hr http://origin @plugin=header_rewrite.so @pparam=hdr_rw.config", + "", + "# regex_remap", + "map http://nhp_rr http://origin @plugin=regex_remap.so @pparam=regex_remap.config", + "map http://nhs_rr http://origin @strategy=nh0 @plugin=regex_remap.so @pparam=regex_remap.config", + "# modify strategy/parent", + "map http://nh0_rr http://origin @strategy=nh0 @plugin=regex_remap.so @pparam=regex_remap.config", + "map http://nh1_rr http://origin @strategy=nh1 @plugin=regex_remap.so @pparam=regex_remap.config", + "map http://nh2_rr http://origin @plugin=regex_remap.so @pparam=regex_remap.config", + "", + "# tslua", + "map http://nhp_lua http://origin @plugin=tslua.so @pparam=strategies.lua", + "map http://nhs_lua http://origin @strategy=nh0 @plugin=tslua.so @pparam=strategies.lua", + "# modify strategy/parent", + "map http://nh0_lua http://origin @strategy=nh0 @plugin=tslua.so @pparam=strategies.lua", + "map http://nh1_lua http://origin @strategy=nh1 @plugin=tslua.so @pparam=strategies.lua", + "map http://nh2_lua http://origin @plugin=tslua.so @pparam=strategies.lua", + ]) + + # Tests + + # stdout for body, stderr for headers + curl_and_args = '-s -o /dev/stdout -D /dev/stderr -x localhost:{}'.format(ts.Variables.port) + + # header rewrite + + # 0 - nhp request to parent.config + tr = urtest.AddTestRun("nhp_hr parent.config through request") + ps = tr.Processes.Default + for ind in range(len(origins)): + origin = origins[ind] + ps.StartBefore(origin, ready=When.PortOpen(origin.Variables.Port)) + tr.StillRunningAfter = origin + ps.StartBefore(dns) + ps.StartBefore(urtest.Processes.ts) + tr.MakeCurlCommand(curl_and_args + " http://nhp_hr/path", ts=ts) + ps.ReturnCode = 0 + ps.Streams.stdout.Content = Testers.ContainsExpression("nh2", "expected nh2") + tr.StillRunningAfter = ts + tr.StillRunningAfter = dns + + # 1 - nhs_hr default request + tr = urtest.AddTestRun("nhs_hr straight through request") + ps = tr.Processes.Default + tr.MakeCurlCommand(curl_and_args + " http://nhs_hr/path", ts=ts) + ps.ReturnCode = 0 + ps.Streams.stdout.Content = Testers.ContainsExpression("nh0", "expected nh0") + tr.StillRunningAfter = ts + + # 2 - nh0_hr default request + tr = urtest.AddTestRun("nh0_hr straight through request") + ps = tr.Processes.Default + tr.MakeCurlCommand(curl_and_args + " http://nh0_hr/path", ts=ts) + ps.ReturnCode = 0 + ps.Streams.stdout.Content = Testers.ContainsExpression("nh0", "expected nh0") + tr.StillRunningAfter = ts + + # 3 - nh1_hr default request + tr = urtest.AddTestRun("nh1_hr straight through request") + ps = tr.Processes.Default + tr.MakeCurlCommand(curl_and_args + " http://nh1_hr/path", ts=ts) + ps.ReturnCode = 0 + ps.Streams.stdout.Content = Testers.ContainsExpression("nh1", "expected nh1") + tr.StillRunningAfter = ts + + # 4 - nh2_hr default request + tr = urtest.AddTestRun("nh2_hr straight through request") + ps = tr.Processes.Default + tr.MakeCurlCommand(curl_and_args + " http://nh2_hr/path", ts=ts) + ps.ReturnCode = 0 + ps.Streams.stdout.Content = Testers.ContainsExpression("nh2", "expected nh2") + tr.StillRunningAfter = ts + + # 5 switch strategies + tr = urtest.AddTestRun("nh0_hr switch to nh1") + ps = tr.Processes.Default + tr.MakeCurlCommand(curl_and_args + ' http://nh0_hr/path -H "Strategy: nh1"', ts=ts) + ps.ReturnCode = 0 + ps.Streams.stdout.Content = Testers.ContainsExpression("nh1", "expected nh1") + tr.StillRunningAfter = ts + tr.StillRunningAfter = dns + + # 6 strategy to parent.config + tr = urtest.AddTestRun("nh1_hr switch to parent.config") + ps = tr.Processes.Default + tr.MakeCurlCommand(curl_and_args + ' http://nh1_hr/path -H "Strategy: null"', ts=ts) + ps.ReturnCode = 0 + ps.Streams.stdout.Content = Testers.ContainsExpression("nh2", "expected nh2") + tr.StillRunningAfter = ts + tr.StillRunningAfter = dns + + # 7 parent.config strategy to strategy + tr = urtest.AddTestRun("nh2_hr switch to nh0") + ps = tr.Processes.Default + tr.MakeCurlCommand(curl_and_args + ' http://nh2_hr/path -H "Strategy: nh0"', ts=ts) + ps.ReturnCode = 0 + ps.Streams.stdout.Content = Testers.ContainsExpression("nh0", "expected nh0") + tr.StillRunningAfter = ts + tr.StillRunningAfter = dns + + # 8 try to switch to non existent strategy + tr = urtest.AddTestRun("nh0_hr switch to nemo (fail)") + ps = tr.Processes.Default + tr.MakeCurlCommand(curl_and_args + ' http://nh0_hr/path -H "Strategy: nemo"', ts=ts) + ps.ReturnCode = 0 + ps.Streams.stdout.Content = Testers.ContainsExpression("nh0", "expected nh0") + tr.StillRunningAfter = ts + tr.StillRunningAfter = dns + + # regex_remap + + # 9 use parent.config + tr = urtest.AddTestRun("nhp_rr parent.config") + ps = tr.Processes.Default + tr.MakeCurlCommand(curl_and_args + ' http://nhp_rr/nhp', ts=ts) + ps.ReturnCode = 0 + ps.Streams.stdout.Content = Testers.ContainsExpression("nh2", "expected nh2") + tr.StillRunningAfter = ts + tr.StillRunningAfter = dns + + # 10 use strategies + tr = urtest.AddTestRun("nhs_rr strategies.yaml") + ps = tr.Processes.Default + tr.MakeCurlCommand(curl_and_args + ' http://nhs_rr/nh', ts=ts) + ps.ReturnCode = 0 + ps.Streams.stdout.Content = Testers.ContainsExpression("nh0", "expected nh0") + tr.StillRunningAfter = ts + tr.StillRunningAfter = dns + + # 11 switch strategies + tr = urtest.AddTestRun("nh0_rr switch to nh1") + ps = tr.Processes.Default + tr.MakeCurlCommand(curl_and_args + ' http://nh0_rr/nh0', ts=ts) + ps.ReturnCode = 0 + ps.Streams.stdout.Content = Testers.ContainsExpression("nh1", "expected nh1") + tr.StillRunningAfter = ts + tr.StillRunningAfter = dns + + # 12 strategy to parent.config + tr = urtest.AddTestRun("nh1_rr switch to parent.config") + ps = tr.Processes.Default + tr.MakeCurlCommand(curl_and_args + ' http://nh1_rr/nh1', ts=ts) + ps.ReturnCode = 0 + ps.Streams.stdout.Content = Testers.ContainsExpression("nh2", "expected nh2") + tr.StillRunningAfter = ts + tr.StillRunningAfter = dns + + # 13 parent.config strategy to strategy + tr = urtest.AddTestRun("nh2_rr switch to nh0") + ps = tr.Processes.Default + tr.MakeCurlCommand(curl_and_args + ' http://nh2_rr/nh2', ts=ts) + ps.ReturnCode = 0 + ps.Streams.stdout.Content = Testers.ContainsExpression("nh0", "expected nh0") + tr.StillRunningAfter = ts + tr.StillRunningAfter = dns + + # 14 switch strategies (fail) + tr = urtest.AddTestRun("nh0_rr switch to nemo") + ps = tr.Processes.Default + tr.MakeCurlCommand(curl_and_args + ' http://nh0_rr/nemo', ts=ts) + ps.ReturnCode = 0 + ps.Streams.stdout.Content = Testers.ContainsExpression("nh0", "expected nh0") + tr.StillRunningAfter = ts + tr.StillRunningAfter = dns + + # tslua + + # 15 parent.config + tr = urtest.AddTestRun("nhp_lua parent.config") + ps = tr.Processes.Default + tr.MakeCurlCommand(curl_and_args + ' http://nhp_lua/nh', ts=ts) + ps.ReturnCode = 0 + ps.Streams.stdout.Content = Testers.ContainsExpression("nh2", "expected nh2") + tr.StillRunningAfter = ts + tr.StillRunningAfter = dns + + # 16 strategies.yaml + tr = urtest.AddTestRun("nhs_lua strategies.yaml") + ps = tr.Processes.Default + tr.MakeCurlCommand(curl_and_args + ' http://nhs_lua/nh', ts=ts) + ps.ReturnCode = 0 + ps.Streams.stdout.Content = Testers.ContainsExpression("nh0", "expected nh0") + tr.StillRunningAfter = ts + tr.StillRunningAfter = dns + + # 17 switch strategies + tr = urtest.AddTestRun("nh0_lua switch to nh1") + ps = tr.Processes.Default + tr.MakeCurlCommand(curl_and_args + ' http://nh0_lua/nh0', ts=ts) + ps.ReturnCode = 0 + ps.Streams.stdout.Content = Testers.ContainsExpression("nh1", "expected nh1") + tr.StillRunningAfter = ts + tr.StillRunningAfter = dns + + # 18 strategy to parent.config + tr = urtest.AddTestRun("nh1_lua switch to parent.config") + ps = tr.Processes.Default + tr.MakeCurlCommand(curl_and_args + ' http://nh1_lua/nh1', ts=ts) + ps.ReturnCode = 0 + ps.Streams.stdout.Content = Testers.ContainsExpression("nh2", "expected nh2") + tr.StillRunningAfter = ts + tr.StillRunningAfter = dns + + # 19 parent.config strategy to strategy + tr = urtest.AddTestRun("nh2_lua switch to nh0") + ps = tr.Processes.Default + tr.MakeCurlCommand(curl_and_args + ' http://nh2_lua/nh2', ts=ts) + ps.ReturnCode = 0 + ps.Streams.stdout.Content = Testers.ContainsExpression("nh0", "expected nh0") + tr.StillRunningAfter = ts + tr.StillRunningAfter = dns + + # 20 switch strategies, fail + tr = urtest.AddTestRun("nh0_lua switch to nemo") + ps = tr.Processes.Default + tr.MakeCurlCommand(curl_and_args + ' http://nh0_lua/nemo', ts=ts) + ps.ReturnCode = 0 + ps.Streams.stdout.Content = Testers.ContainsExpression("nh0", "expected nh0") + tr.StillRunningAfter = ts + tr.StillRunningAfter = dns + + # Overriding the built in ERROR check since we expect some ERROR messages + ts.Disk.diags_log.Content = Testers.ContainsExpression("ERROR", "Some tests are failure tests") + urtest.execute() diff --git a/tests/gold_tests/pluginTest/test_hooks/200.gold b/tests/uranium_tests/pluginTest/test_hooks/200.gold similarity index 100% rename from tests/gold_tests/pluginTest/test_hooks/200.gold rename to tests/uranium_tests/pluginTest/test_hooks/200.gold diff --git a/tests/gold_tests/pluginTest/test_hooks/clean.sh b/tests/uranium_tests/pluginTest/test_hooks/clean.sh similarity index 100% rename from tests/gold_tests/pluginTest/test_hooks/clean.sh rename to tests/uranium_tests/pluginTest/test_hooks/clean.sh diff --git a/tests/gold_tests/pluginTest/test_hooks/hook_add.gold b/tests/uranium_tests/pluginTest/test_hooks/hook_add.gold similarity index 100% rename from tests/gold_tests/pluginTest/test_hooks/hook_add.gold rename to tests/uranium_tests/pluginTest/test_hooks/hook_add.gold diff --git a/tests/gold_tests/pluginTest/test_hooks/log.gold b/tests/uranium_tests/pluginTest/test_hooks/log.gold similarity index 100% rename from tests/gold_tests/pluginTest/test_hooks/log.gold rename to tests/uranium_tests/pluginTest/test_hooks/log.gold diff --git a/tests/gold_tests/pluginTest/test_hooks/log_uds.gold b/tests/uranium_tests/pluginTest/test_hooks/log_uds.gold similarity index 100% rename from tests/gold_tests/pluginTest/test_hooks/log_uds.gold rename to tests/uranium_tests/pluginTest/test_hooks/log_uds.gold diff --git a/tests/gold_tests/pluginTest/test_hooks/replay/body_buffer.test.yaml b/tests/uranium_tests/pluginTest/test_hooks/replay/body_buffer.test.yaml similarity index 99% rename from tests/gold_tests/pluginTest/test_hooks/replay/body_buffer.test.yaml rename to tests/uranium_tests/pluginTest/test_hooks/replay/body_buffer.test.yaml index 8c7f40c8e45..42a57ea5fda 100644 --- a/tests/gold_tests/pluginTest/test_hooks/replay/body_buffer.test.yaml +++ b/tests/uranium_tests/pluginTest/test_hooks/replay/body_buffer.test.yaml @@ -20,7 +20,7 @@ meta: version: "1.0" -autest: +urtest: description: 'Verify request body buffering with request_buffer.so plugin' requires: plugins: [request_buffer.so] diff --git a/tests/gold_tests/pluginTest/test_hooks/replay/zero_post_copy_size.test.yaml b/tests/uranium_tests/pluginTest/test_hooks/replay/zero_post_copy_size.test.yaml similarity index 99% rename from tests/gold_tests/pluginTest/test_hooks/replay/zero_post_copy_size.test.yaml rename to tests/uranium_tests/pluginTest/test_hooks/replay/zero_post_copy_size.test.yaml index 0b9532a3343..5dba2803a28 100644 --- a/tests/gold_tests/pluginTest/test_hooks/replay/zero_post_copy_size.test.yaml +++ b/tests/uranium_tests/pluginTest/test_hooks/replay/zero_post_copy_size.test.yaml @@ -22,7 +22,7 @@ meta: version: "1.0" -autest: +urtest: description: 'Verify post_copy_size=0 with request_buffer_enabled does not cause 403 (issue #6900)' requires: plugins: [request_buffer.so] diff --git a/tests/gold_tests/pluginTest/test_hooks/ssn_delay.gold b/tests/uranium_tests/pluginTest/test_hooks/ssn_delay.gold similarity index 100% rename from tests/gold_tests/pluginTest/test_hooks/ssn_delay.gold rename to tests/uranium_tests/pluginTest/test_hooks/ssn_delay.gold diff --git a/tests/uranium_tests/pluginTest/test_hooks/test_hook_add.py b/tests/uranium_tests/pluginTest/test_hooks/test_hook_add.py new file mode 100644 index 00000000000..1fc90d7e548 --- /dev/null +++ b/tests/uranium_tests/pluginTest/test_hooks/test_hook_add.py @@ -0,0 +1,79 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_hook_add(urtest: UraniumTest) -> None: + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import os + + urtest.Summary = ''' + Test adding hooks + ''' + + urtest.ContinueOnFail = True + + server = urtest.MakeOriginServer("server") + + request_header = {"headers": "GET /argh HTTP/1.1\r\nHost: doesnotmatter\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + server.addResponse("sessionlog.json", request_header, response_header) + + ts = urtest.MakeATSProcess("ts", enable_cache=False) + + ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.tags': 'test', + 'proxy.config.diags.show_location': 0, + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.url_remap.remap_required': 0, + }) + + urtest.PrepareTestPlugin(os.path.join(urtest.Variables.AtsTestPluginsDir, 'hook_add_plugin.so'), ts) + + ts.Disk.remap_config.AddLine("map http://one http://127.0.0.1:{0}".format(server.Variables.Port)) + + ipv4flag = "" + if not Condition.CurlUsingUnixDomainSocket(): + ipv4flag = "--ipv4" + + tr = urtest.AddTestRun() + # Probe server port to check if ready. + tr.Processes.Default.StartBefore(server, ready=When.PortOpen(server.Variables.Port)) + tr.Processes.Default.StartBefore(urtest.Processes.ts) + # + tr.MakeCurlCommand('--verbose {0} --header "Host: one" http://localhost:{1}/argh'.format(ipv4flag, ts.Variables.port), ts=ts) + tr.Processes.Default.ReturnCode = 0 + + # Look at the debug output from the plugin + ts.Disk.traffic_out.Content = "hook_add.gold" + urtest.execute() diff --git a/tests/uranium_tests/pluginTest/test_hooks/test_ssn_start_delay_hook.py b/tests/uranium_tests/pluginTest/test_hooks/test_ssn_start_delay_hook.py new file mode 100644 index 00000000000..5827353d413 --- /dev/null +++ b/tests/uranium_tests/pluginTest/test_hooks/test_ssn_start_delay_hook.py @@ -0,0 +1,81 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_ssn_start_delay_hook(urtest: UraniumTest) -> None: + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import os + + urtest.Summary = ''' + Test adding hooks, and rescheduling the ssn start hook from a non-net thread + ''' + + urtest.ContinueOnFail = True + + server = urtest.MakeOriginServer("server") + + request_header = {"headers": "GET /argh HTTP/1.1\r\nHost: doesnotmatter\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + server.addResponse("sessionlog.json", request_header, response_header) + + ts = urtest.MakeATSProcess("ts") + + ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.tags': 'test', + 'proxy.config.diags.show_location': 0, + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.http.cache.http': 0, + 'proxy.config.url_remap.remap_required': 0, + }) + + urtest.PrepareTestPlugin(os.path.join(urtest.Variables.AtsTestPluginsDir, 'hook_add_plugin.so'), ts, '-delay') + + ts.Disk.remap_config.AddLine("map http://one http://127.0.0.1:{0}".format(server.Variables.Port)) + + ipv4flag = "" + if not Condition.CurlUsingUnixDomainSocket(): + ipv4flag = "--ipv4" + + tr = urtest.AddTestRun() + # Probe server port to check if ready. + tr.Processes.Default.StartBefore(server, ready=When.PortOpen(server.Variables.Port)) + # Probe TS cleartext port to check if ready (probing TLS port causes spurious VCONN hook triggers). + tr.Processes.Default.StartBefore(urtest.Processes.ts, ready=When.PortOpen(ts.Variables.port)) + # + tr.MakeCurlCommand('--verbose {0} --header "Host: one" http://localhost:{1}/argh'.format(ipv4flag, ts.Variables.port), ts=ts) + tr.Processes.Default.ReturnCode = 0 + + # Look at the debug output from the plugin + ts.Disk.traffic_out.Content = "ssn_delay.gold" + urtest.execute() diff --git a/tests/uranium_tests/pluginTest/test_hooks/test_test_hooks.py b/tests/uranium_tests/pluginTest/test_hooks/test_test_hooks.py new file mode 100644 index 00000000000..3160f3fa001 --- /dev/null +++ b/tests/uranium_tests/pluginTest/test_hooks/test_test_hooks.py @@ -0,0 +1,126 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_test_hooks(urtest: UraniumTest) -> None: + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import os + + urtest.Summary = ''' + Test TS API Hooks. + ''' + + urtest.SkipUnless(Condition.HasCurlFeature('http2'),) + urtest.ContinueOnFail = True + + server = urtest.MakeOriginServer("server") + + request_header = {"headers": "GET /argh HTTP/1.1\r\nHost: doesnotmatter\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + server.addResponse("sessionlog.json", request_header, response_header) + + # Disable the cache to make sure each request is forwarded to the origin + # server. + ts = urtest.MakeATSProcess("ts", enable_tls=True, enable_cache=False) + + # test_hooks.so will output test logging to this file. + log_path = os.path.join(ts.Variables.LOGDIR, "log.txt") + urtest.Env["OUTPUT_FILE"] = log_path + + ts.addDefaultSSLFiles() + + ts.Disk.records_config.update( + { + 'proxy.config.proxy_name': 'Poxy_Proxy', # This will be the server name. + 'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir), + 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts.Variables.SSLDir), + 'proxy.config.url_remap.remap_required': 0, + 'proxy.config.diags.debug.enabled': 0, + 'proxy.config.diags.debug.tags': 'http|test_hooks', + }) + + ts.Disk.ssl_multicert_yaml.AddLines( + """ + ssl_multicert: + - dest_ip: "*" + ssl_cert_name: server.pem + ssl_key_name: server.key + """.split("\n")) + + urtest.PrepareTestPlugin(os.path.join(urtest.Variables.AtsTestPluginsDir, 'test_hooks.so'), ts) + + ts.Disk.remap_config.AddLine("map http://one http://127.0.0.1:{0}".format(server.Variables.Port)) + ts.Disk.remap_config.AddLine("map https://one http://127.0.0.1:{0}".format(server.Variables.Port)) + + ipv4flag = "" + if not Condition.CurlUsingUnixDomainSocket(): + ipv4flag = "--ipv4" + + tr = urtest.AddTestRun() + # Probe server port to check if ready. + tr.Processes.Default.StartBefore(server, ready=When.PortOpen(server.Variables.Port)) + tr.Processes.Default.StartBefore(urtest.Processes.ts) + # + tr.MakeCurlCommand('--verbose {0} --header "Host: one" http://localhost:{1}/argh'.format(ipv4flag, ts.Variables.port), ts=ts) + tr.Processes.Default.ReturnCode = 0 + + if not Condition.CurlUsingUnixDomainSocket(): + tr = urtest.AddTestRun() + tr.MakeCurlCommand( + '--verbose --ipv4 --http2 --insecure --header "Host: one" https://localhost:{0}/argh'.format(ts.Variables.ssl_port), + ts=ts) + tr.Processes.Default.ReturnCode = 0 + + tr = urtest.AddTestRun() + tr.MakeCurlCommand( + '--verbose --ipv4 --http1.1 --insecure --header "Host: one" https://localhost:{0}/argh'.format(ts.Variables.ssl_port), + ts=ts) + tr.Processes.Default.ReturnCode = 0 + + # The probing of the ATS port to detect when ATS is ready may be seen by ATS as a VCONN start/close, so filter out these + # events from the log file. + # + tr = urtest.AddTestRun() + tr.Processes.Default.Command = "cd " + urtest.RunDirectory + " ; . " + urtest.TestDirectory + "/clean.sh" + tr.Processes.Default.ReturnCode = 0 + + tr = urtest.AddTestRun() + tr.Processes.Default.Command = "echo check log" + tr.Processes.Default.ReturnCode = 0 + f = tr.Disk.File(log_path) + if Condition.CurlUsingUnixDomainSocket(): + f.Content = "log_uds.gold" + else: + f.Content = "log.gold" + f.Content += Testers.ContainsExpression("Global: event=TS_EVENT_VCONN_CLOSE", "VCONN_CLOSE should trigger 2 times") + urtest.execute() diff --git a/tests/gold_tests/pluginTest/traffic_dump/gold/200_bob_no_sni.gold b/tests/uranium_tests/pluginTest/traffic_dump/gold/200_bob_no_sni.gold similarity index 100% rename from tests/gold_tests/pluginTest/traffic_dump/gold/200_bob_no_sni.gold rename to tests/uranium_tests/pluginTest/traffic_dump/gold/200_bob_no_sni.gold diff --git a/tests/gold_tests/pluginTest/traffic_dump/gold/200_get.gold b/tests/uranium_tests/pluginTest/traffic_dump/gold/200_get.gold similarity index 100% rename from tests/gold_tests/pluginTest/traffic_dump/gold/200_get.gold rename to tests/uranium_tests/pluginTest/traffic_dump/gold/200_get.gold diff --git a/tests/gold_tests/pluginTest/traffic_dump/gold/200_get_sensitive_field.gold b/tests/uranium_tests/pluginTest/traffic_dump/gold/200_get_sensitive_field.gold similarity index 100% rename from tests/gold_tests/pluginTest/traffic_dump/gold/200_get_sensitive_field.gold rename to tests/uranium_tests/pluginTest/traffic_dump/gold/200_get_sensitive_field.gold diff --git a/tests/gold_tests/pluginTest/traffic_dump/gold/200_http10.gold b/tests/uranium_tests/pluginTest/traffic_dump/gold/200_http10.gold similarity index 100% rename from tests/gold_tests/pluginTest/traffic_dump/gold/200_http10.gold rename to tests/uranium_tests/pluginTest/traffic_dump/gold/200_http10.gold diff --git a/tests/gold_tests/pluginTest/traffic_dump/gold/200_post.gold b/tests/uranium_tests/pluginTest/traffic_dump/gold/200_post.gold similarity index 100% rename from tests/gold_tests/pluginTest/traffic_dump/gold/200_post.gold rename to tests/uranium_tests/pluginTest/traffic_dump/gold/200_post.gold diff --git a/tests/gold_tests/pluginTest/traffic_dump/gold/200_sni_bob.gold b/tests/uranium_tests/pluginTest/traffic_dump/gold/200_sni_bob.gold similarity index 100% rename from tests/gold_tests/pluginTest/traffic_dump/gold/200_sni_bob.gold rename to tests/uranium_tests/pluginTest/traffic_dump/gold/200_sni_bob.gold diff --git a/tests/gold_tests/pluginTest/traffic_dump/gold/200_sni_dave.gold b/tests/uranium_tests/pluginTest/traffic_dump/gold/200_sni_dave.gold similarity index 100% rename from tests/gold_tests/pluginTest/traffic_dump/gold/200_sni_dave.gold rename to tests/uranium_tests/pluginTest/traffic_dump/gold/200_sni_dave.gold diff --git a/tests/gold_tests/pluginTest/traffic_dump/gold/4_byte_response_body.gold b/tests/uranium_tests/pluginTest/traffic_dump/gold/4_byte_response_body.gold similarity index 100% rename from tests/gold_tests/pluginTest/traffic_dump/gold/4_byte_response_body.gold rename to tests/uranium_tests/pluginTest/traffic_dump/gold/4_byte_response_body.gold diff --git a/tests/gold_tests/pluginTest/traffic_dump/gold/explicit_target.gold b/tests/uranium_tests/pluginTest/traffic_dump/gold/explicit_target.gold similarity index 100% rename from tests/gold_tests/pluginTest/traffic_dump/gold/explicit_target.gold rename to tests/uranium_tests/pluginTest/traffic_dump/gold/explicit_target.gold diff --git a/tests/gold_tests/pluginTest/traffic_dump/gold/post_with_body.gold b/tests/uranium_tests/pluginTest/traffic_dump/gold/post_with_body.gold similarity index 100% rename from tests/gold_tests/pluginTest/traffic_dump/gold/post_with_body.gold rename to tests/uranium_tests/pluginTest/traffic_dump/gold/post_with_body.gold diff --git a/tests/gold_tests/pluginTest/traffic_dump/gold/two_transactions.gold b/tests/uranium_tests/pluginTest/traffic_dump/gold/two_transactions.gold similarity index 100% rename from tests/gold_tests/pluginTest/traffic_dump/gold/two_transactions.gold rename to tests/uranium_tests/pluginTest/traffic_dump/gold/two_transactions.gold diff --git a/tests/gold_tests/pluginTest/traffic_dump/replay/http3.yaml b/tests/uranium_tests/pluginTest/traffic_dump/replay/http3.yaml similarity index 100% rename from tests/gold_tests/pluginTest/traffic_dump/replay/http3.yaml rename to tests/uranium_tests/pluginTest/traffic_dump/replay/http3.yaml diff --git a/tests/gold_tests/pluginTest/traffic_dump/replay/response_body.yaml b/tests/uranium_tests/pluginTest/traffic_dump/replay/response_body.yaml similarity index 100% rename from tests/gold_tests/pluginTest/traffic_dump/replay/response_body.yaml rename to tests/uranium_tests/pluginTest/traffic_dump/replay/response_body.yaml diff --git a/tests/gold_tests/pluginTest/traffic_dump/replay/traffic_dump.yaml b/tests/uranium_tests/pluginTest/traffic_dump/replay/traffic_dump.yaml similarity index 100% rename from tests/gold_tests/pluginTest/traffic_dump/replay/traffic_dump.yaml rename to tests/uranium_tests/pluginTest/traffic_dump/replay/traffic_dump.yaml diff --git a/tests/gold_tests/pluginTest/traffic_dump/replay/traffic_dump_ip_filter_server.yaml b/tests/uranium_tests/pluginTest/traffic_dump/replay/traffic_dump_ip_filter_server.yaml similarity index 100% rename from tests/gold_tests/pluginTest/traffic_dump/replay/traffic_dump_ip_filter_server.yaml rename to tests/uranium_tests/pluginTest/traffic_dump/replay/traffic_dump_ip_filter_server.yaml diff --git a/tests/gold_tests/pluginTest/traffic_dump/replay/traffic_dump_server.yaml b/tests/uranium_tests/pluginTest/traffic_dump/replay/traffic_dump_server.yaml similarity index 100% rename from tests/gold_tests/pluginTest/traffic_dump/replay/traffic_dump_server.yaml rename to tests/uranium_tests/pluginTest/traffic_dump/replay/traffic_dump_server.yaml diff --git a/tests/gold_tests/pluginTest/traffic_dump/replay/various_sni.yaml b/tests/uranium_tests/pluginTest/traffic_dump/replay/various_sni.yaml similarity index 100% rename from tests/gold_tests/pluginTest/traffic_dump/replay/various_sni.yaml rename to tests/uranium_tests/pluginTest/traffic_dump/replay/various_sni.yaml diff --git a/tests/gold_tests/pluginTest/traffic_dump/ssl/server.key b/tests/uranium_tests/pluginTest/traffic_dump/ssl/server.key similarity index 100% rename from tests/gold_tests/pluginTest/traffic_dump/ssl/server.key rename to tests/uranium_tests/pluginTest/traffic_dump/ssl/server.key diff --git a/tests/gold_tests/pluginTest/traffic_dump/ssl/server.pem b/tests/uranium_tests/pluginTest/traffic_dump/ssl/server.pem similarity index 100% rename from tests/gold_tests/pluginTest/traffic_dump/ssl/server.pem rename to tests/uranium_tests/pluginTest/traffic_dump/ssl/server.pem diff --git a/tests/gold_tests/pluginTest/traffic_dump/ssl/server_combined.pem b/tests/uranium_tests/pluginTest/traffic_dump/ssl/server_combined.pem similarity index 100% rename from tests/gold_tests/pluginTest/traffic_dump/ssl/server_combined.pem rename to tests/uranium_tests/pluginTest/traffic_dump/ssl/server_combined.pem diff --git a/tests/gold_tests/pluginTest/traffic_dump/ssl/signed-foo.key b/tests/uranium_tests/pluginTest/traffic_dump/ssl/signed-foo.key similarity index 100% rename from tests/gold_tests/pluginTest/traffic_dump/ssl/signed-foo.key rename to tests/uranium_tests/pluginTest/traffic_dump/ssl/signed-foo.key diff --git a/tests/gold_tests/pluginTest/traffic_dump/ssl/signed-foo.pem b/tests/uranium_tests/pluginTest/traffic_dump/ssl/signed-foo.pem similarity index 100% rename from tests/gold_tests/pluginTest/traffic_dump/ssl/signed-foo.pem rename to tests/uranium_tests/pluginTest/traffic_dump/ssl/signed-foo.pem diff --git a/tests/gold_tests/pluginTest/traffic_dump/ssl/signer.pem b/tests/uranium_tests/pluginTest/traffic_dump/ssl/signer.pem similarity index 100% rename from tests/gold_tests/pluginTest/traffic_dump/ssl/signer.pem rename to tests/uranium_tests/pluginTest/traffic_dump/ssl/signer.pem diff --git a/tests/uranium_tests/pluginTest/traffic_dump/test_traffic_dump.py b/tests/uranium_tests/pluginTest/traffic_dump/test_traffic_dump.py new file mode 100644 index 00000000000..575de4bc0cd --- /dev/null +++ b/tests/uranium_tests/pluginTest/traffic_dump/test_traffic_dump.py @@ -0,0 +1,451 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_traffic_dump(urtest: UraniumTest) -> None: + """ + Verify traffic_dump functionality. + """ + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import os + import sys + + urtest.Summary = ''' + Verify traffic_dump functionality. + ''' + + urtest.SkipUnless(Condition.PluginExists('traffic_dump.so'),) + + schema_path = os.path.join(urtest.Variables.AtsTestToolsDir, 'lib', 'replay_schema.json') + + # Configure the origin server. + replay_file = "replay/traffic_dump.yaml" + server = urtest.MakeVerifierServerProcess( + "server", "replay/traffic_dump_server.yaml", ssl_cert="ssl/server_combined.pem", ca_cert="ssl/signer.pem") + + # Define ATS and configure it. + ts = urtest.MakeATSProcess("ts", enable_tls=True) + replay_dir = os.path.join(ts.RunDirectory, "ts", "log") + + ts.addSSLfile("ssl/server.pem") + ts.addSSLfile("ssl/server.key") + ts.addSSLfile("ssl/signer.pem") + + ts.Setup.Copy("ssl/signed-foo.pem") + ts.Setup.Copy("ssl/signed-foo.key") + + ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'traffic_dump|http', + 'proxy.config.http.insert_age_in_response': 0, + 'proxy.config.ssl.server.cert.path': ts.Variables.SSLDir, + 'proxy.config.ssl.server.private_key.path': ts.Variables.SSLDir, + 'proxy.config.url_remap.pristine_host_hdr': 1, + 'proxy.config.ssl.CA.cert.filename': f'{ts.Variables.SSLDir}/signer.pem', + 'proxy.config.exec_thread.autoconfig.scale': 1.0, + 'proxy.config.http.host_sni_policy': 2, + 'proxy.config.ssl.TLSv1_3.enabled': 0, + 'proxy.config.ssl.client.verify.server.policy': 'PERMISSIVE', + 'proxy.config.http.connect_ports': f"{server.Variables.http_port}", + }) + + ts.Disk.ssl_multicert_yaml.AddLines( + """ + ssl_multicert: + - dest_ip: "*" + ssl_cert_name: server.pem + ssl_key_name: server.key + """.split("\n")) + + ts.Disk.remap_config.AddLines( + [ + f'map https://www.client_only_tls.com/ http://127.0.0.1:{server.Variables.http_port}', + f'map https://www.tls.com/ https://127.0.0.1:{server.Variables.https_port}', + f'map http://www.connect_target.com/ http://127.0.0.1:{server.Variables.http_port}', + f'map / http://127.0.0.1:{server.Variables.http_port}', + ]) + ts.addPrivateConnectAllowYaml(methods='[ CONNECT, GET, POST ]') + + # Configure traffic_dump. + ts.Disk.plugin_config.AddLine( + f'traffic_dump.so --logdir {replay_dir} --sample 1 --limit 1000000000 ' + '--sensitive-fields "cookie,set-cookie,x-request-1,x-request-2"') + # Configure logging of transactions. This is helpful for the cache test below. + ts.Disk.logging_yaml.AddLines( + ''' + logging: + formats: + - name: basic + format: "%: Read result: %:%:%, Write result: %" + logs: + - filename: transactions + format: basic + '''.split('\n')) + + # Set up trafficserver expectations. + ts.Disk.diags_log.Content = Testers.ContainsExpression( + "loading plugin.*traffic_dump.so", "Verify the traffic_dump plugin got loaded.") + ts.Disk.traffic_out.Content = Testers.ContainsExpression( + f"Initialized with log directory: {replay_dir}", "Verify traffic_dump initialized with the configured directory.") + ts.Disk.traffic_out.Content += Testers.ContainsExpression( + "Initialized with sample pool size of 1 bytes and disk limit of 1000000000 bytes", + "Verify traffic_dump initialized with the configured disk limit.") + ts.Disk.traffic_out.Content += Testers.ContainsExpression( + "Finish a session with log file of.*bytes", "Verify traffic_dump sees the end of sessions and accounts for it.") + ts.Disk.traffic_out.Content += Testers.ContainsExpression( + "Dumping body bytes: false", "Verify that dumping body bytes is enabled.") + + # Set up the json replay file expectations. + replay_file_session_1 = os.path.join(replay_dir, "127", "0000000000000000") + ts.Disk.File(replay_file_session_1, exists=True) + replay_file_session_2 = os.path.join(replay_dir, "127", "0000000000000001") + ts.Disk.File(replay_file_session_2, exists=True) + replay_file_session_3 = os.path.join(replay_dir, "127", "0000000000000002") + ts.Disk.File(replay_file_session_3, exists=True) + replay_file_session_4 = os.path.join(replay_dir, "127", "0000000000000003") + ts.Disk.File(replay_file_session_4, exists=True) + replay_file_session_5 = os.path.join(replay_dir, "127", "0000000000000004") + ts.Disk.File(replay_file_session_5, exists=True) + replay_file_session_6 = os.path.join(replay_dir, "127", "0000000000000005") + ts.Disk.File(replay_file_session_6, exists=True) + replay_file_session_7 = os.path.join(replay_dir, "127", "0000000000000006") + ts.Disk.File(replay_file_session_7, exists=True) + replay_file_session_8 = os.path.join(replay_dir, "127", "0000000000000007") + ts.Disk.File(replay_file_session_8, exists=True) + replay_file_session_9 = os.path.join(replay_dir, "127", "0000000000000008") + ts.Disk.File(replay_file_session_9, exists=True) + replay_file_session_10 = os.path.join(replay_dir, "127", "0000000000000009") + ts.Disk.File(replay_file_session_10, exists=True) + replay_file_session_11 = os.path.join(replay_dir, "127", "000000000000000a") + ts.Disk.File(replay_file_session_11, exists=True) + + # The following will not be written to disk because of a restricted disk limit. + replay_file_session_12 = os.path.join(replay_dir, "127", "000000000000000b") + ts.Disk.File(replay_file_session_12, exists=False) + + # The following will be written to disk because the disk restriction will be + # removed. + replay_file_session_13 = os.path.join(replay_dir, "127", "000000000000000c") + ts.Disk.File(replay_file_session_13, exists=True) + + # The following will not be written to disk because the restriction will be + # re-added. + replay_file_session_14 = os.path.join(replay_dir, "127", "000000000000000d") + ts.Disk.File(replay_file_session_14, exists=False) + + # Run our test traffic. + tr = urtest.AddTestRun("Run the test traffic.") + tr.AddVerifierClientProcess( + "client", + replay_file, + http_ports=[ts.Variables.port], + https_ports=[ts.Variables.ssl_port], + ssl_cert="ssl/server_combined.pem", + ca_cert="ssl/signer.pem") + + tr.Processes.Default.StartBefore(server) + tr.Processes.Default.StartBefore(ts) + tr.StillRunningAfter = server + tr.StillRunningAfter = ts + + # + # Test 1: Verify the correct behavior of two transactions across two sessions. + # + + # Verify the properties of the replay file for the first transaction. + tr = urtest.AddTestRun("Verify the json content of the first session") + http_protocols = "tcp,ip" + verify_replay = "verify_replay.py" + sensitive_fields_arg = ( + "--sensitive-fields cookie " + "--sensitive-fields set-cookie " + "--sensitive-fields x-request-1 " + "--sensitive-fields x-request-2 ") + tr.Setup.CopyAs(verify_replay, urtest.RunDirectory) + tr.Processes.Default.Command = \ + (f'{sys.executable} {verify_replay} {schema_path} {replay_file_session_1} ' + f'{sensitive_fields_arg} --client-http-version "1.1" ' + f'--client-protocols "{http_protocols}"') + tr.Processes.Default.ReturnCode = 0 + tr.StillRunningAfter = server + tr.StillRunningAfter = ts + + # Verify the properties of the replay file for the second transaction. + tr = urtest.AddTestRun("Verify the json content of the second session") + tr.Setup.CopyAs(verify_replay, urtest.RunDirectory) + tr.Processes.Default.Command = \ + (f'{sys.executable} {verify_replay} {schema_path} {replay_file_session_2} ' + f'{sensitive_fields_arg} --client-http-version "1.1" ' + '--request-target "/two"') + tr.Processes.Default.ReturnCode = 0 + tr.StillRunningAfter = server + tr.StillRunningAfter = ts + + # + # Test 2: Verify the correct behavior of an explicit path in the request line. + # + + # Verify recording of a request target with the host specified. + tr = urtest.AddTestRun("Verify the replay file has the explicit target.") + tr.Setup.CopyAs(verify_replay, urtest.RunDirectory) + + tr.Processes.Default.Command = \ + (f"{sys.executable} {verify_replay} {schema_path} {replay_file_session_3} {sensitive_fields_arg} " + "--request-target 'http://www.some.host.com/candy'") + tr.Processes.Default.ReturnCode = 0 + tr.StillRunningAfter = server + tr.StillRunningAfter = ts + + # + # Test 3: Verify correct handling of a POST with body data. + # + + tr = urtest.AddTestRun("Verify the client-request size node for a request with a body.") + tr.Setup.CopyAs(verify_replay, urtest.RunDirectory) + + size_of_verify_replay_file = os.path.getsize(os.path.join(urtest.TestDirectory, verify_replay)) + expected_body_size = 12345 + tr.Processes.Default.Command = \ + (f"{sys.executable} {verify_replay} {schema_path} {replay_file_session_4} {sensitive_fields_arg} " + f"--client-request-size {expected_body_size}") + tr.Processes.Default.ReturnCode = 0 + tr.StillRunningAfter = server + tr.StillRunningAfter = ts + + # + # Test 4: Verify correct handling of a response produced out of the cache. + # + tr = urtest.AddTestRun("Verify that the cached response's replay file looks appropriate.") + tr.Setup.CopyAs(verify_replay, urtest.RunDirectory) + tr.Processes.Default.Command = \ + (f'{sys.executable} {verify_replay} {schema_path} {replay_file_session_6} ' + f'--client-protocols "{http_protocols}"') + tr.Processes.Default.ReturnCode = 0 + tr.StillRunningAfter = server + tr.StillRunningAfter = ts + + # + # Test 5: Verify correct handling of two transactions in a session. + # + tr = urtest.AddTestRun("Verify the dump file of two transactions in a session.") + tr.Setup.CopyAs(verify_replay, urtest.RunDirectory) + tr.Processes.Default.Command = \ + (f'{sys.executable} {verify_replay} {schema_path} {replay_file_session_7} ' + f'--client-protocols "{http_protocols}"') + tr.Processes.Default.ReturnCode = 0 + tr.StillRunningAfter = server + tr.StillRunningAfter = ts + + # + # Test 6: Verify correct protocol dumping of a TLS connection. + # + tr = urtest.AddTestRun("Verify the client protocol stack of a TLS session.") + https_protocols = "tls,tcp,ip" + client_tls_features = "sni:www.tls.com,proxy-verify-mode:0,proxy-provided-cert:true" + tr.Setup.CopyAs(verify_replay, urtest.RunDirectory) + tr.Processes.Default.Command = \ + (f'{sys.executable} {verify_replay} {schema_path} {replay_file_session_8} ' + f'--client-protocols "{https_protocols}" --client-tls-features "{client_tls_features}"') + tr.Processes.Default.ReturnCode = 0 + tr.StillRunningAfter = server + tr.StillRunningAfter = ts + + tr = urtest.AddTestRun("Verify the server TLS protocol stack.") + https_server_stack = "http,tls,tcp,ip" + tr.Setup.CopyAs(verify_replay, urtest.RunDirectory) + server_tls_features = 'proxy-provided-cert:false,sni:www.tls.com,proxy-verify-mode:1' + tr.Processes.Default.Command = \ + (f'{sys.executable} {verify_replay} {schema_path} {replay_file_session_8} --server-protocols ' + f'"{https_server_stack}" --server-tls-features "{server_tls_features}"') + tr.Processes.Default.ReturnCode = 0 + tr.StillRunningAfter = server + tr.StillRunningAfter = ts + + # + # Test 7: Verify correct protocol dumping of TLS and HTTP/2 connections. + # + tr = urtest.AddTestRun("Verify the client HTTP/2 protocol stack.") + h2_protocols = "http,tls,tcp,ip" + tr.Setup.CopyAs(verify_replay, urtest.RunDirectory) + tr.Processes.Default.Command = \ + (f'{sys.executable} {verify_replay} {schema_path} {replay_file_session_9} ' + f'--client-http-version "2" --client-protocols "{h2_protocols}" ' + f'--client-tls-features "{client_tls_features}"') + tr.Processes.Default.ReturnCode = 0 + tr.StillRunningAfter = server + tr.StillRunningAfter = ts + + tr = urtest.AddTestRun("Verify the server HTTP/2 protocol stack.") + tr.Setup.CopyAs(verify_replay, urtest.RunDirectory) + tr.Processes.Default.Command = \ + (f'{sys.executable} {verify_replay} {schema_path} {replay_file_session_9} ' + f'--server-protocols "{https_server_stack}" --server-tls-features "{server_tls_features}"') + tr.Processes.Default.ReturnCode = 0 + tr.StillRunningAfter = server + tr.StillRunningAfter = ts + + # + # Test 8: Verify correct protocol dumping of client-side TLS and server-side HTTP. + # + tr = urtest.AddTestRun("Verify the client TLS protocol stack.") + tr.Setup.CopyAs(verify_replay, urtest.RunDirectory) + tr.Processes.Default.Command = \ + (f'{sys.executable} {verify_replay} {schema_path} {replay_file_session_10} ' + f'--client-http-version "1.1" --client-protocols "{https_protocols}"') + tr.Processes.Default.ReturnCode = 0 + tr.StillRunningAfter = server + tr.StillRunningAfter = ts + + tr = urtest.AddTestRun("Verify the server HTTP protocol stack.") + tr.Setup.CopyAs(verify_replay, urtest.RunDirectory) + http_server_stack = "http,tcp,ip" + tr.Processes.Default.Command = \ + (f'{sys.executable} {verify_replay} {schema_path} {replay_file_session_10} ' + f'--server-protocols "{http_server_stack}"') + tr.Processes.Default.ReturnCode = 0 + tr.StillRunningAfter = server + tr.StillRunningAfter = ts + + # + # Test 9: Verify correct handling of a CONNECT request. + # + + tr = urtest.AddTestRun("Verify handling of a CONNECT request.") + tr.Setup.CopyAs(verify_replay, urtest.RunDirectory) + + tr.Processes.Default.Command = \ + (f"{sys.executable} {verify_replay} {schema_path} {replay_file_session_11} {sensitive_fields_arg} ") + tr.Processes.Default.ReturnCode = 0 + tr.StillRunningAfter = server + tr.StillRunningAfter = ts + + # + # Test 10: Verify that we can change the --limit value. + # + tr = urtest.AddTestRun("Verify changing --limit via traffic_ctl.") + tr.Processes.Default.Command = "traffic_ctl plugin msg traffic_dump.limit 0" + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Env = ts.Env + tr.StillRunningAfter = ts + + # Give ATS some time to process the change + tr = urtest.AddTestRun("Give ATS some time to process the change") + tr.Processes.Default.Command = "sleep 2" + tr.Processes.Default.ReturnCode = 0 + + tr = urtest.AddTestRun("Run some more test traffic with the restricted disk limit.") + tr.AddVerifierClientProcess( + "client-2", + replay_file, + http_ports=[ts.Variables.port], + https_ports=[ts.Variables.ssl_port], + ssl_cert="ssl/server_combined.pem", + ca_cert="ssl/signer.pem", + other_args='--keys 1') + + # Since the limit is zero, we should not see any new replay file created. + tr = urtest.AddTestRun("Verify no new traffic was dumped") + # Sleep 2 seconds to give the replay plugin plenty of time to write the file. + tr.Processes.Default.Command = "sleep 2" + tr.Processes.Default.ReturnCode = 0 + file = tr.Disk.File(replay_file_session_12) + file.Exists = False + + # + # Test 11: Verify that we can remove the disk limit. + # + tr = urtest.AddTestRun("Removing the disk limit via traffic_ctl.") + tr.Processes.Default.Command = "traffic_ctl plugin msg traffic_dump.unlimit" + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Env = ts.Env + tr.StillRunningAfter = ts + + # Give ATS some time to process the change + tr = urtest.AddTestRun("Give ATS some time to process the change") + tr.Processes.Default.Command = "sleep 2" + tr.Processes.Default.ReturnCode = 0 + + tr = urtest.AddTestRun("Run some more test traffic with no disk limit.") + tr.AddVerifierClientProcess( + "client-3", + replay_file, + http_ports=[ts.Variables.port], + https_ports=[ts.Variables.ssl_port], + ssl_cert="ssl/server_combined.pem", + ca_cert="ssl/signer.pem", + other_args='--keys 1') + + # Since the limit is zero, we should not see any new replay file created. + tr = urtest.AddTestRun("Verify the new traffic was dumped") + # Sleep 2 seconds to give the replay plugin plenty of time to write the file. + tr.Processes.Default.Command = "sleep 2" + tr.Processes.Default.ReturnCode = 0 + file = tr.Disk.File(replay_file_session_13) + file.Exists = True + + # + # Test 11: Verify that we can again restrict the disk limit. + # + # Verify that the restriction can be re-added after unlimit was set. This + # verifies correct handling of the boolean controlling unlimited disk space. + # + tr = urtest.AddTestRun("Verify re-adding --limit via traffic_ctl.") + tr.Processes.Default.Command = "traffic_ctl plugin msg traffic_dump.limit 0" + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Env = ts.Env + tr.StillRunningAfter = ts + + # Give ATS some time to process the change + tr = urtest.AddTestRun("Give ATS some time to process the change") + tr.Processes.Default.Command = "sleep 2" + tr.Processes.Default.ReturnCode = 0 + + tr = urtest.AddTestRun("Run test traffic with newly restricted disk limit.") + tr.AddVerifierClientProcess( + "client-4", + replay_file, + http_ports=[ts.Variables.port], + https_ports=[ts.Variables.ssl_port], + ssl_cert="ssl/server_combined.pem", + ca_cert="ssl/signer.pem", + other_args='--keys 1') + + # Since the limit is zero, we should not see any new replay file created. + tr = urtest.AddTestRun("Verify no new traffic was dumped") + # Sleep 2 seconds to give the replay plugin plenty of time to write the file. + tr.Processes.Default.Command = "sleep 2" + tr.Processes.Default.ReturnCode = 0 + file = tr.Disk.File(replay_file_session_14) + file.Exists = False + urtest.execute() diff --git a/tests/uranium_tests/pluginTest/traffic_dump/test_traffic_dump_http3.py b/tests/uranium_tests/pluginTest/traffic_dump/test_traffic_dump_http3.py new file mode 100644 index 00000000000..d6bb983eed5 --- /dev/null +++ b/tests/uranium_tests/pluginTest/traffic_dump/test_traffic_dump_http3.py @@ -0,0 +1,163 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_traffic_dump_http3(urtest: UraniumTest) -> None: + """ + Verify traffic_dump HTTP/3 functionality. + """ + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import os + import sys + + urtest.Summary = ''' + Verify traffic_dump HTTP/3 functionality. + ''' + + urtest.SkipUnless( + Condition.PluginExists('traffic_dump.so'), + Condition.HasATSFeature('TS_USE_QUIC'), + ) + urtest.SkipIf(Condition.true("Skip this test until the TS_EVENT_HTTP_SSN are supported for QUIC connections."),) + + schema_path = os.path.join(urtest.Variables.AtsTestToolsDir, 'lib', 'replay_schema.json') + + # Configure the origin server. + replay_file = "replay/http3.yaml" + server = urtest.MakeVerifierServerProcess("server", replay_file, ssl_cert="ssl/server_combined.pem", ca_cert="ssl/signer.pem") + + # Define ATS and configure it. + ts = urtest.MakeATSProcess("ts", enable_tls=True, enable_quic=True) + ts_log_dir = os.path.join(ts.RunDirectory, "ts", "log") + qlog_dir = os.path.join(ts_log_dir, "qlog_dir") + + ts.addSSLfile("ssl/server.pem") + ts.addSSLfile("ssl/server.key") + ts.addSSLfile("ssl/signer.pem") + + ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'traffic_dump|quic', + 'proxy.config.http.insert_age_in_response': 0, + 'proxy.config.quic.qlog.file_base': qlog_dir, + 'proxy.config.ssl.server.cert.path': ts.Variables.SSLDir, + 'proxy.config.ssl.server.private_key.path': ts.Variables.SSLDir, + 'proxy.config.url_remap.pristine_host_hdr': 1, + 'proxy.config.ssl.CA.cert.filename': f'{ts.Variables.SSLDir}/signer.pem', + 'proxy.config.exec_thread.autoconfig.scale': 1.0, + 'proxy.config.http.host_sni_policy': 2, + 'proxy.config.ssl.client.verify.server.policy': 'PERMISSIVE', + }) + + ts.Disk.ssl_multicert_yaml.AddLines( + """ + ssl_multicert: + - dest_ip: "*" + ssl_cert_name: server.pem + ssl_key_name: server.key + """.split("\n")) + + ts.Disk.remap_config.AddLine(f'map https://www.client_only_tls.com/ http://127.0.0.1:{server.Variables.http_port}') + ts.Disk.remap_config.AddLine(f'map https://www.tls.com/ https://127.0.0.1:{server.Variables.https_port}') + ts.Disk.remap_config.AddLine(f'map / http://127.0.0.1:{server.Variables.http_port}') + + # Configure traffic_dump. + ts.Disk.plugin_config.AddLine( + f'traffic_dump.so --logdir {ts_log_dir} --sample 1 --limit 1000000000 ' + '--sensitive-fields "cookie,set-cookie,x-request-1,x-request-2"') + # Configure logging of transactions. This is helpful for the cache test below. + ts.Disk.logging_yaml.AddLines( + ''' + logging: + formats: + - name: basic + format: "%: Read result: %:%:%, Write result: %" + logs: + - filename: transactions + format: basic + '''.split('\n')) + + # Set up trafficserver expectations. + ts.Disk.diags_log.Content = Testers.ContainsExpression( + "loading plugin.*traffic_dump.so", "Verify the traffic_dump plugin got loaded.") + ts.Disk.traffic_out.Content = Testers.ContainsExpression( + f"Initialized with log directory: {ts_log_dir}", "Verify traffic_dump initialized with the configured directory.") + ts.Disk.traffic_out.Content += Testers.ContainsExpression( + "Initialized with sample pool size of 1 bytes and disk limit of 1000000000 bytes", + "Verify traffic_dump initialized with the configured disk limit.") + ts.Disk.traffic_out.Content += Testers.ContainsExpression( + "Finish a session with log file of.*bytes", "Verify traffic_dump sees the end of sessions and accounts for it.") + + # Set up the json replay file expectations. + replay_file_session_1 = os.path.join(ts_log_dir, "127", "0000000000000000") + ts.Disk.File(replay_file_session_1, exists=True) + + # Execute the first transaction. + tr = urtest.AddTestRun("Run the test traffic.") + tr.AddVerifierClientProcess( + "client", + replay_file, + http_ports=[ts.Variables.port], + https_ports=[ts.Variables.ssl_port], + http3_ports=[ts.Variables.ssl_port], + ssl_cert="ssl/server_combined.pem", + ca_cert="ssl/signer.pem") + + tr.Processes.Default.StartBefore(server) + tr.Processes.Default.StartBefore(ts) + tr.StillRunningAfter = server + tr.StillRunningAfter = ts + + # + # Test 1: Verify the correct behavior of two transactions across two sessions. + # + + # Verify the properties of the replay file for the first transaction. + tr = urtest.AddTestRun("Verify the json content of the first session") + http_protocols = "tcp,ip" + verify_replay = "verify_replay.py" + sensitive_fields_arg = ( + "--sensitive-fields cookie " + "--sensitive-fields set-cookie " + "--sensitive-fields x-request-1 " + "--sensitive-fields x-request-2 ") + tr.Setup.CopyAs(verify_replay, urtest.RunDirectory) + tr.Processes.Default.Command = \ + (f'{sys.executable} {verify_replay} {schema_path} {replay_file_session_1} ' + f'{sensitive_fields_arg} --client-http-version "3" ' + f'--client-protocols "{http_protocols}"') + tr.Processes.Default.ReturnCode = 0 + tr.StillRunningAfter = server + tr.StillRunningAfter = ts + urtest.execute() diff --git a/tests/uranium_tests/pluginTest/traffic_dump/test_traffic_dump_ip_filter.py b/tests/uranium_tests/pluginTest/traffic_dump/test_traffic_dump_ip_filter.py new file mode 100644 index 00000000000..c95f8382dbb --- /dev/null +++ b/tests/uranium_tests/pluginTest/traffic_dump/test_traffic_dump_ip_filter.py @@ -0,0 +1,135 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_traffic_dump_ip_filter(urtest: UraniumTest) -> None: + """ + Verify traffic_dump IP filter functionality. + """ + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import os + import sys + + urtest.Summary = ''' + Verify traffic_dump IP filter functionality. + ''' + + urtest.SkipUnless(Condition.PluginExists('traffic_dump.so'),) + + # Configure the origin server. + replay_file = "replay/traffic_dump.yaml" + server = urtest.MakeVerifierServerProcess("server", "replay/traffic_dump_ip_filter_server.yaml") + + def get_common_ats_process(name, plugin_command, replay_exists): + """ + Get an ATS process with some common configuration. + + These tests have different log expectations, but have generally the same + ATS Process configuration. This function returns a Process with that common + configuration. + + Returns: + A configured ATS Process. + The replay file. + """ + ts = urtest.MakeATSProcess(name) + replay_dir = os.path.join(ts.RunDirectory, name, "log") + ts.Disk.records_config.update({ + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'traffic_dump', + }) + ts.Disk.remap_config.AddLine(f'map / http://127.0.0.1:{server.Variables.http_port}') + # Configure traffic_dump as specified. + ts.Disk.plugin_config.AddLine(plugin_command.format(replay_dir)) + + ts_dump_fil = os.path.join(replay_dir, "127", "0000000000000000") + ts.Disk.File(ts_dump_fil, exists=replay_exists) + return ts, ts_dump_fil + + # Common verification variables. + verify_replay = "verify_replay.py" + schema = os.path.join(urtest.Variables.AtsTestToolsDir, 'lib', 'replay_schema.json') + verify_command_prefix = f'{sys.executable} {verify_replay} {schema}' + + # + # Test 1: Verify -4 works for a specified address. + # + tr = urtest.AddTestRun("Verify that -4 matches 127.0.0.1 as expected") + ts1, ts1_replay_file = get_common_ats_process( + "ts1", 'traffic_dump.so --logdir {0} --sample 1 --limit 1000000000 -4 127.0.0.1', replay_exists=True) + ts1.Disk.traffic_out.Content += Testers.ContainsExpression( + "Filtering to only dump connections with ip: 127.0.0.1", "Verify the IP filter status message.") + tr.AddVerifierClientProcess("client0", replay_file, http_ports=[ts1.Variables.port], other_args='--keys 1') + + tr.Processes.Default.StartBefore(server) + tr.Processes.Default.StartBefore(ts1) + tr.StillRunningAfter = server + tr.StillRunningAfter = ts1 + + # Verify that the expected request body was recorded. + tr = urtest.AddTestRun("Verify that the expected request body was recorded.") + tr.Setup.CopyAs(verify_replay, urtest.RunDirectory) + tr.Processes.Default.Command = f'{verify_command_prefix} {ts1_replay_file}' + tr.Processes.Default.ReturnCode = 0 + tr.StillRunningAfter = server + tr.StillRunningAfter = ts1 + + # + # Test 2: Verify -4 filters out other addresses. + # + tr = urtest.AddTestRun("Verify that -4 filters out our non-matching IP as expected") + ts2, ts2_replay_file = get_common_ats_process( + "ts2", 'traffic_dump.so --logdir {0} --sample 1 --limit 1000000000 -4 1.2.3.4', replay_exists=False) + ts2.Disk.traffic_out.Content += Testers.ContainsExpression( + "Filtering to only dump connections with ip: 1.2.3.4", "Verify the IP filter status message.") + tr.AddVerifierClientProcess("client1", replay_file, http_ports=[ts2.Variables.port], other_args='--keys 1') + + tr.Processes.Default.StartBefore(ts2) + tr.StillRunningAfter = server + tr.StillRunningAfter = ts2 + + # + # Test 3: Verify -4 recognizes an invalid IP address string. + # + tr = urtest.AddTestRun("Verify that -4 detects an invalid IP string") + invalid_ip = "this_is_not_a_valid_ip_string" + ts3, ts3_replay_file = get_common_ats_process( + "ts3", 'traffic_dump.so --logdir {0} --sample 1 --limit 1000000000 -4 ' + invalid_ip, replay_exists=False) + ts3.Disk.diags_log.Content = Testers.ContainsExpression( + f"Problems parsing IP filter address argument: {invalid_ip}", "Verify traffic_dump detects an invalid IPv4 address.") + tr.AddVerifierClientProcess("client2", replay_file, http_ports=[ts3.Variables.port], other_args='--keys 1') + + tr.Processes.Default.StartBefore(ts3) + tr.StillRunningAfter = server + tr.StillRunningAfter = ts3 + urtest.execute() diff --git a/tests/uranium_tests/pluginTest/traffic_dump/test_traffic_dump_response_body.py b/tests/uranium_tests/pluginTest/traffic_dump/test_traffic_dump_response_body.py new file mode 100644 index 00000000000..830d537082b --- /dev/null +++ b/tests/uranium_tests/pluginTest/traffic_dump/test_traffic_dump_response_body.py @@ -0,0 +1,166 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_traffic_dump_response_body(urtest: UraniumTest) -> None: + """ + Verify traffic_dump response body functionality. + """ + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import os + import sys + + urtest.Summary = ''' + Verify traffic_dump response body functionality. + ''' + + urtest.SkipUnless(Condition.PluginExists('traffic_dump.so'),) + + # Configure the origin server. + replay_file = "replay/response_body.yaml" + server = urtest.MakeVerifierServerProcess("server", replay_file) + + # Configure ATS. + ts = urtest.MakeATSProcess("ts", enable_tls=True) + replay_dir = os.path.join(ts.RunDirectory, "ts", "log") + + ts.addSSLfile("ssl/server.pem") + ts.addSSLfile("ssl/server.key") + ts.addSSLfile("ssl/signer.pem") + + ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'traffic_dump', + 'proxy.config.ssl.server.cert.path': ts.Variables.SSLDir, + 'proxy.config.ssl.server.private_key.path': ts.Variables.SSLDir, + 'proxy.config.url_remap.pristine_host_hdr': 1, + 'proxy.config.ssl.CA.cert.filename': f'{ts.Variables.SSLDir}/signer.pem', + 'proxy.config.exec_thread.autoconfig.scale': 1.0, + 'proxy.config.http.host_sni_policy': 2, + 'proxy.config.ssl.TLSv1_3.enabled': 0, + 'proxy.config.ssl.client.verify.server.policy': 'PERMISSIVE', + }) + + ts.Disk.ssl_multicert_yaml.AddLines( + """ + ssl_multicert: + - dest_ip: "*" + ssl_cert_name: server.pem + ssl_key_name: server.key + """.split("\n")) + + ts.Disk.remap_config.AddLines([ + f'map / http://127.0.0.1:{server.Variables.http_port}', + ]) + + # Configure traffic_dump to dump body bytes (-b). + ts.Disk.plugin_config.AddLine(f'traffic_dump.so --logdir {replay_dir} --sample 1 --limit 1000000000 -b') + + ts_dump_0 = os.path.join(replay_dir, "127", "0000000000000000") + ts.Disk.File(ts_dump_0, exists=True) + + ts_dump_1 = os.path.join(replay_dir, "127", "0000000000000001") + ts.Disk.File(ts_dump_1, exists=True) + + ts_dump_2 = os.path.join(replay_dir, "127", "0000000000000002") + ts.Disk.File(ts_dump_2, exists=True) + + ts_dump_3 = os.path.join(replay_dir, "127", "0000000000000003") + ts.Disk.File(ts_dump_3, exists=True) + + ts.Disk.traffic_out.Content = Testers.ContainsExpression( + "Dumping body bytes: true", "Verify that dumping body bytes is enabled.") + + # Run our test traffic. + tr = urtest.AddTestRun("Run the test traffic.") + tr.AddVerifierClientProcess( + "client", + replay_file, + http_ports=[ts.Variables.port], + https_ports=[ts.Variables.ssl_port], + ssl_cert="ssl/server_combined.pem", + ca_cert="ssl/signer.pem") + + tr.Processes.Default.StartBefore(server) + tr.Processes.Default.StartBefore(ts) + tr.StillRunningAfter = server + tr.StillRunningAfter = ts + + # Common verification variables. + verify_replay = "verify_replay.py" + schema = os.path.join(urtest.Variables.AtsTestToolsDir, 'lib', 'replay_schema.json') + verify_command_prefix = f'{sys.executable} {verify_replay} {schema}' + + # + # Verify a response without a body is dumped correctly. + # + tr = urtest.AddTestRun("Verify the json content of the transaction with no response body.") + tr.Setup.CopyAs(verify_replay, urtest.RunDirectory) + tr.Processes.Default.Command = f'{verify_command_prefix} {ts_dump_0}' + tr.Processes.Default.ReturnCode = 0 + tr.StillRunningAfter = server + tr.StillRunningAfter = ts + + # + # Verify a response with a body is dumped correctly. + # + tr = urtest.AddTestRun("Verify the json content of the transaction with a response body.") + tr.Setup.CopyAs(verify_replay, urtest.RunDirectory) + tr.Processes.Default.Command = \ + f'{verify_command_prefix} {ts_dump_1} --response_body "0000000 0000001 "' + tr.Processes.Default.ReturnCode = 0 + tr.StillRunningAfter = server + tr.StillRunningAfter = ts + + # + # Verify a response with a character to be escaped is dumped correctly. + # + tr = urtest.AddTestRun("Verify the json content of the response body with a character to be escaped.") + tr.Setup.CopyAs(verify_replay, urtest.RunDirectory) + tr.Processes.Default.Command = \ + rf'{verify_command_prefix} {ts_dump_2} --response_body 12\"34' + tr.Processes.Default.ReturnCode = 0 + tr.StillRunningAfter = server + tr.StillRunningAfter = ts + # + # Verify an HTTP/2 response with a body is dumped correctly. + # + tr = urtest.AddTestRun("Verify the json content of the response body of an HTTP/2 transaction.") + tr.Setup.CopyAs(verify_replay, urtest.RunDirectory) + tr.Processes.Default.Command = \ + f'{verify_command_prefix} {ts_dump_3} --response_body "0000000 0000001 0000002 "' + tr.Processes.Default.ReturnCode = 0 + tr.StillRunningAfter = server + tr.StillRunningAfter = ts + urtest.execute() diff --git a/tests/uranium_tests/pluginTest/traffic_dump/test_traffic_dump_sni_filter.py b/tests/uranium_tests/pluginTest/traffic_dump/test_traffic_dump_sni_filter.py new file mode 100644 index 00000000000..36a09eb7924 --- /dev/null +++ b/tests/uranium_tests/pluginTest/traffic_dump/test_traffic_dump_sni_filter.py @@ -0,0 +1,149 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_traffic_dump_sni_filter(urtest: UraniumTest) -> None: + """ + Verify traffic_dump functionality. + """ + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import os + import sys + + urtest.Summary = ''' + Verify traffic_dump functionality. + ''' + + urtest.SkipUnless(Condition.PluginExists('traffic_dump.so'),) + + schema_path = os.path.join(urtest.Variables.AtsTestToolsDir, 'lib', 'replay_schema.json') + replay_file = "replay/various_sni.yaml" + server = urtest.MakeVerifierServerProcess( + "server-various-sni", replay_file, ssl_cert="ssl/server_combined.pem", ca_cert="ssl/signer.pem") + + # Define ATS and configure + ts = urtest.MakeATSProcess("ts", enable_tls=True) + replay_dir = os.path.join(ts.RunDirectory, "ts", "log") + + ts.addSSLfile("ssl/server.pem") + ts.addSSLfile("ssl/server.key") + ts.addSSLfile("ssl/signer.pem") + + ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'traffic_dump', + 'proxy.config.ssl.server.cert.path': ts.Variables.SSLDir, + 'proxy.config.ssl.server.private_key.path': ts.Variables.SSLDir, + 'proxy.config.url_remap.pristine_host_hdr': 1, + 'proxy.config.ssl.CA.cert.filename': f'{ts.Variables.SSLDir}/signer.pem', + 'proxy.config.exec_thread.autoconfig.scale': 1.0, + 'proxy.config.http.host_sni_policy': 2, + 'proxy.config.ssl.TLSv1_3.enabled': 0, + 'proxy.config.ssl.client.verify.server.policy': 'PERMISSIVE', + }) + + ts.Disk.ssl_multicert_yaml.AddLines( + """ + ssl_multicert: + - dest_ip: "*" + ssl_cert_name: server.pem + ssl_key_name: server.key + """.split("\n")) + + ts.Disk.remap_config.AddLine(f'map / https://127.0.0.1:{server.Variables.https_port}') + + ts.Disk.sni_yaml.AddLines([ + 'sni:', + '- fqdn: bob.com', + ' verify_client: NONE', + ' host_sni_policy: PERMISSIVE', + ]) + + # Configure traffic_dump's SNI filter to only dump connections with SNI bob.com. + sni_filter = "bob.com" + ts.Disk.plugin_config.AddLine(f'traffic_dump.so --logdir {replay_dir} --sample 1 ' + f'--sni-filter "{sni_filter}"') + + # Set up trafficserver expectations. + ts.Disk.traffic_out.Content += Testers.ContainsExpression( + f"Filtering to only dump connections with SNI: {sni_filter}", "Verify filtering for the expected SNI.") + + ts.Disk.traffic_out.Content += Testers.ContainsExpression( + "Ignore HTTPS session with non-filtered SNI: dave", "Verify that the non-desired SNI session was filtered out.") + + ts.Disk.traffic_out.Content += Testers.ContainsExpression( + "Initialized with sample pool size of 1 bytes and unlimited disk utilization", + "Verify traffic_dump initialized with the configured disk limit.") + + # Set up the json replay file expectations. + replay_file_session_1 = os.path.join(replay_dir, "127", "0000000000000000") + ts.Disk.File(replay_file_session_1, exists=True) + + # The second session should be filtered out because it doesn't have the + # expected SNI (note exists is set to False). + replay_file_session_2 = os.path.join(replay_dir, "127", "0000000000000001") + ts.Disk.File(replay_file_session_2, exists=False) + + # The third session should also be filtered out because it doesn't have any + # SNI (note exists is set to False). + replay_file_session_2 = os.path.join(replay_dir, "127", "0000000000000002") + ts.Disk.File(replay_file_session_2, exists=False) + + # Run the traffic with connections containing various SNI values. + tr = urtest.AddTestRun("Test SNI filter with various SNI values in the handshakes.") + # Use the same port across the two servers so that the remap config will work + # across both. + server_port = server.Variables.http_port + tr.AddVerifierClientProcess( + "client-various-sni", + replay_file, + https_ports=[ts.Variables.ssl_port], + ssl_cert="ssl/server_combined.pem", + ca_cert="ssl/signer.pem") + tr.Processes.Default.StartBefore(server) + tr.Processes.Default.StartBefore(ts) + tr.StillRunningAfter = server + tr.StillRunningAfter = ts + + # Verify the properties of the replay file for the dumped transaction. + tr = urtest.AddTestRun("Verify the json content of the first session") + session_1_protocols = "http,tls,tcp,ip" + session_1_tls_features = 'sni:bob.com,proxy-verify-mode:0,proxy-provided-cert:true' + verify_replay = "verify_replay.py" + tr.Setup.CopyAs(verify_replay, urtest.RunDirectory) + tr.Processes.Default.Command = \ + (f'{sys.executable} {verify_replay} {schema_path} {replay_file_session_1} ' + f'--client-protocols "{session_1_protocols}" --client-tls-features "{session_1_tls_features}"') + tr.Processes.Default.ReturnCode = 0 + urtest.execute() diff --git a/tests/gold_tests/pluginTest/traffic_dump/verify_replay.py b/tests/uranium_tests/pluginTest/traffic_dump/verify_replay.py similarity index 100% rename from tests/gold_tests/pluginTest/traffic_dump/verify_replay.py rename to tests/uranium_tests/pluginTest/traffic_dump/verify_replay.py diff --git a/tests/uranium_tests/pluginTest/transform/test_transaction_data_sink.py b/tests/uranium_tests/pluginTest/transform/test_transaction_data_sink.py new file mode 100644 index 00000000000..ec78cf8826a --- /dev/null +++ b/tests/uranium_tests/pluginTest/transform/test_transaction_data_sink.py @@ -0,0 +1,127 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_transaction_data_sink(urtest: UraniumTest) -> None: + ''' + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + urtest.Summary = ''' + Verify transaction data sink. + ''' + + urtest.SkipUnless(Condition.PluginExists('txn_data_sink.so'),) + + class TransactionDataSyncTest: + + replay_file = "transaction-with-body.replays.yaml" + + def __init__(self): + self._setupOriginServer() + self._setupNameserver() + self._setupTS() + + def _setupOriginServer(self): + self.server = urtest.MakeVerifierServerProcess("server", self.replay_file) + + def _setupNameserver(self): + self.nameserver = urtest.MakeDNServer("dns", default='127.0.0.1') + + def _setupTS(self): + self.ts = urtest.MakeATSProcess("ts", enable_cache=False, enable_tls=True) + self.ts.Disk.records_config.update( + { + "proxy.config.ssl.server.cert.path": f'{self.ts.Variables.SSLDir}', + "proxy.config.ssl.server.private_key.path": f'{self.ts.Variables.SSLDir}', + "proxy.config.ssl.client.verify.server.policy": 'PERMISSIVE', + "proxy.config.dns.nameservers": f"127.0.0.1:{self.nameserver.Variables.Port}", + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'http|txn_data_sink', + }) + self.ts.addDefaultSSLFiles() + self.ts.Disk.remap_config.AddLine(f'map / http://localhost:{self.server.Variables.http_port}/') + self.ts.Disk.ssl_multicert_yaml.AddLines( + """ + ssl_multicert: + - dest_ip: "*" + ssl_cert_name: server.pem + ssl_key_name: server.key + """.split("\n")) + self.ts.Disk.plugin_config.AddLine('txn_data_sink.so') + + # All of the bodies that contained "not_dumped" were not configured to + # be dumped. Therefore it is a bug if they show up in the logs. + self.ts.Disk.traffic_out.Content += Testers.ExcludesExpression('body_not_dumped', "An unexpected body was dumped.") + + # Verify that each of the configured transaction bodies were dumped. + self.ts.Disk.traffic_out.Content += Testers.ContainsExpression( + 'http1.1_cl_response_body_dumped', "The expected HTTP/1.1 Content-Length response body was dumped.") + self.ts.Disk.traffic_out.Content += Testers.ContainsExpression( + 'http1.1_chunked_response_body_dumped', "The expected HTTP/1.1 chunked response body was dumped.") + self.ts.Disk.traffic_out.Content += Testers.ContainsExpression( + 'http1.1_cl_request_body_dumped', "The expected HTTP/1.1 Content-Length request body was dumped.") + self.ts.Disk.traffic_out.Content += Testers.ContainsExpression( + 'http1.1_chunked_request_body_dumped', "The expected HTTP/1.1 chunked request body was dumped.") + self.ts.Disk.traffic_out.Content += Testers.ContainsExpression( + '"http2_response_body_dumped"', "The expected HTTP/2 response body was dumped.") + self.ts.Disk.traffic_out.Content += Testers.ContainsExpression( + 'http2_request_body_dumped', "The expected HTTP/2 request body was dumped.") + + def run(self): + """Configure a TestRun for the test.""" + tr = urtest.AddTestRun() + tr.Processes.Default.StartBefore(self.server) + tr.Processes.Default.StartBefore(self.nameserver) + tr.Processes.Default.StartBefore(self.ts) + tr.AddVerifierClientProcess( + "client", self.replay_file, http_ports=[self.ts.Variables.port], https_ports=[self.ts.Variables.ssl_port]) + tr.StillRunningAfter = self.server + tr.StillRunningAfter = self.nameserver + tr.StillRunningAfter = self.ts + + tr = urtest.AddTestRun("Wait for transaction data sink output") + timeout = 30 + watcher = tr.Processes.Process("watcher") + watcher.Command = f"sleep {timeout}" + watcher.Ready = When.FileContains(self.ts.Disk.traffic_out.Name, "http2_response_body_dumped") + watcher.TimeOut = timeout + tr.TimeOut = timeout + tr.StillRunningAfter = self.server + tr.StillRunningAfter = self.nameserver + tr.StillRunningAfter = self.ts + tr.Processes.Default.StartBefore(watcher) + tr.Processes.Default.Command = "echo await_transaction_data_sink_output" + tr.Processes.Default.ReturnCode = 0 + + TransactionDataSyncTest().run() + urtest.execute() diff --git a/tests/gold_tests/pluginTest/transform/transaction-with-body.replays.yaml b/tests/uranium_tests/pluginTest/transform/transaction-with-body.replays.yaml similarity index 100% rename from tests/gold_tests/pluginTest/transform/transaction-with-body.replays.yaml rename to tests/uranium_tests/pluginTest/transform/transaction-with-body.replays.yaml diff --git a/tests/gold_tests/pluginTest/tsapi/CMakeLists.txt b/tests/uranium_tests/pluginTest/tsapi/CMakeLists.txt similarity index 71% rename from tests/gold_tests/pluginTest/tsapi/CMakeLists.txt rename to tests/uranium_tests/pluginTest/tsapi/CMakeLists.txt index 516d89e514d..9bd2bfd8fba 100644 --- a/tests/gold_tests/pluginTest/tsapi/CMakeLists.txt +++ b/tests/uranium_tests/pluginTest/tsapi/CMakeLists.txt @@ -15,9 +15,9 @@ # ####################### -add_autest_plugin(test_tsapi test_tsapi.cc) -add_autest_plugin(test_TSHttpTxnServerAddrSet test_TSHttpTxnServerAddrSet.cc) -add_autest_plugin(test_TSHttpSsnInfo test_TSHttpSsnInfo.cc) -add_autest_plugin(test_TSVConnPPInfo test_TSVConnPPInfo.cc) -add_autest_plugin(test_TSHttpTxnVerifiedAddr test_TSHttpTxnVerifiedAddr.cc) -add_autest_plugin(test_TSHttpTxnServerAddrSet_retry test_TSHttpTxnServerAddrSet_retry.cc) +add_urtest_plugin(test_tsapi test_tsapi.cc) +add_urtest_plugin(test_TSHttpTxnServerAddrSet test_TSHttpTxnServerAddrSet.cc) +add_urtest_plugin(test_TSHttpSsnInfo test_TSHttpSsnInfo.cc) +add_urtest_plugin(test_TSVConnPPInfo test_TSVConnPPInfo.cc) +add_urtest_plugin(test_TSHttpTxnVerifiedAddr test_TSHttpTxnVerifiedAddr.cc) +add_urtest_plugin(test_TSHttpTxnServerAddrSet_retry test_TSHttpTxnServerAddrSet_retry.cc) diff --git a/tests/gold_tests/pluginTest/tsapi/hrw_verified_addr.conf b/tests/uranium_tests/pluginTest/tsapi/hrw_verified_addr.conf similarity index 100% rename from tests/gold_tests/pluginTest/tsapi/hrw_verified_addr.conf rename to tests/uranium_tests/pluginTest/tsapi/hrw_verified_addr.conf diff --git a/tests/gold_tests/pluginTest/tsapi/log.gold b/tests/uranium_tests/pluginTest/tsapi/log.gold similarity index 100% rename from tests/gold_tests/pluginTest/tsapi/log.gold rename to tests/uranium_tests/pluginTest/tsapi/log.gold diff --git a/tests/gold_tests/pluginTest/tsapi/log_uds.gold b/tests/uranium_tests/pluginTest/tsapi/log_uds.gold similarity index 100% rename from tests/gold_tests/pluginTest/tsapi/log_uds.gold rename to tests/uranium_tests/pluginTest/tsapi/log_uds.gold diff --git a/tests/gold_tests/pluginTest/tsapi/test_TSHttpSsnInfo.cc b/tests/uranium_tests/pluginTest/tsapi/test_TSHttpSsnInfo.cc similarity index 97% rename from tests/gold_tests/pluginTest/tsapi/test_TSHttpSsnInfo.cc rename to tests/uranium_tests/pluginTest/tsapi/test_TSHttpSsnInfo.cc index c87bee36651..db49e509439 100644 --- a/tests/gold_tests/pluginTest/tsapi/test_TSHttpSsnInfo.cc +++ b/tests/uranium_tests/pluginTest/tsapi/test_TSHttpSsnInfo.cc @@ -28,7 +28,7 @@ char PIName[] = PINAME; DbgCtl dbg_ctl{PIName}; -// NOTE: It's important to flush this after writing so that a gold test using this plugin can examine the log before TS +// NOTE: It's important to flush this after writing so that a Uranium test using this plugin can examine the log before TS // terminates. // std::fstream logFile; diff --git a/tests/gold_tests/pluginTest/tsapi/test_TSHttpSsnInfo_curl0.gold b/tests/uranium_tests/pluginTest/tsapi/test_TSHttpSsnInfo_curl0.gold similarity index 100% rename from tests/gold_tests/pluginTest/tsapi/test_TSHttpSsnInfo_curl0.gold rename to tests/uranium_tests/pluginTest/tsapi/test_TSHttpSsnInfo_curl0.gold diff --git a/tests/gold_tests/pluginTest/tsapi/test_TSHttpSsnInfo_nghttp0.gold b/tests/uranium_tests/pluginTest/tsapi/test_TSHttpSsnInfo_nghttp0.gold similarity index 100% rename from tests/gold_tests/pluginTest/tsapi/test_TSHttpSsnInfo_nghttp0.gold rename to tests/uranium_tests/pluginTest/tsapi/test_TSHttpSsnInfo_nghttp0.gold diff --git a/tests/gold_tests/pluginTest/tsapi/test_TSHttpSsnInfo_plugin_log.gold b/tests/uranium_tests/pluginTest/tsapi/test_TSHttpSsnInfo_plugin_log.gold similarity index 100% rename from tests/gold_tests/pluginTest/tsapi/test_TSHttpSsnInfo_plugin_log.gold rename to tests/uranium_tests/pluginTest/tsapi/test_TSHttpSsnInfo_plugin_log.gold diff --git a/tests/gold_tests/pluginTest/tsapi/test_TSHttpTxnServerAddrSet.cc b/tests/uranium_tests/pluginTest/tsapi/test_TSHttpTxnServerAddrSet.cc similarity index 100% rename from tests/gold_tests/pluginTest/tsapi/test_TSHttpTxnServerAddrSet.cc rename to tests/uranium_tests/pluginTest/tsapi/test_TSHttpTxnServerAddrSet.cc diff --git a/tests/gold_tests/pluginTest/tsapi/test_TSHttpTxnServerAddrSet.replay.yaml b/tests/uranium_tests/pluginTest/tsapi/test_TSHttpTxnServerAddrSet.replay.yaml similarity index 100% rename from tests/gold_tests/pluginTest/tsapi/test_TSHttpTxnServerAddrSet.replay.yaml rename to tests/uranium_tests/pluginTest/tsapi/test_TSHttpTxnServerAddrSet.replay.yaml diff --git a/tests/gold_tests/pluginTest/tsapi/test_TSHttpTxnServerAddrSet_retry.cc b/tests/uranium_tests/pluginTest/tsapi/test_TSHttpTxnServerAddrSet_retry.cc similarity index 100% rename from tests/gold_tests/pluginTest/tsapi/test_TSHttpTxnServerAddrSet_retry.cc rename to tests/uranium_tests/pluginTest/tsapi/test_TSHttpTxnServerAddrSet_retry.cc diff --git a/tests/gold_tests/pluginTest/tsapi/test_TSHttpTxnVerifiedAddr.cc b/tests/uranium_tests/pluginTest/tsapi/test_TSHttpTxnVerifiedAddr.cc similarity index 100% rename from tests/gold_tests/pluginTest/tsapi/test_TSHttpTxnVerifiedAddr.cc rename to tests/uranium_tests/pluginTest/tsapi/test_TSHttpTxnVerifiedAddr.cc diff --git a/tests/gold_tests/pluginTest/tsapi/test_TSVConnPPInfo.cc b/tests/uranium_tests/pluginTest/tsapi/test_TSVConnPPInfo.cc similarity index 96% rename from tests/gold_tests/pluginTest/tsapi/test_TSVConnPPInfo.cc rename to tests/uranium_tests/pluginTest/tsapi/test_TSVConnPPInfo.cc index 6aa78118ace..87cec970e63 100644 --- a/tests/gold_tests/pluginTest/tsapi/test_TSVConnPPInfo.cc +++ b/tests/uranium_tests/pluginTest/tsapi/test_TSVConnPPInfo.cc @@ -29,7 +29,7 @@ char PIName[] = PINAME; DbgCtl dbg_ctl{PIName}; -// NOTE: It's important to flush this after writing so that a gold test using this plugin can examine the log before TS +// NOTE: It's important to flush this after writing so that a Uranium test using this plugin can examine the log before TS // terminates. // std::fstream logFile; diff --git a/tests/gold_tests/pluginTest/tsapi/test_TSVConnPPInfo_curl0.gold b/tests/uranium_tests/pluginTest/tsapi/test_TSVConnPPInfo_curl0.gold similarity index 100% rename from tests/gold_tests/pluginTest/tsapi/test_TSVConnPPInfo_curl0.gold rename to tests/uranium_tests/pluginTest/tsapi/test_TSVConnPPInfo_curl0.gold diff --git a/tests/gold_tests/pluginTest/tsapi/test_TSVConnPPInfo_curl1.gold b/tests/uranium_tests/pluginTest/tsapi/test_TSVConnPPInfo_curl1.gold similarity index 100% rename from tests/gold_tests/pluginTest/tsapi/test_TSVConnPPInfo_curl1.gold rename to tests/uranium_tests/pluginTest/tsapi/test_TSVConnPPInfo_curl1.gold diff --git a/tests/gold_tests/pluginTest/tsapi/test_TSVConnPPInfo_plugin_log.gold b/tests/uranium_tests/pluginTest/tsapi/test_TSVConnPPInfo_plugin_log.gold similarity index 100% rename from tests/gold_tests/pluginTest/tsapi/test_TSVConnPPInfo_plugin_log.gold rename to tests/uranium_tests/pluginTest/tsapi/test_TSVConnPPInfo_plugin_log.gold diff --git a/tests/uranium_tests/pluginTest/tsapi/test_test_TSHttpSsnInfo.py b/tests/uranium_tests/pluginTest/tsapi/test_test_TSHttpSsnInfo.py new file mode 100644 index 00000000000..fd385f556db --- /dev/null +++ b/tests/uranium_tests/pluginTest/tsapi/test_test_TSHttpSsnInfo.py @@ -0,0 +1,134 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_test_TSHttpSsnInfo(urtest: UraniumTest) -> None: + ''' + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import os + + urtest.Summary = ''' + Test TS API to get H2 Session info and H2 metrics + ''' + + urtest.SkipUnless(Condition.HasProgram("nghttp", "Nghttp need to be installed on system for this test to work"),) + urtest.ContinueOnFail = True + + # ---- + # Setup Origin Server + # ---- + httpbin = urtest.MakeHttpBinServer("httpbin") + + # 128ytes + post_body = "0123456789abcdef" * 8 + post_body_file = open(os.path.join(urtest.RunDirectory, "post_body"), "w") + post_body_file.write(post_body) + post_body_file.close() + + # ---- + # Setup ATS + # ---- + if Condition.HasATSFeature('TS_USE_QUIC') and Condition.HasCurlFeature('http3'): + ts = urtest.MakeATSProcess("ts", enable_tls=True, enable_quic=True) + else: + ts = urtest.MakeATSProcess("ts", enable_tls=True) + + # add ssl materials like key, certificates for the server + ts.addDefaultSSLFiles() + + ts.Disk.remap_config.AddLines(['map /httpbin/ http://127.0.0.1:{0}/'.format(httpbin.Variables.Port)]) + + ts.Disk.ssl_multicert_yaml.AddLines( + """ + ssl_multicert: + - dest_ip: "*" + ssl_cert_name: server.pem + ssl_key_name: server.key + """.split("\n")) + + urtest.PrepareTestPlugin( + os.path.join(urtest.Variables.AtsBuildUraniumTestsDir, 'pluginTest', 'tsapi', '.libs', 'test_TSHttpSsnInfo.so'), ts) + + ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'http2|http3|quic|test_TSHttpSsnInfo', + 'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir), + 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts.Variables.SSLDir) + }) + + # http2_info.so will output test logging to this file. + log_path = os.path.join(ts.Variables.LOGDIR, "test_TSHttpSsnInfo_plugin_log.txt") + urtest.Env["OUTPUT_FILE"] = log_path + + # ---- + # Test Cases + # ---- + + # H2 SETTINGS, PRIORITY, HEADERS, CONTINUATION, DATA, GOAWAY + tr = urtest.AddTestRun('HTTP/2 Test') + tr.TimeOut = 10 + # grep out the continuation lines because that significantly reduces the output + # size which exponentially reduces the gold file processing time. + tr.Processes.Default.Command = f"nghttp -vn --continuation 'https://localhost:{ts.Variables.ssl_port}/httpbin/post' -d 'post_body' | grep -v 'continuation-test'" + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.StartBefore(httpbin) + tr.Processes.Default.StartBefore(urtest.Processes.ts) + tr.Processes.Default.Streams.stdout = "test_TSHttpSsnInfo_nghttp0.gold" + tr.StillRunningAfter = httpbin + tr.StillRunningAfter = ts + + # H3 + if Condition.HasATSFeature('TS_USE_QUIC') and Condition.HasCurlFeature('http3'): + tr = urtest.AddTestRun('HTTP/3 Test') + tr.TimeOut = 10 + tr.MakeCurlCommand(f"-k --http3 'https://localhost:{ts.Variables.ssl_port}/httpbin/post' -d 'post_body'") + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.stdout = "test_TSHttpSsnInfo_curl0.gold" + tr.StillRunningAfter = httpbin + tr.StillRunningAfter = ts + + tr = urtest.AddTestRun('Check the log output') + tr.Processes.Default.Command = "echo check log" + tr.Processes.Default.ReturnCode = 0 + f = tr.Disk.File(log_path) + f.Content = "test_TSHttpSsnInfo_plugin_log.gold" + f.Content += Testers.ContainsExpression( + "H2 Frames Received:D1,H1,PR.,RS0,S2,PP0,P0,G1,WU0,C1,U0", "Expected numbers of frames should be received") + f.Content += Testers.ContainsExpression( + "H2 OOB\\(11\\)=0,OOB\\(1000\\)=0", "OOB sub_key values should map to UNKNOWN bucket without crashing") + # We cannot test this on H3 now because the test plugin does not work on H3 sessions + # f.Content += Testers.ContainsExpression("H3 Frames Received:D1,H1,Ra0,CP0,S1,PP0,Rb0,G0,Rc0,Rd0,UND0,UND0,UND0,MPI0,U0", + # "Expected numbers of frames should be received") + urtest.execute() diff --git a/tests/uranium_tests/pluginTest/tsapi/test_test_TSHttpTxnServerAddrSet.py b/tests/uranium_tests/pluginTest/tsapi/test_test_TSHttpTxnServerAddrSet.py new file mode 100644 index 00000000000..5b9dd6fadad --- /dev/null +++ b/tests/uranium_tests/pluginTest/tsapi/test_test_TSHttpTxnServerAddrSet.py @@ -0,0 +1,112 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_test_TSHttpTxnServerAddrSet(urtest: UraniumTest) -> None: + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import os + from ports import get_port + + urtest.Summary = ''' + Verify TSHttpTxnServerAddrSet() works as expected. + ''' + + plugin_name = "test_TSHttpTxnServerAddrSet" + + class TestTSHttpTxnServerAddrSet: + """Verify that TSHttpTxnServerAddrSet() works as expected.""" + _replay_file = "test_TSHttpTxnServerAddrSet.replay.yaml" + + def _configure_server(self, tr: 'TestRun') -> 'Process': + """Configure the server process for a TestRun. + + :param tr: The TestRun for which to configure the server process. + """ + self._server = tr.AddVerifierServerProcess("server", self._replay_file) + self._server.Streams.stdout += Testers.ContainsExpression( + "redirect-succeeded", "Verify that the server sent the response.") + return self._server + + def _configure_dns(self, tr: 'TestRun') -> 'Process': + """Configure the DNS process for a TestRun. + + :param tr: The TestRun for which to configure the DNS process. + """ + self._dns = tr.MakeDNServer("dns", default=['127.0.0.1']) + return self._dns + + def _configure_traffic_server(self, tr: 'TestRun') -> 'Process': + """Configure the Traffic Server process for a TestRun. + + :param tr: The TestRun for which to configure the Traffic Server process. + """ + self._ts = tr.MakeATSProcess("ts") + ts = self._ts + + plugin_path = os.path.join( + urtest.Variables.AtsBuildUraniumTestsDir, 'pluginTest', 'tsapi', '.libs', f'{plugin_name}.so') + ts.Setup.Copy(plugin_path, ts.Env['PROXY_CONFIG_PLUGIN_PLUGIN_DIR']) + ts.Disk.plugin_config.AddLine(f"{plugin_path} 127.0.0.1 {self._server.Variables.http_port}") + + ts.Disk.records_config.update( + { + 'proxy.config.dns.nameservers': f'127.0.0.1:{self._dns.Variables.Port}', + 'proxy.config.dns.resolv_conf': 'NULL', + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'http|dns|hostdb|test_plugin', + }) + + # Remap to a nonexisting server and port. The plugin should use + # TSHttpTxnServerAddrSet() to set an actual server address and port. We + # use get_port to guarantee that no one is listing on this port. + bogus_port = get_port(self._server, "bogus_port") + ts.Disk.remap_config.AddLine(f'map / http://non.existent.server.com:{bogus_port}') + + def run(self): + """ Configure the TestRun.""" + tr = urtest.AddTestRun("Verify TSHttpTxnServerAddrSet() works as expected.") + self._configure_dns(tr) + self._configure_server(tr) + self._configure_traffic_server(tr) + tr.AddVerifierClientProcess("client", self._replay_file, http_ports=[self._ts.Variables.port]) + + tr.Processes.Default.Streams.stdout += Testers.ContainsExpression( + "redirect-succeeded", "Verify that the client received the response.") + + tr.Processes.Default.StartBefore(self._dns) + tr.Processes.Default.StartBefore(self._server) + tr.Processes.Default.StartBefore(self._ts) + + test = TestTSHttpTxnServerAddrSet() + test.run() + urtest.execute() diff --git a/tests/uranium_tests/pluginTest/tsapi/test_test_TSHttpTxnServerAddrSet_retry.py b/tests/uranium_tests/pluginTest/tsapi/test_test_TSHttpTxnServerAddrSet_retry.py new file mode 100644 index 00000000000..67720e5f31e --- /dev/null +++ b/tests/uranium_tests/pluginTest/tsapi/test_test_TSHttpTxnServerAddrSet_retry.py @@ -0,0 +1,111 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_test_TSHttpTxnServerAddrSet_retry(urtest: UraniumTest) -> None: + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + ''' + Tests for TSHttpTxnServerAddrSet() retry behavior with and without cache enabled. + ''' + + import os + from ports import get_port + + urtest.Summary = ''' + Verify TSHttpTxnServerAddrSet() retry works with and without cache enabled + ''' + + plugin_name = "test_TSHttpTxnServerAddrSet_retry" + + class TestServerAddrSetRetry: + """Verify retry behavior for TSHttpTxnServerAddrSet() with and without cache.""" + + def _configure_dns(self, tr: 'TestRun', name: str) -> 'Process': + """Configure the DNS process for a TestRun.""" + return tr.MakeDNServer(name, default=['127.0.0.1']) + + def _configure_traffic_server(self, tr: 'TestRun', name: str, dns: 'Process', enable_cache: bool) -> 'Process': + """Configure the Traffic Server process for a TestRun.""" + ts = tr.MakeATSProcess(name, enable_cache=enable_cache) + + plugin_path = os.path.join( + urtest.Variables.AtsBuildUraniumTestsDir, 'pluginTest', 'tsapi', '.libs', f'{plugin_name}.so') + ts.Setup.Copy(plugin_path, ts.Env['PROXY_CONFIG_PLUGIN_PLUGIN_DIR']) + ts.Disk.plugin_config.AddLine(f"{plugin_path}") + + ts.Disk.records_config.update( + { + 'proxy.config.dns.nameservers': f'127.0.0.1:{dns.Variables.Port}', + 'proxy.config.dns.resolv_conf': 'NULL', + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'http|dns|test_TSHttpTxnServerAddrSet_retry', + # Enable retries so we can see if OS_DNS is called multiple times + 'proxy.config.http.connect_attempts_max_retries': 3, + 'proxy.config.http.connect_attempts_timeout': 1, + }) + + # Remap to a nonexisting server - the plugin will set addresses + bogus_port = get_port(ts, "bogus_port") + ts.Disk.remap_config.AddLine(f'map / http://non.existent.server.com:{bogus_port}') + return ts + + def run(self, enable_cache: bool, description: str, name_suffix: str) -> None: + """Configure a TestRun.""" + tr = urtest.AddTestRun(description) + dns = self._configure_dns(tr, f"dns_{name_suffix}") + ts = self._configure_traffic_server(tr, f"ts_{name_suffix}", dns, enable_cache) + + # Make a simple request - it should fail since first address is non-routable + # but the plugin should log whether OS_DNS was called multiple times + tr.Processes.Default.Command = f'curl -vs --connect-timeout 5 http://127.0.0.1:{ts.Variables.port}/ -o /dev/null 2>&1; true' + tr.Processes.Default.ReturnCode = 0 + + tr.Processes.Default.StartBefore(dns) + tr.Processes.Default.StartBefore(ts) + + # Override the default diags.log error check - we use TSError() for test output + # Using '=' instead of '+=' replaces the default "no errors" check + ts.Disk.diags_log.Content = Testers.ContainsExpression("OS_DNS hook called, count=1", "First OS_DNS call logged") + + ts.Disk.diags_log.Content += Testers.ContainsExpression( + "SUCCESS: OS_DNS hook was called", "Plugin was able to retry with different address") + + if enable_cache: + ts.Disk.traffic_out.Content = Testers.ExcludesExpression( + "failed assertion", "ATS should not hit the redirect write-lock assertion") + ts.Disk.traffic_out.Content += Testers.ExcludesExpression( + "received signal 6", "ATS should not abort while retrying origin selection") + + test = TestServerAddrSetRetry() + test.run(enable_cache=False, description="Reproduce issue #12611 without cache", name_suffix="nocache") + test.run(enable_cache=True, description="Verify TSHttpTxnServerAddrSet retry is safe with cache enabled", name_suffix="cache") + urtest.execute() diff --git a/tests/uranium_tests/pluginTest/tsapi/test_test_TSHttpTxnVerifiedAddr.py b/tests/uranium_tests/pluginTest/tsapi/test_test_TSHttpTxnVerifiedAddr.py new file mode 100644 index 00000000000..c5f20450a54 --- /dev/null +++ b/tests/uranium_tests/pluginTest/tsapi/test_test_TSHttpTxnVerifiedAddr.py @@ -0,0 +1,86 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_test_TSHttpTxnVerifiedAddr(urtest: UraniumTest) -> None: + ''' + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import os + + urtest.Summary = ''' + Test TS API to get and set a verified address + ''' + + urtest.ContinueOnFail = True + + # ---- + # Setup Origin Server + # ---- + httpbin = urtest.MakeHttpBinServer("httpbin") + + # ---- + # Setup ATS + # ---- + ts = urtest.MakeATSProcess("ts") + + ts.Disk.remap_config.AddLine(f'map /httpbin/ http://127.0.0.1:{httpbin.Variables.Port}/') + + urtest.PrepareTestPlugin( + os.path.join(urtest.Variables.AtsBuildUraniumTestsDir, 'pluginTest', 'tsapi', '.libs', 'test_TSHttpTxnVerifiedAddr.so'), ts) + + ts.Setup.CopyAs('hrw_verified_addr.conf', urtest.RunDirectory) + ts.Disk.plugin_config.AddLine(f'header_rewrite.so --inbound-ip-source=PLUGIN {urtest.RunDirectory}/hrw_verified_addr.conf') + + ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'http|test_TSHttpTxnVerifiedAddr', + }) + + # ---- + # Test Cases + # ---- + + # The test plugin sets an hard coded IP address as a verified address. + # The verified address should be read by header_rewrite plugin and returned in "ip" response header. + tr = urtest.AddTestRun("Test that a verified address is set by a plugin and read by another plugin") + tr.MakeCurlCommand(f'-v http://127.0.0.1:{ts.Variables.port}/httpbin/get', ts=ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.StartBefore(httpbin) + tr.Processes.Default.StartBefore(urtest.Processes.ts) + tr.Processes.Default.Streams.stderr.Content = Testers.ContainsExpression( + "ip: 1.1.1.1", "Verifiy header_rewrite picked the verified address") + tr.StillRunningAfter = httpbin + tr.StillRunningAfter = ts + urtest.execute() diff --git a/tests/uranium_tests/pluginTest/tsapi/test_test_TSVConnPPInfo.py b/tests/uranium_tests/pluginTest/tsapi/test_test_TSVConnPPInfo.py new file mode 100644 index 00000000000..99339586c6c --- /dev/null +++ b/tests/uranium_tests/pluginTest/tsapi/test_test_TSVConnPPInfo.py @@ -0,0 +1,133 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_test_TSVConnPPInfo(urtest: UraniumTest) -> None: + ''' + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import os + + urtest.Summary = ''' + Test TS API to get PROXY protocol info + ''' + + urtest.SkipUnless( + Condition.HasProgram("nghttp", "Nghttp need to be installed on system for this test to work"), + Condition.HasCurlOption("--haproxy-clientip"), + ) + urtest.ContinueOnFail = True + + # ---- + # Setup Origin Server + # ---- + httpbin = urtest.MakeHttpBinServer("httpbin") + + # 128ytes + post_body = "0123456789abcdef" * 8 + post_body_file = open(os.path.join(urtest.RunDirectory, "post_body"), "w") + post_body_file.write(post_body) + post_body_file.close() + + # ---- + # Setup ATS + # ---- + ts = urtest.MakeATSProcess("ts", enable_tls=True, enable_proxy_protocol=True) + + # add ssl materials like key, certificates for the server + ts.addDefaultSSLFiles() + + ts.Disk.remap_config.AddLines(['map /httpbin/ http://127.0.0.1:{0}/'.format(httpbin.Variables.Port)]) + + ts.Disk.ssl_multicert_yaml.AddLines( + """ + ssl_multicert: + - dest_ip: "*" + ssl_cert_name: server.pem + ssl_key_name: server.key + """.split("\n")) + + urtest.PrepareTestPlugin( + os.path.join(urtest.Variables.AtsBuildUraniumTestsDir, 'pluginTest', 'tsapi', '.libs', 'test_TSVConnPPInfo.so'), ts) + + ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'http|proxyprotocol|test_TSVConnPPInfo', + 'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir), + 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts.Variables.SSLDir) + }) + + # http2_info.so will output test logging to this file. + log_path = os.path.join(ts.Variables.LOGDIR, "test_TSVConnPPInfo_plugin_log.txt") + urtest.Env["OUTPUT_FILE"] = log_path + + # ---- + # Test Cases + # ---- + + # plaintext HTTP + tr = urtest.AddTestRun() + tr.TimeOut = 10 + tr.Processes.Default.Command = ( + f"curl --haproxy-protocol --haproxy-clientip 1.2.3.4 " + f"'http://127.0.0.1:{ts.Variables.proxy_protocol_port}/httpbin/get'") + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.StartBefore(httpbin) + tr.Processes.Default.StartBefore(urtest.Processes.ts) + tr.Processes.Default.Streams.stdout = "test_TSVConnPPInfo_curl0.gold" + tr.StillRunningAfter = httpbin + tr.StillRunningAfter = ts + + # HTTPS + tr = urtest.AddTestRun() + tr.TimeOut = 10 + tr.Processes.Default.Command = ( + f"curl --haproxy-protocol --haproxy-clientip 5.6.7.8 -k " + f"'https://127.0.0.1:{ts.Variables.proxy_protocol_ssl_port}/httpbin/get'") + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.stdout = "test_TSVConnPPInfo_curl1.gold" + tr.StillRunningAfter = httpbin + tr.StillRunningAfter = ts + + tr = urtest.AddTestRun() + tr.Processes.Default.Command = "echo check log" + tr.Processes.Default.ReturnCode = 0 + f = tr.Disk.File(log_path) + f.Content = "test_TSVConnPPInfo_plugin_log.gold" + # curl 8.20+ intentionally uses --haproxy-clientip for both PROXY addresses so the address family matches. + f.Content += Testers.ContainsExpression( + r"PP Info Received:V1,P2,T1,SRC1\.2\.3\.4,DST(127\.0\.0\.1|1\.2\.3\.4)", "Expected information should be received") + f.Content += Testers.ContainsExpression( + r"PP Info Received:V1,P2,T1,SRC5\.6\.7\.8,DST(127\.0\.0\.1|5\.6\.7\.8)", "Expected information should be received") + urtest.execute() diff --git a/tests/gold_tests/pluginTest/tsapi/test_tsapi.cc b/tests/uranium_tests/pluginTest/tsapi/test_tsapi.cc similarity index 98% rename from tests/gold_tests/pluginTest/tsapi/test_tsapi.cc rename to tests/uranium_tests/pluginTest/tsapi/test_tsapi.cc index 743393247e0..fe297012da5 100644 --- a/tests/gold_tests/pluginTest/tsapi/test_tsapi.cc +++ b/tests/uranium_tests/pluginTest/tsapi/test_tsapi.cc @@ -44,7 +44,7 @@ char PIName[] = PINAME; DbgCtl dbg_ctl{PIName}; DbgCtl off_dbg_ctl{"yada-yada-yada"}; -// NOTE: It's important to flush this after writing so that a gold test using this plugin can examine the log before TS +// NOTE: It's important to flush this after writing so that a Uranium test using this plugin can examine the log before TS // terminates. // std::fstream logFile; diff --git a/tests/uranium_tests/pluginTest/tsapi/test_tsapi.py b/tests/uranium_tests/pluginTest/tsapi/test_tsapi.py new file mode 100644 index 00000000000..6fd4fc34791 --- /dev/null +++ b/tests/uranium_tests/pluginTest/tsapi/test_tsapi.py @@ -0,0 +1,135 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_tsapi(urtest: UraniumTest) -> None: + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import os + + urtest.Summary = ''' + Test TS API. + ''' + + urtest.SkipUnless(Condition.HasCurlFeature('http2'),) + urtest.ContinueOnFail = True + + plugin_name = "test_tsapi" + + server = urtest.MakeOriginServer("server") + + request_header = {"headers": "GET / HTTP/1.1\r\nHost: doesnotmatter\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": "112233"} + server.addResponse("sessionlog.json", request_header, response_header) + + request_header = {"headers": "GET /xYz HTTP/1.1\r\nHost: doesnotmatter\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": "445566"} + server.addResponse("sessionlog.json", request_header, response_header) + + # Disable the cache to make sure each request is forwarded to the origin + # server. + ts = urtest.MakeATSProcess("ts", enable_tls=True, enable_cache=False) + + # The test plugin will output test logging to this file. + log_file_name = os.path.join(ts.Variables.LOGDIR, "log.txt") + urtest.Env["OUTPUT_FILE"] = log_file_name + + ts.addDefaultSSLFiles() + + ts.Disk.records_config.update( + { + 'proxy.config.proxy_name': 'Poxy_Proxy', # This will be the server name. + 'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir), + 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts.Variables.SSLDir), + 'proxy.config.url_remap.remap_required': 1, + 'proxy.config.diags.debug.enabled': 3, + 'proxy.config.diags.debug.tags': f'http|{plugin_name}', + }) + + ts.Disk.ssl_multicert_yaml.AddLines( + """ + ssl_multicert: + - dest_ip: "*" + ssl_cert_name: server.pem + ssl_key_name: server.key + """.split("\n")) + + rp = os.path.join(urtest.Variables.AtsBuildUraniumTestsDir, 'pluginTest', 'tsapi', '.libs', f'{plugin_name}.so') + ts.Setup.Copy(rp, ts.Env['PROXY_CONFIG_PLUGIN_PLUGIN_DIR']) + + ts.Disk.remap_config.AddLine( + "map http://myhost.test http://127.0.0.1:{0} @plugin={1} @plugin={1}".format(server.Variables.Port, f"{plugin_name}.so")) + ts.Disk.remap_config.AddLine( + "map https://myhost.test:123 http://127.0.0.1:{0} @plugin={1} @plugin={1}".format( + server.Variables.Port, f"{plugin_name}.so")) + + ipv4flag = "" + if not Condition.CurlUsingUnixDomainSocket(): + ipv4flag = "--ipv4" + + # For some reason, without this delay, traffic_server cannot reliably open the cleartext port for listening without an + # error. + # + tr = urtest.AddTestRun() + tr.Processes.Default.Command = "sleep 3" + tr.Processes.Default.ReturnCode = 0 + + tr = urtest.AddTestRun() + tr.Processes.Default.StartBefore(server) + tr.Processes.Default.StartBefore(ts) + # + tr.MakeCurlCommand( + '--verbose {0} --header "Host: mYhOsT.teSt" hTtP://loCalhOst:{1}/'.format(ipv4flag, ts.Variables.port), ts=ts) + tr.Processes.Default.ReturnCode = 0 + + tr = urtest.AddTestRun() + tr.MakeCurlCommand('--verbose {0} --proxy localhost:{1} http://mYhOsT.teSt/xYz'.format(ipv4flag, ts.Variables.port), ts=ts) + + if not Condition.CurlUsingUnixDomainSocket(): + tr = urtest.AddTestRun() + tr.MakeCurlCommand( + '--verbose --ipv4 --http2 --insecure --header ' + + '"Host: myhost.test:123" HttPs://LocalHost:{}/'.format(ts.Variables.ssl_port), + ts=ts) + tr.Processes.Default.ReturnCode = 0 + + tr = urtest.AddTestRun() + # Change server port number (which can vary) to a fixed string for compare to gold file. + second_log_file_name = os.path.join(ts.Variables.LOGDIR, "log2.txt") + tr.Processes.Default.Command = f"sed 's/{server.Variables.Port}/SERVER_PORT/' < {log_file_name} > {second_log_file_name}" + tr.Processes.Default.ReturnCode = 0 + f = tr.Disk.File(second_log_file_name) + if Condition.CurlUsingUnixDomainSocket(): + f.Content = "log_uds.gold" + else: + f.Content = "log.gold" + urtest.execute() diff --git a/tests/gold_tests/pluginTest/txn_box/basic/basic.replay.yaml b/tests/uranium_tests/pluginTest/txn_box/basic/basic.replay.yaml similarity index 100% rename from tests/gold_tests/pluginTest/txn_box/basic/basic.replay.yaml rename to tests/uranium_tests/pluginTest/txn_box/basic/basic.replay.yaml diff --git a/tests/gold_tests/pluginTest/txn_box/basic/cmp.replay.yaml b/tests/uranium_tests/pluginTest/txn_box/basic/cmp.replay.yaml similarity index 100% rename from tests/gold_tests/pluginTest/txn_box/basic/cmp.replay.yaml rename to tests/uranium_tests/pluginTest/txn_box/basic/cmp.replay.yaml diff --git a/tests/gold_tests/pluginTest/txn_box/basic/ip-addr.replay.yaml b/tests/uranium_tests/pluginTest/txn_box/basic/ip-addr.replay.yaml similarity index 100% rename from tests/gold_tests/pluginTest/txn_box/basic/ip-addr.replay.yaml rename to tests/uranium_tests/pluginTest/txn_box/basic/ip-addr.replay.yaml diff --git a/tests/gold_tests/pluginTest/txn_box/basic/mod.replay.yaml b/tests/uranium_tests/pluginTest/txn_box/basic/mod.replay.yaml similarity index 100% rename from tests/gold_tests/pluginTest/txn_box/basic/mod.replay.yaml rename to tests/uranium_tests/pluginTest/txn_box/basic/mod.replay.yaml diff --git a/tests/gold_tests/pluginTest/txn_box/basic/multi-cfg.1.yaml b/tests/uranium_tests/pluginTest/txn_box/basic/multi-cfg.1.yaml similarity index 100% rename from tests/gold_tests/pluginTest/txn_box/basic/multi-cfg.1.yaml rename to tests/uranium_tests/pluginTest/txn_box/basic/multi-cfg.1.yaml diff --git a/tests/gold_tests/pluginTest/txn_box/basic/multi-cfg.2.yaml b/tests/uranium_tests/pluginTest/txn_box/basic/multi-cfg.2.yaml similarity index 100% rename from tests/gold_tests/pluginTest/txn_box/basic/multi-cfg.2.yaml rename to tests/uranium_tests/pluginTest/txn_box/basic/multi-cfg.2.yaml diff --git a/tests/gold_tests/pluginTest/txn_box/basic/multi-cfg.replay.yaml b/tests/uranium_tests/pluginTest/txn_box/basic/multi-cfg.replay.yaml similarity index 100% rename from tests/gold_tests/pluginTest/txn_box/basic/multi-cfg.replay.yaml rename to tests/uranium_tests/pluginTest/txn_box/basic/multi-cfg.replay.yaml diff --git a/tests/gold_tests/pluginTest/txn_box/basic/redirect.replay.yaml b/tests/uranium_tests/pluginTest/txn_box/basic/redirect.replay.yaml similarity index 100% rename from tests/gold_tests/pluginTest/txn_box/basic/redirect.replay.yaml rename to tests/uranium_tests/pluginTest/txn_box/basic/redirect.replay.yaml diff --git a/tests/gold_tests/pluginTest/txn_box/basic/reply.replay.yaml b/tests/uranium_tests/pluginTest/txn_box/basic/reply.replay.yaml similarity index 100% rename from tests/gold_tests/pluginTest/txn_box/basic/reply.replay.yaml rename to tests/uranium_tests/pluginTest/txn_box/basic/reply.replay.yaml diff --git a/tests/gold_tests/pluginTest/txn_box/basic/rxp.replay.yaml b/tests/uranium_tests/pluginTest/txn_box/basic/rxp.replay.yaml similarity index 100% rename from tests/gold_tests/pluginTest/txn_box/basic/rxp.replay.yaml rename to tests/uranium_tests/pluginTest/txn_box/basic/rxp.replay.yaml diff --git a/tests/gold_tests/pluginTest/txn_box/basic/stat.replay.yaml b/tests/uranium_tests/pluginTest/txn_box/basic/stat.replay.yaml similarity index 100% rename from tests/gold_tests/pluginTest/txn_box/basic/stat.replay.yaml rename to tests/uranium_tests/pluginTest/txn_box/basic/stat.replay.yaml diff --git a/tests/uranium_tests/pluginTest/txn_box/basic/test_txn_box_basic.py b/tests/uranium_tests/pluginTest/txn_box/basic/test_txn_box_basic.py new file mode 100644 index 00000000000..47803b0afb8 --- /dev/null +++ b/tests/uranium_tests/pluginTest/txn_box/basic/test_txn_box_basic.py @@ -0,0 +1,65 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_txn_box_basic(urtest: UraniumTest) -> None: + # @file + # + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + ''' + Basic smoke tests. + ''' + urtest.Summary = ''' + Test basic functions and directives. + ''' + + urtest.SkipUnless(Condition.PluginExists('txn_box.so')) + + tr = urtest.TxnBoxTestAndRun( + "Test basics", + "basic.replay.yaml", + config_path='Auto', + config_key="meta.txn_box.global", + remap=[ + ['http://remap.ex', 'http://remapped.ex', ['--key=meta.txn_box.remap-1', 'basic.replay.yaml']], + ['http://one.ex/path/', 'http://two.ex/path/'], ['http://one.ex'] + ]) + ts = tr.Variables.TS + ts.Disk.records_config.update( + { + 'proxy.config.log.max_secs_per_buffer': 1, + 'proxy.config.http.per_server.connection.max': 500, + 'proxy.config.http.background_fill_completed_threshold': 0.4, + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'txn_box' + }) + urtest.execute() diff --git a/tests/uranium_tests/pluginTest/txn_box/basic/test_txn_box_cmp.py b/tests/uranium_tests/pluginTest/txn_box/basic/test_txn_box_cmp.py new file mode 100644 index 00000000000..3a0056aa77e --- /dev/null +++ b/tests/uranium_tests/pluginTest/txn_box/basic/test_txn_box_cmp.py @@ -0,0 +1,47 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_txn_box_cmp(urtest: UraniumTest) -> None: + # @file + # + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + urtest.Summary = ''' + Comparison checks. + ''' + + urtest.SkipUnless(Condition.PluginExists("txn_box.so")) + + tr = urtest.TxnBoxTestAndRun("Test comparisons", "cmp.replay.yaml", config_path='Auto', config_key="meta.txn_box.global") + ts = tr.Variables.TS + ts.Disk.records_config.update({'proxy.config.diags.debug.enabled': 1, 'proxy.config.diags.debug.tags': 'txn_box'}) + urtest.execute() diff --git a/tests/uranium_tests/pluginTest/txn_box/basic/test_txn_box_ip_addr.py b/tests/uranium_tests/pluginTest/txn_box/basic/test_txn_box_ip_addr.py new file mode 100644 index 00000000000..03dd9d34d76 --- /dev/null +++ b/tests/uranium_tests/pluginTest/txn_box/basic/test_txn_box_ip_addr.py @@ -0,0 +1,55 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_txn_box_ip_addr(urtest: UraniumTest) -> None: + # @file + # + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + # + # Copyright 2020, Verizon Media + + urtest.Summary = ''' + Comparison checks. + ''' + + urtest.SkipUnless(Condition.PluginExists("txn_box.so")) + + tr = urtest.TxnBoxTestAndRun("Test IP address", "ip-addr.replay.yaml", config_path='Auto', config_key="meta.txn_box.global") + ts = tr.Variables.TS + ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'txn_box', + 'proxy.config.http.insert_request_via_str': 0 + }) + urtest.execute() diff --git a/tests/uranium_tests/pluginTest/txn_box/basic/test_txn_box_mod.py b/tests/uranium_tests/pluginTest/txn_box/basic/test_txn_box_mod.py new file mode 100644 index 00000000000..259e5b78e6e --- /dev/null +++ b/tests/uranium_tests/pluginTest/txn_box/basic/test_txn_box_mod.py @@ -0,0 +1,63 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_txn_box_mod(urtest: UraniumTest) -> None: + # @file + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + # + # Copyright 2020, Verizon Media + # + urtest.Summary = ''' + Modifier checks. + ''' + + urtest.SkipUnless(Condition.PluginExists("txn_box.so")) + + replay_file = 'mod.replay.yaml' + tr = urtest.TxnBoxTestAndRun( + "Test modifiers", + replay_file, + remap=[ + ['http://alpha.ex', ['--key=meta.txn-box.alpha', replay_file]], + ['http://bravo.ex', ['--key=meta.txn-box.bravo', replay_file]], + ['http://charlie.ex', ['--key=meta.txn-box.charlie', replay_file]] + ]) + ts = tr.Variables.TS + ts.Setup.Copy(replay_file, ts.Variables.CONFIGDIR) # because it's remap only - not auto-copied. + ts.Disk.records_config.update( + { + 'proxy.config.http.cache.http': 0, + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'txn_box' + }) + urtest.execute() diff --git a/tests/uranium_tests/pluginTest/txn_box/basic/test_txn_box_multi_cfg.py b/tests/uranium_tests/pluginTest/txn_box/basic/test_txn_box_multi_cfg.py new file mode 100644 index 00000000000..114f02ca9ad --- /dev/null +++ b/tests/uranium_tests/pluginTest/txn_box/basic/test_txn_box_multi_cfg.py @@ -0,0 +1,64 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_txn_box_multi_cfg(urtest: UraniumTest) -> None: + # @file + # + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + # + # Copyright 2020, Verizon Media + + urtest.Summary = ''' + Multiple Remap Configurations. + ''' + + urtest.SkipUnless(Condition.PluginExists("txn_box.so")) + + # Point of this is to test two remap configs in isolation and then both as separate remap configs. + tr = urtest.TxnBoxTestAndRun( + "Multiple remap configurations", + "multi-cfg.replay.yaml", + remap=[ + ["http://one.ex", ['multi-cfg.1.yaml']], ["http://two.ex", ['multi-cfg.2.yaml']], + ["http://both.ex", ['multi-cfg.1.yaml', 'multi-cfg.2.yaml']] + ]) + ts = tr.Variables.TS + ts.Setup.Copy("multi-cfg.1.yaml", ts.Variables.CONFIGDIR) + ts.Setup.Copy("multi-cfg.2.yaml", ts.Variables.CONFIGDIR) + ts.Disk.records_config.update( + { + 'proxy.config.log.max_secs_per_buffer': 1, + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'txn_box' + }) + urtest.execute() diff --git a/tests/uranium_tests/pluginTest/txn_box/basic/test_txn_box_redirect.py b/tests/uranium_tests/pluginTest/txn_box/basic/test_txn_box_redirect.py new file mode 100644 index 00000000000..687ef2aab46 --- /dev/null +++ b/tests/uranium_tests/pluginTest/txn_box/basic/test_txn_box_redirect.py @@ -0,0 +1,73 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_txn_box_redirect(urtest: UraniumTest) -> None: + # @file + # + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + # + # Copyright 2020, Verizon Media + + import os.path + + urtest.Summary = ''' + Test that redirect can be used to clip the path for specific URL paths. + ''' + + urtest.SkipUnless(Condition.PluginExists("txn_box.so")) + + replay_file = "redirect.replay.yaml" + + tr = urtest.TxnBoxTestAndRun( + "Redirect", + replay_file, + remap=[ + ['http://base.ex/', ('--key=meta.txn_box.remap', replay_file)], + ['http://unmatched.ex/'] # no TxnBox for this rule. + , + ['http://encode.ex/', ('--key=meta.txn_box.remap-encode', replay_file)], + ['http://decode.ex/', ('--key=meta.txn_box.remap-decode', replay_file)] + ], + verifier_client_args="--verbose info") + + ts = tr.Variables.TS + + ts.Setup.Copy(replay_file, ts.Variables.CONFIGDIR) # because it's remap only - not auto-copied. + + ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'txn_box', + 'proxy.config.http.cache.http': 0 + }) + urtest.execute() diff --git a/tests/uranium_tests/pluginTest/txn_box/basic/test_txn_box_reply.py b/tests/uranium_tests/pluginTest/txn_box/basic/test_txn_box_reply.py new file mode 100644 index 00000000000..1eb1e36d650 --- /dev/null +++ b/tests/uranium_tests/pluginTest/txn_box/basic/test_txn_box_reply.py @@ -0,0 +1,68 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_txn_box_reply(urtest: UraniumTest) -> None: + # @file + # + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + # + # Copyright 2020, Verizon Media + + import os.path + + urtest.Summary = ''' + Test the proxy-reply directive. + ''' + urtest.SkipUnless(Condition.PluginExists("txn_box.so")) + + replay_file = "reply.replay.yaml" + + tr = urtest.TxnBoxTestAndRun( + "proxy-reply", + replay_file, + config_path='Auto', + config_key='meta.txn-box.global', + remap=[['http://base.ex/', ('--key=meta.txn-box.remap', replay_file)], ['http://unmatched.ex/']], + verifier_client_args="--verbose info") + + ts = tr.Variables.TS + + ts.Setup.Copy(replay_file, ts.Variables.CONFIGDIR) # because it's remap only - not auto-copied. + + ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'txn_box|http', + 'proxy.config.http.cache.http': 0 + }) + urtest.execute() diff --git a/tests/uranium_tests/pluginTest/txn_box/basic/test_txn_box_rxp.py b/tests/uranium_tests/pluginTest/txn_box/basic/test_txn_box_rxp.py new file mode 100644 index 00000000000..50faff0d275 --- /dev/null +++ b/tests/uranium_tests/pluginTest/txn_box/basic/test_txn_box_rxp.py @@ -0,0 +1,58 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_txn_box_rxp(urtest: UraniumTest) -> None: + # @file + # + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + # + # Copyright 2020, Verizon Media + # + urtest.Summary = ''' + Regular expression. + ''' + + urtest.SkipUnless(Condition.PluginExists("txn_box.so")) + + tr = urtest.TxnBoxTestAndRun( + "Regular Expressions", + "rxp.replay.yaml", + config_path='Auto', + config_key="meta.txn_box.global", + remap=[ + ['http://app.ex', 'http://app.ex', ['--key=meta.txn_box.remap', 'rxp.replay.yaml']], + ['http://one.ex/path/', 'http://two.ex/path/'], ['http://one.ex'] + ]) + ts = tr.Variables.TS + ts.Disk.records_config.update({'proxy.config.diags.debug.enabled': 1, 'proxy.config.diags.debug.tags': 'txn_box'}) + urtest.execute() diff --git a/tests/uranium_tests/pluginTest/txn_box/basic/test_txn_box_stat.py b/tests/uranium_tests/pluginTest/txn_box/basic/test_txn_box_stat.py new file mode 100644 index 00000000000..4205ddedf05 --- /dev/null +++ b/tests/uranium_tests/pluginTest/txn_box/basic/test_txn_box_stat.py @@ -0,0 +1,81 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_txn_box_stat(urtest: UraniumTest) -> None: + # @file + # + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + # + # Copyright 2020, Verizon Media + # + import os.path + + urtest.Summary = ''' + Plugin statistics. + ''' + + urtest.SkipUnless(Condition.PluginExists("txn_box.so")) + urtest.SkipIf(Condition.true("This needs to be revisit.")) + + tr = urtest.TxnBoxTestAndRun( + "Plugin Stats", + "stat.replay.yaml", + config_path='Auto', + config_key='meta.txn_box.global', + verifier_client_args="--verbose info", + command="traffic_server") + + ts = tr.Variables.TS + ts.Setup.Copy("stat.replay.yaml", ts.Variables.CONFIGDIR) + + ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'txn_box', + 'proxy.config.http.cache.http': 0, + 'proxy.config.http.server_ports': '{0}'.format(ts.Variables.port) + }) + + probe_r = tr.Variables.TEST.AddTestRun() + probe_r.DelayStart = 20 + probe_r.Processes.Default.Command = "traffic_ctl metric get plugin.txn_box.stat-1" + probe_r.Processes.Default.Env = ts.Env + probe_r.Processes.Default.ReturnCode = 0 + probe_r.Processes.Default.Streams.stdout = Testers.ContainsExpression("stat-1 3", "Checking stat-1 value") + + probe_r = tr.Variables.TEST.AddTestRun() + probe_r.Processes.Default.Command = "traffic_ctl metric get plugin.test.stat-2" + probe_r.Processes.Default.Env = ts.Env + probe_r.Processes.Default.ReturnCode = 0 + probe_r.Processes.Default.Streams.stdout = Testers.ContainsExpression("stat-2 105", "Checking stat-2 value") + urtest.execute() diff --git a/tests/uranium_tests/pluginTest/txn_box/basic/test_txn_box_tls.py b/tests/uranium_tests/pluginTest/txn_box/basic/test_txn_box_tls.py new file mode 100644 index 00000000000..490a4175875 --- /dev/null +++ b/tests/uranium_tests/pluginTest/txn_box/basic/test_txn_box_tls.py @@ -0,0 +1,83 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_txn_box_tls(urtest: UraniumTest) -> None: + # @file + # + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + # + # Copyright 2020, Verizon Media + + import os.path + + urtest.Summary = ''' + Basic TLS testing. + ''' + + urtest.SkipUnless(Condition.PluginExists("txn_box.so")) + + tr = urtest.TxnBoxTestAndRun( + "Basic TLS", + "tls.replay.yaml", + remap=[ + ['http://base.ex/', ('--key=meta.txn_box.remap', 'tls.replay.yaml')], + ['https://base.ex/', ('--key=meta.txn_box.remap', 'tls.replay.yaml')] + ], + verifier_client_args="--verbose info", + enable_tls=True) + + ts = tr.Variables.TS + + ts.Setup.Copy("tls.replay.yaml", ts.Variables.CONFIGDIR) # because it's remap only - not auto-copied. + ts.addDefaultSSLFiles() + + ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'txn_box|http|ssl', + 'proxy.config.http.cache.http': 0, + 'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir), + 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts.Variables.SSLDir) + # enable ssl port + , + 'proxy.config.http.server_ports': '{0} {1}:ssl'.format(ts.Variables.port, ts.Variables.ssl_port), + 'proxy.config.ssl.client.verify.server.policy': 'DISABLED' + }) + ts.Disk.ssl_multicert_yaml.AddLines( + """ + ssl_multicert: + - dest_ip: "*" + ssl_cert_name: server.pem + ssl_key_name: server.key + """.split("\n")) + urtest.execute() diff --git a/tests/uranium_tests/pluginTest/txn_box/basic/test_txn_box_tls_cert.py b/tests/uranium_tests/pluginTest/txn_box/basic/test_txn_box_tls_cert.py new file mode 100644 index 00000000000..c5f496879cb --- /dev/null +++ b/tests/uranium_tests/pluginTest/txn_box/basic/test_txn_box_tls_cert.py @@ -0,0 +1,88 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_txn_box_tls_cert(urtest: UraniumTest) -> None: + # @file + # + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + # + # Copyright 2022, Apache Software Foundation + + import os.path + + urtest.Summary = ''' + Test certificate handling. + ''' + + urtest.SkipUnless(Condition.PluginExists("txn_box.so")) + + tr = urtest.TxnBoxTestAndRun( + "TLS Certs", + "tls-cert.replay.yaml", + config_path='Auto', + config_key="meta.txn_box.global", + enable_tls=True, + remap=[ + ['https://alpha.ex/', "https://alpha.ex/"], ['http://alpha.ex/', 'https://alpha.ex/'], + ['http://charlie.ex/', 'https://charlie.ex/'] + ]) + + ts = tr.Variables.TS + + ts.Setup.Copy("../ssl/server.key", os.path.join(ts.Variables.SSLDir, "server.key")) + ts.Setup.Copy("../ssl/server.pem", os.path.join(ts.Variables.SSLDir, "server.pem")) + ts.Setup.Copy("../ssl/bravo-signed.cert", os.path.join(ts.Variables.SSLDir, "bravo-signed.cert")) + + ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'txn_box|http|ssl', + 'proxy.config.http.cache.http': 0, + 'proxy.config.ssl.server.cert.path': ts.Variables.SSLDir, + 'proxy.config.ssl.server.private_key.path': ts.Variables.SSLDir + # enable ssl port + , + 'proxy.config.http.server_ports': '{0} {1}:ssl'.format(ts.Variables.port, ts.Variables.ssl_port), + 'proxy.config.ssl.client.certification_level': 0, + 'proxy.config.ssl.client.verify.server.policy': 'DISABLED', + 'proxy.config.ssl.client.cert.path': ts.Variables.SSLDir, + 'proxy.config.ssl.client.cert.filename': "bravo-signed.cert" + }) + ts.Disk.ssl_multicert_yaml.AddLines( + """ + ssl_multicert: + - dest_ip: "*" + ssl_cert_name: server.pem + ssl_key_name: server.key + """.split("\n")) + urtest.execute() diff --git a/tests/uranium_tests/pluginTest/txn_box/basic/test_txn_box_tuple.py b/tests/uranium_tests/pluginTest/txn_box/basic/test_txn_box_tuple.py new file mode 100644 index 00000000000..49a2c18a581 --- /dev/null +++ b/tests/uranium_tests/pluginTest/txn_box/basic/test_txn_box_tuple.py @@ -0,0 +1,50 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_txn_box_tuple(urtest: UraniumTest) -> None: + # @file + # + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + # + # Copyright 2020, Verizon Media + # + urtest.Summary = ''' + Tuple and related elements checks. + ''' + + urtest.SkipUnless(Condition.PluginExists("txn_box.so")) + + tr = urtest.TxnBoxTestAndRun("Tuples and such", "tuple.replay.yaml", config_path='Auto', config_key="meta.txn_box.global") + ts = tr.Variables.TS + ts.Disk.records_config.update({'proxy.config.diags.debug.enabled': 1, 'proxy.config.diags.debug.tags': 'txn_box'}) + urtest.execute() diff --git a/tests/uranium_tests/pluginTest/txn_box/basic/test_txn_box_txn_debug.py b/tests/uranium_tests/pluginTest/txn_box/basic/test_txn_box_txn_debug.py new file mode 100644 index 00000000000..0d551eae2c4 --- /dev/null +++ b/tests/uranium_tests/pluginTest/txn_box/basic/test_txn_box_txn_debug.py @@ -0,0 +1,88 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_txn_box_txn_debug(urtest: UraniumTest) -> None: + # @file + # + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + # + # Copyright 2021, Yahoo Inc + # + urtest.Summary = ''' + txn-debug directive + ''' + + urtest.SkipUnless(Condition.PluginExists("txn_box.so")) + urtest.SkipIf(Condition.true("This needs to be revisit. TS not finishing up gracefully.")) + + replay_file = "txn-debug.replay.yaml" + + tr = urtest.TxnBoxTestAndRun( + "Test txn-debug enabled", + replay_file, + config_path='Auto', + verifier_client_args="--verbose diag --keys debug-expected", + verifier_server_args="--verbose diag", + config_key="meta.txn_box.global", + suffix="debug-enabled") + ts = tr.Variables.TS + ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'txn_box', + 'proxy.config.crash_log_helper': + '/home/dmeden/code/git/trafficserver/build/_sandbox/txn-debug/ts-debug-enabled/bin/traffic_crashlog' + }) + + ts.Disk.traffic_out.Content += Testers.ContainsExpression( + r"DIAG: None: + # @file + # + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + # + # Copyright 2021, Yahoo Inc + # + urtest.Summary = ''' + txn-error directive + ''' + + urtest.SkipUnless(Condition.PluginExists("txn_box.so")) + urtest.SkipIf(Condition.true("This needs to be revisit. TS not finishing up gracefully.")) + + replay_file = "txn-error.replay.yaml" + records_config_tweaks = {'proxy.config.diags.debug.enabled': 1, 'proxy.config.diags.debug.tags': 'txn_box|http'} + + suffix = "-err" + err_run = urtest.TxnBoxTestAndRun( + "txn-error", + replay_file, + config_path='Auto', + config_key="meta.txn_box.global", + verifier_client_args="--keys base-case no-fixup", + suffix=suffix) + err_test = err_run.Variables.TS + + err_test.Disk.records_config.update(records_config_tweaks) + err_test.Disk.diags_log.Content += Testers.ContainsExpression("ua-req", "Verify hook logging is happening.") + err_test.Disk.diags_log.Content += Testers.ExcludesExpression("pre-remap", "Verify no pre-remap callbacks.") + + suffix = "-bypass" + bp_run = urtest.TxnBoxTestAndRun( + "txn-error bypass", + replay_file, + config_path='Auto', + config_key="meta.txn_box.global", + verifier_client_args="--keys bypass-case", + suffix=suffix) + bp_test = bp_run.Variables.TS + bp_test.Disk.records_config.update(records_config_tweaks) + + bp_test.Disk.diags_log.Content += Testers.ContainsExpression("ua-req", "Verify hook logging is happening.") + bp_test.Disk.diags_log.Content += Testers.ContainsExpression("pre-remap", "Verify no pre-remap callbacks.") + urtest.execute() diff --git a/tests/uranium_tests/pluginTest/txn_box/basic/test_txn_box_txn_open_1.py b/tests/uranium_tests/pluginTest/txn_box/basic/test_txn_box_txn_open_1.py new file mode 100644 index 00000000000..2e4520ab6ec --- /dev/null +++ b/tests/uranium_tests/pluginTest/txn_box/basic/test_txn_box_txn_open_1.py @@ -0,0 +1,62 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_txn_box_txn_open_1(urtest: UraniumTest) -> None: + # @file + # + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + # + # Copyright 2020, Verizon Media + # + ''' + Txn Open (start) hook testing. + ''' + urtest.Summary = ''' + Test transaction start hook. + ''' + + urtest.SkipUnless(Condition.PluginExists("txn_box.so")) + + tr = urtest.TxnBoxTestAndRun("Test txn_open 1", "txn_open_1.replay.yaml", config_path='Auto', config_key="meta.txn_box.global") + + ts = tr.Variables.TS + ts.Disk.records_config.update( + { + 'proxy.config.log.max_secs_per_buffer': 1, + 'proxy.config.http.transaction_no_activity_timeout_out': 120, + 'proxy.config.http.per_server.connection.max': 500, + 'proxy.config.http.background_fill_completed_threshold': 0.4, + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'txn_box' + }) + urtest.execute() diff --git a/tests/uranium_tests/pluginTest/txn_box/basic/test_txn_box_txn_open_2.py b/tests/uranium_tests/pluginTest/txn_box/basic/test_txn_box_txn_open_2.py new file mode 100644 index 00000000000..7e1cb7e6e3f --- /dev/null +++ b/tests/uranium_tests/pluginTest/txn_box/basic/test_txn_box_txn_open_2.py @@ -0,0 +1,67 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_txn_box_txn_open_2(urtest: UraniumTest) -> None: + # @file + # + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + # + # Copyright 2020, Verizon Media + # + ''' + Txn Open (start) hook testing. + ''' + urtest.Summary = ''' + Test transaction start hook. + ''' + + urtest.SkipUnless(Condition.PluginExists("txn_box.so")) + + tr = urtest.TxnBoxTestAndRun( + "Test txn_open 2", + "txn_open_2.replay.yaml", + config_path='Auto', + config_key="meta.txn_box.global", + remap=[["http://one.ex"]]) + + ts = tr.Variables.TS + ts.Disk.records_config.update( + { + 'proxy.config.log.max_secs_per_buffer': 1, + 'proxy.config.http.transaction_no_activity_timeout_out': 120, + 'proxy.config.http.per_server.connection.max': 500, + 'proxy.config.http.background_fill_completed_threshold': 0.4, + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'txn_box' + }) + urtest.execute() diff --git a/tests/uranium_tests/pluginTest/txn_box/basic/test_txn_box_txn_open_3.py b/tests/uranium_tests/pluginTest/txn_box/basic/test_txn_box_txn_open_3.py new file mode 100644 index 00000000000..14663b67dba --- /dev/null +++ b/tests/uranium_tests/pluginTest/txn_box/basic/test_txn_box_txn_open_3.py @@ -0,0 +1,62 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_txn_box_txn_open_3(urtest: UraniumTest) -> None: + # @file + # + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + # + # Copyright 2020, Verizon Media + # + ''' + Txn Open (start) hook testing. + ''' + urtest.Summary = ''' + Test transaction start hook. + ''' + + urtest.SkipUnless(Condition.PluginExists("txn_box.so")) + + tr = urtest.TxnBoxTestAndRun("Test txn_open 3", "txn_open_3.replay.yaml", config_path='Auto', config_key="meta.txn_box.global") + + ts = tr.Variables.TS + ts.Disk.records_config.update( + { + 'proxy.config.log.max_secs_per_buffer': 1, + 'proxy.config.http.transaction_no_activity_timeout_out': 120, + 'proxy.config.http.per_server.connection.max': 500, + 'proxy.config.http.background_fill_completed_threshold': 0.4, + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'txn_box' + }) + urtest.execute() diff --git a/tests/uranium_tests/pluginTest/txn_box/basic/test_txn_box_with.py b/tests/uranium_tests/pluginTest/txn_box/basic/test_txn_box_with.py new file mode 100644 index 00000000000..998cac44db3 --- /dev/null +++ b/tests/uranium_tests/pluginTest/txn_box/basic/test_txn_box_with.py @@ -0,0 +1,58 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_txn_box_with(urtest: UraniumTest) -> None: + # @file + # + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + # + # Copyright 2020, Verizon Media + # + ''' + Basic smoke tests. + ''' + urtest.Summary = ''' + Some test for the with directive. + ''' + + urtest.SkipUnless(Condition.PluginExists("txn_box.so")) + + tr = urtest.TxnBoxTestAndRun("With testing", "with.replay.yaml", config_path='Auto', config_key="meta.txn_box.global") + ts = tr.Variables.TS + ts.Disk.records_config.update( + { + 'proxy.config.log.max_secs_per_buffer': 1, + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'txn_box' + }) + urtest.execute() diff --git a/tests/gold_tests/pluginTest/txn_box/basic/tls-cert.replay.yaml b/tests/uranium_tests/pluginTest/txn_box/basic/tls-cert.replay.yaml similarity index 100% rename from tests/gold_tests/pluginTest/txn_box/basic/tls-cert.replay.yaml rename to tests/uranium_tests/pluginTest/txn_box/basic/tls-cert.replay.yaml diff --git a/tests/gold_tests/pluginTest/txn_box/basic/tls.replay.yaml b/tests/uranium_tests/pluginTest/txn_box/basic/tls.replay.yaml similarity index 100% rename from tests/gold_tests/pluginTest/txn_box/basic/tls.replay.yaml rename to tests/uranium_tests/pluginTest/txn_box/basic/tls.replay.yaml diff --git a/tests/gold_tests/pluginTest/txn_box/basic/tuple.replay.yaml b/tests/uranium_tests/pluginTest/txn_box/basic/tuple.replay.yaml similarity index 100% rename from tests/gold_tests/pluginTest/txn_box/basic/tuple.replay.yaml rename to tests/uranium_tests/pluginTest/txn_box/basic/tuple.replay.yaml diff --git a/tests/gold_tests/pluginTest/txn_box/basic/txn-debug.replay.yaml b/tests/uranium_tests/pluginTest/txn_box/basic/txn-debug.replay.yaml similarity index 100% rename from tests/gold_tests/pluginTest/txn_box/basic/txn-debug.replay.yaml rename to tests/uranium_tests/pluginTest/txn_box/basic/txn-debug.replay.yaml diff --git a/tests/gold_tests/pluginTest/txn_box/basic/txn-error.replay.yaml b/tests/uranium_tests/pluginTest/txn_box/basic/txn-error.replay.yaml similarity index 100% rename from tests/gold_tests/pluginTest/txn_box/basic/txn-error.replay.yaml rename to tests/uranium_tests/pluginTest/txn_box/basic/txn-error.replay.yaml diff --git a/tests/gold_tests/pluginTest/txn_box/basic/txn_open_1.replay.yaml b/tests/uranium_tests/pluginTest/txn_box/basic/txn_open_1.replay.yaml similarity index 100% rename from tests/gold_tests/pluginTest/txn_box/basic/txn_open_1.replay.yaml rename to tests/uranium_tests/pluginTest/txn_box/basic/txn_open_1.replay.yaml diff --git a/tests/gold_tests/pluginTest/txn_box/basic/txn_open_2.replay.yaml b/tests/uranium_tests/pluginTest/txn_box/basic/txn_open_2.replay.yaml similarity index 100% rename from tests/gold_tests/pluginTest/txn_box/basic/txn_open_2.replay.yaml rename to tests/uranium_tests/pluginTest/txn_box/basic/txn_open_2.replay.yaml diff --git a/tests/gold_tests/pluginTest/txn_box/basic/txn_open_3.replay.yaml b/tests/uranium_tests/pluginTest/txn_box/basic/txn_open_3.replay.yaml similarity index 100% rename from tests/gold_tests/pluginTest/txn_box/basic/txn_open_3.replay.yaml rename to tests/uranium_tests/pluginTest/txn_box/basic/txn_open_3.replay.yaml diff --git a/tests/gold_tests/pluginTest/txn_box/basic/with.replay.yaml b/tests/uranium_tests/pluginTest/txn_box/basic/with.replay.yaml similarity index 100% rename from tests/gold_tests/pluginTest/txn_box/basic/with.replay.yaml rename to tests/uranium_tests/pluginTest/txn_box/basic/with.replay.yaml diff --git a/tests/gold_tests/pluginTest/txn_box/ct_header/ct_header.replay.yaml b/tests/uranium_tests/pluginTest/txn_box/ct_header/ct_header.replay.yaml similarity index 100% rename from tests/gold_tests/pluginTest/txn_box/ct_header/ct_header.replay.yaml rename to tests/uranium_tests/pluginTest/txn_box/ct_header/ct_header.replay.yaml diff --git a/tests/uranium_tests/pluginTest/txn_box/ct_header/test_txn_box_ct_header.py b/tests/uranium_tests/pluginTest/txn_box/ct_header/test_txn_box_ct_header.py new file mode 100644 index 00000000000..8cddb7c19aa --- /dev/null +++ b/tests/uranium_tests/pluginTest/txn_box/ct_header/test_txn_box_ct_header.py @@ -0,0 +1,86 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_txn_box_ct_header(urtest: UraniumTest) -> None: + # @file + # + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + # + # Copyright 2020, Verizon Media + # + + import os.path + + urtest.Summary = ''' + Verify txn_box can filter fields as expected. + ''' + + urtest.SkipUnless(Condition.PluginExists("txn_box.so")) + + r = urtest.TxnBoxTestAndRun( + "Test HTTP field manipulation", + "ct_header.replay.yaml", + config_path='Auto', + config_key="meta.txn_box", + remap=[ + ["http://base.ex/"], ["https://base.ex/"], ["https://u.protected.ex/"], ["http://s.protected.ex/"], + ["https://s.protected.ex/"], ["https://protected.ex/"] + ], + enable_tls=True) + ts = r.Variables.TS + + ts.Setup.Copy("../ssl/server.key", os.path.join(ts.Variables.SSLDir, "server.key")) + ts.Setup.Copy("../ssl/server.pem", os.path.join(ts.Variables.SSLDir, "server.pem")) + + ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1 + # , 'proxy.config.diags.debug.tags': 'txn_box|http|ssl' + , + 'proxy.config.diags.debug.tags': 'txn_box', + 'proxy.config.http.cache.http': 0, + 'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir), + 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts.Variables.SSLDir) + # enable ssl port + , + 'proxy.config.http.server_ports': '{0} {1}:ssl'.format(ts.Variables.port, ts.Variables.ssl_port), + 'proxy.config.ssl.client.verify.server.policy': 'DISABLED' + }) + ts.Disk.ssl_multicert_yaml.AddLines( + """ + ssl_multicert: + - dest_ip: "*" + ssl_cert_name: server.pem + ssl_key_name: server.key + """.split("\n")) + urtest.execute() diff --git a/tests/gold_tests/pluginTest/txn_box/example/accept-encoding.replay.yaml b/tests/uranium_tests/pluginTest/txn_box/example/accept-encoding.replay.yaml similarity index 100% rename from tests/gold_tests/pluginTest/txn_box/example/accept-encoding.replay.yaml rename to tests/uranium_tests/pluginTest/txn_box/example/accept-encoding.replay.yaml diff --git a/tests/uranium_tests/pluginTest/txn_box/example/test_txn_box_accept_encoding.py b/tests/uranium_tests/pluginTest/txn_box/example/test_txn_box_accept_encoding.py new file mode 100644 index 00000000000..96f7f1dff90 --- /dev/null +++ b/tests/uranium_tests/pluginTest/txn_box/example/test_txn_box_accept_encoding.py @@ -0,0 +1,61 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_txn_box_accept_encoding(urtest: UraniumTest) -> None: + # @file + # + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + # + # Copyright 2020, Verizon Media + # + ''' + Basic smoke tests. + ''' + urtest.Summary = ''' + Example: Force accept encoding. + ''' + + urtest.SkipUnless(Condition.PluginExists("txn_box.so")) + + tr = urtest.TxnBoxTestAndRun( + "Accept Encoding", "accept-encoding.replay.yaml", config_path='Auto', config_key="meta.txn_box.global") + + tr.Variables.TS.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'txn_box', + 'proxy.config.http.cache.http': 0, + 'proxy.config.http.normalize_ae': 0, + 'proxy.config.url_remap.remap_required': 0 + }) + urtest.execute() diff --git a/tests/gold_tests/pluginTest/txn_box/prod/cors-origin.replay.yaml b/tests/uranium_tests/pluginTest/txn_box/prod/cors-origin.replay.yaml similarity index 100% rename from tests/gold_tests/pluginTest/txn_box/prod/cors-origin.replay.yaml rename to tests/uranium_tests/pluginTest/txn_box/prod/cors-origin.replay.yaml diff --git a/tests/gold_tests/pluginTest/txn_box/prod/cors-referrer.replay.yaml b/tests/uranium_tests/pluginTest/txn_box/prod/cors-referrer.replay.yaml similarity index 100% rename from tests/gold_tests/pluginTest/txn_box/prod/cors-referrer.replay.yaml rename to tests/uranium_tests/pluginTest/txn_box/prod/cors-referrer.replay.yaml diff --git a/tests/gold_tests/pluginTest/txn_box/prod/ip-acl.csv b/tests/uranium_tests/pluginTest/txn_box/prod/ip-acl.csv similarity index 100% rename from tests/gold_tests/pluginTest/txn_box/prod/ip-acl.csv rename to tests/uranium_tests/pluginTest/txn_box/prod/ip-acl.csv diff --git a/tests/gold_tests/pluginTest/txn_box/prod/ip-acl.replay.yaml b/tests/uranium_tests/pluginTest/txn_box/prod/ip-acl.replay.yaml similarity index 100% rename from tests/gold_tests/pluginTest/txn_box/prod/ip-acl.replay.yaml rename to tests/uranium_tests/pluginTest/txn_box/prod/ip-acl.replay.yaml diff --git a/tests/gold_tests/pluginTest/txn_box/prod/mTLS-alpha.replay.yaml b/tests/uranium_tests/pluginTest/txn_box/prod/mTLS-alpha.replay.yaml similarity index 100% rename from tests/gold_tests/pluginTest/txn_box/prod/mTLS-alpha.replay.yaml rename to tests/uranium_tests/pluginTest/txn_box/prod/mTLS-alpha.replay.yaml diff --git a/tests/gold_tests/pluginTest/txn_box/prod/mTLS-bravo.replay.yaml b/tests/uranium_tests/pluginTest/txn_box/prod/mTLS-bravo.replay.yaml similarity index 100% rename from tests/gold_tests/pluginTest/txn_box/prod/mTLS-bravo.replay.yaml rename to tests/uranium_tests/pluginTest/txn_box/prod/mTLS-bravo.replay.yaml diff --git a/tests/gold_tests/pluginTest/txn_box/prod/mTLS.replay.yaml b/tests/uranium_tests/pluginTest/txn_box/prod/mTLS.replay.yaml similarity index 100% rename from tests/gold_tests/pluginTest/txn_box/prod/mTLS.replay.yaml rename to tests/uranium_tests/pluginTest/txn_box/prod/mTLS.replay.yaml diff --git a/tests/gold_tests/pluginTest/txn_box/prod/mTLS.txnbox.yaml b/tests/uranium_tests/pluginTest/txn_box/prod/mTLS.txnbox.yaml similarity index 100% rename from tests/gold_tests/pluginTest/txn_box/prod/mTLS.txnbox.yaml rename to tests/uranium_tests/pluginTest/txn_box/prod/mTLS.txnbox.yaml diff --git a/tests/gold_tests/pluginTest/txn_box/prod/query-delete.replay.yaml b/tests/uranium_tests/pluginTest/txn_box/prod/query-delete.replay.yaml similarity index 100% rename from tests/gold_tests/pluginTest/txn_box/prod/query-delete.replay.yaml rename to tests/uranium_tests/pluginTest/txn_box/prod/query-delete.replay.yaml diff --git a/tests/gold_tests/pluginTest/txn_box/prod/query.replay.yaml b/tests/uranium_tests/pluginTest/txn_box/prod/query.replay.yaml similarity index 100% rename from tests/gold_tests/pluginTest/txn_box/prod/query.replay.yaml rename to tests/uranium_tests/pluginTest/txn_box/prod/query.replay.yaml diff --git a/tests/gold_tests/pluginTest/txn_box/prod/stanley.replay.yaml b/tests/uranium_tests/pluginTest/txn_box/prod/stanley.replay.yaml similarity index 100% rename from tests/gold_tests/pluginTest/txn_box/prod/stanley.replay.yaml rename to tests/uranium_tests/pluginTest/txn_box/prod/stanley.replay.yaml diff --git a/tests/uranium_tests/pluginTest/txn_box/prod/test_txn_box_cors_origin.py b/tests/uranium_tests/pluginTest/txn_box/prod/test_txn_box_cors_origin.py new file mode 100644 index 00000000000..6e98ed62f1b --- /dev/null +++ b/tests/uranium_tests/pluginTest/txn_box/prod/test_txn_box_cors_origin.py @@ -0,0 +1,66 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_txn_box_cors_origin(urtest: UraniumTest) -> None: + # @file + # + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + # + # Copyright 2020, Verizon Media + # + + import os.path + + urtest.Summary = ''' + Production use case: Use the referrer field to set the CORS field in the response + ''' + + urtest.SkipUnless(Condition.PluginExists("txn_box.so")) + + replay_file = "cors-origin.replay.yaml" + + tr = urtest.TxnBoxTestAndRun( + "Redirect", replay_file, config_path='Auto', config_key="meta.txn-box.global", verifier_client_args="--verbose info") + + ts = tr.Variables.TS + + ts.Setup.Copy(replay_file, ts.Variables.CONFIGDIR) # because it's remap only - not auto-copied. + + ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'txn_box', + 'proxy.config.http.cache.http': 0, + 'proxy.config.url_remap.remap_required': 0 + }) + urtest.execute() diff --git a/tests/uranium_tests/pluginTest/txn_box/prod/test_txn_box_cors_referrer.py b/tests/uranium_tests/pluginTest/txn_box/prod/test_txn_box_cors_referrer.py new file mode 100644 index 00000000000..35ca9e2ed1c --- /dev/null +++ b/tests/uranium_tests/pluginTest/txn_box/prod/test_txn_box_cors_referrer.py @@ -0,0 +1,66 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_txn_box_cors_referrer(urtest: UraniumTest) -> None: + # @file + # + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + # + # Copyright 2021, Verizon Media + # + + import os.path + + urtest.Summary = ''' + Production use case: Use the referrer field to set the CORS field in the response + ''' + + urtest.SkipUnless(Condition.PluginExists("txn_box.so")) + + replay_file = "cors-referrer.replay.yaml" + + tr = urtest.TxnBoxTestAndRun( + "Redirect", replay_file, config_path='Auto', config_key="meta.txn-box.global", verifier_client_args="--verbose info") + + ts = tr.Variables.TS + + ts.Setup.Copy(replay_file, ts.Variables.CONFIGDIR) # because it's remap only - not auto-copied. + + ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'txn_box', + 'proxy.config.http.cache.http': 0, + 'proxy.config.url_remap.remap_required': 0 + }) + urtest.execute() diff --git a/tests/uranium_tests/pluginTest/txn_box/prod/test_txn_box_ip_acl.py b/tests/uranium_tests/pluginTest/txn_box/prod/test_txn_box_ip_acl.py new file mode 100644 index 00000000000..25d9e84ff8d --- /dev/null +++ b/tests/uranium_tests/pluginTest/txn_box/prod/test_txn_box_ip_acl.py @@ -0,0 +1,71 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_txn_box_ip_acl(urtest: UraniumTest) -> None: + # @file + # + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + # + # Copyright 2020, Verizon Media + # + + import os.path + + urtest.Summary = ''' + Test using IP spaces as ACLs. + ''' + + urtest.SkipUnless(Condition.PluginExists("txn_box.so")) + + replay_file = "ip-acl.replay.yaml" + + tr = urtest.TxnBoxTestAndRun( + "IP ACL", + replay_file, + config_path='Auto', + config_key='meta.txn-box.global', + remap=[['http://base.ex/'], ['http://docjj.ex/', 'http://docjj.ex', ['--key=meta.txn-box.remap', 'ip-acl.replay.yaml']]], + verifier_client_args="--verbose info") + + ts = tr.Variables.TS + + ts.Setup.Copy(replay_file, ts.Variables.CONFIGDIR) # because it's remap only - not auto-copied. + ts.Setup.Copy("ip-acl.csv", ts.Variables.CONFIGDIR) # Need the IP Space file. + + ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'txn_box', + 'proxy.config.http.cache.http': 0 + }) + urtest.execute() diff --git a/tests/uranium_tests/pluginTest/txn_box/prod/test_txn_box_mTLS.py b/tests/uranium_tests/pluginTest/txn_box/prod/test_txn_box_mTLS.py new file mode 100644 index 00000000000..e0b1d7456e4 --- /dev/null +++ b/tests/uranium_tests/pluginTest/txn_box/prod/test_txn_box_mTLS.py @@ -0,0 +1,97 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_txn_box_mTLS(urtest: UraniumTest) -> None: + # @file + # + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + # + # Copyright 2021, Verizon Media + # + + import os.path + + urtest.Summary = ''' + Athenz style mTLS testing. + ''' + + urtest.SkipUnless(Condition.PluginExists("txn_box.so")) + + ts = urtest.TxnBoxTest( + "mTLS.replay.yaml", + config_path="mTLS.txnbox.yaml", + config_key="txn_box", + remap=[['https://base.ex/', "http://base.ex/"]], + enable_tls=True, + verifier_server_args='--format "{url}"') + + tr_alpha = urtest.TxnBoxRun( + "Athenz mTLS alpha", + replay_path="mTLS-alpha.replay.yaml", + ssl_cert="../ssl/alpha-signed.cert", + suffix="alpha", + verifier_client_args="--verbose info") + + # These are intended to connect but not be "authorized" because the issuer isn't alpha. + tr_bravo = urtest.TxnBoxRun( + "Athenz mTLS bravo", + replay_path="mTLS-bravo.replay.yaml", + ssl_cert="../ssl/bravo-signed.cert", + suffix="bravo", + verifier_client_args="--verbose info") + + ts.Setup.Copy("../ssl/server.key", os.path.join(ts.Variables.SSLDir, "server.key")) + ts.Setup.Copy("../ssl/server.pem", os.path.join(ts.Variables.SSLDir, "server.pem")) + # The bundle contains roots for alpha, bravo, and charlie. + ts.Setup.Copy("../ssl/ca-bundle.pem", os.path.join(ts.Variables.SSLDir, "ca-bundle.pem")) + + ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'txn_box|ssl', + 'proxy.config.http.cache.http': 0, + 'proxy.config.ssl.server.cert.path': ts.Variables.SSLDir, + 'proxy.config.ssl.server.private_key.path': ts.Variables.SSLDir, + 'proxy.config.ssl.CA.cert.path': ts.Variables.SSLDir, + 'proxy.config.ssl.CA.cert.filename': 'ca-bundle.pem', + 'proxy.config.ssl.client.certification_level': 2, + 'proxy.config.http.server_ports': '{0} {1}:ssl'.format(ts.Variables.port, ts.Variables.ssl_port) + }) + ts.Disk.ssl_multicert_yaml.AddLines( + """ + ssl_multicert: + - dest_ip: "*" + ssl_cert_name: server.pem + ssl_key_name: server.key + """.split("\n")) + urtest.execute() diff --git a/tests/uranium_tests/pluginTest/txn_box/prod/test_txn_box_query.py b/tests/uranium_tests/pluginTest/txn_box/prod/test_txn_box_query.py new file mode 100644 index 00000000000..fa3f73a0ea3 --- /dev/null +++ b/tests/uranium_tests/pluginTest/txn_box/prod/test_txn_box_query.py @@ -0,0 +1,72 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_txn_box_query(urtest: UraniumTest) -> None: + # @file + # + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + # + # Copyright 2021, Verizon Media + # + + import os.path + + urtest.Summary = ''' + Production use case: Manipulate specific query parameters. + ''' + + urtest.SkipUnless(Condition.PluginExists("txn_box.so")) + + replay_file = "query.replay.yaml" + + tr = urtest.TxnBoxTestAndRun( + "Query", + replay_file, + remap=[ + ['http://one.ex', ('--key=meta.txn-box.remap.one', replay_file)], + ['http://two.ex', ('--key=meta.txn-box.remap.two', replay_file)], + ['http://three.ex', ('--key=meta.txn-box.remap.three', replay_file)], ['http://unmatched.ex/'] + ], + verifier_client_args="--verbose info") + + ts = tr.Variables.TS + + ts.Setup.Copy(replay_file, ts.Variables.CONFIGDIR) # because it's remap only - not auto-copied. + + ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'txn_box', + 'proxy.config.http.cache.http': 0 + }) + urtest.execute() diff --git a/tests/uranium_tests/pluginTest/txn_box/prod/test_txn_box_query_delete_filter.py b/tests/uranium_tests/pluginTest/txn_box/prod/test_txn_box_query_delete_filter.py new file mode 100644 index 00000000000..7f7be7e353f --- /dev/null +++ b/tests/uranium_tests/pluginTest/txn_box/prod/test_txn_box_query_delete_filter.py @@ -0,0 +1,67 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_txn_box_query_delete_filter(urtest: UraniumTest) -> None: + # @file + # + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + # + # Copyright 2021, Verizon Media + # + + import os.path + + urtest.Summary = ''' + Production use case: Manipulate specific query parameters. + ''' + + urtest.SkipUnless(Condition.PluginExists("txn_box.so")) + + replay_file = "query-delete.replay.yaml" + + tr = urtest.TxnBoxTestAndRun( + "Query Delete with RXP", + replay_file, + config_path='Auto', + config_key="meta.txn-box.bravo", + verifier_client_args="--verbose info") + + ts = tr.Variables.TS + + ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'txn_box', + 'proxy.config.http.cache.http': 0 + }) + urtest.execute() diff --git a/tests/uranium_tests/pluginTest/txn_box/prod/test_txn_box_query_delete_regex.py b/tests/uranium_tests/pluginTest/txn_box/prod/test_txn_box_query_delete_regex.py new file mode 100644 index 00000000000..84f83fcca13 --- /dev/null +++ b/tests/uranium_tests/pluginTest/txn_box/prod/test_txn_box_query_delete_regex.py @@ -0,0 +1,67 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_txn_box_query_delete_regex(urtest: UraniumTest) -> None: + # @file + # + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + # + # Copyright 2021, Verizon Media + # + + import os.path + + urtest.Summary = ''' + Production use case: Manipulate specific query parameters. + ''' + + urtest.SkipUnless(Condition.PluginExists("txn_box.so")) + + replay_file = "query-delete.replay.yaml" + + tr = urtest.TxnBoxTestAndRun( + "Query Delete with RXP", + replay_file, + config_path='Auto', + config_key="meta.txn-box.alpha", + verifier_client_args="--verbose info") + + ts = tr.Variables.TS + + ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'txn_box', + 'proxy.config.http.cache.http': 0 + }) + urtest.execute() diff --git a/tests/uranium_tests/pluginTest/txn_box/prod/test_txn_box_stanley.py b/tests/uranium_tests/pluginTest/txn_box/prod/test_txn_box_stanley.py new file mode 100644 index 00000000000..ffec5b9b180 --- /dev/null +++ b/tests/uranium_tests/pluginTest/txn_box/prod/test_txn_box_stanley.py @@ -0,0 +1,72 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_txn_box_stanley(urtest: UraniumTest) -> None: + # @file + # + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + # + # Copyright 2020, Verizon Media + # + + import os.path + + urtest.Summary = ''' + Production use case: Stanley's "remap" plugin. + ''' + + urtest.SkipUnless(Condition.PluginExists("txn_box.so")) + + replay_file = "stanley.replay.yaml" + + tr = urtest.TxnBoxTestAndRun( + "Redirect", + replay_file, + remap=[ + ['http://nowhere.com', 'http://0.0.0.0/health', ('--key=meta.txn-box.remap', replay_file)], + ['http://calendar.yahoo.com/', 'http://0.0.0.0/health', ('--key=meta.txn-box.remap', replay_file)], + ['/', 'http://0.0.0.0/health', ('--key=meta.txn-box.remap', replay_file)] + ], + verifier_client_args="--verbose info") + + ts = tr.Variables.TS + + ts.Setup.Copy(replay_file, ts.Variables.CONFIGDIR) # because it's remap only - not auto-copied. + + ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'txn_box', + 'proxy.config.http.cache.http': 0 + }) + urtest.execute() diff --git a/tests/uranium_tests/pluginTest/txn_box/prod/test_txn_box_vznith_1.py b/tests/uranium_tests/pluginTest/txn_box/prod/test_txn_box_vznith_1.py new file mode 100644 index 00000000000..f42f1252bd4 --- /dev/null +++ b/tests/uranium_tests/pluginTest/txn_box/prod/test_txn_box_vznith_1.py @@ -0,0 +1,68 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_txn_box_vznith_1(urtest: UraniumTest) -> None: + # @file + # + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + # + # Copyright 2020, Verizon Media + # + + import os.path + + urtest.Summary = ''' + Production use case: Use query parameters to modify the proxy request path in remap. + ''' + + urtest.SkipUnless(Condition.PluginExists("txn_box.so")) + + replay_file = "vznith-1.replay.yaml" + + tr = urtest.TxnBoxTestAndRun( + "Redirect", + replay_file, + remap=[['http://base.ex/edge/file', ('--key=meta.txn-box.remap', replay_file)], ['http://unmatched.ex/']], + verifier_client_args="--verbose info") + + ts = tr.Variables.TS + + ts.Setup.Copy(replay_file, ts.Variables.CONFIGDIR) # because it's remap only - not auto-copied. + + ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'txn_box', + 'proxy.config.http.cache.http': 0 + }) + urtest.execute() diff --git a/tests/uranium_tests/pluginTest/txn_box/prod/test_txn_box_yts_3489.py b/tests/uranium_tests/pluginTest/txn_box/prod/test_txn_box_yts_3489.py new file mode 100644 index 00000000000..52bd922b038 --- /dev/null +++ b/tests/uranium_tests/pluginTest/txn_box/prod/test_txn_box_yts_3489.py @@ -0,0 +1,73 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_txn_box_yts_3489(urtest: UraniumTest) -> None: + # @file + # + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + # + # Copyright 2020, Verizon Media + # + + import os.path + + urtest.Summary = ''' + Production use case: Use redirect to clip the path for specific URL paths. + ''' + + urtest.SkipUnless(Condition.PluginExists("txn_box.so")) + + replay_file = "yts-3489.replay.yaml" + + tr = urtest.TxnBoxTestAndRun( + "Redirect", + replay_file, + remap=[ + ['http://base.ex/', ('--key=meta.txn-box.remap', replay_file)], + ['http://alt.ex/', ('--key=meta.txn-box.remap-alt', replay_file)], + ['http://join.ex/', ('--key=meta.txn-box.remap-join', replay_file)], + ['http://concat.ex/', ('--key=meta.txn-box.remap-concat', replay_file)], ['http://unmatched.ex/'] + ], + verifier_client_args="--verbose info") + + ts = tr.Variables.TS + + ts.Setup.Copy(replay_file, ts.Variables.CONFIGDIR) # because it's remap only - not auto-copied. + + ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'txn_box', + 'proxy.config.http.cache.http': 0 + }) + urtest.execute() diff --git a/tests/gold_tests/pluginTest/txn_box/prod/vznith-1.replay.yaml b/tests/uranium_tests/pluginTest/txn_box/prod/vznith-1.replay.yaml similarity index 100% rename from tests/gold_tests/pluginTest/txn_box/prod/vznith-1.replay.yaml rename to tests/uranium_tests/pluginTest/txn_box/prod/vznith-1.replay.yaml diff --git a/tests/gold_tests/pluginTest/txn_box/prod/yts-3489.replay.yaml b/tests/uranium_tests/pluginTest/txn_box/prod/yts-3489.replay.yaml similarity index 100% rename from tests/gold_tests/pluginTest/txn_box/prod/yts-3489.replay.yaml rename to tests/uranium_tests/pluginTest/txn_box/prod/yts-3489.replay.yaml diff --git a/tests/gold_tests/pluginTest/txn_box/ramp/multi-ramp-1.cfg.yaml b/tests/uranium_tests/pluginTest/txn_box/ramp/multi-ramp-1.cfg.yaml similarity index 100% rename from tests/gold_tests/pluginTest/txn_box/ramp/multi-ramp-1.cfg.yaml rename to tests/uranium_tests/pluginTest/txn_box/ramp/multi-ramp-1.cfg.yaml diff --git a/tests/gold_tests/pluginTest/txn_box/ramp/multi-ramp-2.cfg.yaml b/tests/uranium_tests/pluginTest/txn_box/ramp/multi-ramp-2.cfg.yaml similarity index 100% rename from tests/gold_tests/pluginTest/txn_box/ramp/multi-ramp-2.cfg.yaml rename to tests/uranium_tests/pluginTest/txn_box/ramp/multi-ramp-2.cfg.yaml diff --git a/tests/gold_tests/pluginTest/txn_box/ramp/multi-ramp-3.cfg.yaml b/tests/uranium_tests/pluginTest/txn_box/ramp/multi-ramp-3.cfg.yaml similarity index 100% rename from tests/gold_tests/pluginTest/txn_box/ramp/multi-ramp-3.cfg.yaml rename to tests/uranium_tests/pluginTest/txn_box/ramp/multi-ramp-3.cfg.yaml diff --git a/tests/gold_tests/pluginTest/txn_box/ramp/multi-ramp-4.cfg.yaml b/tests/uranium_tests/pluginTest/txn_box/ramp/multi-ramp-4.cfg.yaml similarity index 100% rename from tests/gold_tests/pluginTest/txn_box/ramp/multi-ramp-4.cfg.yaml rename to tests/uranium_tests/pluginTest/txn_box/ramp/multi-ramp-4.cfg.yaml diff --git a/tests/gold_tests/pluginTest/txn_box/ramp/multi-ramp.replay.yaml b/tests/uranium_tests/pluginTest/txn_box/ramp/multi-ramp.replay.yaml similarity index 100% rename from tests/gold_tests/pluginTest/txn_box/ramp/multi-ramp.replay.yaml rename to tests/uranium_tests/pluginTest/txn_box/ramp/multi-ramp.replay.yaml diff --git a/tests/gold_tests/pluginTest/txn_box/ramp/multi_ramp_common.py b/tests/uranium_tests/pluginTest/txn_box/ramp/multi_ramp_common.py similarity index 100% rename from tests/gold_tests/pluginTest/txn_box/ramp/multi_ramp_common.py rename to tests/uranium_tests/pluginTest/txn_box/ramp/multi_ramp_common.py diff --git a/tests/gold_tests/pluginTest/txn_box/ramp/ramp.logging.yaml b/tests/uranium_tests/pluginTest/txn_box/ramp/ramp.logging.yaml similarity index 100% rename from tests/gold_tests/pluginTest/txn_box/ramp/ramp.logging.yaml rename to tests/uranium_tests/pluginTest/txn_box/ramp/ramp.logging.yaml diff --git a/tests/gold_tests/pluginTest/txn_box/ramp/ramp.replay.yaml b/tests/uranium_tests/pluginTest/txn_box/ramp/ramp.replay.yaml similarity index 100% rename from tests/gold_tests/pluginTest/txn_box/ramp/ramp.replay.yaml rename to tests/uranium_tests/pluginTest/txn_box/ramp/ramp.replay.yaml diff --git a/tests/uranium_tests/pluginTest/txn_box/ramp/test_txn_box_multi_ramp_1.py b/tests/uranium_tests/pluginTest/txn_box/ramp/test_txn_box_multi_ramp_1.py new file mode 100644 index 00000000000..b700a3f1f69 --- /dev/null +++ b/tests/uranium_tests/pluginTest/txn_box/ramp/test_txn_box_multi_ramp_1.py @@ -0,0 +1,67 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_txn_box_multi_ramp_1(urtest: UraniumTest) -> None: + # @file + # + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + # + # Copyright 2020, Verizon Media + # + import os.path + + urtest.Summary = ''' + Multi-bucketing (style 1). + ''' + + urtest.SkipUnless(Condition.PluginExists("txn_box.so")) + + RepeatCount = 1000 + CFG_PATH = "multi-ramp-1.cfg.yaml" + tr = urtest.TxnBoxTestAndRun( + "Multi bucketing 1", + "multi-ramp.replay.yaml", + remap=[['http://base.ex/', 'http://base.ex/', [CFG_PATH]], ['https://base.ex/', 'https://base.ex/', [CFG_PATH]]], + verifier_client_args='--verbose info --format "{{url}}" --repeat {}'.format(RepeatCount), + verifier_server_args='--verbose info --format "{url}"', + enable_tls=True) + + with open(f"{tr.TestDirectory}/multi_ramp_common.py") as f: + code = compile(f.read(), "multi_ramp_common.py", 'exec') + exec(code) + + ramp_test_fixup(tr) + + ts = tr.Variables.TXNBOX_TS + ts.Setup.Copy(CFG_PATH, ts.Variables.CONFIGDIR) + urtest.execute() diff --git a/tests/uranium_tests/pluginTest/txn_box/ramp/test_txn_box_multi_ramp_2.py b/tests/uranium_tests/pluginTest/txn_box/ramp/test_txn_box_multi_ramp_2.py new file mode 100644 index 00000000000..682e9bcb213 --- /dev/null +++ b/tests/uranium_tests/pluginTest/txn_box/ramp/test_txn_box_multi_ramp_2.py @@ -0,0 +1,66 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_txn_box_multi_ramp_2(urtest: UraniumTest) -> None: + # @file + # + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + # + # Copyright 2020, Verizon Media + # + import os.path + + urtest.Summary = ''' + Multi-bucketing (style 2). + ''' + + urtest.SkipUnless(Condition.PluginExists("txn_box.so")) + + RepeatCount = 1000 + CFG_PATH = "multi-ramp-2.cfg.yaml" + tr = urtest.TxnBoxTestAndRun( + "Multi bucketing 2", + "multi-ramp.replay.yaml", + remap=[['http://base.ex/', 'http://base.ex/', [CFG_PATH]], ['https://base.ex/', 'https://base.ex/', [CFG_PATH]]], + verifier_client_args='--verbose info --format "{{url}}" --repeat {}'.format(RepeatCount), + verifier_server_args='--verbose info --format "{url}"', + enable_tls=True) + + with open(f"{tr.TestDirectory}/multi_ramp_common.py") as f: + code = compile(f.read(), "multi_ramp_common.py", 'exec') + exec(code) + + ramp_test_fixup(tr) + ts = tr.Variables.TXNBOX_TS + ts.Setup.Copy(CFG_PATH, ts.Variables.CONFIGDIR) + urtest.execute() diff --git a/tests/uranium_tests/pluginTest/txn_box/ramp/test_txn_box_multi_ramp_3.py b/tests/uranium_tests/pluginTest/txn_box/ramp/test_txn_box_multi_ramp_3.py new file mode 100644 index 00000000000..da42843f594 --- /dev/null +++ b/tests/uranium_tests/pluginTest/txn_box/ramp/test_txn_box_multi_ramp_3.py @@ -0,0 +1,66 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_txn_box_multi_ramp_3(urtest: UraniumTest) -> None: + # @file + # + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + # + # Copyright 2020, Verizon Media + # + import os.path + + urtest.Summary = ''' + Multi-bucketing (style 3). + ''' + + urtest.SkipUnless(Condition.PluginExists("txn_box.so")) + + RepeatCount = 1000 + CFG_PATH = "multi-ramp-3.cfg.yaml" + tr = urtest.TxnBoxTestAndRun( + "Multi bucketing 3", + "multi-ramp.replay.yaml", + remap=[['http://base.ex/', 'http://base.ex/', [CFG_PATH]], ['https://base.ex/', 'https://base.ex/', [CFG_PATH]]], + verifier_client_args='--verbose info --format "{{url}}" --repeat {}'.format(RepeatCount), + verifier_server_args='--verbose info --format "{url}"', + enable_tls=True) + + with open(f"{tr.TestDirectory}/multi_ramp_common.py") as f: + code = compile(f.read(), "multi_ramp_common.py", 'exec') + exec(code) + + ramp_test_fixup(tr) + ts = tr.Variables.TXNBOX_TS + ts.Setup.Copy(CFG_PATH, ts.Variables.CONFIGDIR) + urtest.execute() diff --git a/tests/uranium_tests/pluginTest/txn_box/ramp/test_txn_box_ramp.py b/tests/uranium_tests/pluginTest/txn_box/ramp/test_txn_box_ramp.py new file mode 100644 index 00000000000..842e1674ea7 --- /dev/null +++ b/tests/uranium_tests/pluginTest/txn_box/ramp/test_txn_box_ramp.py @@ -0,0 +1,78 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_txn_box_ramp(urtest: UraniumTest) -> None: + # @file + # + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + # + # Copyright 2020, Verizon Media + # + + import os.path + import tempfile + + urtest.Summary = ''' + Test traffic ramping. + ''' + + urtest.SkipUnless(Condition.PluginExists("txn_box.so")) + + RepeatCount = 100 + replay_file = 'ramp.replay.yaml' + + # Make a server representing a staging host to which we can ramp. + server_ramp = urtest.MakeVerifierServerProcess('pv-server-staging', replay_file) + # Make it so that server-ramp is used in the txn_box config. + orig_replay_file = os.path.join(urtest.TestDirectory, replay_file) + old_replay_content = open(orig_replay_file).read() + new_replay_content = old_replay_content.replace('{server_port}', str(server_ramp.Variables.http_port)) + # There's no need to delete temp_replay because it will be cleaned up with the TestDirectory. + temp_replay = tempfile.NamedTemporaryFile(dir=urtest.RunDirectory, delete=False) + # Reference this new replay file for all other TestRun processes. + replay_file = temp_replay.name + open(replay_file, 'w').write(new_replay_content) + + tr = urtest.TxnBoxTestAndRun( + "Ramping", + replay_file, + remap=[('http://one.ex', 'http://three.ex', ('--key=meta.txn_box.remap', replay_file))], + verifier_client_args=f"--verbose diag --repeat {RepeatCount}", + verifier_server_args=f"--verbose diag") + + ts = tr.Variables.TS + ts.StartBefore(server_ramp) + ts.Setup.Copy(replay_file, ts.Variables.CONFIGDIR) + ts.Setup.Copy("ramp.logging.yaml", os.path.join(ts.Variables.CONFIGDIR, "logging.yaml")) + ts.Disk.records_config.update({'proxy.config.log.max_secs_per_buffer': 1}) + urtest.execute() diff --git a/tests/gold_tests/pluginTest/txn_box/remap/remap-base.replay.yaml b/tests/uranium_tests/pluginTest/txn_box/remap/remap-base.replay.yaml similarity index 100% rename from tests/gold_tests/pluginTest/txn_box/remap/remap-base.replay.yaml rename to tests/uranium_tests/pluginTest/txn_box/remap/remap-base.replay.yaml diff --git a/tests/uranium_tests/pluginTest/txn_box/remap/test_txn_box_remap_base.py b/tests/uranium_tests/pluginTest/txn_box/remap/test_txn_box_remap_base.py new file mode 100644 index 00000000000..f3a43b28f06 --- /dev/null +++ b/tests/uranium_tests/pluginTest/txn_box/remap/test_txn_box_remap_base.py @@ -0,0 +1,90 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_txn_box_remap_base(urtest: UraniumTest) -> None: + # @file + # + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + # + # Copyright 2020, Verizon Media + # + import os.path + + urtest.Summary = ''' + Basic remap testing. + ''' + + urtest.SkipUnless(Condition.PluginExists("txn_box.so")) + + tr = urtest.TxnBoxTestAndRun( + "Remap basics", + "remap-base.replay.yaml", + remap=[ + ['http://1.remap.ex/path', 'http://1.remapped.ex', ['--key=meta.txn_box.remap-1', 'remap-base.replay.yaml']], + ['http://2.remap.ex/path', 'http://2.remapped.ex', ['--key=meta.txn_box.remap-2', 'remap-base.replay.yaml']], + ['http://3.remap.ex/path', 'http://3.remapped.ex', ['--key=meta.txn_box.remap-3', 'remap-base.replay.yaml']], + ['http://4.remap.ex/path', 'http://4.remapped.ex', ['--key=meta.txn_box.remap-4', 'remap-base.replay.yaml']], + ['http://5.remap.ex/path', 'http://5.remapped.ex', ['--key=meta.txn_box.remap-5', 'remap-base.replay.yaml']], + ['http://base.ex'] + ], + enable_tls=True) + + ts = tr.Variables.TS + + ts.Setup.Copy("remap-base.replay.yaml", ts.Variables.CONFIGDIR) # because it's remap only - not auto-copied. + ts.Setup.Copy("../ssl/server.key", os.path.join(ts.Variables.SSLDir, "server.key")) + ts.Setup.Copy("../ssl/server.pem", os.path.join(ts.Variables.SSLDir, "server.pem")) + + ts = tr.Variables.TS + ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'txn_box', + 'proxy.config.reverse_proxy.enabled': 1 + + # enable ssl port + , + 'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir), + 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts.Variables.SSLDir), + 'proxy.config.http.server_ports': '{0} {1}:ssl'.format(ts.Variables.port, ts.Variables.ssl_port), + 'proxy.config.ssl.client.verify.server.policy': "disabled" + }) + + ts.Disk.ssl_multicert_yaml.AddLines( + """ + ssl_multicert: + - dest_ip: "*" + ssl_cert_name: server.pem + ssl_key_name: server.key + """.split("\n")) + urtest.execute() diff --git a/tests/gold_tests/pluginTest/txn_box/smoke/smoke-2.replay.yaml b/tests/uranium_tests/pluginTest/txn_box/smoke/smoke-2.replay.yaml similarity index 100% rename from tests/gold_tests/pluginTest/txn_box/smoke/smoke-2.replay.yaml rename to tests/uranium_tests/pluginTest/txn_box/smoke/smoke-2.replay.yaml diff --git a/tests/gold_tests/pluginTest/txn_box/smoke/smoke.replay.yaml b/tests/uranium_tests/pluginTest/txn_box/smoke/smoke.replay.yaml similarity index 100% rename from tests/gold_tests/pluginTest/txn_box/smoke/smoke.replay.yaml rename to tests/uranium_tests/pluginTest/txn_box/smoke/smoke.replay.yaml diff --git a/tests/uranium_tests/pluginTest/txn_box/smoke/test_txn_box_smoke.py b/tests/uranium_tests/pluginTest/txn_box/smoke/test_txn_box_smoke.py new file mode 100644 index 00000000000..b6d1a9018f8 --- /dev/null +++ b/tests/uranium_tests/pluginTest/txn_box/smoke/test_txn_box_smoke.py @@ -0,0 +1,59 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_txn_box_smoke(urtest: UraniumTest) -> None: + # @file + # + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + # + # Copyright 2020, Verizon Media + # + ''' + Basic smoke tests. + ''' + urtest.Summary = ''' + Test basic functions and directives. + ''' + + urtest.SkipUnless(Condition.PluginExists("txn_box.so")) + + urtest.TxnBoxTestAndRun( + "Smoke Test", + "smoke.replay.yaml", + config_path='Auto', + config_key="meta.txn_box.global", + remap=[ + ('http://example.one/3', 'http://example.one/3', ('--key=meta.txn_box.remap-1', 'smoke.replay.yaml')), + ('http://example.one', 'http://example.one') + ]) + urtest.execute() diff --git a/tests/uranium_tests/pluginTest/txn_box/smoke/test_txn_box_smoke_2.py b/tests/uranium_tests/pluginTest/txn_box/smoke/test_txn_box_smoke_2.py new file mode 100644 index 00000000000..cc7f4d046b8 --- /dev/null +++ b/tests/uranium_tests/pluginTest/txn_box/smoke/test_txn_box_smoke_2.py @@ -0,0 +1,60 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_txn_box_smoke_2(urtest: UraniumTest) -> None: + # @file + # + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + # + # Copyright 2020, Verizon Media + # + ''' + Basic smoke tests. + ''' + urtest.Summary = ''' + Test basic functions and directives via remaps. + ''' + + urtest.SkipUnless(Condition.PluginExists("txn_box.so")) + + replay_file = 'smoke-2.replay.yaml' + urtest.TxnBoxTestAndRun( + "Smoke 2 Test", + replay_file, + config_path='Auto', + config_key="meta.txn_box.global", + remap=[ + ['http://alpha.ex/', ('--key=meta.txn_box.remap.alpha', replay_file)], + ['http://bravo.ex/', ('--key=meta.txn_box.remap.bravo', replay_file)] + ]) + urtest.execute() diff --git a/tests/gold_tests/pluginTest/txn_box/ssl/alpha-ca.key b/tests/uranium_tests/pluginTest/txn_box/ssl/alpha-ca.key similarity index 100% rename from tests/gold_tests/pluginTest/txn_box/ssl/alpha-ca.key rename to tests/uranium_tests/pluginTest/txn_box/ssl/alpha-ca.key diff --git a/tests/gold_tests/pluginTest/txn_box/ssl/alpha-ca.pem b/tests/uranium_tests/pluginTest/txn_box/ssl/alpha-ca.pem similarity index 100% rename from tests/gold_tests/pluginTest/txn_box/ssl/alpha-ca.pem rename to tests/uranium_tests/pluginTest/txn_box/ssl/alpha-ca.pem diff --git a/tests/gold_tests/pluginTest/txn_box/ssl/alpha-signed.cert b/tests/uranium_tests/pluginTest/txn_box/ssl/alpha-signed.cert similarity index 100% rename from tests/gold_tests/pluginTest/txn_box/ssl/alpha-signed.cert rename to tests/uranium_tests/pluginTest/txn_box/ssl/alpha-signed.cert diff --git a/tests/gold_tests/pluginTest/txn_box/ssl/alpha-signed.key b/tests/uranium_tests/pluginTest/txn_box/ssl/alpha-signed.key similarity index 100% rename from tests/gold_tests/pluginTest/txn_box/ssl/alpha-signed.key rename to tests/uranium_tests/pluginTest/txn_box/ssl/alpha-signed.key diff --git a/tests/gold_tests/pluginTest/txn_box/ssl/alpha-signed.pem b/tests/uranium_tests/pluginTest/txn_box/ssl/alpha-signed.pem similarity index 100% rename from tests/gold_tests/pluginTest/txn_box/ssl/alpha-signed.pem rename to tests/uranium_tests/pluginTest/txn_box/ssl/alpha-signed.pem diff --git a/tests/gold_tests/pluginTest/txn_box/ssl/bravo-ca.key b/tests/uranium_tests/pluginTest/txn_box/ssl/bravo-ca.key similarity index 100% rename from tests/gold_tests/pluginTest/txn_box/ssl/bravo-ca.key rename to tests/uranium_tests/pluginTest/txn_box/ssl/bravo-ca.key diff --git a/tests/gold_tests/pluginTest/txn_box/ssl/bravo-ca.pem b/tests/uranium_tests/pluginTest/txn_box/ssl/bravo-ca.pem similarity index 100% rename from tests/gold_tests/pluginTest/txn_box/ssl/bravo-ca.pem rename to tests/uranium_tests/pluginTest/txn_box/ssl/bravo-ca.pem diff --git a/tests/gold_tests/pluginTest/txn_box/ssl/bravo-signed.cert b/tests/uranium_tests/pluginTest/txn_box/ssl/bravo-signed.cert similarity index 100% rename from tests/gold_tests/pluginTest/txn_box/ssl/bravo-signed.cert rename to tests/uranium_tests/pluginTest/txn_box/ssl/bravo-signed.cert diff --git a/tests/gold_tests/pluginTest/txn_box/ssl/bravo-signed.key b/tests/uranium_tests/pluginTest/txn_box/ssl/bravo-signed.key similarity index 100% rename from tests/gold_tests/pluginTest/txn_box/ssl/bravo-signed.key rename to tests/uranium_tests/pluginTest/txn_box/ssl/bravo-signed.key diff --git a/tests/gold_tests/pluginTest/txn_box/ssl/bravo-signed.pem b/tests/uranium_tests/pluginTest/txn_box/ssl/bravo-signed.pem similarity index 100% rename from tests/gold_tests/pluginTest/txn_box/ssl/bravo-signed.pem rename to tests/uranium_tests/pluginTest/txn_box/ssl/bravo-signed.pem diff --git a/tests/gold_tests/pluginTest/txn_box/ssl/ca-bundle.pem b/tests/uranium_tests/pluginTest/txn_box/ssl/ca-bundle.pem similarity index 100% rename from tests/gold_tests/pluginTest/txn_box/ssl/ca-bundle.pem rename to tests/uranium_tests/pluginTest/txn_box/ssl/ca-bundle.pem diff --git a/tests/gold_tests/pluginTest/txn_box/ssl/charlie-ca.key b/tests/uranium_tests/pluginTest/txn_box/ssl/charlie-ca.key similarity index 100% rename from tests/gold_tests/pluginTest/txn_box/ssl/charlie-ca.key rename to tests/uranium_tests/pluginTest/txn_box/ssl/charlie-ca.key diff --git a/tests/gold_tests/pluginTest/txn_box/ssl/charlie-ca.pem b/tests/uranium_tests/pluginTest/txn_box/ssl/charlie-ca.pem similarity index 100% rename from tests/gold_tests/pluginTest/txn_box/ssl/charlie-ca.pem rename to tests/uranium_tests/pluginTest/txn_box/ssl/charlie-ca.pem diff --git a/tests/gold_tests/pluginTest/txn_box/ssl/charlie-signed.cert b/tests/uranium_tests/pluginTest/txn_box/ssl/charlie-signed.cert similarity index 100% rename from tests/gold_tests/pluginTest/txn_box/ssl/charlie-signed.cert rename to tests/uranium_tests/pluginTest/txn_box/ssl/charlie-signed.cert diff --git a/tests/gold_tests/pluginTest/txn_box/ssl/charlie-signed.key b/tests/uranium_tests/pluginTest/txn_box/ssl/charlie-signed.key similarity index 100% rename from tests/gold_tests/pluginTest/txn_box/ssl/charlie-signed.key rename to tests/uranium_tests/pluginTest/txn_box/ssl/charlie-signed.key diff --git a/tests/gold_tests/pluginTest/txn_box/ssl/charlie-signed.pem b/tests/uranium_tests/pluginTest/txn_box/ssl/charlie-signed.pem similarity index 100% rename from tests/gold_tests/pluginTest/txn_box/ssl/charlie-signed.pem rename to tests/uranium_tests/pluginTest/txn_box/ssl/charlie-signed.pem diff --git a/tests/gold_tests/pluginTest/txn_box/ssl/pv-server.cert b/tests/uranium_tests/pluginTest/txn_box/ssl/pv-server.cert similarity index 100% rename from tests/gold_tests/pluginTest/txn_box/ssl/pv-server.cert rename to tests/uranium_tests/pluginTest/txn_box/ssl/pv-server.cert diff --git a/tests/gold_tests/pluginTest/txn_box/ssl/server.key b/tests/uranium_tests/pluginTest/txn_box/ssl/server.key similarity index 100% rename from tests/gold_tests/pluginTest/txn_box/ssl/server.key rename to tests/uranium_tests/pluginTest/txn_box/ssl/server.key diff --git a/tests/gold_tests/pluginTest/txn_box/ssl/server.pem b/tests/uranium_tests/pluginTest/txn_box/ssl/server.pem similarity index 100% rename from tests/gold_tests/pluginTest/txn_box/ssl/server.pem rename to tests/uranium_tests/pluginTest/txn_box/ssl/server.pem diff --git a/tests/gold_tests/pluginTest/txn_box/static_file/static_file.replay.yaml b/tests/uranium_tests/pluginTest/txn_box/static_file/static_file.replay.yaml similarity index 100% rename from tests/gold_tests/pluginTest/txn_box/static_file/static_file.replay.yaml rename to tests/uranium_tests/pluginTest/txn_box/static_file/static_file.replay.yaml diff --git a/tests/gold_tests/pluginTest/txn_box/static_file/static_file.txt b/tests/uranium_tests/pluginTest/txn_box/static_file/static_file.txt similarity index 100% rename from tests/gold_tests/pluginTest/txn_box/static_file/static_file.txt rename to tests/uranium_tests/pluginTest/txn_box/static_file/static_file.txt diff --git a/tests/uranium_tests/pluginTest/txn_box/static_file/test_txn_box_static_file.py b/tests/uranium_tests/pluginTest/txn_box/static_file/test_txn_box_static_file.py new file mode 100644 index 00000000000..0a716aa8860 --- /dev/null +++ b/tests/uranium_tests/pluginTest/txn_box/static_file/test_txn_box_static_file.py @@ -0,0 +1,73 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_txn_box_static_file(urtest: UraniumTest) -> None: + ''' + Static file serving and handling. + ''' + import os.path + + # @file + # + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + # + # Copyright 2020, Verizon Media + # + + urtest.Summary = ''' + Server static file as response body. + ''' + + urtest.SkipUnless(Condition.PluginExists("txn_box.so")) + + r = urtest.TxnBoxTestAndRun( + "Static file support", + "static_file.replay.yaml", + config_path='Auto', + config_key="meta.txn-box.global", + remap=[['http://base.ex', ['--key=meta.txn-box.remap', 'static_file.replay.yaml']]]) + ts = r.Variables.TS + ts.Setup.Copy("static_file.txt", ts.Variables.CONFIGDIR) + ts.Setup.Copy("unreadable.txt", ts.Variables.CONFIGDIR) + ts.Disk.records_config.update({'proxy.config.diags.debug.enabled': 1, 'proxy.config.diags.debug.tags': 'txn_box|http'}) + ts.Disk.diags_log.Content = Testers.ContainsExpression( + 'Unable to read file ".*unreadable.txt" for text block "unreadable"', + "Verify that losing access to text block content is logged.") + + r.StillRunningAfter = ts + + tr = urtest.AddTestRun("Make text block file unreadable") + tr.StillRunningBefore = ts + tr.Processes.Default.Command = f"chmod 000 {os.path.join(ts.Variables.CONFIGDIR, 'unreadable.txt')} && sleep 2" + tr.Processes.Default.ReturnCode = 0 + urtest.execute() diff --git a/tests/gold_tests/pluginTest/txn_box/static_file/unreadable.txt b/tests/uranium_tests/pluginTest/txn_box/static_file/unreadable.txt similarity index 100% rename from tests/gold_tests/pluginTest/txn_box/static_file/unreadable.txt rename to tests/uranium_tests/pluginTest/txn_box/static_file/unreadable.txt diff --git a/tests/gold_tests/pluginTest/uri_signing/config.json b/tests/uranium_tests/pluginTest/uri_signing/config.json similarity index 100% rename from tests/gold_tests/pluginTest/uri_signing/config.json rename to tests/uranium_tests/pluginTest/uri_signing/config.json diff --git a/tests/gold_tests/pluginTest/uri_signing/gold/200.gold b/tests/uranium_tests/pluginTest/uri_signing/gold/200.gold similarity index 100% rename from tests/gold_tests/pluginTest/uri_signing/gold/200.gold rename to tests/uranium_tests/pluginTest/uri_signing/gold/200.gold diff --git a/tests/gold_tests/pluginTest/uri_signing/gold/403.gold b/tests/uranium_tests/pluginTest/uri_signing/gold/403.gold similarity index 100% rename from tests/gold_tests/pluginTest/uri_signing/gold/403.gold rename to tests/uranium_tests/pluginTest/uri_signing/gold/403.gold diff --git a/tests/gold_tests/pluginTest/uri_signing/run_sign.sh b/tests/uranium_tests/pluginTest/uri_signing/run_sign.sh similarity index 100% rename from tests/gold_tests/pluginTest/uri_signing/run_sign.sh rename to tests/uranium_tests/pluginTest/uri_signing/run_sign.sh diff --git a/tests/gold_tests/pluginTest/uri_signing/signer.json b/tests/uranium_tests/pluginTest/uri_signing/signer.json similarity index 100% rename from tests/gold_tests/pluginTest/uri_signing/signer.json rename to tests/uranium_tests/pluginTest/uri_signing/signer.json diff --git a/tests/uranium_tests/pluginTest/uri_signing/test_uri_signing.py b/tests/uranium_tests/pluginTest/uri_signing/test_uri_signing.py new file mode 100644 index 00000000000..c2bfd4a084f --- /dev/null +++ b/tests/uranium_tests/pluginTest/uri_signing/test_uri_signing.py @@ -0,0 +1,273 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_uri_signing(urtest: UraniumTest) -> None: + ''' + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + urtest.Summary = ''' + Test uri_signing plugin + ''' + + urtest.ContinueOnFail = False + + # Skip if plugins not present. + urtest.SkipUnless(Condition.PluginExists('uri_signing.so')) + + server = urtest.MakeOriginServer("server") + + # Default origin test + req_header = { + "headers": "GET / HTTP/1.1\r\nHost: www.example.com\r\n\r\n", + "timestamp": "1469733493.993", + "body": "", + } + res_header = { + "headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", + "timestamp": "1469733493.993", + "body": "", + } + server.addResponse("sessionfile.log", req_header, res_header) + + # Test case for normal + req_header = { + "headers": "GET /someasset.ts HTTP/1.1\r\nHost: somehost\r\n\r\n", + "timestamp": "1469733493.993", + "body": "", + } + + res_header = { + "headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", + "timestamp": "1469733493.993", + "body": "somebody", + } + + server.addResponse("sessionfile.log", req_header, res_header) + + # Test case for crossdomain + req_header = { + "headers": "GET /crossdomain.xml HTTP/1.1\r\nHost: somehost\r\n\r\n", + "timestamp": "1469733493.993", + "body": "", + } + + res_header = { + "headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", + "timestamp": "1469733493.993", + "body": "", + } + + server.addResponse("sessionfile.log", req_header, res_header) + + # http://user:password@host:port/path;params?query#fragment + + # Define default ATS + ts = urtest.MakeATSProcess("ts", enable_cache=False) + #ts = Test.MakeATSProcess("ts", "traffic_server_valgrind.sh") + + ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'uri_signing|http', + # 'proxy.config.plugin.dynamic_reload_mode': 0, + # 'proxy.config.diags.debug.tags': 'uri_signing', + }) + + # Use unchanged incoming URL. + ts.Disk.remap_config.AddLine( + 'map http://somehost/ http://127.0.0.1:{}/'.format(server.Variables.Port) + + ' @plugin=uri_signing.so @pparam={}/config.json'.format(urtest.RunDirectory)) + + # Install configuration + ts.Setup.CopyAs('config.json', urtest.RunDirectory) + ts.Setup.CopyAs('run_sign.sh', urtest.RunDirectory) + ts.Setup.CopyAs('signer.json', urtest.RunDirectory) + #ts.Setup.CopyAs('traffic_server_valgrind.sh', Test.RunDirectory) + + curl_and_args = '-q -v -x localhost:{} '.format(ts.Variables.port) + + # 0 - reject unsigned request + tr = urtest.AddTestRun("unsigned request") + ps = tr.Processes.Default + ps.StartBefore(ts) + ps.StartBefore(server, ready=When.PortOpen(server.Variables.Port)) + tr.MakeCurlCommand(curl_and_args + 'http://somehost/someasset.ts', ts=ts) + ps.ReturnCode = 0 + ps.Streams.stderr = "gold/403.gold" + tr.StillRunningAfter = server + tr.StillRunningAfter = ts + + # 1 - accept a passthru request + tr = urtest.AddTestRun("passthru request") + ps = tr.Processes.Default + tr.MakeCurlCommand(curl_and_args + 'http://somehost/crossdomain.xml', ts=ts) + ps.ReturnCode = 0 + ps.Streams.stderr = "gold/200.gold" + tr.StillRunningAfter = server + tr.StillRunningAfter = ts + + # 2 - good token, signed "forever" (run_sign.sh 0) + tr = urtest.AddTestRun("good signed") + ps = tr.Processes.Default + tr.MakeCurlCommand( + curl_and_args + + '"http://somehost/someasset.ts?URISigningPackage=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJpc3N1ZXIiLCJleHAiOjE5MjMwNTYwODR9.zw_wFQ-wvrWmfPLGj3hAUWn-GOHkiJZi2but4KV0paY"', + ts=ts) + ps.ReturnCode = 0 + ps.Streams.stderr = "gold/200.gold" + tr.StillRunningAfter = server + tr.StillRunningAfter = ts + + # 3 - expired token (run_sign.sh 1) + tr = urtest.AddTestRun("expired signed") + ps = tr.Processes.Default + tr.MakeCurlCommand( + curl_and_args + + '"http://somehost/someasset.ts?URISigningPackage=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJpc3N1ZXIiLCJleHAiOjF9.GkdlOPHQc6BqS4Q6x79GeYuVFO2zuGbaPZZsJfD6ir8"', + ts=ts) + ps.ReturnCode = 0 + ps.Streams.stderr = "gold/403.gold" + tr.StillRunningAfter = server + tr.StillRunningAfter = ts + + # 4 - good token, different key (run_sign.sh 2) + tr = urtest.AddTestRun("good token, second key") + ps = tr.Processes.Default + tr.MakeCurlCommand( + curl_and_args + + '"http://somehost/someasset.ts?URISigningPackage=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJpc3N1ZXIiLCJleHAiOjE5MjMwNTYwODR9.ozH4sNwgcOlTZT0l4RQlVCH_osxz9yI1HCBesEv-jYg"', + ts=ts) + ps.ReturnCode = 0 + ps.Streams.stderr = "gold/200.gold" + tr.StillRunningAfter = server + tr.StillRunningAfter = ts + + # 5 - good token, inline + tr = urtest.AddTestRun("good signed") + ps = tr.Processes.Default + tr.MakeCurlCommand( + curl_and_args + + '"http://somehost/URISigningPackage=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJpc3N1ZXIiLCJleHAiOjE5MjMwNTYwODR9.zw_wFQ-wvrWmfPLGj3hAUWn-GOHkiJZi2but4KV0paY/someasset.ts"', + ts=ts) + ps.ReturnCode = 0 + ps.Streams.stderr = "gold/200.gold" + tr.StillRunningAfter = server + tr.StillRunningAfter = ts + + # 6 - expired token, inline + tr = urtest.AddTestRun("expired signed") + ps = tr.Processes.Default + tr.MakeCurlCommand( + curl_and_args + + '"http://somehost/URISigningPackage=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJpc3N1ZXIiLCJleHAiOjF9.GkdlOPHQc6BqS4Q6x79GeYuVFO2zuGbaPZZsJfD6ir8/someasset.ts"', + ts=ts) + ps.ReturnCode = 0 + ps.Streams.stderr = "gold/403.gold" + tr.StillRunningAfter = server + tr.StillRunningAfter = ts + + # 7 - good token, param + tr = urtest.AddTestRun("good signed, param") + ps = tr.Processes.Default + tr.MakeCurlCommand( + curl_and_args + + '"http://somehost/someasset.ts;URISigningPackage=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJpc3N1ZXIiLCJleHAiOjE5MjMwNTYwODR9.zw_wFQ-wvrWmfPLGj3hAUWn-GOHkiJZi2but4KV0paY"', + ts=ts) + ps.ReturnCode = 0 + ps.Streams.stderr = "gold/200.gold" + tr.StillRunningAfter = server + tr.StillRunningAfter = ts + + # 8 - expired token, param + tr = urtest.AddTestRun("expired signed, param") + ps = tr.Processes.Default + tr.MakeCurlCommand( + curl_and_args + + '"http://somehost/someasset.ts;URISigningPackage=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJpc3N1ZXIiLCJleHAiOjF9.GkdlOPHQc6BqS4Q6x79GeYuVFO2zuGbaPZZsJfD6ir8"', + ts=ts) + ps.ReturnCode = 0 + ps.Streams.stderr = "gold/403.gold" + tr.StillRunningAfter = server + tr.StillRunningAfter = ts + + # 9 - let's cookie this + tr = urtest.AddTestRun("good signed cookie") + ps = tr.Processes.Default + tr.MakeCurlCommand( + curl_and_args + + '"http://somehost/someasset.ts" -H "Cookie: URISigningPackage=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJpc3N1ZXIiLCJleHAiOjE5MjMwNTYwODR9.zw_wFQ-wvrWmfPLGj3hAUWn-GOHkiJZi2but4KV0paY"', + ts=ts) + ps.ReturnCode = 0 + ps.Streams.stderr = "gold/200.gold" + tr.StillRunningAfter = server + tr.StillRunningAfter = ts + + # 10 - expired cookie token + tr = urtest.AddTestRun("expired signed cooked") + ps = tr.Processes.Default + tr.MakeCurlCommand( + curl_and_args + + '"http://somehost/someasset.ts" -H "Cookie: URISigningPackage=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJpc3N1ZXIiLCJleHAiOjF9.GkdlOPHQc6BqS4Q6x79GeYuVFO2zuGbaPZZsJfD6ir8"', + ts=ts) + ps.ReturnCode = 0 + ps.Streams.stderr = "gold/403.gold" + tr.StillRunningAfter = server + tr.StillRunningAfter = ts + + # 11 - multiple cookies + tr = urtest.AddTestRun("multiple cookies, expired then good") + ps = tr.Processes.Default + tr.MakeCurlCommand( + curl_and_args + + '"http://somehost/someasset.ts" -H "Cookie: URISigningPackage=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJpc3N1ZXIiLCJleHAiOjF9.GkdlOPHQc6BqS4Q6x79GeYuVFO2zuGbaPZZsJfD6ir8;URISigningPackage=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJpc3N1ZXIiLCJleHAiOjE5MjMwNTYwODR9.zw_wFQ-wvrWmfPLGj3hAUWn-GOHkiJZi2but4KV0paY"', + ts=ts) + ps.ReturnCode = 0 + ps.Streams.stderr = "gold/200.gold" + tr.StillRunningAfter = server + tr.StillRunningAfter = ts + + # 12 - Check missing iss from the payload + tr = urtest.AddTestRun("Missing iss field in the payload") + ps = tr.Processes.Default + tr.MakeCurlCommand( + curl_and_args + + '"http://somehost/someasset.ts?URISigningPackage=ewogICJ0eXAiOiAiSldUIiwKICAiYWxnIjogIkhTMjU2Igp9.ewogICJleHAiOiAxOTIzMDU2MDg0Cn0.zw_wFQ-wvrWmfPLGj3hAUWn-GOHkiJZi2but4KV0paY"', + ts=ts) + ps.ReturnCode = 0 + ps.Streams.stderr = "gold/403.gold" + ts.Disk.traffic_out.Content = Testers.ContainsExpression( + "Initial JWT Failure: iss is missing, must be present", "should fail the validation") + tr.StillRunningAfter = server + tr.StillRunningAfter = ts + urtest.execute() diff --git a/tests/gold_tests/pluginTest/url_sig/run_sign.sh b/tests/uranium_tests/pluginTest/url_sig/run_sign.sh similarity index 100% rename from tests/gold_tests/pluginTest/url_sig/run_sign.sh rename to tests/uranium_tests/pluginTest/url_sig/run_sign.sh diff --git a/tests/uranium_tests/pluginTest/url_sig/test_url_sig.py b/tests/uranium_tests/pluginTest/url_sig/test_url_sig.py new file mode 100644 index 00000000000..b90cabb856b --- /dev/null +++ b/tests/uranium_tests/pluginTest/url_sig/test_url_sig.py @@ -0,0 +1,426 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_url_sig(urtest: UraniumTest) -> None: + ''' + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import hashlib + import hmac + + urtest.Summary = ''' + Test url_sig plugin + ''' + + urtest.ContinueOnFail = True + + # Skip if plugins not present. + urtest.SkipUnless(Condition.PluginExists('url_sig.so')) + + # Skip since tests assume client addresses are IPs + urtest.SkipIf(Condition.CurlUsingUnixDomainSocket()) + + # Set up to check the output after the tests have run. + # + url_sig_log_id = urtest.Disk.File("url_sig_short.log") + url_sig_log_id.Content = "url_sig.gold" + + server = urtest.MakeOriginServer("server") + + request_header = { + "headers": "GET /foo/abcde/qrstuvwxyz HTTP/1.1\r\nHost: just.any.thing\r\n\r\n", + "timestamp": "1469733493.993", + "body": "" + } + # expected response from the origin server + response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + # add response to the server dictionary + server.addResponse("sessionfile.log", request_header, response_header) + + # Add responses for excl_regex URLs that should bypass signature checks + crossdomain_request = { + "headers": "GET /crossdomain.xml HTTP/1.1\r\nHost: just.any.thing\r\n\r\n", + "timestamp": "1469733493.993", + "body": "" + } + crossdomain_response = { + "headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", + "timestamp": "1469733493.993", + "body": "crossdomain" + } + server.addResponse("sessionfile.log", crossdomain_request, crossdomain_response) + + clientaccess_request = { + "headers": "GET /clientaccesspolicy.xml HTTP/1.1\r\nHost: just.any.thing\r\n\r\n", + "timestamp": "1469733493.993", + "body": "" + } + clientaccess_response = { + "headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", + "timestamp": "1469733493.993", + "body": "clientaccess" + } + server.addResponse("sessionfile.log", clientaccess_request, clientaccess_response) + + test_html_request = { + "headers": "GET /test.html HTTP/1.1\r\nHost: just.any.thing\r\n\r\n", + "timestamp": "1469733493.993", + "body": "" + } + test_html_response = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": "test"} + server.addResponse("sessionfile.log", test_html_request, test_html_response) + + # Define default ATS. Disable the cache to make sure each request is forwarded + # to the origin server. + ts = urtest.MakeATSProcess("ts", enable_tls=True, enable_cache=False) + + ts.addDefaultSSLFiles() + + ts.Disk.records_config.update( + { + # 'proxy.config.diags.debug.enabled': 1, + # 'proxy.config.diags.debug.tags': 'http|url_sig', + 'proxy.config.proxy_name': 'Poxy_Proxy', # This will be the server name. + 'proxy.config.ssl.server.cert.path': ts.Variables.SSLDir, + 'proxy.config.ssl.server.private_key.path': ts.Variables.SSLDir, + }) + + ts.Disk.ssl_multicert_yaml.AddLines( + """ + ssl_multicert: + - dest_ip: "*" + ssl_cert_name: server.pem + ssl_key_name: server.key + """.split("\n")) + + # Use unchanged incoming URL. + # + ts.Setup.Copy("url_sig.config", ts.Variables.CONFIGDIR) + ts.Disk.remap_config.AddLine( + f'map http://one.two.three/ http://127.0.0.1:{server.Variables.Port}/' + ' @plugin=url_sig.so @pparam=url_sig.config') + + # Use unchanged incoming HTTPS URL. + # + ts.Disk.remap_config.AddLine( + f'map https://one.two.three/ http://127.0.0.1:{server.Variables.Port}/' + ' @plugin=url_sig.so @pparam=url_sig.config') + + # Use pristine URL, incoming URL unchanged. + # + ts.Disk.remap_config.AddLine( + f'map http://four.five.six/ http://127.0.0.1:{server.Variables.Port}/' + + ' @plugin=url_sig.so @pparam=url_sig.config @pparam=pristineurl') + + # Use pristine URL, incoming URL changed. + # + ts.Disk.remap_config.AddLine( + f'map http://seven.eight.nine/ http://127.0.0.1:{server.Variables.Port}' + + ' @plugin=url_sig.so @pparam=url_sig.config @pparam=PristineUrl') + + # Use config with all settings set + # + ts.Setup.Copy("url_sig.all.config", ts.Variables.CONFIGDIR) + ts.Disk.remap_config.AddLine( + f'map http://ten.eleven.twelve/ http://127.0.0.1:{server.Variables.Port}/' + + ' @plugin=url_sig.so @pparam=url_sig.all.config') + + # Validation failure tests. + + LogTee = f" 2>&1 | grep '^<' | tee -a {urtest.RunDirectory}/url_sig_long.log" + + # Bad client / MD5 / P=101 / URL pristine / URL altered. + # + tr = urtest.AddTestRun("Bad client IP should fail signature check") + p = tr.MakeCurlCommand( + f"--verbose --proxy http://127.0.0.1:{ts.Variables.port} 'http://seven.eight.nine/" + + "foo/abcde/qrstuvwxyz?C=127.0.0.2&E=33046620008&A=2&K=13&P=101&S=d1f352d4f1d931ad2f441013402d93f8'" + LogTee, + ts=ts) + p.StartBefore(ts) + p.StartBefore(server, ready=When.PortOpen(server.Variables.Port)) + p.ReturnCode = 0 + p.Streams.stdout = Testers.ContainsExpression("HTTP.*403", "Should receive 403 Forbidden") + + # With client / MD5 / P=010 / URL pristine / URL altered -- Expired. + # + tr = urtest.AddTestRun("Expired signature should fail") + p = tr.MakeCurlCommand( + f"--verbose --proxy http://127.0.0.1:{ts.Variables.port} 'http://seven.eight.nine/" + + "foo/abcde/qrstuvwxyz?C=127.0.0.1&E=1&A=2&K=13&P=010&S=f237aad1fa010234d7bf8108a0e36387'" + LogTee, + ts=ts) + p.ReturnCode = 0 + p.Streams.stdout = Testers.ContainsExpression("HTTP.*403", "Should receive 403 Forbidden") + + # With client / No algorithm / P=101 / URL pristine / URL altered. + # + tr = urtest.AddTestRun("Missing algorithm parameter should fail") + p = tr.MakeCurlCommand( + f"--verbose --proxy http://127.0.0.1:{ts.Variables.port} 'http://seven.eight.nine/" + + "foo/abcde/qrstuvwxyz?C=127.0.0.1&E=33046620008&K=13&P=101&S=d1f352d4f1d931ad2f441013402d93f8'" + LogTee, + ts=ts) + p.ReturnCode = 0 + p.Streams.stdout = Testers.ContainsExpression("HTTP.*403", "Should receive 403 Forbidden") + + # With client / Bad algorithm / P=101 / URL pristine / URL altered. + # + tr = urtest.AddTestRun("Invalid algorithm (A=3) should fail") + p = tr.MakeCurlCommand( + f"--verbose --proxy http://127.0.0.1:{ts.Variables.port} 'http://seven.eight.nine/" + + "foo/abcde/qrstuvwxyz?C=127.0.0.1&E=33046620008&A=3&K=13&P=101&S=d1f352d4f1d931ad2f441013402d93f8'" + LogTee, + ts=ts) + p.ReturnCode = 0 + p.Streams.stdout = Testers.ContainsExpression("HTTP.*403", "Should receive 403 Forbidden") + + # With client / MD5 / No parts / URL pristine / URL altered. + # + tr = urtest.AddTestRun("Missing parts parameter should fail") + p = tr.MakeCurlCommand( + f"--verbose --proxy http://127.0.0.1:{ts.Variables.port} 'http://seven.eight.nine/" + + "foo/abcde/qrstuvwxyz?C=127.0.0.1&E=33046620008&A=2&K=13&S=d1f352d4f1d931ad2f441013402d93f8'" + LogTee, + ts=ts) + p.ReturnCode = 0 + p.Streams.stdout = Testers.ContainsExpression("HTTP.*403", "Should receive 403 Forbidden") + + # With client / MD5 / P=10 (bad) / URL pristine / URL altered. + # + tr = urtest.AddTestRun("Invalid parts value (P=10) should fail") + p = tr.MakeCurlCommand( + f"--verbose --proxy http://127.0.0.1:{ts.Variables.port} 'http://seven.eight.nine/" + + "foo/abcde/qrstuvwxyz?C=127.0.0.1&E=33046620008&A=2&K=13&P=10&S=d1f352d4f1d931ad2f441013402d93f8'" + LogTee, + ts=ts) + p.ReturnCode = 0 + p.Streams.stdout = Testers.ContainsExpression("HTTP.*403", "Should receive 403 Forbidden") + + # With client / MD5 / P=101 / URL pristine / URL altered -- No signature. + # + tr = urtest.AddTestRun("Missing signature parameter should fail") + p = tr.MakeCurlCommand( + f"--verbose --proxy http://127.0.0.1:{ts.Variables.port} 'http://seven.eight.nine/" + + "foo/abcde/qrstuvwxyz?C=127.0.0.1&E=33046620008&A=2&K=13&P=101'" + LogTee, + ts=ts) + p.ReturnCode = 0 + p.Streams.stdout = Testers.ContainsExpression("HTTP.*403", "Should receive 403 Forbidden") + + # With client / MD5 / P=101 / URL pristine / URL altered -- Bad signature. + # + tr = urtest.AddTestRun("Incorrect signature should fail") + p = tr.MakeCurlCommand( + f"--verbose --proxy http://127.0.0.1:{ts.Variables.port} 'http://seven.eight.nine/" + + "foo/abcde/qrstuvwxyz?C=127.0.0.1&E=33046620008&A=2&K=13&P=101&S=d1f452d4f1d931ad2f441013402d93f8'" + LogTee, + ts=ts) + p.ReturnCode = 0 + p.Streams.stdout = Testers.ContainsExpression("HTTP.*403", "Should receive 403 Forbidden") + + # With client / MD5 / P=101 / URL pristine / URL altered -- Spurious &. + # + tr = urtest.AddTestRun("Spurious ampersand should fail signature check") + p = tr.MakeCurlCommand( + f"--verbose --proxy http://127.0.0.1:{ts.Variables.port} 'http://seven.eight.nine/" + + "foo/abcde/qrstuvwxyz?C=127.0.0.1&E=33046620008&A=2&&K=13&P=101&S=d1f352d4f1d931ad2f441013402d93f8#'" + LogTee, + ts=ts) + p.ReturnCode = 0 + p.Streams.stdout = Testers.ContainsExpression("HTTP.*403", "Should receive 403 Forbidden") + + # With client / MD5 / Only C parameter -- truncated query string. + # + tr = urtest.AddTestRun("Truncated query string with only client IP should fail") + p = tr.MakeCurlCommand( + f"--verbose --proxy http://127.0.0.1:{ts.Variables.port} 'http://seven.eight.nine/" + "foo/abcde/qrstuvwxyz?C=127.0.0.1'" + + LogTee, + ts=ts) + p.ReturnCode = 0 + p.Streams.stdout = Testers.ContainsExpression("HTTP.*403", "Should receive 403 Forbidden") + + # With client / MD5 / C parameter last in query -- missing trailing delimiter. + # + tr = urtest.AddTestRun("Client IP as final query parameter should fail") + p = tr.MakeCurlCommand( + f"--verbose --proxy http://127.0.0.1:{ts.Variables.port} 'http://seven.eight.nine/" + + "foo/abcde/qrstuvwxyz?E=33046620008&A=2&K=13&P=101&S=d1f352d4f1d931ad2f441013402d93f8&C=127.0.0.1'" + LogTee, + ts=ts) + p.ReturnCode = 0 + p.Streams.stdout = Testers.ContainsExpression("HTTP.*403", "Should receive 403 Forbidden") + + # With client / MD5 / C parameter has empty value. + # + tr = urtest.AddTestRun("Empty client IP value should fail") + p = tr.MakeCurlCommand( + f"--verbose --proxy http://127.0.0.1:{ts.Variables.port} 'http://seven.eight.nine/" + + "foo/abcde/qrstuvwxyz?C=&E=33046620008&A=2&K=13&P=101&S=d1f352d4f1d931ad2f441013402d93f8'" + LogTee, + ts=ts) + p.ReturnCode = 0 + p.Streams.stdout = Testers.ContainsExpression("HTTP.*403", "Should receive 403 Forbidden") + + # Success tests. + + # Test excl_regex feature - URLs matching the exclusion regex should bypass signature checks. + # The url_sig.all.config has: excl_regex = (/crossdomain.xml|/clientaccesspolicy.xml|/test.html) + # + tr = urtest.AddTestRun("Excluded URL /crossdomain.xml should bypass signature check") + p = tr.MakeCurlCommand( + f"--verbose --proxy http://127.0.0.1:{ts.Variables.port} 'http://ten.eleven.twelve/crossdomain.xml'" + LogTee, ts=ts) + p.ReturnCode = 0 + p.Streams.stdout = Testers.ContainsExpression("HTTP.*200", "Should receive 200 OK without signature") + + # Test another excluded URL. + # + tr = urtest.AddTestRun("Excluded URL /clientaccesspolicy.xml should bypass signature check") + p = tr.MakeCurlCommand( + f"--verbose --proxy http://127.0.0.1:{ts.Variables.port} 'http://ten.eleven.twelve/clientaccesspolicy.xml'" + LogTee, ts=ts) + p.ReturnCode = 0 + p.Streams.stdout = Testers.ContainsExpression("HTTP.*200", "Should receive 200 OK without signature") + + # Test third excluded URL. + # + tr = urtest.AddTestRun("Excluded URL /test.html should bypass signature check") + p = tr.MakeCurlCommand( + f"--verbose --proxy http://127.0.0.1:{ts.Variables.port} 'http://ten.eleven.twelve/test.html'" + LogTee, ts=ts) + p.ReturnCode = 0 + p.Streams.stdout = Testers.ContainsExpression("HTTP.*200", "Should receive 200 OK without signature") + + # Test that non-excluded URL still requires signature. + # + tr = urtest.AddTestRun("Non-excluded URL /other.html should require signature and fail") + p = tr.MakeCurlCommand( + f"--verbose --proxy http://127.0.0.1:{ts.Variables.port} 'http://ten.eleven.twelve/other.html'" + LogTee, ts=ts) + p.ReturnCode = 0 + p.Streams.stdout = Testers.ContainsExpression("HTTP.*403", "Should receive 403 Forbidden without signature") + + # With client / SHA1 / P=1 / URL pristine / URL not altered. + # + tr = urtest.AddTestRun("Valid SHA1 signature with client IP should succeed") + p = tr.MakeCurlCommand( + f"--verbose --proxy http://127.0.0.1:{ts.Variables.port} 'http://four.five.six/" + + "foo/abcde/qrstuvwxyz?C=127.0.0.1&E=33046618556&A=1&K=15&P=1&S=f4103561a23adab7723a89b9831d77e0afb61d92'" + LogTee, + ts=ts) + p.ReturnCode = 0 + p.Streams.stdout = Testers.ContainsExpression("HTTP.*200", "Should receive 200 OK") + + # No client / MD5 / P=1 / URL pristine / URL altered. + # + tr = urtest.AddTestRun("Valid MD5 signature without client IP should succeed") + p = tr.MakeCurlCommand( + f"--verbose --proxy http://127.0.0.1:{ts.Variables.port} 'http://seven.eight.nine/" + + "foo/abcde/qrstuvwxyz?E=33046618586&A=2&K=0&P=1&S=0364efa28afe345544596705b92d20ac'" + LogTee, + ts=ts) + p.ReturnCode = 0 + p.Streams.stdout = Testers.ContainsExpression("HTTP.*200", "Should receive 200 OK") + + # With client / MD5 / P=010 / URL pristine / URL altered. + # + tr = urtest.AddTestRun("Valid MD5 signature with client IP and P=010 should succeed") + p = tr.MakeCurlCommand( + f"--verbose --proxy http://127.0.0.1:{ts.Variables.port} 'http://seven.eight.nine/" + + "foo/abcde/qrstuvwxyz?C=127.0.0.1&E=33046619717&A=2&K=13&P=010&S=f237aad1fa010234d7bf8108a0e36387'" + LogTee, + ts=ts) + p.ReturnCode = 0 + p.Streams.stdout = Testers.ContainsExpression("HTTP.*200", "Should receive 200 OK") + + # With client / MD5 / P=101 / URL pristine / URL altered. + # + tr = urtest.AddTestRun("Valid MD5 signature with client IP and P=101 should succeed") + p = tr.MakeCurlCommand( + f"--verbose --proxy http://127.0.0.1:{ts.Variables.port} 'http://seven.eight.nine/" + + "foo/abcde/qrstuvwxyz?C=127.0.0.1&E=33046620008&A=2&K=13&P=101&S=d1f352d4f1d931ad2f441013402d93f8'" + LogTee, + ts=ts) + p.ReturnCode = 0 + p.Streams.stdout = Testers.ContainsExpression("HTTP.*200", "Should receive 200 OK") + + def sign(payload, key): + secret = bytes(key, 'utf-8') + data = bytes(payload, 'utf-8') + md = bytes(hmac.new(secret, data, digestmod=hashlib.sha1).digest().hex(), 'utf-8') + return md.decode("utf-8") + + # No client / SHA1 / P=1 / URL not pristine / URL not altered. + # + path = "foo/abcde/qrstuvwxyz?E=33046618506&A=1&K=7&P=1&S=" + to_sign = f"127.0.0.1:{server.Variables.Port}/{path}" + url = "http://one.two.three/" + path + sign(to_sign, "dqsgopTSM_doT6iAysasQVUKaPykyb6e") + + tr = urtest.AddTestRun("Valid SHA1 signature without client IP (non-pristine URL) should succeed") + p = tr.MakeCurlCommand(f"--verbose --proxy http://127.0.0.1:{ts.Variables.port} '{url}'" + LogTee, ts=ts) + p.ReturnCode = 0 + p.Streams.stdout = Testers.ContainsExpression("HTTP.*200", "Should receive 200 OK") + + # With client / MD5 / P=101 / URL pristine / URL altered. + # uses url_type pristine in config + tr = urtest.AddTestRun("Valid MD5 signature with pristine URL config should succeed") + p = tr.MakeCurlCommand( + f"--verbose --proxy http://127.0.0.1:{ts.Variables.port} 'http://ten.eleven.twelve/" + + "foo/abcde/qrstuvwxyz?C=127.0.0.1&E=33046620008&A=2&K=13&P=101&S=586ef8e808caeeea025c525c89ff2638'" + LogTee, + ts=ts) + p.ReturnCode = 0 + p.Streams.stdout = Testers.ContainsExpression("HTTP.*200", "Should receive 200 OK") + + # Single fail test - check for bad path param inserted + # With client / MD5 / P=101 / URL pristine / URL altered. Bad Path Param + # uses url_type pristine in config + tr = urtest.AddTestRun("Bad path parameter injection should fail signature check") + p = tr.MakeCurlCommand( + f"--verbose --proxy http://127.0.0.1:{ts.Variables.port} 'http://ten.eleven.twelve/" + + "foo/abcde/qrstuvwxyz;badparam=true?C=127.0.0.1&E=33046620008&A=2&K=13&P=101&S=586ef8e808caeeea025c525c89ff2638'" + LogTee, + ts=ts) + p.ReturnCode = 0 + p.Streams.stdout = Testers.ContainsExpression("HTTP.*403", "Should receive 403 Forbidden") + + # Success + # With client / SHA1 / P=1 / URL pristine / URL altered. Base64 Encoded Path Param + tr = urtest.AddTestRun("Valid base64 encoded path parameter signature should succeed") + p = tr.MakeCurlCommand( + f"--verbose --proxy http://127.0.0.1:{ts.Variables.port} 'http://ten.eleven.twelve/" + + "foo/abcde;urlsig=Qz0xMjcuMC4wLjE7RT0zMzA0NjYyMDAwODtBPTI7Sz0xMztQPTEwMTtTPTA1MDllZjljY2VlNjUxZWQ1OTQxM2MyZjE3YmVhODZh/qrstuvwxyz'" + + LogTee, + ts=ts) + p.ReturnCode = 0 + p.Streams.stdout = Testers.ContainsExpression("HTTP.*200", "Should receive 200 OK") + + # Success + # This test must be last since it converts from the long to the short log output + # No client / SHA1 / P=1 / URL not pristine / URL not altered -- HTTPS. + # + path = "foo/abcde/qrstuvwxyz?E=33046618506&A=1&K=7&P=1&S=" + to_sign = f"127.0.0.1:{server.Variables.Port}/{path}" + url = f"https://127.0.0.1:{ts.Variables.ssl_port}/{path}" + sign(to_sign, "dqsgopTSM_doT6iAysasQVUKaPykyb6e") + + tr = urtest.AddTestRun("Valid SHA1 signature over HTTPS should succeed") + p = tr.MakeCurlCommandMulti( + f"{{curl_base}} --verbose --http1.1 --insecure --header 'Host: one.two.three' '{url}'" + LogTee + + f" ; grep -F -e '< HTTP' -e Authorization {ts.RunDirectory}/url_sig_long.log > {ts.RunDirectory}/url_sig_short.log ", + ts=ts) + p.ReturnCode = 0 + p.Streams.stdout = Testers.ContainsExpression("HTTP.*200", "Should receive 200 OK") + + # Overriding the built in ERROR check since we expect some ERROR messages + ts.Disk.diags_log.Content = Testers.ContainsExpression("ERROR", "Some tests are failure tests") + ts.Disk.diags_log.Content += Testers.ExcludesExpression("Error parsing", "Verify that we can accept long comment lines") + urtest.execute() diff --git a/tests/gold_tests/pluginTest/url_sig/url_sig.all.config b/tests/uranium_tests/pluginTest/url_sig/url_sig.all.config similarity index 100% rename from tests/gold_tests/pluginTest/url_sig/url_sig.all.config rename to tests/uranium_tests/pluginTest/url_sig/url_sig.all.config diff --git a/tests/gold_tests/pluginTest/url_sig/url_sig.config b/tests/uranium_tests/pluginTest/url_sig/url_sig.config similarity index 100% rename from tests/gold_tests/pluginTest/url_sig/url_sig.config rename to tests/uranium_tests/pluginTest/url_sig/url_sig.config diff --git a/tests/gold_tests/pluginTest/url_sig/url_sig.gold b/tests/uranium_tests/pluginTest/url_sig/url_sig.gold similarity index 100% rename from tests/gold_tests/pluginTest/url_sig/url_sig.gold rename to tests/uranium_tests/pluginTest/url_sig/url_sig.gold diff --git a/tests/gold_tests/pluginTest/webp_transform/replay/webp_chunked_cap.replay.yaml b/tests/uranium_tests/pluginTest/webp_transform/replay/webp_chunked_cap.replay.yaml similarity index 100% rename from tests/gold_tests/pluginTest/webp_transform/replay/webp_chunked_cap.replay.yaml rename to tests/uranium_tests/pluginTest/webp_transform/replay/webp_chunked_cap.replay.yaml diff --git a/tests/uranium_tests/pluginTest/webp_transform/test_webp_transform_cache_refused.py b/tests/uranium_tests/pluginTest/webp_transform/test_webp_transform_cache_refused.py new file mode 100644 index 00000000000..e32da7aae63 --- /dev/null +++ b/tests/uranium_tests/pluginTest/webp_transform/test_webp_transform_cache_refused.py @@ -0,0 +1,111 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_webp_transform_cache_refused(urtest: UraniumTest) -> None: + ''' + An over-cap response that webp_transform refuses with a 502 must not poison the + cache. + + The refused path produces a zero-length body and rewrites the client status to + 502 in the send-response-headers hook. The cacheable object, however, is driven + by the origin's 200 server response plus the transform's (empty) output, and the + read hook already stamped a transformed Content-Type onto that server response. + If nothing marks the refused response uncacheable, ATS can store a 200 with an + empty body labeled image/webp and serve that poisoned entry to later clients + while the original requester saw a 502. + + This test enables caching (and forces caching of responses without explicit + freshness headers), drives the over-cap chunked body twice through the same URL, + and asserts BOTH requests are refused with a 502 and an empty body. A cached + poisoned 200 would show up as a 200 (and/or a non-zero body) on the second + request. + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + urtest.Summary = 'An over-cap refused (502) response must not poison the cache' + + urtest.SkipUnless(Condition.PluginExists('webp_transform.so')) + + urtest.ContinueOnFail = True + + server = urtest.MakeVerifierServerProcess("server", "replay/webp_chunked_cap.replay.yaml") + + ts = urtest.MakeATSProcess("ts", enable_cache=True) + ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'webp_transform', + # Cache aggressively: store 200s even without explicit freshness headers, + # so a refused response that is not marked no-store WOULD be cached. This + # makes the poisoning observable if the fix regresses. + 'proxy.config.http.cache.required_headers': 0, + 'proxy.config.http.cache.ignore_client_cc_max_age': 1, + }) + ts.Disk.plugin_config.AddLine('webp_transform.so convert_to_webp') + ts.Disk.remap_config.AddLine('map http://127.0.0.1:{0}/ http://127.0.0.1:{0}/'.format(server.Variables.http_port)) + + ts.Disk.diags_log.Content = Testers.ContainsExpression( + "response body exceeds cap", "The in-transform cap must trip for the chunked body with no Content-Length") + + # Request 1: origin is contacted, the cap trips mid-stream, client gets a 502. + tr = urtest.AddTestRun("first request: over-cap chunked body refused with 502") + tr.MakeCurlCommand( + '-sS -D - -o /dev/null -w "size_download=%{{size_download}}" -x 127.0.0.1:{0} ' + '-H "Accept: image/webp" -H "uuid: chunked-huge" http://127.0.0.1:{1}/chunked-huge.jpg'.format( + ts.Variables.port, server.Variables.http_port), + ts=ts) + tr.Processes.Default.StartBefore(server) + tr.Processes.Default.StartBefore(ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.stdout = Testers.ContainsExpression( + "HTTP/1.1 502", "First over-cap request must be refused with a 502") + tr.Processes.Default.Streams.stdout += Testers.ContainsExpression( + "size_download=0", "First refused response must have an empty body") + + # Request 2: if the refused response was cached, this hit would serve a poisoned + # 200 with an empty (or mislabeled) body. The fix marks the refused response + # uncacheable, so this must also be a fresh 502 with an empty body. + tr2 = urtest.AddTestRun("second request: must not be served a cached poisoned 200") + tr2.MakeCurlCommand( + '-sS -D - -o /dev/null -w "size_download=%{{size_download}}" -x 127.0.0.1:{0} ' + '-H "Accept: image/webp" -H "uuid: chunked-huge" http://127.0.0.1:{1}/chunked-huge.jpg'.format( + ts.Variables.port, server.Variables.http_port), + ts=ts) + tr2.Processes.Default.ReturnCode = 0 + tr2.Processes.Default.Streams.stdout = Testers.ContainsExpression( + "HTTP/1.1 502", "Second request must also be a 502, not a cached poisoned 200") + tr2.Processes.Default.Streams.stdout += Testers.ExcludesExpression( + "HTTP/1.1 200", "Second request must not be served a cached 200 (cache poisoning)") + tr2.Processes.Default.Streams.stdout += Testers.ContainsExpression( + "size_download=0", "Second response body must be empty; no cached oversized/empty image") + urtest.execute() diff --git a/tests/uranium_tests/pluginTest/webp_transform/test_webp_transform_chunked_cap.py b/tests/uranium_tests/pluginTest/webp_transform/test_webp_transform_chunked_cap.py new file mode 100644 index 00000000000..048ea33957f --- /dev/null +++ b/tests/uranium_tests/pluginTest/webp_transform/test_webp_transform_chunked_cap.py @@ -0,0 +1,95 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_webp_transform_chunked_cap(urtest: UraniumTest) -> None: + ''' + webp_transform must refuse an over-cap chunked response + rather than forward an oversized, mislabeled body. + + When the origin advertises a Content-Length over the cap, webp_transform + declines the transform up front and the original response passes through. A + chunked response has no Content-Length, so that early check cannot fire and the + per-transaction cap inside consume() is the only bound. The transform produces + nothing until handleInputComplete, so when the cap is exceeded it drops the + buffer, produces no body, and rewrites the status to 502 in the + send-response-headers hook. The client gets a 502 with an empty body rather + than the oversized image. (Transaction::error() cannot be used; it asserts once + the response is in flight, so the status is changed at send time instead.) + + This test drives a 20 MiB image/jpeg framed with Transfer-Encoding: chunked + through the plugin and confirms the cap trips mid-stream, ATS does not buffer + the whole body or crash, and the client receives a 502 with a zero-length body + rather than the full 20 MiB image. + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + urtest.Summary = 'webp_transform returns 502 for an over-cap chunked no-Content-Length response' + + urtest.SkipUnless(Condition.PluginExists('webp_transform.so')) + + urtest.ContinueOnFail = True + + server = urtest.MakeVerifierServerProcess("server", "replay/webp_chunked_cap.replay.yaml") + + ts = urtest.MakeATSProcess("ts", enable_cache=False) + ts.Disk.records_config.update({ + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'webp_transform', + }) + ts.Disk.plugin_config.AddLine('webp_transform.so convert_to_webp') + ts.Disk.remap_config.AddLine('map http://127.0.0.1:{0}/ http://127.0.0.1:{0}/'.format(server.Variables.http_port)) + + # No Content-Length, so the up-front decline cannot fire; the cap trips inside + # consume(), which emits this message at ERROR level to diags.log. Asserting it + # both confirms the in-transform cap engaged and tells autest the ERROR line is + # expected (the default check fails on any ERROR in diags.log). + ts.Disk.diags_log.Content = Testers.ContainsExpression( + "response body exceeds cap", "The in-transform cap must trip for a chunked body with no Content-Length") + + tr = urtest.AddTestRun("over-cap chunked body is refused with a 502 and no body") + # -w reports the downloaded body size; braces are doubled so str.format leaves + # the curl %{...} variable intact. + tr.MakeCurlCommand( + '-sS -D - -o /dev/null -w "size_download=%{{size_download}}" -x 127.0.0.1:{0} ' + '-H "Accept: image/webp" -H "uuid: chunked-huge" http://127.0.0.1:{1}/chunked-huge.jpg'.format( + ts.Variables.port, server.Variables.http_port), + ts=ts) + tr.Processes.Default.StartBefore(server) + tr.Processes.Default.StartBefore(ts) + tr.Processes.Default.ReturnCode = 0 + # The client gets a 502 with a zero-length body: the oversized image is refused, + # not forwarded. A full response would download 20 MiB. + tr.Processes.Default.Streams.stdout = Testers.ContainsExpression("HTTP/1.1 502", "Over-cap image must be refused with a 502") + tr.Processes.Default.Streams.stdout += Testers.ContainsExpression( + "size_download=0", "Body must be empty; the full 20 MiB image must not be forwarded") + urtest.execute() diff --git a/tests/uranium_tests/pluginTest/webp_transform/test_webp_transform_decode_limit.py b/tests/uranium_tests/pluginTest/webp_transform/test_webp_transform_decode_limit.py new file mode 100644 index 00000000000..27d3f84fc1c --- /dev/null +++ b/tests/uranium_tests/pluginTest/webp_transform/test_webp_transform_decode_limit.py @@ -0,0 +1,138 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_webp_transform_decode_limit(urtest: UraniumTest) -> None: + ''' + A small image that declares dimensions over the ImageMagick decode limits must + not be decoded into a giant pixel buffer; the transform reverts to the original + bytes instead of crashing or exhausting memory. + + The byte cap (max_buffer_size) does not help here: a tiny encoded image can sit + well under the cap yet declare enormous dimensions that decode into gigabytes of + pixels. TSPluginInit installs Magick::ResourceLimits (width/height/area/memory/ + map and disk(0)) so such an image fails as a caught Magick::Error and the plugin + reverts to the original bytes. + + This test serves a minimal, over-wide WebP (VP8L) image: a real RIFF/WEBP/VP8L + signature followed by a bit-packed header declaring 16129x2 (16129 > the + plugin's 16000 px width limit), with no bitstream payload beyond the header. + ImageMagick's decoder reads the declared width/height straight out of that + header and throws before it would ever need pixel data; the plugin catches it, + logs an ImageMagick error, and forwards the original bytes. The test asserts + ATS does not crash (the client still gets a 200), the decode-limit error is + logged, and the original body is returned unchanged (not a converted jpeg). + + The plugin's has_signature_for() guard checks the declared encoding's magic + bytes before ImageMagick ever sees the body, so the served body must carry a + real signature -- an arbitrary/mislabeled body would be caught by that guard + instead and never reach the decode-limit code path this test targets. WebP is + used (rather than PNG or JPEG) because it is the only one of the three + signatures the plugin recognizes that can be built entirely from bytes <=0x7f; + the origin server writes the body via a UTF-8 encode, so any byte over 0x7f + would not survive the round trip unchanged. + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import struct + + urtest.Summary = 'An over-dimension image is rejected by the decode limits and reverts to the original' + + urtest.SkipUnless(Condition.PluginExists('webp_transform.so')) + + urtest.ContinueOnFail = True + + # A minimal WebP (VP8L) image, 16129 px wide by 2 px tall. 16129 > the plugin's + # 16000 px width ResourceLimit, so ImageMagick rejects it on read. There is no + # bitstream data beyond the 5-byte VP8L header (signature byte + packed + # width/height), so the body is a few dozen bytes, far under the buffer cap -- + # the decode-side limit is what must catch it, not the byte cap. 16129x2 is + # also chosen so every byte of the packed header is <=0x7f (see module + # docstring for why that matters). + W, H = 16129, 2 + vp8l_payload = b'\x2f' + struct.pack('jpeg conversion; the body + # carries a real RIFF/WEBP/VP8L signature so has_signature_for() lets it + # through to ImageMagick, which reads the over-limit dimensions from the VP8L + # header. + server.addResponse( + "sessionlog.json", { + "headers": "GET /overwide.webp HTTP/1.1\r\nHost: *\r\n\r\n", + "timestamp": "1", + "body": "" + }, { + "headers": + "HTTP/1.1 200 OK\r\nContent-Type: image/webp\r\nContent-Length: {0}\r\nConnection: close\r\n\r\n".format(WEBP_LEN), + "timestamp": "1", + "body": webp_body + }) + + ts = urtest.MakeATSProcess("ts", enable_cache=False) + ts.Disk.records_config.update({ + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'webp_transform', + }) + ts.Disk.plugin_config.AddLine('webp_transform.so convert_to_jpeg') + ts.Disk.remap_config.AddLine('map http://127.0.0.1:{0}/ http://127.0.0.1:{0}/'.format(server.Variables.Port)) + + # The decode-limit failure is caught and logged as an ImageMagick error at ERROR + # level. Asserting it confirms the resource limit engaged (not the byte cap) and + # tells autest the ERROR line is expected. + ts.Disk.diags_log.Content = Testers.ContainsExpression( + "ImageMagick.. error", "The decode-side ResourceLimit must reject the over-dimension image") + + tr = urtest.AddTestRun("over-dimension image reverts to original instead of crashing") + tr.MakeCurlCommand( + '-sS -D - -o /dev/null -w "size_download=%{{size_download}}" -x 127.0.0.1:{0} ' + '-H "Accept: image/jpeg" http://127.0.0.1:{1}/overwide.webp'.format(ts.Variables.port, server.Variables.Port), + ts=ts) + tr.Processes.Default.StartBefore(server) + tr.Processes.Default.StartBefore(ts) + tr.Processes.Default.ReturnCode = 0 + # ATS must stay up and return the original image, not crash and not 502 (the byte + # cap is not exceeded). The reverted body is the original bytes, so its size + # equals the source image, not a smaller converted jpeg. + tr.Processes.Default.Streams.stdout = Testers.ContainsExpression( + "HTTP/1.1 200", "Client must get a 200 (decode failed over the limit, original forwarded), not a crash or 502") + tr.Processes.Default.Streams.stdout += Testers.ContainsExpression( + "size_download={0}".format(WEBP_LEN), "Original bytes must be forwarded unchanged, not a converted jpeg") + urtest.execute() diff --git a/tests/uranium_tests/pluginTest/webp_transform/test_webp_transform_max_buffer_size.py b/tests/uranium_tests/pluginTest/webp_transform/test_webp_transform_max_buffer_size.py new file mode 100644 index 00000000000..3e304ccce79 --- /dev/null +++ b/tests/uranium_tests/pluginTest/webp_transform/test_webp_transform_max_buffer_size.py @@ -0,0 +1,129 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_webp_transform_max_buffer_size(urtest: UraniumTest) -> None: + ''' + The max_buffer_size plugin argument overrides the default 16 MiB buffer cap, and + malformed values are rejected so a bad config cannot silently disable the cap. + + Two ATS instances: + - one loads webp_transform with max_buffer_size=1M and is driven with a 2 MiB + Content-Length image, proving the override is parsed (K/M/G suffix) and + applied: the transform is declined at the 1 MiB cap, not the 16 MiB default. + - one loads webp_transform with several malformed max_buffer_size values + (negative, bad suffix, suffix-multiply overflow) and asserts each is rejected + with "keeping default 16777216", proving a bad value falls back to the safe + default rather than wrapping to a huge or unintended cap. + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + urtest.Summary = 'max_buffer_size overrides the cap and rejects malformed values' + + urtest.SkipUnless(Condition.PluginExists('webp_transform.so')) + + urtest.ContinueOnFail = True + + # ---- Instance 1: a valid 1M override takes effect ---- + server = urtest.MakeOriginServer("server") + + # 2 MiB image/jpeg: over the 1 MiB override, well under the 16 MiB default. With + # the override in effect the up-front decline fires at 1 MiB; with the default it + # would not. + TWO_MIB = 2 * 1024 * 1024 + server.addResponse( + "sessionlog.json", { + "headers": "GET /two_mib.jpg HTTP/1.1\r\nHost: *\r\n\r\n", + "timestamp": "1", + "body": "" + }, { + "headers": + "HTTP/1.1 200 OK\r\nContent-Type: image/jpeg\r\nContent-Length: {0}\r\nConnection: close\r\n\r\n".format(TWO_MIB), + "timestamp": "1", + "body": "A" * TWO_MIB + }) + + ts = urtest.MakeATSProcess("ts", enable_cache=False) + ts.Disk.records_config.update({ + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'webp_transform', + }) + ts.Disk.plugin_config.AddLine('webp_transform.so convert_to_webp max_buffer_size=1M') + ts.Disk.remap_config.AddLine('map http://127.0.0.1:{0}/ http://127.0.0.1:{0}/'.format(server.Variables.Port)) + + # 1M == 1048576. Seeing the decline name 1048576 (not the 16777216 default) + # proves parse_size parsed the M suffix and the override is what is enforced. + ts.Disk.traffic_out.Content = Testers.ContainsExpression( + "exceeds cap 1048576", "max_buffer_size=1M must override the default and decline at 1 MiB") + + tr = urtest.AddTestRun("2 MiB body declined at the 1 MiB override") + tr.MakeCurlCommand( + '-sS -D - -o /dev/null -x 127.0.0.1:{0} -H "Accept: image/webp" http://127.0.0.1:{1}/two_mib.jpg'.format( + ts.Variables.port, server.Variables.Port), + ts=ts) + tr.Processes.Default.StartBefore(server) + tr.Processes.Default.StartBefore(ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.stdout = Testers.ContainsExpression("HTTP/1.1 200", "Declined response is a 200 passthrough") + tr.Processes.Default.Streams.stdout += Testers.ContainsExpression( + "[Cc]ontent-[Tt]ype: image/jpeg", "Declined response keeps its original image/jpeg type") + + # ---- Instance 2: malformed values are rejected and keep the default ---- + ts_bad = urtest.MakeATSProcess("ts_bad", enable_cache=False) + ts_bad.Disk.records_config.update({ + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'webp_transform', + }) + # Negative (sign guard), bad suffix, and a value that fits in u64 but overflows + # size_t when multiplied by the G suffix (multiply-overflow guard). All must be + # rejected; none may install a cap, so the 16 MiB default must survive. + ts_bad.Disk.plugin_config.AddLine( + 'webp_transform.so convert_to_webp max_buffer_size=-1 max_buffer_size=8X max_buffer_size=20000000000G') + ts_bad.Disk.remap_config.AddLine('map http://127.0.0.1:{0}/ http://127.0.0.1:{0}/'.format(server.Variables.Port)) + + # TSError() writes these to diags.log at ERROR level. Asserting them here both + # confirms parse_size rejected each value and tells autest the ERROR lines are + # expected (the default check fails on any ERROR in diags.log). + ts_bad.Disk.diags_log.Content = Testers.ContainsExpression( + "invalid max_buffer_size=-1, keeping default 16777216", "Negative value must be rejected") + ts_bad.Disk.diags_log.Content += Testers.ContainsExpression( + "invalid max_buffer_size=8X, keeping default 16777216", "Bad suffix must be rejected") + ts_bad.Disk.diags_log.Content += Testers.ContainsExpression( + "invalid max_buffer_size=20000000000G, keeping default 16777216", "Suffix-multiply overflow must be rejected") + + tr_bad = urtest.AddTestRun("malformed max_buffer_size values are rejected, default retained") + tr_bad.MakeCurlCommand( + '-sS -o /dev/null -x 127.0.0.1:{0} http://127.0.0.1:{1}/two_mib.jpg'.format(ts_bad.Variables.port, server.Variables.Port), + ts=ts_bad) + tr_bad.Processes.Default.StartBefore(ts_bad) + tr_bad.Processes.Default.ReturnCode = 0 + urtest.execute() diff --git a/tests/uranium_tests/pluginTest/webp_transform/test_webp_transform_size_cap.py b/tests/uranium_tests/pluginTest/webp_transform/test_webp_transform_size_cap.py new file mode 100644 index 00000000000..814db15a9fa --- /dev/null +++ b/tests/uranium_tests/pluginTest/webp_transform/test_webp_transform_size_cap.py @@ -0,0 +1,143 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_webp_transform_size_cap(urtest: UraniumTest) -> None: + ''' + webp_transform must not buffer unbounded response bodies. + + ImageTransform buffered the entire origin response in memory before handing it + to ImageMagick, so a large or malicious image response could exhaust proxy + memory, and a decode could throw an exception the narrow catch did not handle. + + The fix bounds this two ways: when the origin advertises a Content-Length over + the 16 MiB cap, the transform is declined up front and the original response + passes through untouched and keeps its original Content-Type; bodies without a + usable Content-Length are still bounded by a per-transaction cap inside the + transform. + + This test drives a 20 MiB image/jpeg with a Content-Length through ATS with + webp_transform loaded (convert_to_webp) over both HTTP/1.1 and HTTP/2, and + confirms the client gets a 200 with Content-Type image/jpeg (declined, not a + mislabeled image/webp) rather than ATS buffering the body or crashing. + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + urtest.Summary = 'webp_transform must not buffer unbounded response bodies' + + urtest.SkipUnless( + Condition.PluginExists('webp_transform.so'), + Condition.HasCurlFeature('http2'), + ) + + urtest.ContinueOnFail = True + + server = urtest.MakeOriginServer("server") + + # A 20 MiB image/jpeg with a Content-Length over the 16 MiB cap. ImageMagick + # would reject these bytes, so the point is that the transform is declined up + # front and ATS never buffers or decodes them. + BIG = 20 * 1024 * 1024 + body = "A" * BIG + server.addResponse( + "sessionlog.json", { + "headers": "GET /huge.jpg HTTP/1.1\r\nHost: *\r\n\r\n", + "timestamp": "1", + "body": "" + }, { + "headers": "HTTP/1.1 200 OK\r\nContent-Type: image/jpeg\r\nContent-Length: {0}\r\n\r\n".format(BIG), + "timestamp": "1", + "body": body + }) + + ts = urtest.MakeATSProcess("ts", enable_tls=True, enable_cache=False) + ts.addDefaultSSLFiles() + + ts.Disk.ssl_multicert_yaml.AddLines( + """ + ssl_multicert: + - dest_ip: "*" + ssl_cert_name: server.pem + ssl_key_name: server.key + """.split("\n")) + + ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'webp_transform', + 'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir), + 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts.Variables.SSLDir), + }) + + ts.Disk.plugin_config.AddLine('webp_transform.so convert_to_webp') + + # Plugin debug goes to traffic.out. Asserting the decline message naming the + # default 16 MiB cap (16777216) proves the plugin actually engaged and declined, + # rather than the response merely passing through untouched. + ts.Disk.traffic_out.Content = Testers.ContainsExpression( + "exceeds cap 16777216", "Plugin must engage and decline at the default 16 MiB cap") + + # Identity rule for the HTTP/1.1 forward-proxy run, plus a catch-all so the + # HTTP/2 reverse-proxy run resolves to the same origin. + ts.Disk.remap_config.AddLine('map http://127.0.0.1:{0}/ http://127.0.0.1:{0}/'.format(server.Variables.Port)) + ts.Disk.remap_config.AddLine('map / http://127.0.0.1:{0}/'.format(server.Variables.Port)) + + # The buffering DoS is in the origin-response transform, so it is independent of + # the client protocol. Exercise both H1 and H2 to confirm the oversized body is + # declined and the client gets a truthful 200 image/jpeg either way. + tr = urtest.AddTestRun("HTTP/1.1 client: oversized body declined, original type preserved") + tr.MakeCurlCommandMulti( + '{curl} -sS -D - -o /dev/null -x 127.0.0.1:TSPORT -H "Accept: image/webp" http://127.0.0.1:OPORT/huge.jpg'.replace( + 'TSPORT', str(ts.Variables.port)).replace('OPORT', str(server.Variables.Port)), + ts=ts) + tr.Processes.Default.StartBefore(ts) + tr.Processes.Default.StartBefore(server) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.stdout = Testers.ContainsExpression("HTTP/1.1 200", "H1 client must see 200 OK, not a crash") + # The response must keep its truthful image/jpeg type. A mislabeled image/webp + # would mean the original bytes were forwarded under a transformed Content-Type. + tr.Processes.Default.Streams.stdout += Testers.ContainsExpression( + "[Cc]ontent-[Tt]ype: image/jpeg", "Declined response must keep its original image/jpeg type") + tr.Processes.Default.Streams.stdout += Testers.ExcludesExpression( + "image/webp", "Declined response must not be mislabeled image/webp") + + tr2 = urtest.AddTestRun("HTTP/2 client: oversized body declined, original type preserved") + tr2.MakeCurlCommand( + '--http2 -k -sS -D - -o /dev/null -H "Accept: image/webp" https://127.0.0.1:{0}/huge.jpg'.format(ts.Variables.ssl_port), + ts=ts) + tr2.Processes.Default.ReturnCode = 0 + tr2.Processes.Default.Streams.stdout = Testers.ContainsExpression("HTTP/2 200", "H2 client must see 200 OK, not a crash") + tr2.Processes.Default.Streams.stdout += Testers.ContainsExpression( + "[Cc]ontent-[Tt]ype: image/jpeg", "Declined response must keep its original image/jpeg type") + tr2.Processes.Default.Streams.stdout += Testers.ExcludesExpression( + "image/webp", "Declined response must not be mislabeled image/webp") + urtest.execute() diff --git a/tests/gold_tests/pluginTest/webp_transform/webp_transform_invalid_input.test.yaml b/tests/uranium_tests/pluginTest/webp_transform/webp_transform_invalid_input.test.yaml similarity index 99% rename from tests/gold_tests/pluginTest/webp_transform/webp_transform_invalid_input.test.yaml rename to tests/uranium_tests/pluginTest/webp_transform/webp_transform_invalid_input.test.yaml index ec7f1d59cd2..a777c7cdbde 100644 --- a/tests/gold_tests/pluginTest/webp_transform/webp_transform_invalid_input.test.yaml +++ b/tests/uranium_tests/pluginTest/webp_transform/webp_transform_invalid_input.test.yaml @@ -18,7 +18,7 @@ meta: version: "1.0" -autest: +urtest: description: 'Verify webp_transform safely passes through invalid image bodies' requires: plugins: [webp_transform.so] diff --git a/tests/gold_tests/pluginTest/xdebug/x_cache_info/none.in b/tests/uranium_tests/pluginTest/xdebug/x_cache_info/none.in similarity index 100% rename from tests/gold_tests/pluginTest/xdebug/x_cache_info/none.in rename to tests/uranium_tests/pluginTest/xdebug/x_cache_info/none.in diff --git a/tests/gold_tests/pluginTest/xdebug/x_cache_info/one.in b/tests/uranium_tests/pluginTest/xdebug/x_cache_info/one.in similarity index 100% rename from tests/gold_tests/pluginTest/xdebug/x_cache_info/one.in rename to tests/uranium_tests/pluginTest/xdebug/x_cache_info/one.in diff --git a/tests/gold_tests/pluginTest/xdebug/x_cache_info/out.gold b/tests/uranium_tests/pluginTest/xdebug/x_cache_info/out.gold similarity index 100% rename from tests/gold_tests/pluginTest/xdebug/x_cache_info/out.gold rename to tests/uranium_tests/pluginTest/xdebug/x_cache_info/out.gold diff --git a/tests/uranium_tests/pluginTest/xdebug/x_cache_info/test_x_cache_info.py b/tests/uranium_tests/pluginTest/xdebug/x_cache_info/test_x_cache_info.py new file mode 100644 index 00000000000..2a803c43a3e --- /dev/null +++ b/tests/uranium_tests/pluginTest/xdebug/x_cache_info/test_x_cache_info.py @@ -0,0 +1,91 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_x_cache_info(urtest: UraniumTest) -> None: + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import sys + + urtest.Summary = ''' + Test xdebug plugin X-Cache-Info header + ''' + + server = urtest.MakeOriginServer("server") + started = False + + request_header = {"headers": "GET /argh HTTP/1.1\r\nHost: doesnotmatter\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + server.addResponse("sessionlog.json", request_header, response_header) + + ts = urtest.MakeATSProcess("ts") + + ts.Disk.records_config.update( + { + 'proxy.config.url_remap.remap_required': 0, + 'proxy.config.diags.debug.enabled': 0, + 'proxy.config.diags.debug.tags': 'http' + }) + + ts.Disk.plugin_config.AddLine('xdebug.so --enable=x-cache-info') + + ts.Disk.remap_config.AddLine("map http://one http://127.0.0.1:{0}".format(server.Variables.Port)) + ts.Disk.remap_config.AddLine("map http://two http://127.0.0.1:{0}".format(server.Variables.Port)) + ts.Disk.remap_config.AddLine("regex_map http://three[0-9]+ http://127.0.0.1:{0}".format(server.Variables.Port)) + + urtest.Setup.Copy(f'{urtest.Variables.AtsTestToolsDir}') + + files = ["none", "one", "two", "three"] + + for file in files: + urtest.Setup.Copy(f'{urtest.TestDirectory}/{file}.in') + + def sendMsg(msgFile): + nonlocal started + tr = urtest.AddTestRun() + tr.Processes.Default.Command = f"( {sys.executable} tools/tcp_client.py 127.0.0.1 {ts.Variables.port} {msgFile}.in ; echo '======' ) | sed 's/:{server.Variables.Port}/:SERVER_PORT/' >> out.log 2>&1" + tr.Processes.Default.ReturnCode = 0 + + if not started: + tr.Processes.Default.StartBefore(urtest.Processes.ts) + tr.Processes.Default.StartBefore(urtest.Processes.server) + started = True + + for file in files: + sendMsg(file) + + tr = urtest.AddTestRun() + tr.Processes.Default.Command = "echo test gold" + tr.Processes.Default.ReturnCode = 0 + f = tr.Disk.File("out.log") + f.Content = "out.gold" + urtest.execute() diff --git a/tests/gold_tests/pluginTest/xdebug/x_cache_info/three.in b/tests/uranium_tests/pluginTest/xdebug/x_cache_info/three.in similarity index 100% rename from tests/gold_tests/pluginTest/xdebug/x_cache_info/three.in rename to tests/uranium_tests/pluginTest/xdebug/x_cache_info/three.in diff --git a/tests/gold_tests/pluginTest/xdebug/x_cache_info/two.in b/tests/uranium_tests/pluginTest/xdebug/x_cache_info/two.in similarity index 100% rename from tests/gold_tests/pluginTest/xdebug/x_cache_info/two.in rename to tests/uranium_tests/pluginTest/xdebug/x_cache_info/two.in diff --git a/tests/gold_tests/pluginTest/xdebug/x_effective_url/four.in b/tests/uranium_tests/pluginTest/xdebug/x_effective_url/four.in similarity index 100% rename from tests/gold_tests/pluginTest/xdebug/x_effective_url/four.in rename to tests/uranium_tests/pluginTest/xdebug/x_effective_url/four.in diff --git a/tests/gold_tests/pluginTest/xdebug/x_effective_url/none.in b/tests/uranium_tests/pluginTest/xdebug/x_effective_url/none.in similarity index 100% rename from tests/gold_tests/pluginTest/xdebug/x_effective_url/none.in rename to tests/uranium_tests/pluginTest/xdebug/x_effective_url/none.in diff --git a/tests/gold_tests/pluginTest/xdebug/x_effective_url/one.in b/tests/uranium_tests/pluginTest/xdebug/x_effective_url/one.in similarity index 100% rename from tests/gold_tests/pluginTest/xdebug/x_effective_url/one.in rename to tests/uranium_tests/pluginTest/xdebug/x_effective_url/one.in diff --git a/tests/gold_tests/pluginTest/xdebug/x_effective_url/out.gold b/tests/uranium_tests/pluginTest/xdebug/x_effective_url/out.gold similarity index 100% rename from tests/gold_tests/pluginTest/xdebug/x_effective_url/out.gold rename to tests/uranium_tests/pluginTest/xdebug/x_effective_url/out.gold diff --git a/tests/uranium_tests/pluginTest/xdebug/x_effective_url/test_x_effective_url.py b/tests/uranium_tests/pluginTest/xdebug/x_effective_url/test_x_effective_url.py new file mode 100644 index 00000000000..1ca95263f7d --- /dev/null +++ b/tests/uranium_tests/pluginTest/xdebug/x_effective_url/test_x_effective_url.py @@ -0,0 +1,107 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_x_effective_url(urtest: UraniumTest) -> None: + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import sys + + urtest.Summary = ''' + Test xdebug plugin X-Effective header + ''' + + server = urtest.MakeOriginServer("server") + + request_header = {"headers": "GET /argh HTTP/1.1\r\nHost: doesnotmatter\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + server.addResponse("sessionlog.json", request_header, response_header) + + request_header_two = { + "headers": "GET /two/argh HTTP/1.1\r\nHost: doesnotmatter\r\n\r\n", + "timestamp": "1469733493.993", + "body": "" + } + server.addResponse("sessionlog.json", request_header_two, response_header) + + # request_header_three would be identical to request_header. + + request_header_four = { + "headers": "GET /four/argh,urgh HTTP/1.1\r\nHost: doesnotmatter\r\n\r\n", + "timestamp": "1469733493.993", + "body": "" + } + server.addResponse("sessionlog.json", request_header_four, response_header) + + ts = urtest.MakeATSProcess("ts") + + ts.Disk.records_config.update({ + 'proxy.config.url_remap.remap_required': 0, + 'proxy.config.diags.debug.enabled': 0, + }) + + ts.Disk.plugin_config.AddLine('xdebug.so --enable=x-effective-url') + + ts.Disk.remap_config.AddLine("map http://one http://127.0.0.1:{0}".format(server.Variables.Port)) + ts.Disk.remap_config.AddLine("map http://two http://127.0.0.1:{0}".format(server.Variables.Port) + "/two") + ts.Disk.remap_config.AddLine( + "regex_map http://three[0-9]+ http://127.0.0.1:{0}".format(server.Variables.Port) # Host: three123 + ) + ts.Disk.remap_config.AddLine("regex_map http://four http://127.0.0.1:{0}".format(server.Variables.Port) + "/four") + + tr = urtest.AddTestRun() + tr.Processes.Default.StartBefore(urtest.Processes.ts) + tr.Processes.Default.StartBefore(urtest.Processes.server) + tr.Processes.Default.Command = "cp {}/tcp_client.py {}/tcp_client.py".format( + urtest.Variables.AtsTestToolsDir, urtest.RunDirectory) + tr.Processes.Default.ReturnCode = 0 + + def sendMsg(msgFile): + + tr = urtest.AddTestRun() + tr.Processes.Default.Command = ( + f"( {sys.executable} {urtest.RunDirectory}/tcp_client.py 127.0.0.1 {ts.Variables.port} {urtest.TestDirectory}/{msgFile}.in" + f" ; echo '======' ) | sed 's/:{server.Variables.Port}/:SERVER_PORT/' >> {urtest.RunDirectory}/out.log 2>&1 ") + tr.Processes.Default.ReturnCode = 0 + + sendMsg('none') + sendMsg('one') + sendMsg('two') + sendMsg('three') + sendMsg('four') + + tr = urtest.AddTestRun() + tr.Processes.Default.Command = "echo test out.gold" + tr.Processes.Default.ReturnCode = 0 + f = tr.Disk.File("out.log") + f.Content = "out.gold" + urtest.execute() diff --git a/tests/gold_tests/pluginTest/xdebug/x_effective_url/three.in b/tests/uranium_tests/pluginTest/xdebug/x_effective_url/three.in similarity index 100% rename from tests/gold_tests/pluginTest/xdebug/x_effective_url/three.in rename to tests/uranium_tests/pluginTest/xdebug/x_effective_url/three.in diff --git a/tests/gold_tests/pluginTest/xdebug/x_effective_url/two.in b/tests/uranium_tests/pluginTest/xdebug/x_effective_url/two.in similarity index 100% rename from tests/gold_tests/pluginTest/xdebug/x_effective_url/two.in rename to tests/uranium_tests/pluginTest/xdebug/x_effective_url/two.in diff --git a/tests/uranium_tests/pluginTest/xdebug/x_probe/test_x_probe.py b/tests/uranium_tests/pluginTest/xdebug/x_probe/test_x_probe.py new file mode 100644 index 00000000000..52e4f2b1dd8 --- /dev/null +++ b/tests/uranium_tests/pluginTest/xdebug/x_probe/test_x_probe.py @@ -0,0 +1,101 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_x_probe(urtest: UraniumTest) -> None: + ''' + Verify xdebug plugin probe functionality. + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + urtest.Summary = 'Test xdebug plugin probe functionality' + urtest.ContinueOnFail = True + urtest.SkipUnless(Condition.PluginExists('xdebug.so')) + + class XDebugProbeTest: + """ + Test the xdebug probe functionality which transforms the response body + to include request and response headers in a multipart format. + + The probe feature: + - Changes Content-Type to text/plain + - Injects request headers before the original body + - Injects response headers after the original body + - Uses multipart boundary separators + - Disables caching due to body modification + """ + + _replay_file: str = "x_probe.replay.yaml" + + def __init__(self) -> None: + tr = urtest.AddTestRun("xdebug probe test") + self._setupOriginServer(tr) + self._setupTS(tr) + self._setupClient(tr) + + def _setupOriginServer(self, tr: 'TestRun') -> None: + """Configure the origin server using Proxy Verifier. + :param tr: TestRun to add the server to. + """ + self._server = tr.AddVerifierServerProcess("server", self._replay_file) + + def _setupTS(self, tr: 'TestRun') -> None: + """Configure ATS with xdebug plugin enabled for probe functionality. + :param tr: TestRun to add the ATS process to. + """ + self._ts = urtest.MakeATSProcess("ts") + + self._ts.Disk.records_config.update( + { + "proxy.config.diags.debug.enabled": 1, + "proxy.config.diags.debug.tags": "xdebug", + }) + + self._ts.Disk.plugin_config.AddLine('xdebug.so --enable=probe') + self._ts.Disk.remap_config.AddLine(f"map / http://127.0.0.1:{self._server.Variables.http_port}") + + def _setupClient(self, tr: 'TestRun') -> None: + """Test basic probe functionality with header injection. + :param tr: TestRun to add the test to. + """ + tr.Processes.Default.StartBefore(self._server) + tr.Processes.Default.StartBefore(self._ts) + tr.AddVerifierClientProcess("client", self._replay_file, http_ports=[self._ts.Variables.port]) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.stdout += Testers.ContainsExpression( + 'ATS xDebug Probe Injection Boundary', "ATS xDebug Probe Injection Boundary should be present") + tr.Processes.Default.Streams.stdout += Testers.ContainsExpression('xDebugProbeAt', "xDebugProbeAt should be present") + tr.Processes.Default.Streams.stdout += Testers.ContainsExpression("'x-no-value' : ''", "x-no-value should be present") + + # Execute the test + XDebugProbeTest() + urtest.execute() diff --git a/tests/gold_tests/pluginTest/xdebug/x_probe/x_probe.replay.yaml b/tests/uranium_tests/pluginTest/xdebug/x_probe/x_probe.replay.yaml similarity index 100% rename from tests/gold_tests/pluginTest/xdebug/x_probe/x_probe.replay.yaml rename to tests/uranium_tests/pluginTest/xdebug/x_probe/x_probe.replay.yaml diff --git a/tests/gold_tests/pluginTest/xdebug/x_probe_full_json/gold/jq_escaped.gold b/tests/uranium_tests/pluginTest/xdebug/x_probe_full_json/gold/jq_escaped.gold similarity index 100% rename from tests/gold_tests/pluginTest/xdebug/x_probe_full_json/gold/jq_escaped.gold rename to tests/uranium_tests/pluginTest/xdebug/x_probe_full_json/gold/jq_escaped.gold diff --git a/tests/gold_tests/pluginTest/xdebug/x_probe_full_json/gold/jq_hex.gold b/tests/uranium_tests/pluginTest/xdebug/x_probe_full_json/gold/jq_hex.gold similarity index 100% rename from tests/gold_tests/pluginTest/xdebug/x_probe_full_json/gold/jq_hex.gold rename to tests/uranium_tests/pluginTest/xdebug/x_probe_full_json/gold/jq_hex.gold diff --git a/tests/gold_tests/pluginTest/xdebug/x_probe_full_json/gold/jq_nobody.gold b/tests/uranium_tests/pluginTest/xdebug/x_probe_full_json/gold/jq_nobody.gold similarity index 100% rename from tests/gold_tests/pluginTest/xdebug/x_probe_full_json/gold/jq_nobody.gold rename to tests/uranium_tests/pluginTest/xdebug/x_probe_full_json/gold/jq_nobody.gold diff --git a/tests/uranium_tests/pluginTest/xdebug/x_probe_full_json/test_x_probe_full_json.py b/tests/uranium_tests/pluginTest/xdebug/x_probe_full_json/test_x_probe_full_json.py new file mode 100644 index 00000000000..30e57389d15 --- /dev/null +++ b/tests/uranium_tests/pluginTest/xdebug/x_probe_full_json/test_x_probe_full_json.py @@ -0,0 +1,182 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_x_probe_full_json(urtest: UraniumTest) -> None: + ''' + Verify xdebug plugin probe-full-json functionality. + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + urtest.Summary = 'Test xdebug plugin probe-full-json functionality' + urtest.ContinueOnFail = True + urtest.SkipUnless(Condition.PluginExists('xdebug.so')) + urtest.SkipUnless(Condition.HasProgram("jq", "jq is required to validate JSON output")) + + class XDebugProbeFullJsonTest: + """ + Test the xdebug probe-full-json functionality which transforms the response body + to include request headers, response body, and response headers in a complete JSON format. + + The probe-full-json feature: + - Changes Content-Type to application/json + - Generates a complete JSON object containing all debug information + - Includes client/server request headers, response body, and client/server response headers + - Disables caching due to body modification + """ + + _replay_file: str = "x_probe_full_json.replay.yaml" + + def __init__(self) -> None: + self._servers_are_started: bool = False + self._setupOriginServer() + self._setupTS() + self._setupClient() + self._setupJqValidation() + + def _setupOriginServer(self) -> None: + """Configure the origin server using Proxy Verifier. + """ + self._server = urtest.MakeVerifierServerProcess("server", self._replay_file) + + def _setupTS(self) -> None: + """Configure ATS with xdebug plugin enabled for probe-full-json functionality. + """ + self._ts = urtest.MakeATSProcess("ts") + + self._ts.Disk.records_config.update( + { + "proxy.config.diags.debug.enabled": 1, + "proxy.config.diags.debug.tags": "xdebug", + }) + + self._ts.Disk.plugin_config.AddLine('xdebug.so --enable=probe-full-json') + self._ts.Disk.remap_config.AddLine(f"map / http://127.0.0.1:{self._server.Variables.http_port}") + + def _startServersIfNeeded(self, tr: 'TestRun') -> None: + '''Start the servers if they are not already started. + :param tr: TestRun to add the test to. + ''' + if not self._servers_are_started: + tr.Processes.Default.StartBefore(self._server) + tr.Processes.Default.StartBefore(self._ts) + self._servers_are_started = True + + def _setupClient(self) -> None: + """Test basic probe-full-json functionality with JSON output. + """ + tr = urtest.AddTestRun("Verify probe-full-json functionality") + self._startServersIfNeeded(tr) + tr.AddVerifierClientProcess("client", self._replay_file, http_ports=[self._ts.Variables.port]) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.stdout += Testers.ContainsExpression( + 'X-Original-Content-Type: text/html', "X-Original-Content-Type of text/html should be present") + tr.Processes.Default.Streams.stdout += Testers.ContainsExpression( + 'X-Original-Content-Type: application/octet-stream', + "X-Original-Content-Type of application/octet-stream should be present") + # CSP test should produce valid JSON (single quotes not escaped) + tr.Processes.Default.Streams.stdout += Testers.ContainsExpression( + "default-src 'self'; script-src 'self' 'unsafe-inline'", + "CSP header with single quotes should be present in valid JSON") + + def _setupJqValidation(self) -> None: + """Use curl to get the response body and pipe through jq to validate JSON. + """ + tr = urtest.AddTestRun("Escaped text/html content.") + self._startServersIfNeeded(tr) + tr.MakeCurlCommand( + f'-s -H"uuid: 1" -H "Host: example.com" -H "X-Debug: probe-full-json" ' + f'http://127.0.0.1:{self._ts.Variables.port}/test | ' + "jq '.\"client-request\".\"uuid\",.\"server-body\",.\"proxy-response\".\"x-response\"'", + ts=self._ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.stdout = "gold/jq_escaped.gold" + + tr = urtest.AddTestRun("Hex-encoded application/octet-stream content.") + self._startServersIfNeeded(tr) + tr.MakeCurlCommand( + f'-s -H"uuid: 2" -H "Host: example.com" -H "X-Debug: probe-full-json" ' + f'http://127.0.0.1:{self._ts.Variables.port}/binary | ' + "jq '.\"client-request\".\"uuid\",.\"server-body\",.\"proxy-response\".\"x-response\"'", + ts=self._ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.stdout += "gold/jq_hex.gold" + + tr = urtest.AddTestRun("=hex") + self._startServersIfNeeded(tr) + tr.MakeCurlCommand( + f'-s -H"uuid: 1" -H "Host: example.com" -H "X-Debug: probe-full-json=hex" ' + f'http://127.0.0.1:{self._ts.Variables.port}/test | ' + "jq '.\"server-body\"'", + ts=self._ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.stdout += Testers.ContainsExpression( + '3c21444f43545950452068746d6c3e', "Should contain hex-encoded HTML content (forced hex override)") + + tr = urtest.AddTestRun("=escape") + self._startServersIfNeeded(tr) + tr.MakeCurlCommand( + f'-s -H"uuid: 1" -H "Host: example.com" -H "X-Debug: probe-full-json=escape" ' + f'http://127.0.0.1:{self._ts.Variables.port}/test | ' + "jq '.\"client-request\".\"uuid\",.\"server-body\",.\"proxy-response\".\"x-response\"'", + ts=self._ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.stdout += "gold/jq_escaped.gold" + + tr = urtest.AddTestRun("=nobody") + self._startServersIfNeeded(tr) + tr.MakeCurlCommand( + f'-s -H"uuid: 1" -H "Host: example.com" -H "X-Debug: probe-full-json=nobody" ' + f'http://127.0.0.1:{self._ts.Variables.port}/test | ' + "jq '.\"client-request\".\"uuid\",.\"server-body\",.\"proxy-response\".\"x-response\"'", + ts=self._ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.stdout += "gold/jq_nobody.gold" + + tr = urtest.AddTestRun("CSP header with single quotes (valid JSON)") + self._startServersIfNeeded(tr) + tr.MakeCurlCommand( + f'-s -H"uuid: 3" -H "Host: example.com" -H "X-Debug: probe-full-json" ' + f'http://127.0.0.1:{self._ts.Variables.port}/csp-test | ' + "jq '.\"server-response\".\"content-security-policy\"'", + ts=self._ts) + tr.Processes.Default.ReturnCode = 0 + # Verify jq can parse it (would fail if single quotes were escaped as \') + # and that the CSP header contains single quotes + tr.Processes.Default.Streams.stdout += Testers.ContainsExpression( + "'self'", "CSP header should contain single quotes (not escaped)") + tr.Processes.Default.Streams.stdout += Testers.ContainsExpression( + "'unsafe-inline'", "CSP header should contain 'unsafe-inline' directive") + + # Execute the test + XDebugProbeFullJsonTest() + urtest.execute() diff --git a/tests/gold_tests/pluginTest/xdebug/x_probe_full_json/x_probe_full_json.replay.yaml b/tests/uranium_tests/pluginTest/xdebug/x_probe_full_json/x_probe_full_json.replay.yaml similarity index 100% rename from tests/gold_tests/pluginTest/xdebug/x_probe_full_json/x_probe_full_json.replay.yaml rename to tests/uranium_tests/pluginTest/xdebug/x_probe_full_json/x_probe_full_json.replay.yaml diff --git a/tests/gold_tests/pluginTest/xdebug/x_probe_full_json_slow_origin/slow-body-server.sh b/tests/uranium_tests/pluginTest/xdebug/x_probe_full_json_slow_origin/slow-body-server.sh similarity index 100% rename from tests/gold_tests/pluginTest/xdebug/x_probe_full_json_slow_origin/slow-body-server.sh rename to tests/uranium_tests/pluginTest/xdebug/x_probe_full_json_slow_origin/slow-body-server.sh diff --git a/tests/uranium_tests/pluginTest/xdebug/x_probe_full_json_slow_origin/test_x_probe_full_json_slow_origin.py b/tests/uranium_tests/pluginTest/xdebug/x_probe_full_json_slow_origin/test_x_probe_full_json_slow_origin.py new file mode 100644 index 00000000000..11444593b6e --- /dev/null +++ b/tests/uranium_tests/pluginTest/xdebug/x_probe_full_json_slow_origin/test_x_probe_full_json_slow_origin.py @@ -0,0 +1,102 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_x_probe_full_json_slow_origin(urtest: UraniumTest) -> None: + ''' + Verify xdebug plugin probe-full-json does not hang with slow origin servers. + + This test reproduces a bug where the transform would enter an infinite loop + when the origin server delays sending the response body. The bug occurs because + the transform would reenable and call back upstream even when no data was + available to consume. + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + urtest.Summary = ''' + Test xdebug probe-full-json with slow origin to verify no infinite loop + ''' + + urtest.SkipUnless(Condition.PluginExists('xdebug.so')) + urtest.SkipUnless(Condition.HasProgram("nc", "nc (netcat) is required for custom slow server")) + + # Configure ATS + ts = urtest.MakeATSProcess("ts", enable_cache=False) + + ts.Disk.records_config.update( + { + "proxy.config.diags.debug.enabled": 1, + "proxy.config.diags.debug.tags": "xdebug_transform", + # Set reasonable timeouts + "proxy.config.http.transaction_no_activity_timeout_in": 10, + "proxy.config.http.transaction_no_activity_timeout_out": 10, + }) + + ts.Disk.plugin_config.AddLine('xdebug.so --enable=probe-full-json') + + # Reserve a port for the custom slow server + urtest.GetTcpPort("server_port") + + ts.Disk.remap_config.AddLine(f"map / http://127.0.0.1:{urtest.Variables.server_port}/") + + # Start the custom slow-body server + server = urtest.Processes.Process( + "server", + f"bash -c '{urtest.TestDirectory}/slow-body-server.sh {urtest.Variables.server_port} {urtest.RunDirectory}/server_request.txt'" + ) + + # Test with probe-full-json=nobody (which triggers the bug most easily) + tr = urtest.AddTestRun("Verify probe-full-json with slow body delivery") + tr.TimeOut = 15 # Should complete well under this; timeout indicates hang/loop + + # Make the request - use timeout to detect if the request hangs + tr.Processes.Default.Command = ( + f'timeout 8 curl -s -o /dev/null -w "%{{http_code}}" ' + f'-H "Host: example.com" ' + f'-H "X-Debug: probe-full-json=nobody" ' + f'http://127.0.0.1:{ts.Variables.port}/test') + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.StartBefore(server) + tr.Processes.Default.StartBefore(ts) + + # Should get 200, not timeout (which would cause non-zero return and 124 output) + tr.Processes.Default.Streams.stdout = Testers.ContainsExpression("200", "Should receive 200 OK, not timeout") + + # Verify no infinite loop by checking the logs + # The bug manifests as the transform being called with "bytes of body is expected" + # but no data consumed. Every "expected" should be followed by "consumed". + tr2 = urtest.AddTestRun("Verify no infinite loop in transform") + tr2.Processes.Default.Command = f"bash {urtest.TestDirectory}/verify_no_loop.sh {ts.Variables.LOGDIR}/traffic.out" + tr2.Processes.Default.ReturnCode = 0 + tr2.Processes.Default.Streams.stdout = Testers.ContainsExpression( + "PASS", "Verification script should pass - every 'expected' followed by 'consumed'") + urtest.execute() diff --git a/tests/gold_tests/pluginTest/xdebug/x_probe_full_json_slow_origin/verify_no_loop.sh b/tests/uranium_tests/pluginTest/xdebug/x_probe_full_json_slow_origin/verify_no_loop.sh similarity index 100% rename from tests/gold_tests/pluginTest/xdebug/x_probe_full_json_slow_origin/verify_no_loop.sh rename to tests/uranium_tests/pluginTest/xdebug/x_probe_full_json_slow_origin/verify_no_loop.sh diff --git a/tests/gold_tests/pluginTest/xdebug/x_remap/four.in b/tests/uranium_tests/pluginTest/xdebug/x_remap/four.in similarity index 100% rename from tests/gold_tests/pluginTest/xdebug/x_remap/four.in rename to tests/uranium_tests/pluginTest/xdebug/x_remap/four.in diff --git a/tests/gold_tests/pluginTest/xdebug/x_remap/fwd1.in b/tests/uranium_tests/pluginTest/xdebug/x_remap/fwd1.in similarity index 100% rename from tests/gold_tests/pluginTest/xdebug/x_remap/fwd1.in rename to tests/uranium_tests/pluginTest/xdebug/x_remap/fwd1.in diff --git a/tests/gold_tests/pluginTest/xdebug/x_remap/fwd2.in b/tests/uranium_tests/pluginTest/xdebug/x_remap/fwd2.in similarity index 100% rename from tests/gold_tests/pluginTest/xdebug/x_remap/fwd2.in rename to tests/uranium_tests/pluginTest/xdebug/x_remap/fwd2.in diff --git a/tests/gold_tests/pluginTest/xdebug/x_remap/fwd3.in b/tests/uranium_tests/pluginTest/xdebug/x_remap/fwd3.in similarity index 100% rename from tests/gold_tests/pluginTest/xdebug/x_remap/fwd3.in rename to tests/uranium_tests/pluginTest/xdebug/x_remap/fwd3.in diff --git a/tests/gold_tests/pluginTest/xdebug/x_remap/fwd4.in b/tests/uranium_tests/pluginTest/xdebug/x_remap/fwd4.in similarity index 100% rename from tests/gold_tests/pluginTest/xdebug/x_remap/fwd4.in rename to tests/uranium_tests/pluginTest/xdebug/x_remap/fwd4.in diff --git a/tests/gold_tests/pluginTest/xdebug/x_remap/fwd5.in b/tests/uranium_tests/pluginTest/xdebug/x_remap/fwd5.in similarity index 100% rename from tests/gold_tests/pluginTest/xdebug/x_remap/fwd5.in rename to tests/uranium_tests/pluginTest/xdebug/x_remap/fwd5.in diff --git a/tests/gold_tests/pluginTest/xdebug/x_remap/none.in b/tests/uranium_tests/pluginTest/xdebug/x_remap/none.in similarity index 100% rename from tests/gold_tests/pluginTest/xdebug/x_remap/none.in rename to tests/uranium_tests/pluginTest/xdebug/x_remap/none.in diff --git a/tests/gold_tests/pluginTest/xdebug/x_remap/one.in b/tests/uranium_tests/pluginTest/xdebug/x_remap/one.in similarity index 100% rename from tests/gold_tests/pluginTest/xdebug/x_remap/one.in rename to tests/uranium_tests/pluginTest/xdebug/x_remap/one.in diff --git a/tests/gold_tests/pluginTest/xdebug/x_remap/out.gold b/tests/uranium_tests/pluginTest/xdebug/x_remap/out.gold similarity index 100% rename from tests/gold_tests/pluginTest/xdebug/x_remap/out.gold rename to tests/uranium_tests/pluginTest/xdebug/x_remap/out.gold diff --git a/tests/uranium_tests/pluginTest/xdebug/x_remap/test_x_remap.py b/tests/uranium_tests/pluginTest/xdebug/x_remap/test_x_remap.py new file mode 100644 index 00000000000..165d0cb7eba --- /dev/null +++ b/tests/uranium_tests/pluginTest/xdebug/x_remap/test_x_remap.py @@ -0,0 +1,102 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_x_remap(urtest: UraniumTest) -> None: + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import sys + + urtest.Summary = ''' + Test xdebug plugin X-Remap, Probe and fwd headers + ''' + + server = urtest.MakeOriginServer("server", options={'--load': (urtest.TestDirectory + '/x_remap-observer.py')}) + + request_header = {"headers": "GET /argh HTTP/1.1\r\nHost: doesnotmatter\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + server.addResponse("sessionlog.json", request_header, response_header) + + ts = urtest.MakeATSProcess("ts") + + ts.Disk.records_config.update( + { + 'proxy.config.url_remap.remap_required': 0, + 'proxy.config.diags.debug.enabled': 0, + # 'proxy.config.diags.debug.tags': 'http|xdebug' + # 'proxy.config.diags.debug.tags': 'xdebug' + }) + + ts.Disk.plugin_config.AddLine('xdebug.so --enable=x-remap,probe') + + ts.Disk.remap_config.AddLine("map http://one http://127.0.0.1:{0}".format(server.Variables.Port)) + ts.Disk.remap_config.AddLine("map http://two http://127.0.0.1:{0}".format(server.Variables.Port)) + ts.Disk.remap_config.AddLine("regex_map http://three[0-9]+ http://127.0.0.1:{0}".format(server.Variables.Port)) + + tr = urtest.AddTestRun() + tr.Processes.Default.StartBefore(urtest.Processes.ts) + tr.Processes.Default.StartBefore(urtest.Processes.server) + tr.Processes.Default.Command = "cp {}/tcp_client.py {}/tcp_client.py".format( + urtest.Variables.AtsTestToolsDir, urtest.RunDirectory) + tr.Processes.Default.ReturnCode = 0 + + def sendMsg(msgFile): + + tr = urtest.AddTestRun() + tr.Processes.Default.Command = ( + f"( {sys.executable} {urtest.RunDirectory}/tcp_client.py 127.0.0.1 {ts.Variables.port} {urtest.TestDirectory}/{msgFile}.in" + f" ; echo '======' ) | sed 's/:{server.Variables.Port}/:SERVER_PORT/' >> {urtest.RunDirectory}/out.log 2>&1 ") + tr.Processes.Default.ReturnCode = 0 + + sendMsg('none') + sendMsg('one') + sendMsg('two') + sendMsg('three') + sendMsg('four') + sendMsg('fwd1') + sendMsg('fwd2') + sendMsg('fwd3') + sendMsg('fwd4') + sendMsg('fwd5') + + tr = urtest.AddTestRun() + tr.Processes.Default.Command = "echo test out.gold" + tr.Processes.Default.ReturnCode = 0 + f = tr.Disk.File("out.log") + f.Content = "out.gold" + + tr = urtest.AddTestRun() + tr.Processes.Default.Command = "echo test x_remap.gold" + tr.Processes.Default.ReturnCode = 0 + f = tr.Disk.File("x_remap.log") + f.Content = "x_remap.gold" + urtest.execute() diff --git a/tests/gold_tests/pluginTest/xdebug/x_remap/three.in b/tests/uranium_tests/pluginTest/xdebug/x_remap/three.in similarity index 100% rename from tests/gold_tests/pluginTest/xdebug/x_remap/three.in rename to tests/uranium_tests/pluginTest/xdebug/x_remap/three.in diff --git a/tests/gold_tests/pluginTest/xdebug/x_remap/two.in b/tests/uranium_tests/pluginTest/xdebug/x_remap/two.in similarity index 100% rename from tests/gold_tests/pluginTest/xdebug/x_remap/two.in rename to tests/uranium_tests/pluginTest/xdebug/x_remap/two.in diff --git a/tests/gold_tests/pluginTest/xdebug/x_remap/x_remap-observer.py b/tests/uranium_tests/pluginTest/xdebug/x_remap/x_remap-observer.py similarity index 100% rename from tests/gold_tests/pluginTest/xdebug/x_remap/x_remap-observer.py rename to tests/uranium_tests/pluginTest/xdebug/x_remap/x_remap-observer.py diff --git a/tests/gold_tests/pluginTest/xdebug/x_remap/x_remap.gold b/tests/uranium_tests/pluginTest/xdebug/x_remap/x_remap.gold similarity index 100% rename from tests/gold_tests/pluginTest/xdebug/x_remap/x_remap.gold rename to tests/uranium_tests/pluginTest/xdebug/x_remap/x_remap.gold diff --git a/tests/gold_tests/pluginTest/xdebug/x_remap_long_url/x_remap_long_url.test.yaml b/tests/uranium_tests/pluginTest/xdebug/x_remap_long_url/x_remap_long_url.test.yaml similarity index 99% rename from tests/gold_tests/pluginTest/xdebug/x_remap_long_url/x_remap_long_url.test.yaml rename to tests/uranium_tests/pluginTest/xdebug/x_remap_long_url/x_remap_long_url.test.yaml index af01f26f6b3..9cfd34bd1d2 100644 --- a/tests/gold_tests/pluginTest/xdebug/x_remap_long_url/x_remap_long_url.test.yaml +++ b/tests/uranium_tests/pluginTest/xdebug/x_remap_long_url/x_remap_long_url.test.yaml @@ -17,7 +17,7 @@ meta: version: "1.0" -autest: +urtest: description: 'xdebug X-Remap injection must not OOB read on long URLs' requires: plugins: [xdebug.so] diff --git a/tests/gold_tests/post/delay_client.sh b/tests/uranium_tests/post/delay_client.sh similarity index 100% rename from tests/gold_tests/post/delay_client.sh rename to tests/uranium_tests/post/delay_client.sh diff --git a/tests/gold_tests/post/delay_client2.sh b/tests/uranium_tests/post/delay_client2.sh similarity index 100% rename from tests/gold_tests/post/delay_client2.sh rename to tests/uranium_tests/post/delay_client2.sh diff --git a/tests/gold_tests/post/delay_client3.sh b/tests/uranium_tests/post/delay_client3.sh similarity index 100% rename from tests/gold_tests/post/delay_client3.sh rename to tests/uranium_tests/post/delay_client3.sh diff --git a/tests/gold_tests/post/expect_client.py b/tests/uranium_tests/post/expect_client.py similarity index 100% rename from tests/gold_tests/post/expect_client.py rename to tests/uranium_tests/post/expect_client.py diff --git a/tests/gold_tests/post/gold/post-h1.gold b/tests/uranium_tests/post/gold/post-h1.gold similarity index 100% rename from tests/gold_tests/post/gold/post-h1.gold rename to tests/uranium_tests/post/gold/post-h1.gold diff --git a/tests/gold_tests/post/gold/post-h2.gold b/tests/uranium_tests/post/gold/post-h2.gold similarity index 100% rename from tests/gold_tests/post/gold/post-h2.gold rename to tests/uranium_tests/post/gold/post-h2.gold diff --git a/tests/gold_tests/post/http_utils.py b/tests/uranium_tests/post/http_utils.py similarity index 100% rename from tests/gold_tests/post/http_utils.py rename to tests/uranium_tests/post/http_utils.py diff --git a/tests/gold_tests/post/replay/expect-continue.replay.yaml b/tests/uranium_tests/post/replay/expect-continue.replay.yaml similarity index 100% rename from tests/gold_tests/post/replay/expect-continue.replay.yaml rename to tests/uranium_tests/post/replay/expect-continue.replay.yaml diff --git a/tests/gold_tests/post/replay/post-continue.replay.yaml b/tests/uranium_tests/post/replay/post-continue.replay.yaml similarity index 100% rename from tests/gold_tests/post/replay/post-continue.replay.yaml rename to tests/uranium_tests/post/replay/post-continue.replay.yaml diff --git a/tests/gold_tests/post/replay/post-no-cl.test.yaml b/tests/uranium_tests/post/replay/post-no-cl.test.yaml similarity index 99% rename from tests/gold_tests/post/replay/post-no-cl.test.yaml rename to tests/uranium_tests/post/replay/post-no-cl.test.yaml index 13d54192aeb..d8f420fe34a 100644 --- a/tests/gold_tests/post/replay/post-no-cl.test.yaml +++ b/tests/uranium_tests/post/replay/post-no-cl.test.yaml @@ -17,7 +17,7 @@ meta: version: "1.0" -autest: +urtest: description: 'Verify ATS rejects unspecified length POST requests with a 411.' dns: diff --git a/tests/uranium_tests/post/test_expect_tests.py b/tests/uranium_tests/post/test_expect_tests.py new file mode 100644 index 00000000000..48cad372701 --- /dev/null +++ b/tests/uranium_tests/post/test_expect_tests.py @@ -0,0 +1,107 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_expect_tests(urtest: UraniumTest) -> None: + ''' + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import sys + + class ExpectTest: + + _expect_client: str = 'expect_client.py' + _http_utils: str = 'http_utils.py' + _replay_file: str = 'replay/expect-continue.replay.yaml' + + def __init__(self): + tr = urtest.AddTestRun('Verify Expect: 100-Continue handling.') + self._setup_dns(tr) + self._setup_origin(tr) + self._setup_trafficserver(tr) + self._setup_client(tr) + + def _setup_dns(self, tr: 'TestRun') -> None: + '''Set up the DNS server. + + :param tr: The TestRun to which to add the DNS server. + ''' + dns = tr.MakeDNServer('dns', default='127.0.0.1') + self._dns = dns + + def _setup_origin(self, tr: 'TestRun') -> None: + '''Set up the origin server. + + :param tr: The TestRun to which to add the origin server. + ''' + server = tr.AddVerifierServerProcess("server", replay_path=self._replay_file) + self._server = server + + def _setup_trafficserver(self, tr: 'TestRun') -> None: + '''Set up the traffic server. + + :param tr: The TestRun to which to add the traffic server. + ''' + ts = tr.MakeATSProcess("ts", enable_cache=False) + self._ts = ts + ts.Disk.remap_config.AddLine(f'map / http://backend.example.com:{self._server.Variables.http_port}') + ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'http', + 'proxy.config.dns.nameservers': f"127.0.0.1:{self._dns.Variables.Port}", + 'proxy.config.dns.resolv_conf': 'NULL', + 'proxy.config.http.send_100_continue_response': 1, + }) + + def _setup_client(self, tr: 'TestRun') -> None: + '''Set up the client. + + :param tr: The TestRun to which to add the client. + ''' + tr.Setup.CopyAs(self._expect_client) + tr.Setup.CopyAs(self._http_utils) + tr.Processes.Default.Command = \ + f'{sys.executable} {self._expect_client} 127.0.0.1 {self._ts.Variables.port} -s example.com' + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.StartBefore(self._dns) + tr.Processes.Default.StartBefore(self._server) + tr.Processes.Default.StartBefore(self._ts) + tr.Processes.Default.Streams.stdout += Testers.ContainsExpression( + 'HTTP/1.1 100', 'Verify the 100 Continue response was received.') + tr.Processes.Default.Streams.stdout += Testers.ContainsExpression( + 'HTTP/1.1 200', 'Verify the 200 OK response was received.') + + urtest.Summary = 'Verify Expect: 100-Continue handling.' + ExpectTest() + urtest.execute() diff --git a/tests/uranium_tests/post/test_post_continue.py b/tests/uranium_tests/post/test_post_continue.py new file mode 100644 index 00000000000..8ce7ee79a3e --- /dev/null +++ b/tests/uranium_tests/post/test_post_continue.py @@ -0,0 +1,301 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_post_continue(urtest: UraniumTest) -> None: + ''' + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import os + + # ---- + # Setup Test + # ---- + urtest.Summary = ''' + Test the Expect header in post + ''' + # Require HTTP/2 enabled Curl + urtest.SkipIf(Condition.CurlUsingUnixDomainSocket()) + urtest.SkipUnless(Condition.HasCurlFeature('http2'),) + urtest.ContinueOnFail = True + + # ---- + # Setup httpbin Origin Server + # ---- + replay_file = "replay/post-continue.replay.yaml" + server = urtest.MakeVerifierServerProcess("server", replay_file) + + # ---- + # Setup ATS + # ---- + ts = urtest.MakeATSProcess("ts", enable_tls=True, enable_cache=False) + + ts2 = urtest.MakeATSProcess("ts2", enable_tls=True, enable_cache=False) + + # add ssl materials like key, certificates for the server + ts.addDefaultSSLFiles() + ts2.addDefaultSSLFiles() + + ts.Disk.remap_config.AddLine('map / http://127.0.0.1:{0}'.format(server.Variables.http_port)) + ts.Disk.ssl_multicert_yaml.AddLines( + """ + ssl_multicert: + - dest_ip: "*" + ssl_cert_name: server.pem + ssl_key_name: server.key + """.split("\n")) + ts.Disk.records_config.update( + { + 'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir), + 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts.Variables.SSLDir), + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'http', + }) + ts2.Disk.remap_config.AddLine('map / http://127.0.0.1:{0}'.format(server.Variables.http_port)) + ts2.Disk.ssl_multicert_yaml.AddLines( + """ + ssl_multicert: + - dest_ip: "*" + ssl_cert_name: server.pem + ssl_key_name: server.key + """.split("\n")) + ts2.Disk.records_config.update( + { + 'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir), + 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts.Variables.SSLDir), + 'proxy.config.diags.debug.enabled': 0, + 'proxy.config.diags.debug.tags': 'http', + 'proxy.config.http.send_100_continue_response': 1 + }) + + big_post_body = "0123456789" * 131070 + big_post_body_file = open(os.path.join(urtest.RunDirectory, "big_post_body"), "w") + big_post_body_file.write(big_post_body) + big_post_body_file.close() + + test_run = urtest.AddTestRun("http1.1 POST small body with Expect header") + test_run.Processes.Default.StartBefore(server) + test_run.Processes.Default.StartBefore(ts) + test_run.MakeCurlCommand( + '-v -o /dev/null --http1.1 -H "uuid: post" -H "Expect: 100-continue" -d "small body" -k https://127.0.0.1:{0}/post'.format( + ts.Variables.ssl_port), + ts=ts) + test_run.Processes.Default.Streams.All = "gold/post-h1.gold" + test_run.Processes.Default.Streams.All += Testers.ContainsExpression("HTTP/1.1 100 continue", "Has Expect header") + test_run.Processes.Default.Streams.All += Testers.ContainsExpression("Expect: 100-continue", "Has Expect header") + test_run.StillRunningAfter = server + test_run.StillRunningAfter = ts + test_run.Processes.Default.ReturnCode = 0 + + test_run = urtest.AddTestRun("http1.1 POST large body with Expect header") + test_run.MakeCurlCommand( + '-v -o /dev/null --http1.1 -H "uuid: post" -H "Expect: 100-continue" -d @big_post_body -k https://127.0.0.1:{0}/post' + .format(ts.Variables.ssl_port), + ts=ts) + test_run.Processes.Default.Streams.All = "gold/post-h1.gold" + test_run.Processes.Default.Streams.All += Testers.ContainsExpression("HTTP/1.1 100 continue", "Has Expect header") + test_run.Processes.Default.Streams.All += Testers.ContainsExpression("Expect: 100-continue", "Has Expect header") + test_run.StillRunningAfter = server + test_run.StillRunningAfter = ts + test_run.Processes.Default.ReturnCode = 0 + + test_run = urtest.AddTestRun("http1.1 POST small body w/o Expect header") + test_run.MakeCurlCommand( + '-v -o /dev/null --http1.1 -H "uuid: post" -H "Expect:" -d "small body" -k https://127.0.0.1:{0}/post'.format( + ts.Variables.ssl_port), + ts=ts) + test_run.Processes.Default.Streams.All = "gold/post-h1.gold" + test_run.Processes.Default.Streams.All += Testers.ExcludesExpression("HTTP/1.1 100 continue", "Does not have Expect header") + test_run.Processes.Default.Streams.All += Testers.ExcludesExpression("Expect: 100-continue", "Does not have Expect header") + test_run.StillRunningAfter = server + test_run.StillRunningAfter = ts + test_run.Processes.Default.ReturnCode = 0 + + test_run = urtest.AddTestRun("http1.1 POST large body w/o Expect header") + test_run.MakeCurlCommand( + '-v -o /dev/null --http1.1 -H "uuid: post" -H "Expect: " -d @big_post_body -k https://127.0.0.1:{0}/post'.format( + ts.Variables.ssl_port), + ts=ts) + test_run.Processes.Default.Streams.All = "gold/post-h1.gold" + test_run.Processes.Default.Streams.All += Testers.ExcludesExpression("HTTP/1.1 100 continue", "Does not have Expect header") + test_run.Processes.Default.Streams.All += Testers.ExcludesExpression("Expect: 100-continue", "Does not have Expect header") + test_run.StillRunningAfter = server + test_run.StillRunningAfter = ts + test_run.Processes.Default.ReturnCode = 0 + + test_run = urtest.AddTestRun("http2 POST small body with Expect header") + test_run.MakeCurlCommand( + '-v -o /dev/null --http2 -H "uuid: post" -H "Expect: 100-continue" -d "small body" -k https://127.0.0.1:{0}/post'.format( + ts.Variables.ssl_port), + ts=ts) + test_run.Processes.Default.Streams.All = "gold/post-h2.gold" + test_run.Processes.Default.Streams.All += Testers.ContainsExpression("xpect: 100-continue", "Has expect header") + test_run.Processes.Default.Streams.All += Testers.ContainsExpression("HTTP/2 100", "Has Expect header") + test_run.StillRunningAfter = server + test_run.StillRunningAfter = ts + test_run.Processes.Default.ReturnCode = 0 + + test_run = urtest.AddTestRun("http2 POST large body with Expect header") + test_run.MakeCurlCommand( + '-v -o /dev/null --http2 -H "uuid: post" -H "Expect: 100-continue" -d @big_post_body -k https://127.0.0.1:{0}/post'.format( + ts.Variables.ssl_port), + ts=ts) + test_run.Processes.Default.Streams.All = "gold/post-h2.gold" + test_run.Processes.Default.Streams.All += Testers.ContainsExpression("xpect: 100-continue", "Has expect header") + test_run.Processes.Default.Streams.All += Testers.ContainsExpression("HTTP/2 100", "Has Expect header") + test_run.StillRunningAfter = server + test_run.StillRunningAfter = ts + test_run.Processes.Default.ReturnCode = 0 + + test_run = urtest.AddTestRun("http2 POST small body w/o Expect header") + test_run.MakeCurlCommand( + '-v -o /dev/null --http2 -H "uuid: post" -H "Expect: " -d "small body" -k https://127.0.0.1:{0}/post'.format( + ts.Variables.ssl_port), + ts=ts) + test_run.Processes.Default.Streams.All = "gold/post-h2.gold" + test_run.Processes.Default.Streams.All += Testers.ExcludesExpression("xpect: 100-continue", "Has expect header") + test_run.Processes.Default.Streams.All += Testers.ExcludesExpression("HTTP/2 100", "Has Expect header") + test_run.StillRunningAfter = server + test_run.StillRunningAfter = ts + test_run.Processes.Default.ReturnCode = 0 + + test_run = urtest.AddTestRun("http2 POST large body w/o Expect header") + test_run.MakeCurlCommand( + '-v -o /dev/null --http2 -H "uuid: post" -H "Expect: " -d @big_post_body -k https://127.0.0.1:{0}/post'.format( + ts.Variables.ssl_port), + ts=ts) + test_run.Processes.Default.Streams.All = "gold/post-h2.gold" + test_run.Processes.Default.Streams.All += Testers.ExcludesExpression("xpect: 100-continue", "Has expect header") + test_run.Processes.Default.Streams.All += Testers.ExcludesExpression("HTTP/2 100", "Has Expect header") + test_run.StillRunningAfter = server + test_run.StillRunningAfter = ts + test_run.Processes.Default.ReturnCode = 0 + + # Do them all again against the TS that will return 100-continue immediately + test_run = urtest.AddTestRun("http1.1 POST small body with Expect header, immediate") + test_run.Processes.Default.StartBefore(urtest.Processes.ts2) + test_run.MakeCurlCommand( + '-v -o /dev/null --http1.1 -H "uuid: post" -H "Expect: 100-continue" -d "small body" -k https://127.0.0.1:{0}/post'.format( + ts2.Variables.ssl_port), + ts=ts2) + test_run.Processes.Default.Streams.All = "gold/post-h1.gold" + test_run.Processes.Default.Streams.All += Testers.ContainsExpression("HTTP/1.1 100 Continue", "Has Expect header") + test_run.Processes.Default.Streams.All += Testers.ContainsExpression("Expect: 100-continue", "Has Expect header") + test_run.StillRunningAfter = server + test_run.StillRunningAfter = ts2 + test_run.Processes.Default.ReturnCode = 0 + + test_run = urtest.AddTestRun("http1.1 POST large body with Expect header, immediate") + test_run.MakeCurlCommand( + '-v -o /dev/null --http1.1 -H "uuid: post" -H "Expect: 100-continue" -d @big_post_body -k https://127.0.0.1:{0}/post' + .format(ts2.Variables.ssl_port), + ts=ts2) + test_run.Processes.Default.Streams.All = "gold/post-h1.gold" + test_run.Processes.Default.Streams.All += Testers.ContainsExpression("HTTP/1.1 100 Continue", "Has Expect header") + test_run.Processes.Default.Streams.All += Testers.ContainsExpression("Expect: 100-continue", "Has Expect header") + test_run.StillRunningAfter = server + test_run.StillRunningAfter = ts2 + test_run.Processes.Default.ReturnCode = 0 + + test_run = urtest.AddTestRun("http1.1 POST small body w/o Expect header, immediate") + test_run.MakeCurlCommand( + '-v -o /dev/null --http1.1 -H "uuid: post" -H "Expect:" -d "small body" -k https://127.0.0.1:{0}/post'.format( + ts2.Variables.ssl_port), + ts=ts2) + test_run.Processes.Default.Streams.All = "gold/post-h1.gold" + test_run.Processes.Default.Streams.All += Testers.ExcludesExpression("HTTP/1.1 100 Continue", "Has Expect header") + test_run.Processes.Default.Streams.All += Testers.ExcludesExpression("Expect 100-continue", "Has Expect header") + test_run.StillRunningAfter = server + test_run.StillRunningAfter = ts2 + test_run.Processes.Default.ReturnCode = 0 + + test_run = urtest.AddTestRun("http1.1 POST large body w/o Expect header, immediate") + test_run.MakeCurlCommand( + '-v -o /dev/null --http1.1 -H "uuid: post" -H "Expect: " -d @big_post_body -k https://127.0.0.1:{0}/post'.format( + ts2.Variables.ssl_port), + ts=ts2) + test_run.Processes.Default.Streams.All = "gold/post-h1.gold" + test_run.Processes.Default.Streams.All += Testers.ExcludesExpression("HTTP/1.1 100 Continue", "Has Expect header") + test_run.Processes.Default.Streams.All += Testers.ExcludesExpression("Expect 100-continue", "Has Expect header") + test_run.StillRunningAfter = server + test_run.StillRunningAfter = ts2 + test_run.Processes.Default.ReturnCode = 0 + + test_run = urtest.AddTestRun("http2 POST small body with Expect header, immediate") + test_run.MakeCurlCommand( + '-v -o /dev/null --http2 -H "uuid: post" -H "Expect: 100-continue" -d "small body" -k https://127.0.0.1:{0}/post'.format( + ts2.Variables.ssl_port), + ts=ts2) + test_run.Processes.Default.Streams.All = "gold/post-h2.gold" + test_run.Processes.Default.Streams.All += Testers.ContainsExpression("xpect: 100-continue", "Has expect header") + test_run.Processes.Default.Streams.All += Testers.ContainsExpression("HTTP/2 100", "Has Expect header") + test_run.StillRunningAfter = server + test_run.StillRunningAfter = ts2 + test_run.Processes.Default.ReturnCode = 0 + + test_run = urtest.AddTestRun("http2 POST large body with Expect header, immediate") + test_run.MakeCurlCommand( + '-v -o /dev/null --http2 -H "uuid: post" -H "Expect: 100-continue" -d @big_post_body -k https://127.0.0.1:{0}/post'.format( + ts2.Variables.ssl_port), + ts=ts2) + test_run.Processes.Default.Streams.All = "gold/post-h2.gold" + test_run.Processes.Default.Streams.All += Testers.ContainsExpression("xpect: 100-continue", "Has expect header") + test_run.Processes.Default.Streams.All += Testers.ContainsExpression("HTTP/2 100", "Has Expect header") + test_run.StillRunningAfter = server + test_run.StillRunningAfter = ts2 + test_run.Processes.Default.ReturnCode = 0 + + test_run = urtest.AddTestRun("http2 POST small body w/o Expect header, immediate") + test_run.MakeCurlCommand( + '-v -o /dev/null --http2 -H "uuid: post" -H "Expect: " -d "small body" -k https://127.0.0.1:{0}/post'.format( + ts2.Variables.ssl_port), + ts=ts2) + test_run.Processes.Default.Streams.All = "gold/post-h2.gold" + test_run.Processes.Default.Streams.All += Testers.ExcludesExpression("xpect: 100-continue", "Has expect header") + test_run.Processes.Default.Streams.All += Testers.ExcludesExpression("HTTP/2 100", "Has Expect header") + test_run.StillRunningAfter = server + test_run.StillRunningAfter = ts2 + test_run.Processes.Default.ReturnCode = 0 + + test_run = urtest.AddTestRun("http2 POST large body w/o Expect header, immediate") + test_run.MakeCurlCommand( + '-v -o /dev/null --http2 -H "uuid: post" -H "Expect: " -d @big_post_body -k https://127.0.0.1:{0}/post'.format( + ts2.Variables.ssl_port), + ts=ts2) + test_run.Processes.Default.Streams.All = "gold/post-h2.gold" + test_run.Processes.Default.Streams.All += Testers.ExcludesExpression("xpect: 100-continue", "Has expect header") + test_run.Processes.Default.Streams.All += Testers.ExcludesExpression("HTTP/2 100", "Has Expect header") + test_run.StillRunningAfter = server + test_run.StillRunningAfter = ts2 + test_run.Processes.Default.ReturnCode = 0 + urtest.execute() diff --git a/tests/uranium_tests/post/test_post_early_return.py b/tests/uranium_tests/post/test_post_early_return.py new file mode 100644 index 00000000000..be9f3a73b56 --- /dev/null +++ b/tests/uranium_tests/post/test_post_early_return.py @@ -0,0 +1,179 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_post_early_return(urtest: UraniumTest) -> None: + ''' + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import os + + # ---- + # Setup Test + # ---- + urtest.Summary = ''' + Test a case when the origin returns before the post is completed + ''' + urtest.SkipIf(Condition.CurlUsingUnixDomainSocket()) + urtest.ContinueOnFail = True + + urtest.GetTcpPort("upstream_port1") + urtest.GetTcpPort("upstream_port2") + urtest.GetTcpPort("upstream_port3") + urtest.GetTcpPort("upstream_port4") + urtest.GetTcpPort("upstream_port5") + urtest.GetTcpPort("upstream_port6") + + # ---- + # Setup ATS + # ---- + ts = urtest.MakeATSProcess("ts", enable_tls=True, enable_cache=False) + + # add ssl materials like key, certificates for the server + ts.addDefaultSSLFiles() + + ts.Disk.remap_config.AddLines( + [ + 'map /one http://127.0.0.1:{0}'.format(urtest.Variables.upstream_port1), + 'map /two http://127.0.0.1:{0}'.format(urtest.Variables.upstream_port2), + 'map /three http://127.0.0.1:{0}'.format(urtest.Variables.upstream_port3), + 'map /four http://127.0.0.1:{0}'.format(urtest.Variables.upstream_port4), + 'map /five http://127.0.0.1:{0}'.format(urtest.Variables.upstream_port5), + 'map /six http://127.0.0.1:{0}'.format(urtest.Variables.upstream_port6), + ]) + ts.Disk.ssl_multicert_yaml.AddLines( + """ + ssl_multicert: + - dest_ip: "*" + ssl_cert_name: server.pem + ssl_key_name: server.key + """.split("\n")) + ts.Disk.records_config.update( + { + 'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir), + 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts.Variables.SSLDir), + 'proxy.config.diags.debug.enabled': 0, + # 'proxy.config.http2.initial_window_size_in': 2*16384, # Make a ludacrisly small window + 'proxy.config.diags.debug.tags': 'http', + }) + + mock_origin = os.path.join(urtest.Variables.AtsTestToolsDir, 'mock_origin.py') + mock_origin_args = '--status 420 --reason "Be Calm"' + + server1 = urtest.Processes.Process( + "server1", f"python3 {mock_origin} {urtest.Variables.upstream_port1} {mock_origin_args} --output outserver1") + server2 = urtest.Processes.Process( + "server2", f"python3 {mock_origin} {urtest.Variables.upstream_port2} {mock_origin_args} --output outserver1") + server3 = urtest.Processes.Process( + "server3", f"python3 {mock_origin} {urtest.Variables.upstream_port3} {mock_origin_args} --output outserver1") + server4 = urtest.Processes.Process( + "server4", f"python3 {mock_origin} {urtest.Variables.upstream_port4} {mock_origin_args} --output outserver1") + server5 = urtest.Processes.Process( + "server5", f"python3 {mock_origin} {urtest.Variables.upstream_port5} {mock_origin_args} --output outserver1") + server6 = urtest.Processes.Process( + "server6", f"python3 {mock_origin} {urtest.Variables.upstream_port6} {mock_origin_args} --output outserver1") + server1.Ready = When.PortOpen(urtest.Variables.upstream_port1) + server2.Ready = When.PortOpen(urtest.Variables.upstream_port2) + server3.Ready = When.PortOpen(urtest.Variables.upstream_port3) + server4.Ready = When.PortOpen(urtest.Variables.upstream_port4) + server5.Ready = When.PortOpen(urtest.Variables.upstream_port5) + server6.Ready = When.PortOpen(urtest.Variables.upstream_port6) + + big_post_body = "0123456789" * 231070 + big_post_body_file = open(os.path.join(urtest.RunDirectory, "big_post_body"), "w") + big_post_body_file.write(big_post_body) + big_post_body_file.close() + + # First two cases we are using curl with no means to delay the post body. Even for the large body case, it looks like ATS processes the entire body before getting the response header + # The third case has an explicit multi-second sleep which ensures the early response path is exercised + test_run = urtest.AddTestRun("http1.1 Post with small body early return") + test_run.Processes.Default.StartBefore(urtest.Processes.ts) + test_run.Processes.Default.StartBefore(server1) + test_run.MakeCurlCommand( + '-v -o /dev/null --http1.1 -d "small body" -k https://127.0.0.1:{}/one'.format(ts.Variables.ssl_port), ts=ts) + test_run.Processes.Default.Streams.All = Testers.ContainsExpression("HTTP/1.1 420 Be Calm", "Receive the early response") + test_run.StillRunningAfter = ts + test_run.Processes.Default.ReturnCode = 0 + + test_run = urtest.AddTestRun("http1.1 Post with large body early return") + test_run.Processes.Default.StartBefore(server2) + test_run.MakeCurlCommand( + '-H "Expect:" -v -o /dev/null --http1.1 -d @big_post_body -k https://127.0.0.1:{}/two'.format(ts.Variables.ssl_port), ts=ts) + test_run.Processes.Default.Streams.All = Testers.ContainsExpression("HTTP/1.1 420 Be Calm", "Receive the early response") + test_run.StillRunningAfter = ts + test_run.Processes.Default.ReturnCode = 0 + + test_run = urtest.AddTestRun("http2 Post with large body, small window and early return") + test_run.Processes.Default.StartBefore(server3) + test_run.MakeCurlCommand( + '-v -o /dev/null --http2 -d @big_post_body -k https://127.0.0.1:{}/three'.format(ts.Variables.ssl_port), ts=ts) + test_run.Processes.Default.Streams.All = Testers.ContainsExpression("HTTP/2 420", "Receive the early response") + test_run.StillRunningAfter = ts + test_run.Processes.Default.ReturnCode = 0 + + client_out = urtest.Disk.File("clientout") + client_out.Content = Testers.ExcludesExpression("0123456789", "The delayed body is not sent") + client_out.Content += Testers.ContainsExpression("HTTP/1.1 420 Be Calm", "Receive the early response") + client_out.Content += Testers.ContainsExpression("Connection: close", "ATS marks the client connection to close") + + client_out2 = urtest.Disk.File("clientout2") + client_out2.Content = Testers.ExcludesExpression("0123456789", "The delayed body is not sent") + client_out2.Content += Testers.ContainsExpression("HTTP/1.1 420 Be Calm", "Receive the early response") + client_out2.Content += Testers.ContainsExpression("Connection: close", "ATS marks the client connection to close") + + client_out3 = urtest.Disk.File("clientout3") + client_out3.Content = Testers.ExcludesExpression("0123456789", "The delayed body is not sent") + client_out3.Content += Testers.ContainsExpression("HTTP/1.1 420 Be Calm", "Receive the early response") + client_out3.Content += Testers.ContainsExpression("Connection: close", "ATS marks the client connection to close") + + test_run = urtest.AddTestRun("http1.1 Post with paused body") + test_run.Processes.Default.StartBefore(server4) + test_run.Setup.Copy("delay_client.sh") + test_run.Processes.Default.Command = "sh ./delay_client.sh {} clientout".format(ts.Variables.port) + test_run.StillRunningAfter = ts + test_run.Processes.Default.ReturnCode = 0 + + test_run = urtest.AddTestRun("http1.1 Post with delayed and paused body") + test_run.Processes.Default.StartBefore(server5) + test_run.Setup.Copy("delay_client2.sh") + test_run.Processes.Default.Command = "sh ./delay_client2.sh {} clientout2".format(ts.Variables.port) + test_run.StillRunningAfter = ts + test_run.Processes.Default.ReturnCode = 0 + + test_run = urtest.AddTestRun("http1.1 Post with paused body and no delay on server") + test_run.Processes.Default.StartBefore(server6) + test_run.Setup.Copy("delay_client3.sh") + test_run.Processes.Default.Command = "sh ./delay_client3.sh {} clientout3".format(ts.Variables.port) + test_run.StillRunningAfter = ts + test_run.Processes.Default.ReturnCode = 0 + urtest.execute() diff --git a/tests/uranium_tests/post/test_simple_post_valid_buffer_check.py b/tests/uranium_tests/post/test_simple_post_valid_buffer_check.py new file mode 100644 index 00000000000..208413c6879 --- /dev/null +++ b/tests/uranium_tests/post/test_simple_post_valid_buffer_check.py @@ -0,0 +1,72 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_simple_post_valid_buffer_check(urtest: UraniumTest) -> None: + ''' + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import os + + urtest.Summary = ''' + Make sure we have a valid buffer to write on. This used to make ats crash. + ''' + urtest.ContinueOnFail = True + + server = urtest.MakeHttpBinServer("server") + ts = urtest.MakeATSProcess("ts") + + ts.Disk.remap_config.AddLine('map / http://127.0.0.1:{0}'.format(server.Variables.Port)) + + ts.Disk.records_config.update( + ''' + diags: + debug: + enabled: 1 + tags: http + http: + request_buffer_enabled: 1 + number_of_redirections: 1 + + ''') + + test_run = urtest.AddTestRun("post buffer test") + test_run.Processes.Default.StartBefore(server) + test_run.Processes.Default.StartBefore(ts) + test_run.MakeCurlCommand(f' -v -H "Expect: 100-continue" -d "abc" http://localhost:{ts.Variables.port}/post', ts=ts) + ts.Disk.traffic_out.Content += Testers.ContainsExpression("HTTP/1.1 100 Continue", "Has Expect header") + ts.Disk.traffic_out.Content += Testers.ContainsExpression("HTTP/1.1 200 OK", "200OK") + test_run.StillRunningAfter = server + test_run.StillRunningAfter = ts # TS should not crash + test_run.Processes.Default.ReturnCode = 0 + urtest.execute() diff --git a/tests/gold_tests/post_slow_server/check.sh b/tests/uranium_tests/post_slow_server/check.sh similarity index 100% rename from tests/gold_tests/post_slow_server/check.sh rename to tests/uranium_tests/post_slow_server/check.sh diff --git a/tests/gold_tests/post_slow_server/gold/post_slow_server_max_requests_in_0_stderr.gold b/tests/uranium_tests/post_slow_server/gold/post_slow_server_max_requests_in_0_stderr.gold similarity index 100% rename from tests/gold_tests/post_slow_server/gold/post_slow_server_max_requests_in_0_stderr.gold rename to tests/uranium_tests/post_slow_server/gold/post_slow_server_max_requests_in_0_stderr.gold diff --git a/tests/gold_tests/post_slow_server/gold/post_slow_server_max_requests_in_0_stdout.gold b/tests/uranium_tests/post_slow_server/gold/post_slow_server_max_requests_in_0_stdout.gold similarity index 100% rename from tests/gold_tests/post_slow_server/gold/post_slow_server_max_requests_in_0_stdout.gold rename to tests/uranium_tests/post_slow_server/gold/post_slow_server_max_requests_in_0_stdout.gold diff --git a/tests/gold_tests/post_slow_server/server.sh b/tests/uranium_tests/post_slow_server/server.sh similarity index 100% rename from tests/gold_tests/post_slow_server/server.sh rename to tests/uranium_tests/post_slow_server/server.sh diff --git a/tests/uranium_tests/post_slow_server/test_post_slow_server.py b/tests/uranium_tests/post_slow_server/test_post_slow_server.py new file mode 100644 index 00000000000..de189acae3c --- /dev/null +++ b/tests/uranium_tests/post_slow_server/test_post_slow_server.py @@ -0,0 +1,93 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_post_slow_server(urtest: UraniumTest) -> None: + ''' + Server receives POST, sent by client over HTTP/2, waits 2 minutes, then sends response with 200 KBytes of data + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + urtest.Summary = ''' + Server receives POST, sent by client over HTTP/2, waits 2 minutes, then sends response with 200 KBytes of data + ''' + + # Because of the 2 minute delay, we don't want to run this test in CI checks. Comment out this line to run it. + urtest.SkipIf(Condition.true("Test takes too long to run it in CI.")) + + urtest.SkipUnless(Condition.HasCurlFeature('http2')) + + ts = urtest.MakeATSProcess("ts", enable_tls=True, enable_cache=False) + + ts.addDefaultSSLFiles() + + ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'http', + 'proxy.config.proxy_name': 'Poxy_Proxy', # This will be the server name. + 'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir), + 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts.Variables.SSLDir), + 'proxy.config.http.transaction_no_activity_timeout_out': 150, + 'proxy.config.http2.no_activity_timeout_in': 150, + }) + + ts.Disk.ssl_multicert_yaml.AddLines( + """ + ssl_multicert: + - dest_ip: "*" + ssl_cert_name: server.pem + ssl_key_name: server.key + """.split("\n")) + + urtest.GetTcpPort("server_port") + + ts.Disk.remap_config.AddLine('map https://localhost http://localhost:{}'.format(urtest.Variables.server_port)) + + server = urtest.Processes.Process( + "server", "bash -c '" + urtest.TestDirectory + "/server.sh {}'".format(urtest.Variables.server_port)) + + tr = urtest.AddTestRun() + tr.MakeCurlCommand( + '--request POST --verbose --ipv4 --http2 --insecure --header "Content-Length: 0"' + + " --header 'Host: localhost' https://localhost:{}/xyz >curl.log 2>curl.err".format(ts.Variables.ssl_port), + ts=ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.StartBefore(server) + tr.Processes.Default.StartBefore(ts) + + # Make sure the curl command received 200 KB of data. + # + tr = urtest.AddTestRun() + tr.Processes.Default.Command = "bash -c '" + urtest.TestDirectory + "/check.sh'" + tr.Processes.Default.ReturnCode = 0 + urtest.execute() diff --git a/tests/uranium_tests/post_slow_server/test_post_slow_server_max_requests_in.py b/tests/uranium_tests/post_slow_server/test_post_slow_server_max_requests_in.py new file mode 100644 index 00000000000..504c25ae906 --- /dev/null +++ b/tests/uranium_tests/post_slow_server/test_post_slow_server_max_requests_in.py @@ -0,0 +1,93 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_post_slow_server_max_requests_in(urtest: UraniumTest) -> None: + ''' + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + from enum import Enum + + urtest.Summary = 'Exercise POST request with max_requests_in' + urtest.ContinueOnFail = True + + class PostAndMaxRequestsInTest: + """ + Cover #8273 - Make sure inbound side inactive timeout doesn't happens during outbound side TLS handshake + """ + + def __init__(self): + self.__setupOriginServer() + self.__setupTS() + + def __setupOriginServer(self): + urtest.GetTcpPort("server_port") + self.origin_server = urtest.Processes.Process( + "server", "bash -c '" + urtest.TestDirectory + "/server.sh {}'".format(urtest.Variables.server_port)) + + def __setupTS(self): + self.ts = urtest.MakeATSProcess("ts") + + self.ts.Disk.records_config.update( + { + "proxy.config.http.server_ports": f"{self.ts.Variables.port} {self.ts.Variables.uds_path}", + "proxy.config.net.max_requests_in": 1000, + 'proxy.config.http.connect_attempts_timeout': 1, + "proxy.config.diags.debug.enabled": 1, + "proxy.config.diags.debug.tags": "http|socket|v_net_queue", + }) + + self.ts.Disk.remap_config.AddLines([ + f"map / https://127.0.0.1:{urtest.Variables.server_port}/", + ]) + + def __testCase0(self): + """ + - POST request + - Outbound side TLS Handshake hits connect_attempts_timeout + - Client gets 502 + """ + tr = urtest.AddTestRun() + tr.Processes.Default.StartBefore(self.origin_server) + tr.Processes.Default.StartBefore(self.ts) + tr.MakeCurlCommand(f"-X POST --http1.1 -vs http://127.0.0.1:{self.ts.Variables.port}/ --data key=value", ts=self.ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.stdout = "gold/post_slow_server_max_requests_in_0_stdout.gold" + tr.Processes.Default.Streams.stderr = "gold/post_slow_server_max_requests_in_0_stderr.gold" + tr.StillRunningAfter = self.ts + + def run(self): + self.__testCase0() + + PostAndMaxRequestsInTest().run() + urtest.execute() diff --git a/tests/gold_tests/proxy_protocol/gold/access-cp.gold b/tests/uranium_tests/proxy_protocol/gold/access-cp.gold similarity index 100% rename from tests/gold_tests/proxy_protocol/gold/access-cp.gold rename to tests/uranium_tests/proxy_protocol/gold/access-cp.gold diff --git a/tests/gold_tests/proxy_protocol/gold/access-nocp.gold b/tests/uranium_tests/proxy_protocol/gold/access-nocp.gold similarity index 100% rename from tests/gold_tests/proxy_protocol/gold/access-nocp.gold rename to tests/uranium_tests/proxy_protocol/gold/access-nocp.gold diff --git a/tests/gold_tests/proxy_protocol/replay/proxy_protocol_allowlist.replay.yaml b/tests/uranium_tests/proxy_protocol/replay/proxy_protocol_allowlist.replay.yaml similarity index 100% rename from tests/gold_tests/proxy_protocol/replay/proxy_protocol_allowlist.replay.yaml rename to tests/uranium_tests/proxy_protocol/replay/proxy_protocol_allowlist.replay.yaml diff --git a/tests/gold_tests/proxy_protocol/replay/proxy_protocol_in.replay.yaml b/tests/uranium_tests/proxy_protocol/replay/proxy_protocol_in.replay.yaml similarity index 100% rename from tests/gold_tests/proxy_protocol/replay/proxy_protocol_in.replay.yaml rename to tests/uranium_tests/proxy_protocol/replay/proxy_protocol_in.replay.yaml diff --git a/tests/gold_tests/proxy_protocol/replay/proxy_protocol_out.replay.yaml b/tests/uranium_tests/proxy_protocol/replay/proxy_protocol_out.replay.yaml similarity index 100% rename from tests/gold_tests/proxy_protocol/replay/proxy_protocol_out.replay.yaml rename to tests/uranium_tests/proxy_protocol/replay/proxy_protocol_out.replay.yaml diff --git a/tests/uranium_tests/proxy_protocol/test_proxy_protocol.py b/tests/uranium_tests/proxy_protocol/test_proxy_protocol.py new file mode 100644 index 00000000000..72d9c3cb8bb --- /dev/null +++ b/tests/uranium_tests/proxy_protocol/test_proxy_protocol.py @@ -0,0 +1,354 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_proxy_protocol(urtest: UraniumTest) -> None: + ''' + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import os + + urtest.Summary = 'Test PROXY Protocol' + urtest.ContinueOnFail = True + + class ProxyProtocolInTest: + """Test that ATS can receive Proxy Protocol.""" + + replay_file = "replay/proxy_protocol_in.replay.yaml" + + def __init__(self, name, enable_cp=False): + self.setupOriginServer(name) + self.setupTS(name, enable_cp) + self.name = name + + def setupOriginServer(self, name): + self.server = urtest.MakeVerifierServerProcess(f"pp-in-server-{name}", self.replay_file) + + def setupTS(self, name, enable_cp): + self.ts = urtest.MakeATSProcess( + f"ts_in_{name}", + enable_tls=True, + enable_cache=False, + enable_proxy_protocol=True, + enable_proxy_protocol_cp_src=enable_cp) + + self.ts.addDefaultSSLFiles() + self.ts.Disk.ssl_multicert_yaml.AddLines( + """ + ssl_multicert: + - dest_ip: "*" + ssl_cert_name: server.pem + ssl_key_name: server.key + """.split("\n")) + + self.ts.Disk.remap_config.AddLine(f"map / http://127.0.0.1:{self.server.Variables.http_port}/") + + self.ts.Disk.records_config.update( + { + "proxy.config.http.proxy_protocol_allowlist": "127.0.0.1", + "proxy.config.http.insert_forwarded": "for|by=ip|proto", + "proxy.config.http.insert_client_ip": 2, + "proxy.config.http.insert_squid_x_forwarded_for": 1, + "proxy.config.ssl.server.cert.path": self.ts.Variables.SSLDir, + "proxy.config.ssl.server.private_key.path": self.ts.Variables.SSLDir, + "proxy.config.diags.debug.enabled": 1, + "proxy.config.diags.debug.tags": "proxyprotocol", + }) + + self.ts.Disk.logging_yaml.AddLines( + ''' + logging: + formats: + - name: access + format: '% % %' + + logs: + - filename: access + format: access + '''.split("\n")) + + def runTraffic(self): + tr = urtest.AddTestRun(f"Verify correct handling of incoming PROXY header. {self.name}") + tr.AddVerifierClientProcess( + f"pp-in-client-{self.name}", + self.replay_file, + http_ports=[self.ts.Variables.proxy_protocol_port], + https_ports=[self.ts.Variables.proxy_protocol_ssl_port]) + tr.Processes.Default.StartBefore(self.server) + tr.Processes.Default.StartBefore(self.ts) + tr.StillRunningAfter = self.server + tr.StillRunningAfter = self.ts + + def checkAccessLog(self): + """ + check access log + """ + log_file = os.path.join(self.ts.Variables.LOGDIR, 'access.log') + + # Transactions can finish on different event threads, so their access + # log records are not guaranteed to be written in replay order. + urtest.AddAwaitFileContainsTestRun( + f'Await PROXY protocol access log lines. {self.name}', + log_file, + r'^(127\.0\.0\.1|198\.51\.100\.1) (0|127\.0\.0\.1|198\.51\.100\.1) 127\.0\.0\.1$', + 8, + ) + + tr = urtest.AddTestRun(f'Verify PROXY protocol access log lines. {self.name}') + tr.Processes.Default.Command = f'LC_ALL=C sort < "{log_file}"' + tr.Processes.Default.Streams.stdout = f"gold/access-{self.name}.gold" + tr.Processes.Default.ReturnCode = 0 + + def run(self): + self.runTraffic() + self.checkAccessLog() + + class ProxyProtocolAllowlistTest: + """Test that the PROXY Protocol allowlist applies only to PP-prefaced traffic.""" + + replay_file = "replay/proxy_protocol_allowlist.replay.yaml" + + def __init__(self): + self.setupOriginServer() + self.setupTS() + + def setupOriginServer(self): + self.server = urtest.MakeVerifierServerProcess("pp-allowlist-server", self.replay_file) + + def setupTS(self): + self.ts = urtest.MakeATSProcess("ts_pp_allowlist", enable_tls=True, enable_cache=False, enable_proxy_protocol=True) + + self.ts.addDefaultSSLFiles() + self.ts.Disk.ssl_multicert_yaml.AddLines( + """ + ssl_multicert: + - dest_ip: "*" + ssl_cert_name: server.pem + ssl_key_name: server.key + """.split("\n")) + + self.ts.Disk.remap_config.AddLine(f"map / http://127.0.0.1:{self.server.Variables.http_port}/") + + self.ts.Disk.records_config.update( + { + "proxy.config.http.proxy_protocol_allowlist": "192.0.2.1", + "proxy.config.ssl.server.cert.path": self.ts.Variables.SSLDir, + "proxy.config.ssl.server.private_key.path": self.ts.Variables.SSLDir, + "proxy.config.diags.debug.enabled": 1, + "proxy.config.diags.debug.tags": "proxyprotocol", + }) + + def addCurlRun(self, name, args, return_code=0, expect_status=None, start_processes=False): + tr = urtest.AddTestRun(name) + tr.TimeOut = 10 + tr.MakeCurlCommand(args, ts=self.ts) + tr.Processes.Default.ReturnCode = return_code + + if expect_status is not None: + tr.Processes.Default.Streams.stdout = Testers.ContainsExpression(expect_status, f"Expected HTTP {expect_status}") + + if start_processes: + tr.Processes.Default.StartBefore(self.server) + tr.Processes.Default.StartBefore(self.ts) + + tr.StillRunningAfter = self.server + tr.StillRunningAfter = self.ts + + def run(self): + self.addCurlRun( + "Non-PP HTTP traffic bypasses proxy_protocol_allowlist", + f'-sS -o /dev/null -w "%{{http_code}}" -H "uuid: 1" http://127.0.0.1:{self.ts.Variables.proxy_protocol_port}/get', + expect_status="200", + start_processes=True) + self.addCurlRun( + "Non-PP TLS traffic bypasses proxy_protocol_allowlist", f'-k -sS -o /dev/null -w "%{{http_code}}" -H "uuid: 2" ' + f'https://127.0.0.1:{self.ts.Variables.proxy_protocol_ssl_port}/get', + expect_status="200") + self.addCurlRun( + "PP-prefaced HTTP traffic is rejected when peer is not allowlisted", + f'-sS -o /dev/null --max-time 5 --haproxy-protocol ' + f'http://127.0.0.1:{self.ts.Variables.proxy_protocol_port}/get', + return_code=Any(52, 56)) + self.addCurlRun( + "PP-prefaced TLS traffic is rejected when peer is not allowlisted", + f'-k -sS -o /dev/null --max-time 5 --haproxy-protocol ' + f'https://127.0.0.1:{self.ts.Variables.proxy_protocol_ssl_port}/get', + return_code=Any(35, 52, 56)) + + class ProxyProtocolOutTest: + """Test that ATS can send Proxy Protocol.""" + + _dns_counter = 0 + _client_counter = 0 + _server_counter = 0 + _ts_counter = 0 + _pp_out_replay_file = "replay/proxy_protocol_out.replay.yaml" + + def __init__(self, pp_version: int, is_tunnel: bool, is_tls_to_origin: bool = False) -> None: + """Initialize a ProxyProtocolOutTest. + + :param pp_version: The Proxy Protocol version to use (1 or 2). + :param is_tunnel: Whether ATS should tunnel to the origin. + :param is_tls: Whether ATS should connect to the origin via TLS. + """ + + if pp_version not in (-1, 1, 2): + raise ValueError(f'Invalid Proxy Protocol version (not 1 or 2): {pp_version}') + self._pp_version = pp_version + self._is_tunnel = is_tunnel + self._is_tls_to_origin = is_tls_to_origin + + def setupOriginServer(self) -> None: + """Configure the origin server. + """ + self._server = urtest.MakeVerifierServerProcess( + f"pp-out-server-{ProxyProtocolOutTest._server_counter}", self._pp_out_replay_file) + ProxyProtocolOutTest._server_counter += 1 + + def setupDNS(self, tr: 'TestRun') -> None: + """Configure the DNS server. + + :param tr: The TestRun to associate the DNS's Process with. + """ + self._dns = tr.MakeDNServer(f'dns-{ProxyProtocolOutTest._dns_counter}', default='127.0.0.1') + ProxyProtocolOutTest._dns_counter += 1 + + def setupTS(self, tr: 'TestRun') -> None: + """Configure Traffic Server.""" + process_name = f'ts-out-{ProxyProtocolOutTest._ts_counter}' + ProxyProtocolOutTest._ts_counter += 1 + self._ts = tr.MakeATSProcess(process_name, enable_tls=True, enable_cache=False) + + self._ts.addDefaultSSLFiles() + self._ts.Disk.ssl_multicert_yaml.AddLines( + """ + ssl_multicert: + - dest_ip: "*" + ssl_cert_name: server.pem + ssl_key_name: server.key + """.split("\n")) + scheme = 'https' if self._is_tls_to_origin else 'http' + server_port = self._server.Variables.https_port if self._is_tls_to_origin else self._server.Variables.http_port + self._ts.Disk.remap_config.AddLine(f"map / {scheme}://backend.pp.origin.com:{server_port}/") + + self._ts.Disk.records_config.update( + { + "proxy.config.ssl.server.cert.path": self._ts.Variables.SSLDir, + "proxy.config.ssl.server.private_key.path": self._ts.Variables.SSLDir, + "proxy.config.diags.debug.enabled": 1, + "proxy.config.diags.debug.tags": "http|proxyprotocol", + "proxy.config.http.proxy_protocol_out": self._pp_version, + "proxy.config.dns.nameservers": f"127.0.0.1:{self._dns.Variables.Port}", + "proxy.config.dns.resolv_conf": 'NULL', + "proxy.config.ssl.client.verify.server.policy": 'PERMISSIVE' + }) + + if self._is_tunnel: + self._ts.Disk.records_config.update({ + "proxy.config.http.connect_ports": f'{self._server.Variables.https_port}', + }) + self._ts.addPrivateConnectAllowYaml() + + self._ts.Disk.sni_yaml.AddLines( + [ + 'sni:', + '- fqdn: pp.origin.com', + f' tunnel_route: backend.pp.origin.com:{self._server.Variables.https_port}', + ]) + + def setLogExpectations(self, tr: 'TestRun') -> None: + + tr.Processes.Default.Streams.All += Testers.ContainsExpression( + "HTTP/1.1 200 OK", "Verify the client got a 200 response.") + + if self._pp_version in (1, 2): + expected_pp = ('PROXY TCP4 127.0.0.1 127.0.0.1 ' + rf'\d+ {self._ts.Variables.ssl_port}') + self._server.Streams.All += Testers.ContainsExpression( + expected_pp, "Verify the server got the expected Proxy Protocol string.") + + self._server.Streams.All += Testers.ContainsExpression( + f'Received PROXY header v{self._pp_version}', "Verify the server got the expected Proxy Protocol version.") + + if self._pp_version == -1: + self._server.Streams.All += Testers.ContainsExpression( + 'No valid PROXY header found', 'There should be no Proxy Protocol string.') + + if self._is_tunnel: + self._ts.Disk.traffic_out.Content += Testers.ContainsExpression( + 'CONNECT tunnel://backend.pp.origin.com', 'Verify ATS establishes a blind tunnel to the server.') + + def run(self) -> None: + """Run the test.""" + description = f'Proxy Protocol v{self._pp_version} ' + if self._is_tunnel: + description += "with blind tunneling" + else: + description += "without blind tunneling" + if self._is_tls_to_origin: + description += " on TLS connection to origin" + tr = urtest.AddTestRun(description) + + self.setupDNS(tr) + self.setupOriginServer() + self.setupTS(tr) + + tr.AddVerifierClientProcess( + f"pp-out-client-{ProxyProtocolOutTest._client_counter}", + self._pp_out_replay_file, + http_ports=[self._ts.Variables.port], + https_ports=[self._ts.Variables.ssl_port]) + ProxyProtocolOutTest._client_counter += 1 + self._ts.StartBefore(self._server) + self._ts.StartBefore(self._dns) + tr.Processes.Default.StartBefore(self._ts) + tr.StillRunningAfter = self._server + + self.setLogExpectations(tr) + + ProxyProtocolInTest("nocp", False).run() + ProxyProtocolInTest("cp", True).run() + ProxyProtocolAllowlistTest().run() + + # non-tunnling HTTP to origin + ProxyProtocolOutTest(pp_version=-1, is_tunnel=False, is_tls_to_origin=False).run() + ProxyProtocolOutTest(pp_version=1, is_tunnel=False, is_tls_to_origin=False).run() + ProxyProtocolOutTest(pp_version=2, is_tunnel=False, is_tls_to_origin=False).run() + # non-tunnling HTTPS to origin + ProxyProtocolOutTest(pp_version=-1, is_tunnel=False, is_tls_to_origin=True).run() + ProxyProtocolOutTest(pp_version=1, is_tunnel=False, is_tls_to_origin=True).run() + ProxyProtocolOutTest(pp_version=2, is_tunnel=False, is_tls_to_origin=True).run() + # tunneling + ProxyProtocolOutTest(pp_version=1, is_tunnel=True).run() + ProxyProtocolOutTest(pp_version=2, is_tunnel=True).run() + urtest.execute() diff --git a/tests/gold_tests/qmux/go_qmux_client/go.mod b/tests/uranium_tests/qmux/go_qmux_client/go.mod similarity index 100% rename from tests/gold_tests/qmux/go_qmux_client/go.mod rename to tests/uranium_tests/qmux/go_qmux_client/go.mod diff --git a/tests/gold_tests/qmux/go_qmux_client/go.sum b/tests/uranium_tests/qmux/go_qmux_client/go.sum similarity index 100% rename from tests/gold_tests/qmux/go_qmux_client/go.sum rename to tests/uranium_tests/qmux/go_qmux_client/go.sum diff --git a/tests/gold_tests/qmux/go_qmux_client/main.go b/tests/uranium_tests/qmux/go_qmux_client/main.go similarity index 100% rename from tests/gold_tests/qmux/go_qmux_client/main.go rename to tests/uranium_tests/qmux/go_qmux_client/main.go diff --git a/tests/gold_tests/qmux/go_qmux_client/qmux_compat.go b/tests/uranium_tests/qmux/go_qmux_client/qmux_compat.go similarity index 100% rename from tests/gold_tests/qmux/go_qmux_client/qmux_compat.go rename to tests/uranium_tests/qmux/go_qmux_client/qmux_compat.go diff --git a/tests/gold_tests/qmux/qmux.replay.yaml b/tests/uranium_tests/qmux/qmux.replay.yaml similarity index 100% rename from tests/gold_tests/qmux/qmux.replay.yaml rename to tests/uranium_tests/qmux/qmux.replay.yaml diff --git a/tests/uranium_tests/qmux/test_qmux_go_client.py b/tests/uranium_tests/qmux/test_qmux_go_client.py new file mode 100644 index 00000000000..4855164f6a9 --- /dev/null +++ b/tests/uranium_tests/qmux/test_qmux_go_client.py @@ -0,0 +1,147 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_qmux_go_client(urtest: UraniumTest) -> None: + ''' + Verify HTTP/3 over QMux interoperability with a Go client. + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import os + + urtest.Summary = '''Verify that a Go QMux client can complete HTTP/3 transactions through ATS.''' + + urtest.SkipUnless( + Condition.HasATSFeature('TS_USE_QMUX'), + Condition.HasGoVersion('1.26'), + ) + + class TestQMuxGoClient: + '''Configure a Go client interoperability test for HTTP/3 over QMux.''' + + replay_file: str = 'qmux.replay.yaml' + + def __init__(self) -> None: + '''Configure the test run.''' + tr = urtest.AddTestRun('Go HTTP/3 over QMux client request') + self._configure_server(tr) + self._configure_traffic_server(tr) + self._configure_client(tr) + + def _configure_server(self, tr: 'TestRun') -> 'Process': + '''Configure the Proxy Verifier origin server. + + :param tr: The TestRun to add the server process to. + :return: The server process. + ''' + server = tr.AddVerifierServerProcess('server', self.replay_file, verbose=False) + self._server = server + return server + + def _configure_traffic_server(self, tr: 'TestRun') -> 'Process': + '''Configure Traffic Server. + + :param tr: The TestRun to add the Traffic Server process to. + :return: The Traffic Server process. + ''' + ts = tr.MakeATSProcess('ts', enable_tls=True, enable_cache=False) + self._ts = ts + + ts.StartupTimeout = 60 + ts.addDefaultSSLFiles() + ts.Disk.ssl_multicert_yaml.AddLines( + ''' + ssl_multicert: + - dest_ip: "*" + ssl_cert_name: server.pem + ssl_key_name: server.key + '''.split('\n')) + ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'qmux|http3', + 'proxy.config.http.server_ports': (f'{ts.Variables.port} {ts.Variables.ssl_port}:ssl:proto=h3qx-01'), + 'proxy.config.ssl.server.cert.path': ts.Variables.SSLDir, + 'proxy.config.ssl.server.private_key.path': ts.Variables.SSLDir, + }) + ts.Disk.remap_config.AddLine(f'map / http://127.0.0.1:{self._server.Variables.http_port}') + ts.Disk.logging_yaml.AddLines( + ''' + logging: + formats: + - name: qmux_access + format: 'c_alpn=% client_version=% c_method=% c_url=%' + + logs: + - filename: qmux_access + format: qmux_access + '''.split('\n')) + + access_log = urtest.Disk.File(os.path.join(ts.Variables.LOGDIR, 'qmux_access.log'), exists=True) + access_log.Content = Testers.ContainsExpression( + r'c_alpn=h3qx-01 client_version=http/3 c_method=GET ' + r'c_url=https://qmux\.example\.com:[0-9]+/qmux-get-empty', + 'ATS should log the empty QMux request as HTTP/3 over the h3qx-01 ALPN.') + access_log.Content += Testers.ContainsExpression( + r'c_alpn=h3qx-01 client_version=http/3 c_method=POST ' + r'c_url=https://qmux\.example\.com:[0-9]+/qmux-post-large', + 'ATS should log the large QMux request as HTTP/3 over the h3qx-01 ALPN.') + return ts + + def _configure_client(self, tr: 'TestRun') -> 'Process': + '''Configure the Go QMux client. + + :param tr: The TestRun to add the client process to. + :return: The client process. + ''' + tr.Setup.Copy('go_qmux_client') + client = tr.Processes.Default + client.Env['GOFLAGS'] = '-mod=readonly -modcacherw' + client.Env['GOCACHE'] = os.path.join(tr.RunDirectory, 'gocache') + client.Env['GOMODCACHE'] = os.path.join(tr.RunDirectory, 'gomodcache') + client.Env['GOTOOLCHAIN'] = 'local' + client.Command = ( + f'cd "{os.path.join(tr.RunDirectory, "go_qmux_client")}" && ' + f'go run . --addr 127.0.0.1:{self._ts.Variables.ssl_port} ' + f'--authority qmux.example.com:{self._ts.Variables.ssl_port} ' + '--server-name qmux.example.com') + client.ReturnCode = 0 + client.Streams.stdout = Testers.ContainsExpression( + 'completed 3 QMux HTTP/3 requests: alpn=h3qx-01', + 'The Go client should complete all HTTP/3 requests over one QMux session.') + client.StartBefore(self._server) + client.StartBefore(self._ts) + return client + + TestQMuxGoClient() + urtest.execute() diff --git a/tests/gold_tests/records/gold/full_records.yaml b/tests/uranium_tests/records/gold/full_records.yaml similarity index 99% rename from tests/gold_tests/records/gold/full_records.yaml rename to tests/uranium_tests/records/gold/full_records.yaml index 1123e2cbb12..d1502ce4b8e 100644 --- a/tests/gold_tests/records/gold/full_records.yaml +++ b/tests/uranium_tests/records/gold/full_records.yaml @@ -163,6 +163,7 @@ records: max_open_read_retries: -1 max_open_write_retries: 1 max_stale_age: 604800 + max_stale_age_percent: 0 open_read_retry_time: 10 open_write_fail_action: 0 post_method: 0 diff --git a/tests/gold_tests/records/gold/no_newline.yaml b/tests/uranium_tests/records/gold/no_newline.yaml similarity index 100% rename from tests/gold_tests/records/gold/no_newline.yaml rename to tests/uranium_tests/records/gold/no_newline.yaml diff --git a/tests/gold_tests/records/gold/records.yaml.cold_test0.gold b/tests/uranium_tests/records/gold/records.yaml.cold_test0.gold similarity index 100% rename from tests/gold_tests/records/gold/records.yaml.cold_test0.gold rename to tests/uranium_tests/records/gold/records.yaml.cold_test0.gold diff --git a/tests/gold_tests/records/gold/records.yaml.cold_test2.gold b/tests/uranium_tests/records/gold/records.yaml.cold_test2.gold similarity index 100% rename from tests/gold_tests/records/gold/records.yaml.cold_test2.gold rename to tests/uranium_tests/records/gold/records.yaml.cold_test2.gold diff --git a/tests/gold_tests/records/gold/records.yaml.cold_test4.gold b/tests/uranium_tests/records/gold/records.yaml.cold_test4.gold similarity index 100% rename from tests/gold_tests/records/gold/records.yaml.cold_test4.gold rename to tests/uranium_tests/records/gold/records.yaml.cold_test4.gold diff --git a/tests/gold_tests/records/gold/records.yaml.cold_test5.gold b/tests/uranium_tests/records/gold/records.yaml.cold_test5.gold similarity index 100% rename from tests/gold_tests/records/gold/records.yaml.cold_test5.gold rename to tests/uranium_tests/records/gold/records.yaml.cold_test5.gold diff --git a/tests/gold_tests/records/gold/renamed_records.gold b/tests/uranium_tests/records/gold/renamed_records.gold similarity index 100% rename from tests/gold_tests/records/gold/renamed_records.gold rename to tests/uranium_tests/records/gold/renamed_records.gold diff --git a/tests/gold_tests/records/gold/renamed_records.yaml b/tests/uranium_tests/records/gold/renamed_records.yaml similarity index 100% rename from tests/gold_tests/records/gold/renamed_records.yaml rename to tests/uranium_tests/records/gold/renamed_records.yaml diff --git a/tests/gold_tests/records/legacy_config/full_records.config b/tests/uranium_tests/records/legacy_config/full_records.config similarity index 99% rename from tests/gold_tests/records/legacy_config/full_records.config rename to tests/uranium_tests/records/legacy_config/full_records.config index d7b640dae32..128ce0c8623 100644 --- a/tests/gold_tests/records/legacy_config/full_records.config +++ b/tests/uranium_tests/records/legacy_config/full_records.config @@ -183,6 +183,7 @@ CONFIG proxy.config.http.cache.open_write_fail_action INT 0 CONFIG proxy.config.http.cache.when_to_revalidate INT 0 CONFIG proxy.config.http.cache.required_headers INT 2 CONFIG proxy.config.http.cache.max_stale_age INT 604800 +CONFIG proxy.config.http.cache.max_stale_age_percent INT 0 CONFIG proxy.config.http.cache.range.lookup INT 1 CONFIG proxy.config.http.cache.range.write INT 0 CONFIG proxy.config.http.cache.heuristic_min_lifetime INT 3600 diff --git a/tests/gold_tests/records/legacy_config/no_newline.config b/tests/uranium_tests/records/legacy_config/no_newline.config similarity index 100% rename from tests/gold_tests/records/legacy_config/no_newline.config rename to tests/uranium_tests/records/legacy_config/no_newline.config diff --git a/tests/gold_tests/records/legacy_config/old_records.config b/tests/uranium_tests/records/legacy_config/old_records.config similarity index 100% rename from tests/gold_tests/records/legacy_config/old_records.config rename to tests/uranium_tests/records/legacy_config/old_records.config diff --git a/tests/gold_tests/records/legacy_config/override_map.config b/tests/uranium_tests/records/legacy_config/override_map.config similarity index 100% rename from tests/gold_tests/records/legacy_config/override_map.config rename to tests/uranium_tests/records/legacy_config/override_map.config diff --git a/tests/gold_tests/records/legacy_config/override_value.config b/tests/uranium_tests/records/legacy_config/override_value.config similarity index 100% rename from tests/gold_tests/records/legacy_config/override_value.config rename to tests/uranium_tests/records/legacy_config/override_value.config diff --git a/tests/uranium_tests/records/test_records_config_to_yaml.py b/tests/uranium_tests/records/test_records_config_to_yaml.py new file mode 100644 index 00000000000..937188bff1d --- /dev/null +++ b/tests/uranium_tests/records/test_records_config_to_yaml.py @@ -0,0 +1,88 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_records_config_to_yaml(urtest: UraniumTest) -> None: + ''' + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + import os + import yaml + + urtest.Summary = 'Testing ATS records to yaml script.' + + file_suffix = 1 + + # case 1 TLS with no data + tr = urtest.AddTestRun("Test records to yaml convert script - full file") + + tr.Setup.Copy(os.path.join(urtest.Variables.RepoDir, "tools/records/convert2yaml.py")) + tr.Setup.Copy('legacy_config/full_records.config') + tr.Processes.Default.Command = f'python3 convert2yaml.py -f full_records.config --output generated{file_suffix}.yaml --yaml --mute' + f = tr.Disk.File(f"generated{file_suffix}.yaml") + f.Content = "gold/full_records.yaml" + + file_suffix = file_suffix + 1 + + tr = urtest.AddTestRun("Test records to yaml convert script -only renamed records.") + tr.Setup.Copy(os.path.join(urtest.Variables.RepoDir, "tools/records/convert2yaml.py")) + tr.Setup.Copy('legacy_config/old_records.config') + tr.Processes.Default.Command = f'python3 convert2yaml.py -f old_records.config --output generated{file_suffix}.yaml --yaml' + tr.Processes.Default.Stream = 'gold/renamed_records.gold' + f = tr.Disk.File(f"generated{file_suffix}.yaml") + f.Content = "gold/renamed_records.yaml" + + tr = urtest.AddTestRun("Test errors when trying to override values ") + tr.Setup.Copy(os.path.join(urtest.Variables.RepoDir, "tools/records/convert2yaml.py")) + tr.Setup.Copy('legacy_config/override_value.config') + tr.Processes.Default.Command = f'python3 convert2yaml.py -f override_value.config --output generated{file_suffix}.yaml --yaml -m' + tr.Processes.Default.Streams.stdout += Testers.ContainsExpression( + "We cannot continue with 'proxy.config.ssl.client.verify.server.policy' at line '3' as a value node will be overridden", + "Error should be present") + + tr = urtest.AddTestRun("Test errors when trying to override maps") + tr.Setup.Copy(os.path.join(urtest.Variables.RepoDir, "tools/records/convert2yaml.py")) + tr.Setup.Copy('legacy_config/override_map.config') + tr.Processes.Default.Command = f'python3 convert2yaml.py -f override_map.config --output generated{file_suffix}.yaml --yaml -m' + tr.Processes.Default.Streams.stdout += Testers.ContainsExpression( + "We cannot continue with 'proxy.config.ssl.client.verify.server' at line '3' as an existing YAML map will be overridden.", + "Error should be present") + + file_suffix = file_suffix + 1 + + tr = urtest.AddTestRun("Test config file without trailing newline") + tr.Setup.Copy(os.path.join(urtest.Variables.RepoDir, "tools/records/convert2yaml.py")) + tr.Setup.Copy('legacy_config/no_newline.config') + tr.Processes.Default.Command = f'python3 convert2yaml.py -f no_newline.config --output generated{file_suffix}.yaml --yaml --mute' + f = tr.Disk.File(f"generated{file_suffix}.yaml") + f.Content = "gold/no_newline.yaml" + urtest.execute() diff --git a/tests/uranium_tests/records/test_records_runroot_precedence.py b/tests/uranium_tests/records/test_records_runroot_precedence.py new file mode 100644 index 00000000000..0ef7c22424d --- /dev/null +++ b/tests/uranium_tests/records/test_records_runroot_precedence.py @@ -0,0 +1,185 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_records_runroot_precedence(urtest: UraniumTest) -> None: + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import os + + urtest.Summary = ''' + Verify that when runroot is active, path records from records.yaml are + overridden by the resolved Layout paths (precedence: env var > runroot > records.yaml). + + When --run-root (or TS_RUNROOT) is set and the PROXY_CONFIG_* environment + variables for path records are unset, RecConfigOverrideFromEnvironment() + returns the actual Layout path (e.g. Layout::bindir, Layout::logdir) which + was populated from runroot.yaml — effectively making runroot.yaml override + records.yaml for these path records. + ''' + urtest.ContinueOnFail = True + urtest.SkipUnless( + urtest.Variables.BINDIR.startswith(urtest.Variables.PREFIX), "need to guarantee bin path starts with prefix for runroot") + + ts = urtest.MakeATSProcess("ts") + ts_dir = os.path.join(urtest.RunDirectory, "ts") + + # Set deliberately WRONG values in records.yaml for all 4 runroot-managed + # path records. If runroot override works, these values must NOT be used. + ts.Disk.records_config.append_to_document( + ''' + bin_path: wrong_bin_path + local_state_dir: wrong_runtime + log: + logfile_dir: wrong_log + plugin: + plugin_dir: wrong_plugin + ''') + + # Test 3 setup: env var that must win over both runroot and records.yaml. + ts.Env['PROXY_CONFIG_DIAGS_DEBUG_TAGS'] = 'env_wins' + ts.Disk.records_config.update( + ''' + diags: + debug: + enabled: 0 + tags: config_value + ''') + + # Build the ATS command: + # - Unset the 4 path env vars (the test framework always sets them, + # which masks the runroot code path). + # - Set TS_RUNROOT to the sandbox dir so the runroot mechanism activates. + original_cmd = ts.Command + ts.Command = ( + "env" + " -u PROXY_CONFIG_BIN_PATH" + " -u PROXY_CONFIG_LOCAL_STATE_DIR" + " -u PROXY_CONFIG_LOG_LOGFILE_DIR" + " -u PROXY_CONFIG_PLUGIN_PLUGIN_DIR" + f" TS_RUNROOT={ts_dir}" + f" {original_cmd}") + + # --------------------------------------------------------------------------- + # Test 0: Create runroot.yaml that maps to the sandbox layout, then start ATS. + # + # The runroot.yaml must exist before ATS starts because TS_RUNROOT triggers + # Layout::runroot_setup() during initialization. We write a runroot.yaml + # whose paths match the sandbox structure the test framework already created + # (traffic_layout init would create a different FHS-style layout that does + # not match the sandbox, so we write it manually). + # --------------------------------------------------------------------------- + runroot_yaml = os.path.join(ts_dir, 'runroot.yaml') + + runroot_lines = [ + f"prefix: {ts_dir}", + f"bindir: {os.path.join(ts_dir, 'bin')}", + f"sbindir: {os.path.join(ts_dir, 'bin')}", + f"sysconfdir: {os.path.join(ts_dir, 'config')}", + f"logdir: {os.path.join(ts_dir, 'log')}", + f"libexecdir: {os.path.join(ts_dir, 'plugin')}", + f"localstatedir: {os.path.join(ts_dir, 'runtime')}", + f"runtimedir: {os.path.join(ts_dir, 'runtime')}", + f"cachedir: {os.path.join(ts_dir, 'cache')}", + ] + runroot_content = "\\n".join(runroot_lines) + "\\n" + + tr = urtest.AddTestRun("Create runroot.yaml") + tr.Processes.Default.Command = f"mkdir -p {ts_dir} && printf '{runroot_content}' > {runroot_yaml}" + tr.Processes.Default.ReturnCode = 0 + + # --------------------------------------------------------------------------- + # Test 1: Start ATS with runroot active + # --------------------------------------------------------------------------- + tr = urtest.AddTestRun("Start ATS with runroot") + tr.Processes.Default.Command = 'echo start' + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.StartBefore(ts) + tr.StillRunningAfter = ts + + # ATS must not crash (the original nullptr bug) and must complete startup. + ts.Disk.traffic_out.Content = Testers.ExcludesExpression( + "basic_string", "must not crash with 'basic_string: construction from null'") + ts.Disk.traffic_out.Content += Testers.ContainsExpression("records parsing completed", "ATS should complete records parsing") + + # Verify the override log messages appear in traffic.out. + # The errata notes from RecYAMLDecoder are printed by RecCoreInit(). + ts.Disk.traffic_out.Content += Testers.ContainsExpression( + "'proxy.config.bin_path' overridden with .* by runroot", "bin_path override by runroot must be logged") + ts.Disk.traffic_out.Content += Testers.ContainsExpression( + "'proxy.config.local_state_dir' overridden with .* by runroot", "local_state_dir override by runroot must be logged") + ts.Disk.traffic_out.Content += Testers.ContainsExpression( + "'proxy.config.log.logfile_dir' overridden with .* by runroot", "logfile_dir override by runroot must be logged") + ts.Disk.traffic_out.Content += Testers.ContainsExpression( + "'proxy.config.plugin.plugin_dir' overridden with .* by runroot", "plugin_dir override by runroot must be logged") + ts.Disk.traffic_out.Content += Testers.ContainsExpression( + "'proxy.config.diags.debug.tags' overridden with 'env_wins' by environment variable", + "diags.debug.tags override by environment variable must be logged") + + # --------------------------------------------------------------------------- + # Test 2: Verify path records do NOT contain the records.yaml values. + # + # Because runroot is active and env vars are unset, the records should hold + # the resolved Layout paths from runroot.yaml, not the records.yaml values. + # --------------------------------------------------------------------------- + tr = urtest.AddTestRun("Verify runroot overrides records.yaml for path records") + tr.Processes.Default.Command = ( + 'traffic_ctl config get' + ' proxy.config.bin_path' + ' proxy.config.local_state_dir' + ' proxy.config.log.logfile_dir' + ' proxy.config.plugin.plugin_dir') + tr.Processes.Default.Env = ts.Env + tr.Processes.Default.ReturnCode = 0 + tr.StillRunningAfter = ts + + # The deliberately wrong records.yaml values must NOT appear. + tr.Processes.Default.Streams.stdout = Testers.ExcludesExpression( + 'wrong_bin_path', 'bin_path must be overridden by runroot, not records.yaml') + tr.Processes.Default.Streams.stdout += Testers.ExcludesExpression( + 'wrong_runtime', 'local_state_dir must be overridden by runroot, not records.yaml') + tr.Processes.Default.Streams.stdout += Testers.ExcludesExpression( + 'wrong_log', 'logfile_dir must be overridden by runroot, not records.yaml') + tr.Processes.Default.Streams.stdout += Testers.ExcludesExpression( + 'wrong_plugin', 'plugin_dir must be overridden by runroot, not records.yaml') + + # --------------------------------------------------------------------------- + # Test 3: Verify env vars still take highest precedence over runroot. + # --------------------------------------------------------------------------- + tr = urtest.AddTestRun("Verify env var overrides both runroot and records.yaml") + tr.Processes.Default.Command = 'traffic_ctl config get proxy.config.diags.debug.tags' + tr.Processes.Default.Env = ts.Env + tr.Processes.Default.ReturnCode = 0 + tr.StillRunningAfter = ts + tr.Processes.Default.Streams.stdout = Testers.ContainsExpression( + 'proxy.config.diags.debug.tags: env_wins', 'Env var must override both runroot and records.yaml') + urtest.execute() diff --git a/tests/uranium_tests/records/test_records_yaml.py b/tests/uranium_tests/records/test_records_yaml.py new file mode 100644 index 00000000000..30c651b270c --- /dev/null +++ b/tests/uranium_tests/records/test_records_yaml.py @@ -0,0 +1,225 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_records_yaml(urtest: UraniumTest) -> None: + ''' + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + from jsonrpc import Notification, Request, Response + + urtest.Summary = 'Basic records test. Testing the new records.yaml logic and making sure it works as expected.' + + ts = urtest.MakeATSProcess("ts") + + ts.Disk.records_config.update( + ''' + diags: + debug: + enabled: 1 + tags: rpc|rec + ssl: + client: + cert: + filename: null + filenamee: some.txt + filenam: some2.txt + dns: + nameservers: null + test: + not_registered: + + # we expect this to be parsed and added to the records but inform that + # the record field is not registered. + field1: !!int 1 + + # we expect this to fail and report while parsing as the field is not + # registered nor have a valid type + # field2: 0 # we cant test this now as traffic_layout will output the error and setup.cli.ext will fail + # to encode json + field2: 0 + + cache: + # test multipliers with and without tag + ram_cache: + size: 30G + ''') + + # 0 - We want to make sure that the unregistered records are still being detected. + tr = urtest.AddTestRun("Load unregistered records") + tr.Processes.Default.Command = 'echo 1' + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.StartBefore(ts) + + var1 = 'proxy.config.test.not_registered.field1' + ts.Disk.diags_log.Content = Testers.ContainsExpression(f"Unrecognized configuration value '{var1}'", "Field should be ignored") + var2 = 'proxy.config.test.not_registered.field2' + ts.Disk.diags_log.Content += Testers.ContainsExpression(f"Unrecognized configuration value '{var2}", "Field should be ignored") + + ts.Disk.traffic_out.Content += Testers.ContainsExpression( + f"Ignoring field 'filenamee' \\[proxy.config.ssl.client.cert.filenamee\\]", "Field should be ignored") + ts.Disk.traffic_out.Content += Testers.ContainsExpression( + f"Ignoring field 'filenam' \\[proxy.config.ssl.client.cert.filenam\\]", "Field should be ignored") + + # 1 + tr = urtest.AddTestRun("Query unregistered records.") + + def check_response(resp: Response): + if resp.is_error(): + return (False, resp.error_as_str()) + + lst = resp.result['recordList'] + + if len(lst) != 2: + return (False, "Should be 2 records only.") + + for el in lst: + rec = el['record'] + if rec['registered'] != "false": + return (False, f"{rec['record_name']} should not be registered") + + return (True, "All good") + + tr.AddJsonRPCClientRequest( + ts, + Request.admin_lookup_records([{ + "record_name_regex": "proxy.config.test.not_registered.field", + "rec_types": ["1", "16"] + }])) + + # do our own check on the response. + tr.Processes.Default.Streams.stdout = Testers.CustomJSONRPCResponse(check_response) + + # 2 + tctl = urtest.AddTestRun("Test field with multiplier") + tctl.Processes.Default.Command = 'traffic_ctl config get proxy.config.cache.ram_cache.size' + tctl.Processes.Default.Env = ts.Env + tctl.Processes.Default.ReturnCode = 0 + tctl.Processes.Default.Streams.stdout = Testers.ContainsExpression( + 'proxy.config.cache.ram_cache.size: 32212254720', 'Should hold the configured value.') + + # The whole idea is to test how ATS handles having multiple docs in the same records.yaml + # file. + # Every subsequent document will overwrite the previous one, this tests are meant to + # exercise that logic. + + ts2 = urtest.MakeATSProcess("ts2") + + # We are adding a new doc at the end of the original one created by the unit test. + ts2.Disk.records_config.append_to_document( + ''' + diags: + debug: + enabled: 0 + tags: rpc|rec + ''') + + # This will append a new doc in the same file, this config value should overwrite + # the previous one. + ts2.Disk.records_config.append_to_document(''' + diags: + debug: + tags: filemanager + ''') + + # After all the loading completed we want to have enabled=0 and tags=filemanager + + tr = urtest.AddTestRun("Start a new ATS instance.") + tr.Processes.Default.Command = 'echo 1' + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.StartBefore(ts2) + tr.Processes.StillRunningAfter = ts2 + + tr2 = urtest.AddTestRun("Test multiple docs from the same file") + tr2.Processes.Default.Command = 'traffic_ctl config get proxy.config.diags.debug.enabled proxy.config.diags.debug.tags' + tr2.Processes.Default.Env = ts2.Env + tr2.Processes.Default.ReturnCode = 0 + + # Make sure it's what we want. + tr2.Processes.Default.Streams.stdout += Testers.ContainsExpression( + 'proxy.config.diags.debug.enabled: 0', 'Config should show debug disabled') + tr2.Processes.Default.Streams.stdout += Testers.ContainsExpression( + 'proxy.config.diags.debug.tags: filemanager', 'Config should show a different tag') + + ts2.Disk.records_config.append_to_document( + ''' + dns: + resolv_conf: NULL + nameservers: null + local_ipv6: Null + ssl: + client: + cert: + filename: ~ + ''') + + tr3 = urtest.AddTestRun("test null string") + tr3.Processes.Default.Command = 'traffic_ctl config get proxy.config.dns.resolv_conf proxy.config.dns.local_ipv6 proxy.config.dns.nameservers proxy.config.ssl.client.cert.filename' + tr3.Processes.Default.Env = ts2.Env + tr3.Processes.Default.ReturnCode = 0 + + # Make sure it's what we want. + tr3.Processes.Default.Streams.stdout += Testers.ContainsExpression( + 'proxy.config.dns.resolv_conf: null', 'should be set to null') + tr3.Processes.Default.Streams.stdout += Testers.ContainsExpression( + 'proxy.config.dns.nameservers: null', 'should be set to null') + tr3.Processes.Default.Streams.stdout += Testers.ContainsExpression( + 'proxy.config.ssl.client.cert.filename: null', 'should be set to null') + tr3.Processes.Default.Streams.stdout += Testers.ContainsExpression('proxy.config.dns.local_ipv6: null', 'should be set to null') + + ts3 = urtest.MakeATSProcess("ts3") + ts3.Disk.records_config.update( + ''' + some_invalid_field_should_not_block_further_docs_from_the_parser_logic: OK + ''') + + ts3.Disk.records_config.append_to_document(''' + diags: + debug: + tags: rpc|rec + ''') + + # We want to make sure that any error in any of the docs will not stop ATS from loading + # the rest of the nodes. + tr4 = urtest.AddTestRun("test parsing multiple docs with invalid fields.") + tr4.Processes.Default.Command = 'traffic_ctl config get proxy.config.diags.debug.tags' + tr4.Processes.Default.Env = ts3.Env + tr.Processes.Default.StartBefore(ts3) + tr.Processes.StillRunningAfter = ts3 + tr4.Processes.Default.ReturnCode = 0 + + # This record should be parsed after the invalid field. Value should be the one + # configured. + tr4.Processes.Default.Streams.stdout += Testers.ContainsExpression( + 'proxy.config.diags.debug.tags: rpc|rec', 'should be the configured value') + urtest.execute() diff --git a/tests/uranium_tests/records/test_traffic_ctl_cold_config.py b/tests/uranium_tests/records/test_traffic_ctl_cold_config.py new file mode 100644 index 00000000000..3dec7bec5e1 --- /dev/null +++ b/tests/uranium_tests/records/test_traffic_ctl_cold_config.py @@ -0,0 +1,115 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_traffic_ctl_cold_config(urtest: UraniumTest) -> None: + ''' + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import os + from jsonrpc import Notification, Request, Response + + urtest.Summary = 'Basic records test. Testing the new records.yaml logic and making sure it works as expected.' + + ts = urtest.MakeATSProcess("ts") + + ts.Disk.records_config.update( + ''' + accept_threads: 1 + cache: + limits: + http: + max_alts: 5 + diags: + debug: + enabled: 0 + tags: http|dns + ''') + + # 0 - We want to make sure that the unregistered records are still being detected. + tr = urtest.AddTestRun("Test Append value to existing records.yaml") + + tr.Processes.Default.Command = 'traffic_ctl config set proxy.config.diags.debug.tags rpc --cold' + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Env = ts.Env + tr.Processes.Default.StartBefore(ts) + ts.Disk.records_config.Content = 'gold/records.yaml.cold_test0.gold' + + # 1 + tr = urtest.AddTestRun("Get value from latest added node.") + tr.Processes.Default.Command = 'traffic_ctl config get proxy.config.diags.debug.tags --cold' + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Env = ts.Env + + tr.Processes.Default.Streams.stdout += Testers.ContainsExpression( + 'proxy.config.diags.debug.tags: rpc', 'Config should show the right tags') + + # 2 + tr = urtest.AddTestRun("Test modify latest yaml document from records.yaml") + tr.Processes.Default.Command = 'traffic_ctl config set proxy.config.diags.debug.tags http -u -c' + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Env = ts.Env + ts.Disk.records_config.Content = 'gold/records.yaml.cold_test2.gold' + + # 3 + tr = urtest.AddTestRun("Get value from latest added node 1.") + tr.Processes.Default.Command = 'traffic_ctl config get proxy.config.diags.debug.tags --cold' + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Env = ts.Env + + tr.Processes.Default.Streams.stdout += Testers.ContainsExpression( + 'proxy.config.diags.debug.tags: http', 'Config should show the right tags') + + # 4 + tr = urtest.AddTestRun("Append a new field node using a tag") + tr.Processes.Default.Command = 'traffic_ctl config set proxy.config.cache.limits.http.max_alts 1 -t int -c' + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Env = ts.Env + ts.Disk.records_config.Content = 'gold/records.yaml.cold_test4.gold' + + # 5 + file = os.path.join(ts.Variables.CONFIGDIR, "new_records.yaml") + tr = urtest.AddTestRun("Adding a new node(with update flag set) to a non existing file") + tr.Processes.Default.Command = f'traffic_ctl config set proxy.config.cache.limits.http.max_alts 3 -u -c {file}' + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Env = ts.Env + tr.Disk.File(file).Content = 'gold/records.yaml.cold_test5.gold' + + # 5 + file = os.path.join(ts.Variables.CONFIGDIR, "new_records2.yaml") + tr = urtest.AddTestRun("Adding a new node to a non existing file") + tr.Processes.Default.Command = f'traffic_ctl config set proxy.config.cache.limits.http.max_alts 3 -c {file}' + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Env = ts.Env + tr.Disk.File(file).Content = 'gold/records.yaml.cold_test5.gold' + urtest.execute() diff --git a/tests/uranium_tests/records/test_ts_max_records_param.py b/tests/uranium_tests/records/test_ts_max_records_param.py new file mode 100644 index 00000000000..a6171e614b9 --- /dev/null +++ b/tests/uranium_tests/records/test_ts_max_records_param.py @@ -0,0 +1,77 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_ts_max_records_param(urtest: UraniumTest) -> None: + ''' + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + from jsonrpc import Notification, Request, Response + + urtest.Summary = 'Basic test for traffic_server --maxRecords behavior when setting different values.' + + maxRecords = 1000 + + # 0 - maxRecords below the default value(2048). Traffic server should warn about this and use the default value. + ts = urtest.MakeATSProcess(f"ts{maxRecords}", command=f"traffic_server --maxRecords {maxRecords}") + tr = urtest.AddTestRun(f"--maxRecords {maxRecords}") + tr.Processes.Default.Command = 'echo 1' + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.StartBefore(ts) + tr.StillRunningAfter = ts + ts.Streams.All = Testers.ContainsExpression( + f"Passed maxRecords value={maxRecords} is lower than the default value 2048. Default will be used.", + "It should use the default value") + + # 1 - maxRecords with just invalid number. Traffic server should warn about this and use the default value. + maxRecords = "abc" + ts = urtest.MakeATSProcess(f"ts{maxRecords}", command=f"traffic_server --maxRecords {maxRecords}") + tr = urtest.AddTestRun(f"--maxRecords {maxRecords}") + tr.Processes.Default.Command = 'echo 1' + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.StartBefore(ts) + tr.StillRunningAfter = ts + ts.Streams.All = Testers.ContainsExpression( + f"Invalid 0 value for maxRecords. Default 2048 will be used.", "It should use the default value") + + # 2 - maxRecords over the default value + maxRecords = 5000 + ts = urtest.MakeATSProcess(f"ts{maxRecords}", command=f"traffic_server --maxRecords {maxRecords}") + tr = urtest.AddTestRun(f"--maxRecords {maxRecords}") + tr.Processes.Default.Command = 'echo 1' + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.StartBefore(ts) + tr.StillRunningAfter = ts + # At least it should not crash. + ts.Disk.traffic_out.Content = Testers.ContainsExpression(f"NOTE: records parsing completed", "should all be good") + urtest.execute() diff --git a/tests/gold_tests/redirect/.gitignore b/tests/uranium_tests/redirect/.gitignore similarity index 100% rename from tests/gold_tests/redirect/.gitignore rename to tests/uranium_tests/redirect/.gitignore diff --git a/tests/gold_tests/redirect/gold/number_of_redirections_0.gold b/tests/uranium_tests/redirect/gold/number_of_redirections_0.gold similarity index 100% rename from tests/gold_tests/redirect/gold/number_of_redirections_0.gold rename to tests/uranium_tests/redirect/gold/number_of_redirections_0.gold diff --git a/tests/gold_tests/redirect/gold/number_of_redirections_1.gold b/tests/uranium_tests/redirect/gold/number_of_redirections_1.gold similarity index 100% rename from tests/gold_tests/redirect/gold/number_of_redirections_1.gold rename to tests/uranium_tests/redirect/gold/number_of_redirections_1.gold diff --git a/tests/gold_tests/redirect/gold/number_of_redirections_2.gold b/tests/uranium_tests/redirect/gold/number_of_redirections_2.gold similarity index 100% rename from tests/gold_tests/redirect/gold/number_of_redirections_2.gold rename to tests/uranium_tests/redirect/gold/number_of_redirections_2.gold diff --git a/tests/gold_tests/redirect/gold/redirect.gold b/tests/uranium_tests/redirect/gold/redirect.gold similarity index 100% rename from tests/gold_tests/redirect/gold/redirect.gold rename to tests/uranium_tests/redirect/gold/redirect.gold diff --git a/tests/gold_tests/redirect/gold/redirect_log.gold b/tests/uranium_tests/redirect/gold/redirect_log.gold similarity index 100% rename from tests/gold_tests/redirect/gold/redirect_log.gold rename to tests/uranium_tests/redirect/gold/redirect_log.gold diff --git a/tests/gold_tests/redirect/gold/redirect_post.gold b/tests/uranium_tests/redirect/gold/redirect_post.gold similarity index 100% rename from tests/gold_tests/redirect/gold/redirect_post.gold rename to tests/uranium_tests/redirect/gold/redirect_post.gold diff --git a/tests/gold_tests/redirect/gold/redirect_stale.gold b/tests/uranium_tests/redirect/gold/redirect_stale.gold similarity index 100% rename from tests/gold_tests/redirect/gold/redirect_stale.gold rename to tests/uranium_tests/redirect/gold/redirect_stale.gold diff --git a/tests/gold_tests/redirect/replay/redirect_srv1_replay.yaml b/tests/uranium_tests/redirect/replay/redirect_srv1_replay.yaml similarity index 100% rename from tests/gold_tests/redirect/replay/redirect_srv1_replay.yaml rename to tests/uranium_tests/redirect/replay/redirect_srv1_replay.yaml diff --git a/tests/gold_tests/redirect/replay/redirect_srv2_replay.yaml b/tests/uranium_tests/redirect/replay/redirect_srv2_replay.yaml similarity index 100% rename from tests/gold_tests/redirect/replay/redirect_srv2_replay.yaml rename to tests/uranium_tests/redirect/replay/redirect_srv2_replay.yaml diff --git a/tests/gold_tests/redirect/replay/redirect_srv3_replay.yaml b/tests/uranium_tests/redirect/replay/redirect_srv3_replay.yaml similarity index 100% rename from tests/gold_tests/redirect/replay/redirect_srv3_replay.yaml rename to tests/uranium_tests/redirect/replay/redirect_srv3_replay.yaml diff --git a/tests/gold_tests/redirect/replay/redirect_to_same_origin_on_cache.replay.yaml b/tests/uranium_tests/redirect/replay/redirect_to_same_origin_on_cache.replay.yaml similarity index 100% rename from tests/gold_tests/redirect/replay/redirect_to_same_origin_on_cache.replay.yaml rename to tests/uranium_tests/redirect/replay/redirect_to_same_origin_on_cache.replay.yaml diff --git a/tests/uranium_tests/redirect/test_number_of_redirects.py b/tests/uranium_tests/redirect/test_number_of_redirects.py new file mode 100644 index 00000000000..29b28f93038 --- /dev/null +++ b/tests/uranium_tests/redirect/test_number_of_redirects.py @@ -0,0 +1,119 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_number_of_redirects(urtest: UraniumTest) -> None: + ''' + Specific test for number_of_redirections config. + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import os + + urtest.Summary = ''' + Test redirection/location & number of redirects(number_of_redirections config) + ''' + + class NumberOfRedirectionsTest: + ''' + Handy class to test with number_of_redirections values. Three servers will be created and request + will flow between them. Depending on the configured number_of_redirections, some request may be + followed by ATS and some directly by the client(curl) + ''' + + def __init__(self, testName, numberOfRedirections): + self._numberOfRedirections = numberOfRedirections + self._tr = urtest.AddTestRun(testName) + self.setup_ts(f'ts{self._numberOfRedirections}') + self.setup_dns() + self.setup_verifier_servers() + self.add_config() + + def setup_ts(self, name="ts"): + self._ts = urtest.MakeATSProcess(name, enable_cache=False) + + def setup_verifier_servers(self): + self._srv3 = urtest.MakeVerifierServerProcess(f"srv3_{self._numberOfRedirections}", "replay/redirect_srv3_replay.yaml") + self._srv2 = urtest.MakeVerifierServerProcess( + f"srv2_{self._numberOfRedirections}", + "replay/redirect_srv2_replay.yaml", + context={"vs_http_port": self._srv3.Variables.http_port}) + self._srv1 = urtest.MakeVerifierServerProcess( + f"srv1_{self._numberOfRedirections}", + "replay/redirect_srv1_replay.yaml", + context={"vs_http_port": self._srv2.Variables.http_port}) + + def setup_dns(self): + self._dns = urtest.MakeDNServer(f"dns_{self._numberOfRedirections}") + self._dns.addRecords(records={"a.test": ["127.0.0.1"]}) + self._dns.addRecords(records={"b.test": ["127.0.0.1"]}) + self._dns.addRecords(records={"c.test": ["127.0.0.1"]}) + + def add_config(self): + self._ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'http|dns|redirect|http_redirect', + 'proxy.config.http.number_of_redirections': self._numberOfRedirections, + 'proxy.config.dns.nameservers': f'127.0.0.1:{self._dns.Variables.Port}', + 'proxy.config.dns.resolv_conf': 'NULL', + 'proxy.config.url_remap.remap_required': 0, # need this so the domain gets a chance to be evaluated through DNS + 'proxy.config.http.redirect.actions': 'self:follow', # redirects to self are not followed by default + }) + self._ts.Disk.remap_config.AddLines( + [ + 'map a.test/ping http://a.test:{0}/'.format(self._srv1.Variables.http_port), + 'map b.test/pong http://b.test:{0}/'.format(self._srv2.Variables.http_port), + 'map c.test/pang http://c.test:{0}/'.format(self._srv3.Variables.http_port), + ]) + + def run(self): + self._tr.Processes.Default.StartBefore(self._srv1) + self._tr.Processes.Default.StartBefore(self._srv2) + self._tr.Processes.Default.StartBefore(self._srv3) + self._tr.Processes.Default.StartBefore(self._dns) + self._tr.Processes.Default.StartBefore(self._ts) + self._tr.MakeCurlCommand( + "-L -v a.test/ping --proxy 127.0.0.1:{0} -H 'uuid: redirect_test_1'".format(self._ts.Variables.port), ts=self._ts) + self._tr.Processes.Default.Streams.All = f"gold/number_of_redirections_{self._numberOfRedirections}.gold" + self._tr.ReturnCode = 0 + self._tr.StillRunningAfter = self._ts + + # No redirect, curl will get 302 and do the rest of the redirection. + NumberOfRedirectionsTest("Test number_of_redirections=0", 0).run() + # Single redirect, ATS will follow 1 redirect and the client the last one. + NumberOfRedirectionsTest("Test number_of_redirections=1", 1).run() + # The client will just get 200OK and no redirect will be done by it, TS will follow the two + # 302 Redirect. + NumberOfRedirectionsTest("Test number_of_redirections=2", 2).run() + # If adding more, need to touch the server side as well. It won't be enough. + urtest.execute() diff --git a/tests/uranium_tests/redirect/test_redirect.py b/tests/uranium_tests/redirect/test_redirect.py new file mode 100644 index 00000000000..48df84e0c5d --- /dev/null +++ b/tests/uranium_tests/redirect/test_redirect.py @@ -0,0 +1,205 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_redirect(urtest: UraniumTest) -> None: + ''' + Test redirection + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import os + import sys + + urtest.Summary = ''' + Test redirection + ''' + + urtest.ContinueOnFail = True + + ts = urtest.MakeATSProcess("ts", enable_cache=False) + redirect_serv = urtest.MakeOriginServer("re_server") + dest_serv = urtest.MakeOriginServer("dest_server") + dns = urtest.MakeDNServer("dns") + + ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'http|dns|redirect', + 'proxy.config.http.number_of_redirections': 1, + 'proxy.config.dns.nameservers': '127.0.0.1:{0}'.format(dns.Variables.Port), + 'proxy.config.dns.resolv_conf': 'NULL', + 'proxy.config.url_remap.remap_required': 0, # need this so the domain gets a chance to be evaluated through DNS + 'proxy.config.http.redirect.actions': 'self:follow', # redirects to self are not followed by default + }) + + ts.Disk.logging_yaml.AddLines( + ''' + logging: + formats: + - name: custom + format: "client_url=% cache_result: code=% subcode=%" + logs: + - filename: the_log + format: custom + '''.split("\n")) + + urtest.Setup.Copy(os.path.join(urtest.Variables.AtsTestToolsDir, 'tcp_client.py')) + + redirect_request_header = {"headers": "GET /redirect HTTP/1.1\r\nHost: *\r\n\r\n", "timestamp": "5678", "body": ""} + redirect_response_header = { + "headers": "HTTP/1.1 302 Found\r\nLocation: http://127.0.0.1:{0}/redirectDest\r\n\r\n".format(dest_serv.Variables.Port), + "timestamp": "5678", + "body": "" + } + redirect_serv.addResponse("sessionfile.log", redirect_request_header, redirect_response_header) + + dest_request_header = {"headers": "GET /redirectDest HTTP/1.1\r\nHost: *\r\n\r\n", "timestamp": "11", "body": ""} + dest_response_header = {"headers": "HTTP/1.1 204 No Content\r\nConnection: close\r\n\r\n", "timestamp": "22", "body": ""} + dest_serv.addResponse("sessionfile.log", dest_request_header, dest_response_header) + + dns.addRecords(records={"iwillredirect.test": ["127.0.0.1"]}) + + data_dirname = 'redirect_generated_test_data' + data_path = os.path.join(urtest.TestDirectory, data_dirname) + os.makedirs(data_path, exist_ok=True) + + # Here and below: spaces are deliberately omitted from the test run names because autest creates directories using these names. + tr = urtest.AddTestRun("FollowsRedirectWithAbsoluteLocationURI") + # Here and below: because autest's Copy does not behave like standard cp, it's easiest to write all of our files out and copy last. + command_path = os.path.join(data_path, tr.Name) + with open(command_path, 'w') as f: + f.write('GET /redirect HTTP/1.1\r\nHost: iwillredirect.test:{port}\r\n\r\n'.format(port=redirect_serv.Variables.Port)) + tr.Processes.Default.Command = f"{sys.executable} tcp_client.py 127.0.0.1 {ts.Variables.port} {command_path} | egrep -v '^(Date: |Server: ATS/)'" + tr.Processes.Default.StartBefore(ts) + tr.Processes.Default.StartBefore(redirect_serv) + tr.Processes.Default.StartBefore(dest_serv) + tr.Processes.Default.StartBefore(dns) + tr.Processes.Default.Streams.stdout = "gold/redirect.gold" + tr.Processes.Default.ReturnCode = 0 + + redirect_request_header = { + "headers": "GET /redirect-relative-path HTTP/1.1\r\nHost: *\r\n\r\n", + "timestamp": "5678", + "body": "" + } + redirect_response_header = {"headers": "HTTP/1.1 302 Found\r\nLocation: /redirect\r\n\r\n", "timestamp": "5678", "body": ""} + redirect_serv.addResponse("sessionfile.log", redirect_request_header, redirect_response_header) + + redirect_request_header = {"headers": "GET /redirect HTTP/1.1\r\nHost: *\r\n\r\n", "timestamp": "5678", "body": ""} + redirect_response_header = {"headers": "HTTP/1.1 204 No Content\r\nConnection: close\r\n\r\n", "timestamp": "22", "body": ""} + redirect_serv.addResponse("sessionfile.log", redirect_request_header, redirect_response_header) + + tr = urtest.AddTestRun("FollowsRedirectWithRelativeLocationURI") + command_path = os.path.join(data_path, tr.Name) + with open(command_path, 'w') as f: + f.write( + 'GET /redirect-relative-path HTTP/1.1\r\nHost: iwillredirect.test:{port}\r\n\r\n'.format( + port=redirect_serv.Variables.Port)) + tr.Processes.Default.Command = f"{sys.executable} tcp_client.py 127.0.0.1 {ts.Variables.port} {command_path} | egrep -v '^(Date: |Server: ATS/)'" + tr.StillRunningAfter = ts + tr.StillRunningAfter = redirect_serv + tr.StillRunningAfter = dest_serv + tr.StillRunningAfter = dns + tr.Processes.Default.Streams.stdout = "gold/redirect.gold" + tr.Processes.Default.ReturnCode = 0 + + redirect_request_header = { + "headers": "GET /redirect-relative-path-no-leading-slash HTTP/1.1\r\nHost: *\r\n\r\n", + "timestamp": "5678", + "body": "" + } + redirect_response_header = {"headers": "HTTP/1.1 302 Found\r\nLocation: redirect\r\n\r\n", "timestamp": "5678", "body": ""} + redirect_serv.addResponse("sessionfile.log", redirect_request_header, redirect_response_header) + + tr = urtest.AddTestRun("FollowsRedirectWithRelativeLocationURIMissingLeadingSlash") + command_path = os.path.join(data_path, tr.Name) + with open(command_path, 'w') as f: + f.write( + 'GET /redirect-relative-path-no-leading-slash HTTP/1.1\r\nHost: iwillredirect.test:{port}\r\n\r\n'.format( + port=redirect_serv.Variables.Port)) + tr.Processes.Default.Command = f"{sys.executable} tcp_client.py 127.0.0.1 {ts.Variables.port} {command_path} | egrep -v '^(Date: |Server: ATS/)'" + tr.StillRunningAfter = ts + tr.StillRunningAfter = redirect_serv + tr.StillRunningAfter = dest_serv + tr.StillRunningAfter = dns + tr.Processes.Default.Streams.stdout = "gold/redirect.gold" + tr.Processes.Default.ReturnCode = 0 + + for status, phrase in sorted({ + 301: 'Moved Permanently', + 302: 'Found', + 303: 'See Other', + 305: 'Use Proxy', + 307: 'Temporary Redirect', + 308: 'Permanent Redirect', + }.items()): + + redirect_request_header = { + "headers": ("GET /redirect{0} HTTP/1.1\r\n" + "Host: *\r\n\r\n").format(status), + "timestamp": "5678", + "body": "" + } + redirect_response_header = { + "headers": ("HTTP/1.1 {0} {1}\r\n" + "Connection: close\r\n" + "Location: /redirect\r\n\r\n").format(status, phrase), + "timestamp": "5678", + "body": "" + } + redirect_serv.addResponse("sessionfile.log", redirect_request_header, redirect_response_header) + + tr = urtest.AddTestRun("FollowsRedirect{0}".format(status)) + command_path = os.path.join(data_path, tr.Name) + with open(command_path, 'w') as f: + f.write( + ('GET /redirect{0} HTTP/1.1\r\n' + 'Host: iwillredirect.test:{1}\r\n\r\n').format(status, redirect_serv.Variables.Port)) + tr.Processes.Default.Command = f"{sys.executable} tcp_client.py 127.0.0.1 {ts.Variables.port} {command_path} | egrep -v '^(Date: |Server: ATS/)'" + tr.StillRunningAfter = ts + tr.StillRunningAfter = redirect_serv + tr.StillRunningAfter = dest_serv + tr.StillRunningAfter = dns + tr.Processes.Default.Streams.stdout = "gold/redirect.gold" + tr.Processes.Default.ReturnCode = 0 + + urtest.Setup.Copy('wait_for_log.sh') + + tr = urtest.AddTestRun("wait_for_log") + tr.Processes.Default.Command = ( + './wait_for_log.sh {} {}'.format(os.path.join(ts.Variables.LOGDIR, 'the_log.log'), redirect_serv.Variables.Port)) + tr.Processes.Default.Streams.stdout = "gold/redirect_log.gold" + tr.Processes.Default.ReturnCode = 0 + + urtest.Setup.Copy(data_path) + urtest.execute() diff --git a/tests/uranium_tests/redirect/test_redirect_actions.py b/tests/uranium_tests/redirect/test_redirect_actions.py new file mode 100644 index 00000000000..ad4b1132458 --- /dev/null +++ b/tests/uranium_tests/redirect/test_redirect_actions.py @@ -0,0 +1,287 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_redirect_actions(urtest: UraniumTest) -> None: + ''' + Test redirection behavior to invalid addresses + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + from enum import Enum + import re + import os + import socket + import sys + + urtest.Summary = ''' + Test redirection behavior to invalid addresses + ''' + + urtest.ContinueOnFail = False + + urtest.Setup.Copy(os.path.join(urtest.Variables.AtsTestToolsDir, 'tcp_client.py')) + + dns = urtest.MakeDNServer('dns') + # This record is used in each test case to get the initial redirect response from the origin that we will handle. + dnsRecords = {'iwillredirect.test': ['127.0.0.1']} + + host = socket.gethostname() + ipv4addrs = set() + try: + ipv4addrs = set([ip for (family, _, _, _, (ip, *_)) in socket.getaddrinfo(host, port=None) if socket.AF_INET == family]) + except socket.gaierror: + pass + + ipv6addrs = set() + try: + ipv6addrs = set( + [ + "[{0}]".format(ip.split('%')[0]) + for (family, _, _, _, (ip, *_)) in socket.getaddrinfo(host, port=None) + if socket.AF_INET6 == family and 'fe80' != ip[0:4] + ]) # Skip link-local addresses. + except socket.gaierror: + pass + + # If the hostname is set to a loopback address, use the ip command to obtain a non-loopback IP address. + if any(ip.startswith('127.') for ip in ipv4addrs): + import json + from subprocess import check_output + + data = json.loads(check_output(['ip', '-json', 'address', 'show'])) + + ipv4addrs = set( + [ + addr["local"] for iface in data if iface["link_type"] != "loopback" for addr in iface.get("addr_info", []) + if addr["family"] == "inet" + ]) + ipv6addrs = set( + [ + addr["local"] for iface in data if iface["link_type"] != "loopback" for addr in iface.get("addr_info", []) + if addr["family"] == "inet6" and addr["scope"] != "link" + ]) + + origin = urtest.MakeOriginServer('origin', ip='0.0.0.0') + ArbitraryTimestamp = '12345678' + + # This is for cases when the content is actually fetched from the invalid address. + request_header = { + 'headers': ('GET / HTTP/1.1\r\n' + 'Host: *\r\n\r\n'), + 'timestamp': ArbitraryTimestamp, + 'body': '' + } + response_header = { + 'headers': ('HTTP/1.1 204 No Content\r\n' + 'Connection: close\r\n\r\n'), + 'timestamp': ArbitraryTimestamp, + 'body': '' + } + origin.addResponse('sessionfile.log', request_header, response_header) + + # Map scenarios to trafficserver processes. + trafficservers = {} + + data_dirname = 'redirect_actions_generated_test_data' + data_path = os.path.join(urtest.TestDirectory, data_dirname) + os.makedirs(data_path, exist_ok=True) + + def normalizeForAutest(value): + ''' + autest uses "test run" names to build file and directory names, so we must transform them in case there are incompatible or + annoying characters. + This means we can also use them in URLs. + ''' + if not value: + return None + return re.sub(r'[^a-z0-9-]', '_', value, flags=re.I) + + def makeTestCase(redirectTarget, expectedAction, scenario): + ''' + Helper method that creates a "meta-test" from which autest generates a test case. + + :param redirectTarget: The target address of a redirect from origin to be handled. + :param scenario: Defines the ACL to configure and the addresses to test. + ''' + + config = ','.join( + ':'.join(t) for t in sorted((addr.name.lower(), action.name.lower()) for (addr, action) in scenario.items())) + + normRedirectTarget = normalizeForAutest(redirectTarget) + normConfig = normalizeForAutest(config) + tr = urtest.AddTestRun('With_Config_{0}_Redirect_to_{1}'.format(normConfig, normRedirectTarget)) + + if trafficservers: + tr.StillRunningAfter = origin + tr.StillRunningAfter = dns + else: + tr.Processes.Default.StartBefore(origin) + tr.Processes.Default.StartBefore(dns) + + if config not in trafficservers: + trafficservers[config] = urtest.MakeATSProcess('ts_{0}'.format(normConfig), enable_cache=False) + trafficservers[config].Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'http|dns|redirect', + 'proxy.config.http.number_of_redirections': 1, + 'proxy.config.dns.nameservers': '127.0.0.1:{0}'.format(dns.Variables.Port), + 'proxy.config.dns.resolv_conf': 'NULL', + 'proxy.config.url_remap.remap_required': 0, + 'proxy.config.http.redirect.actions': config, + 'proxy.config.http.connect_attempts_timeout': 5, + 'proxy.config.http.connect_attempts_max_retries': 0, + }) + tr.Processes.Default.StartBefore(trafficservers[config]) + else: + tr.StillRunningAfter = trafficservers[config] + + testDomain = 'testdomain{0}.test'.format(normRedirectTarget) + # The micro DNS server can't tell us whether it has a record of the domain already, so we use a dictionary to avoid duplicates. + # We remove any surrounding brackets that are common to IPv6 addresses. + if redirectTarget: + dnsRecords[testDomain] = [redirectTarget.strip('[]')] + + # A GET request parameterized on the config and on the target. + request_header = { + 'headers': ('GET /redirect?config={0}&target={1} HTTP/1.1\r\n' + 'Host: *\r\n\r\n').format(normConfig, normRedirectTarget), + 'timestamp': ArbitraryTimestamp, + 'body': '' + } + # Returns a redirect to the test domain for the given target & the port number for the TS of the given config. + response_header = { + 'headers': + ('HTTP/1.1 307 Temporary Redirect\r\n' + 'Location: http://{0}:{1}/\r\n' + 'Connection: close\r\n\r\n').format(testDomain, origin.Variables.Port), + 'timestamp': ArbitraryTimestamp, + 'body': '' + } + origin.addResponse('sessionfile.log', request_header, response_header) + + # Generate the request data file. + command_path = os.path.join(data_path, tr.Name) + with open(command_path, 'w') as f: + f.write( + ('GET /redirect?config={0}&target={1} HTTP/1.1\r\n' + 'Host: iwillredirect.test:{2}\r\n\r\n').format(normConfig, normRedirectTarget, origin.Variables.Port)) + # Set the command with the appropriate URL. + port = trafficservers[config].Variables.port + dir_path = os.path.join(data_dirname, tr.Name) + tr.Processes.Default.Command = \ + (f"bash -o pipefail -c '{sys.executable} tcp_client.py 127.0.0.1 {port} " + f"{dir_path} | head -n 1'") + tr.Processes.Default.ReturnCode = 0 + # Generate and set the 'gold file' to check stdout + goldFilePath = os.path.join(data_path, '{0}.gold'.format(tr.Name)) + with open(goldFilePath, 'w') as f: + f.write(expectedAction.value['expectedStatusLine']) + tr.Processes.Default.Streams.stdout = goldFilePath + + class AddressE(Enum): + ''' + Classes of addresses are mapped to example addresses. + ''' + Private = ('10.0.0.1', '[fc00::1]') + Loopback = ( + ['127.1.2.3']) # [::1] is omitted here because it is likely overwritten by Self, and there are no others in IPv6. + Multicast = ('224.1.2.3', '[ff42::]') + Linklocal = ('169.254.0.1', '[fe80::]') + Routable = ('72.30.35.10', '[2001:4998:58:1836::10]') # Do not Follow redirects to these in an automated test. + Self = ipv4addrs | ipv6addrs # Addresses of this host. + Default = None # All addresses apply, nothing in particular to test. + + class ActionE(Enum): + # Title case because 'return' is a Python keyword. + Return = {'config': 'return', 'expectedStatusLine': 'HTTP/1.1 307 Temporary Redirect\r\n'} + Reject = {'config': 'reject', 'expectedStatusLine': 'HTTP/1.1 403 Forbidden\r\n'} + Follow = {'config': 'follow', 'expectedStatusLine': 'HTTP/1.1 204 No Content\r\n'} + + # Added to test failure modes. + Break = {'expectedStatusLine': 'HTTP/1.1 500 Cannot find server.\r\n'} + + scenarios = [ + { + # Follow to loopback, but alternately reject/return others. + AddressE.Private: ActionE.Reject, + AddressE.Loopback: ActionE.Follow, + AddressE.Multicast: ActionE.Reject, + AddressE.Linklocal: ActionE.Return, + AddressE.Routable: ActionE.Reject, + AddressE.Self: ActionE.Return, + AddressE.Default: ActionE.Reject, + }, + { + # Follow to loopback, but alternately reject/return others, flipped from the previous scenario. + AddressE.Private: ActionE.Return, + AddressE.Loopback: ActionE.Follow, + AddressE.Multicast: ActionE.Return, + AddressE.Linklocal: ActionE.Reject, + AddressE.Routable: ActionE.Return, + AddressE.Self: ActionE.Reject, + AddressE.Default: ActionE.Return, + }, + { + # Return loopback, but reject everything else. + AddressE.Loopback: ActionE.Return, + AddressE.Default: ActionE.Reject, + }, + { + # Reject loopback, but return everything else. + AddressE.Loopback: ActionE.Reject, + AddressE.Default: ActionE.Return, + }, + { + # Return everything. + AddressE.Default: ActionE.Return, + }, + ] + + for scenario in scenarios: + for addressClass in AddressE: + if not addressClass.value: + # Default has no particular addresses to test. + continue + for address in addressClass.value: + expectedAction = scenario[addressClass] if addressClass in scenario else scenario[AddressE.Default] + makeTestCase(redirectTarget=address, expectedAction=expectedAction, scenario=scenario) + + # Test redirects to names that cannot be resolved. + makeTestCase(redirectTarget=None, expectedAction=ActionE.Break, scenario=scenario) + + dns.addRecords(records=dnsRecords) + + # Make sure this runs only after local files have been created. + urtest.Setup.Copy(data_path) + urtest.execute() diff --git a/tests/uranium_tests/redirect/test_redirect_post.py b/tests/uranium_tests/redirect/test_redirect_post.py new file mode 100644 index 00000000000..7ebbd1ae322 --- /dev/null +++ b/tests/uranium_tests/redirect/test_redirect_post.py @@ -0,0 +1,112 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_redirect_post(urtest: UraniumTest) -> None: + ''' + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + urtest.Summary = ''' + Test basic post redirection + ''' + + # TODO figure out how to use this + MAX_REDIRECT = 99 + + urtest.SkipUnless(Condition.HasProgram("truncate", "truncate need to be installed on system for this test to work")) + + urtest.ContinueOnFail = True + + ts = urtest.MakeATSProcess("ts", enable_cache=False) + redirect_serv1 = urtest.MakeOriginServer("re_server1") + redirect_serv2 = urtest.MakeOriginServer("re_server2") + dest_serv = urtest.MakeOriginServer("dest_server") + + ts.Disk.records_config.update( + { + 'proxy.config.http.number_of_redirections': MAX_REDIRECT, + 'proxy.config.http.post_copy_size': 919430601, + 'proxy.config.http.redirect.actions': 'self:follow', # redirects to self are not followed by default + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'http', + }) + + redirect_request_header = { + "headers": "POST /redirect1 HTTP/1.1\r\nHost: *\r\nContent-Length: 52428800\r\n\r\n", + "timestamp": "5678", + "body": "" + } + redirect_response_header = { + "headers": "HTTP/1.1 302 Found\r\nLocation: http://127.0.0.1:{0}/redirect2\r\n\r\n".format(redirect_serv2.Variables.Port), + "timestamp": "5678", + "body": "" + } + + redirect_request_header2 = { + "headers": "POST /redirect2 HTTP/1.1\r\nHost: *\r\nContent-Length: 52428800\r\n\r\n", + "timestamp": "5678", + "body": "" + } + redirect_response_header2 = { + "headers": "HTTP/1.1 302 Found\r\nLocation: http://127.0.0.1:{0}/redirectDest\r\n\r\n".format(dest_serv.Variables.Port), + "timestamp": "5678", + "body": "" + } + + dest_request_header = { + "headers": "POST /redirectDest HTTP/1.1\r\nHost: *\r\nContent-Length: 52428800\r\n\r\n", + "timestamp": "11", + "body": "" + } + dest_response_header = {"headers": "HTTP/1.1 204 No Content\r\n\r\n", "timestamp": "22", "body": ""} + + redirect_serv1.addResponse("sessionfile.log", redirect_request_header, redirect_response_header) + redirect_serv2.addResponse("sessionfile.log", redirect_request_header2, redirect_response_header2) + dest_serv.addResponse("sessionfile.log", dest_request_header, dest_response_header) + + ts.Disk.remap_config.AddLine( + 'map http://127.0.0.1:{0} http://127.0.0.1:{1}'.format(ts.Variables.port, redirect_serv1.Variables.Port)) + + tr = urtest.AddTestRun() + tr.MakeCurlCommandMulti( + 'touch largefile.txt && truncate -s 50M largefile.txt && {{curl}} -H "Expect: " -i http://127.0.0.1:{0}/redirect1 -F "filename=@./largefile.txt" && rm -f largefile.txt' + .format(ts.Variables.port), + ts=ts) + tr.TimeOut = 10 + tr.Processes.Default.StartBefore(ts) + tr.Processes.Default.StartBefore(redirect_serv1) + tr.Processes.Default.StartBefore(redirect_serv2) + tr.Processes.Default.StartBefore(dest_serv) + tr.Processes.Default.Streams.stdout = "gold/redirect_post.gold" + tr.Processes.Default.ReturnCode = 0 + urtest.execute() diff --git a/tests/uranium_tests/redirect/test_redirect_stale.py b/tests/uranium_tests/redirect/test_redirect_stale.py new file mode 100644 index 00000000000..957387162c0 --- /dev/null +++ b/tests/uranium_tests/redirect/test_redirect_stale.py @@ -0,0 +1,118 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_redirect_stale(urtest: UraniumTest) -> None: + ''' + Test that redirects will be followed when refreshing stale cache objects. + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + urtest.Summary = ''' + Test that redirects will be followed when refreshing stale cache objects. + ''' + + server = urtest.MakeOriginServer("server") + + ArbitraryTimestamp = '12345678' + + request_header = { + 'headers': ('GET /obj HTTP/1.1\r\n' + 'Host: *\r\n\r\n'), + 'timestamp': ArbitraryTimestamp, + 'body': '' + } + response_header = { + 'headers': ('HTTP/1.1 302 Found\r\n' + 'Location: http://127.0.0.1:{}/obj2\r\n\r\n'.format(server.Variables.Port)), + 'timestamp': ArbitraryTimestamp, + 'body': '' + } + server.addResponse('sessionfile.log', request_header, response_header) + + request_header = { + 'headers': ('GET /obj2 HTTP/1.1\r\n' + 'Host: *\r\n\r\n'), + 'timestamp': ArbitraryTimestamp, + 'body': '' + } + response_header = { + 'headers': ('HTTP/1.1 200 OK\r\n' + 'X-Obj: obj2\r\n' + 'Cache-Control: max-age=2\r\n' + 'Content-Length: 0\r\n\r\n'), + 'timestamp': ArbitraryTimestamp, + 'body': '' + } + server.addResponse('sessionfile.log', request_header, response_header) + + ts = urtest.MakeATSProcess("ts") + + ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'http|dns|cache|redirect', + 'proxy.config.http.cache.required_headers': 0, # Only Content-Length header required for caching. + 'proxy.config.http.push_method_enabled': 1, + 'proxy.config.url_remap.remap_required': 0, + 'proxy.config.http.redirect.actions': 'routable:follow,loopback:follow,self:follow', + 'proxy.config.http.number_of_redirections': 1 + }) + + # Set up to check the output after the tests have run. + # + log_id = urtest.Disk.File("log2.txt") + log_id.Content = "gold/redirect_stale.gold" + + tr = urtest.AddTestRun() + tr.Processes.Default.StartBefore(ts) + tr.Processes.Default.StartBefore(server) + tr.Processes.Default.Command = ( + r"printf 'GET /obj HTTP/1.1\r\nHost: 127.0.0.1:{}\r\n\r\n' | nc localhost {} >> log.txt".format( + server.Variables.Port, ts.Variables.port)) + tr.Processes.Default.ReturnCode = 0 + + # Wait for the response in cache to become stale, then GET it again. + # + tr = urtest.AddTestRun() + tr.Processes.Default.Command = ( + r"sleep 4 ; printf 'GET /obj HTTP/1.1\r\nHost: 127.0.0.1:{}\r\n\r\n' | nc localhost {} >> log.txt".format( + server.Variables.Port, ts.Variables.port)) + tr.Processes.Default.ReturnCode = 0 + + # Filter out inconsistent content in test output. + # + tr = urtest.AddTestRun() + tr.Processes.Default.Command = ( + r"grep -v -e '^Date: ' -e '^Age: ' -e '^Connection: ' -e '^Server: ATS/' log.txt | tr -d '\r'> log2.txt") + tr.Processes.Default.ReturnCode = 0 + urtest.execute() diff --git a/tests/uranium_tests/redirect/test_redirect_to_same_origin_on_cache.py b/tests/uranium_tests/redirect/test_redirect_to_same_origin_on_cache.py new file mode 100644 index 00000000000..db2ebc701d5 --- /dev/null +++ b/tests/uranium_tests/redirect/test_redirect_to_same_origin_on_cache.py @@ -0,0 +1,160 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_redirect_to_same_origin_on_cache(urtest: UraniumTest) -> None: + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import functools + from typing import Any, Callable, Dict, Optional + + from ports import get_port + + urtest.Summary = 'Tests SNI port-based routing' + + TestParams = Dict[str, Any] + + class TestRedirectToSameOriginOnCache: + """Configure a test for reproducing #9275.""" + + replay_filepath_one: str = "replay/redirect_to_same_origin_on_cache.replay.yaml" + client_counter: int = 0 + server_counter: int = 0 + ts_counter: int = 0 + + def __init__(self, name: str, /, autorun) -> None: + """Initialize the test. + + :param name: The name of the test. + """ + self.name = name + self.autorun = autorun + + def _init_run(self) -> "TestRun": + """Initialize processes for the test run.""" + + tr = urtest.AddTestRun(self.name) + server_one = TestRedirectToSameOriginOnCache.configure_server(tr, "oof.com") + self._configure_traffic_server(tr, server_one) + dns = TestRedirectToSameOriginOnCache.configure_dns(tr, server_one, self._dns_port) + + tr.Processes.Default.StartBefore(server_one) + tr.Processes.Default.StartBefore(dns) + tr.Processes.Default.StartBefore(self._ts) + + return { + "tr": tr, + "ts": self._ts, + "server_one": server_one, + "port_one": self._port_one, + } + + @classmethod + def runner(cls, name: str, autorun: bool = True) -> Optional[Callable]: + """Create a runner for a test case. + + :param autorun: Run the test case once it's set up. Default is True. + """ + test = cls(name, autorun=autorun)._prepare_test_case + return test + + def _prepare_test_case(self, func: Callable) -> Callable: + """Set up a test case and possibly run it. + + :param func: The test case to set up. + """ + functools.wraps(func) + test_params = self._init_run() + + def wrapper(*args, **kwargs) -> Any: + return func(test_params, *args, **kwargs) + + if self.autorun: + wrapper() + return wrapper + + @staticmethod + def configure_server(tr: "TestRun", domain: str): + server = tr.AddVerifierServerProcess( + f"server{TestRedirectToSameOriginOnCache.server_counter}.{domain}", + TestRedirectToSameOriginOnCache.replay_filepath_one, + other_args="--format \"{url}\"") + TestRedirectToSameOriginOnCache.server_counter += 1 + + return server + + @staticmethod + def configure_dns(tr: "TestRun", server_one: "Process", dns_port: int): + dns = tr.MakeDNServer("dns", port=dns_port, default="127.0.0.1") + return dns + + def _configure_traffic_server(self, tr: "TestRun", server_one: "Process"): + """Configure Traffic Server. + + :param tr: The TestRun object to associate the ts process with. + """ + ts = tr.MakeATSProcess( + f"ts-{TestRedirectToSameOriginOnCache.ts_counter}", select_ports=False, enable_tls=True, enable_cache=True) + TestRedirectToSameOriginOnCache.ts_counter += 1 + + self._port_one = get_port(ts, "PortOne") + self._dns_port = get_port(ts, "DNSPort") + ts.Disk.records_config.update( + { + 'proxy.config.http.server_ports': f"{self._port_one}", + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': "cache|dns|http|redirect|remap", + 'proxy.config.dns.nameservers': f"127.0.0.1:{self._dns_port}", + 'proxy.config.dns.resolv_conf': 'NULL', + 'proxy.config.http.redirect.actions': "self:follow", + 'proxy.config.http.number_of_redirections': 1, + }) + + ts.Disk.remap_config.AddLine(f"map oof.com http://oof.backend.com:{server_one.Variables.http_port}") + ts.Disk.plugin_config.AddLine("xdebug.so --enable=x-cache,x-cache-key,via") + + self._ts = ts + + # Tests start. + + @TestRedirectToSameOriginOnCache.runner("Redirect to same origin with cache") + def test1(params: TestParams) -> None: + client = params["tr"].AddVerifierClientProcess( + f"client0", + TestRedirectToSameOriginOnCache.replay_filepath_one, + http_ports=[params["port_one"]], + other_args="--format \"{url}\" --keys \"/a/path/resource\"") + + params["tr"].Processes.Default.ReturnCode = 0 + params["tr"].Processes.Default.Streams.stdout.Content += Testers.IncludesExpression("200 OK", "We should get the resource.") + + urtest.execute() diff --git a/tests/gold_tests/redirect/wait_for_log.sh b/tests/uranium_tests/redirect/wait_for_log.sh similarity index 100% rename from tests/gold_tests/redirect/wait_for_log.sh rename to tests/uranium_tests/redirect/wait_for_log.sh diff --git a/tests/gold_tests/redirect/zone.json b/tests/uranium_tests/redirect/zone.json similarity index 100% rename from tests/gold_tests/redirect/zone.json rename to tests/uranium_tests/redirect/zone.json diff --git a/tests/gold_tests/remap/all_acl_combinations.py b/tests/uranium_tests/remap/all_acl_combinations.py similarity index 100% rename from tests/gold_tests/remap/all_acl_combinations.py rename to tests/uranium_tests/remap/all_acl_combinations.py diff --git a/tests/gold_tests/remap/base.replay.yaml b/tests/uranium_tests/remap/base.replay.yaml similarity index 100% rename from tests/gold_tests/remap/base.replay.yaml rename to tests/uranium_tests/remap/base.replay.yaml diff --git a/tests/gold_tests/remap/deactivate_ip_allow.py b/tests/uranium_tests/remap/deactivate_ip_allow.py similarity index 100% rename from tests/gold_tests/remap/deactivate_ip_allow.py rename to tests/uranium_tests/remap/deactivate_ip_allow.py diff --git a/tests/gold_tests/remap/deny_head_post.replay.yaml b/tests/uranium_tests/remap/deny_head_post.replay.yaml similarity index 100% rename from tests/gold_tests/remap/deny_head_post.replay.yaml rename to tests/uranium_tests/remap/deny_head_post.replay.yaml diff --git a/tests/gold_tests/remap/gold/200OK_test.gold b/tests/uranium_tests/remap/gold/200OK_test.gold similarity index 100% rename from tests/gold_tests/remap/gold/200OK_test.gold rename to tests/uranium_tests/remap/gold/200OK_test.gold diff --git a/tests/gold_tests/remap/gold/200OK_test_uds.gold b/tests/uranium_tests/remap/gold/200OK_test_uds.gold similarity index 100% rename from tests/gold_tests/remap/gold/200OK_test_uds.gold rename to tests/uranium_tests/remap/gold/200OK_test_uds.gold diff --git a/tests/gold_tests/remap/gold/lookupTest.gold b/tests/uranium_tests/remap/gold/lookupTest.gold similarity index 100% rename from tests/gold_tests/remap/gold/lookupTest.gold rename to tests/uranium_tests/remap/gold/lookupTest.gold diff --git a/tests/gold_tests/remap/gold/map-with-recv-port-ip.gold b/tests/uranium_tests/remap/gold/map-with-recv-port-ip.gold similarity index 100% rename from tests/gold_tests/remap/gold/map-with-recv-port-ip.gold rename to tests/uranium_tests/remap/gold/map-with-recv-port-ip.gold diff --git a/tests/gold_tests/remap/gold/map-with-recv-port-unix.gold b/tests/uranium_tests/remap/gold/map-with-recv-port-unix.gold similarity index 100% rename from tests/gold_tests/remap/gold/map-with-recv-port-unix.gold rename to tests/uranium_tests/remap/gold/map-with-recv-port-unix.gold diff --git a/tests/gold_tests/remap/gold/remap-200.gold b/tests/uranium_tests/remap/gold/remap-200.gold similarity index 100% rename from tests/gold_tests/remap/gold/remap-200.gold rename to tests/uranium_tests/remap/gold/remap-200.gold diff --git a/tests/gold_tests/remap/gold/remap-404.gold b/tests/uranium_tests/remap/gold/remap-404.gold similarity index 100% rename from tests/gold_tests/remap/gold/remap-404.gold rename to tests/uranium_tests/remap/gold/remap-404.gold diff --git a/tests/gold_tests/remap/gold/remap-DNS-200.gold b/tests/uranium_tests/remap/gold/remap-DNS-200.gold similarity index 100% rename from tests/gold_tests/remap/gold/remap-DNS-200.gold rename to tests/uranium_tests/remap/gold/remap-DNS-200.gold diff --git a/tests/gold_tests/remap/gold/remap-DNS-ipv6-200.gold b/tests/uranium_tests/remap/gold/remap-DNS-ipv6-200.gold similarity index 100% rename from tests/gold_tests/remap/gold/remap-DNS-ipv6-200.gold rename to tests/uranium_tests/remap/gold/remap-DNS-ipv6-200.gold diff --git a/tests/gold_tests/remap/gold/remap-hitATS-404.gold b/tests/uranium_tests/remap/gold/remap-hitATS-404.gold similarity index 100% rename from tests/gold_tests/remap/gold/remap-hitATS-404.gold rename to tests/uranium_tests/remap/gold/remap-hitATS-404.gold diff --git a/tests/gold_tests/remap/gold/remap-https-200.gold b/tests/uranium_tests/remap/gold/remap-https-200.gold similarity index 100% rename from tests/gold_tests/remap/gold/remap-https-200.gold rename to tests/uranium_tests/remap/gold/remap-https-200.gold diff --git a/tests/gold_tests/remap/gold/remap-https-200_2.gold b/tests/uranium_tests/remap/gold/remap-https-200_2.gold similarity index 100% rename from tests/gold_tests/remap/gold/remap-https-200_2.gold rename to tests/uranium_tests/remap/gold/remap-https-200_2.gold diff --git a/tests/gold_tests/remap/gold/remap-https-200_3.gold b/tests/uranium_tests/remap/gold/remap-https-200_3.gold similarity index 100% rename from tests/gold_tests/remap/gold/remap-https-200_3.gold rename to tests/uranium_tests/remap/gold/remap-https-200_3.gold diff --git a/tests/gold_tests/remap/gold/remap-ip-resolve.gold b/tests/uranium_tests/remap/gold/remap-ip-resolve.gold similarity index 100% rename from tests/gold_tests/remap/gold/remap-ip-resolve.gold rename to tests/uranium_tests/remap/gold/remap-ip-resolve.gold diff --git a/tests/gold_tests/remap/gold/remap-redirect.gold b/tests/uranium_tests/remap/gold/remap-redirect.gold similarity index 100% rename from tests/gold_tests/remap/gold/remap-redirect.gold rename to tests/uranium_tests/remap/gold/remap-redirect.gold diff --git a/tests/gold_tests/remap/gold/remap-referer-hit.gold b/tests/uranium_tests/remap/gold/remap-referer-hit.gold similarity index 100% rename from tests/gold_tests/remap/gold/remap-referer-hit.gold rename to tests/uranium_tests/remap/gold/remap-referer-hit.gold diff --git a/tests/gold_tests/remap/gold/remap-referer-miss.gold b/tests/uranium_tests/remap/gold/remap-referer-miss.gold similarity index 100% rename from tests/gold_tests/remap/gold/remap-referer-miss.gold rename to tests/uranium_tests/remap/gold/remap-referer-miss.gold diff --git a/tests/gold_tests/remap/gold/remap-ws-metrics-uds.gold b/tests/uranium_tests/remap/gold/remap-ws-metrics-uds.gold similarity index 100% rename from tests/gold_tests/remap/gold/remap-ws-metrics-uds.gold rename to tests/uranium_tests/remap/gold/remap-ws-metrics-uds.gold diff --git a/tests/gold_tests/remap/gold/remap-ws-metrics.gold b/tests/uranium_tests/remap/gold/remap-ws-metrics.gold similarity index 100% rename from tests/gold_tests/remap/gold/remap-ws-metrics.gold rename to tests/uranium_tests/remap/gold/remap-ws-metrics.gold diff --git a/tests/gold_tests/remap/gold/remap-ws-upgrade-400.gold b/tests/uranium_tests/remap/gold/remap-ws-upgrade-400.gold similarity index 100% rename from tests/gold_tests/remap/gold/remap-ws-upgrade-400.gold rename to tests/uranium_tests/remap/gold/remap-ws-upgrade-400.gold diff --git a/tests/gold_tests/remap/gold/remap-ws-upgrade.gold b/tests/uranium_tests/remap/gold/remap-ws-upgrade.gold similarity index 100% rename from tests/gold_tests/remap/gold/remap-ws-upgrade.gold rename to tests/uranium_tests/remap/gold/remap-ws-upgrade.gold diff --git a/tests/gold_tests/remap/gold/remap-zero-200.gold b/tests/uranium_tests/remap/gold/remap-zero-200.gold similarity index 100% rename from tests/gold_tests/remap/gold/remap-zero-200.gold rename to tests/uranium_tests/remap/gold/remap-zero-200.gold diff --git a/tests/gold_tests/remap/gold/remap2-200.gold b/tests/uranium_tests/remap/gold/remap2-200.gold similarity index 100% rename from tests/gold_tests/remap/gold/remap2-200.gold rename to tests/uranium_tests/remap/gold/remap2-200.gold diff --git a/tests/gold_tests/remap/reload_1.replay.yaml b/tests/uranium_tests/remap/reload_1.replay.yaml similarity index 100% rename from tests/gold_tests/remap/reload_1.replay.yaml rename to tests/uranium_tests/remap/reload_1.replay.yaml diff --git a/tests/gold_tests/remap/reload_2.replay.yaml b/tests/uranium_tests/remap/reload_2.replay.yaml similarity index 100% rename from tests/gold_tests/remap/reload_2.replay.yaml rename to tests/uranium_tests/remap/reload_2.replay.yaml diff --git a/tests/gold_tests/remap/reload_3.replay.yaml b/tests/uranium_tests/remap/reload_3.replay.yaml similarity index 100% rename from tests/gold_tests/remap/reload_3.replay.yaml rename to tests/uranium_tests/remap/reload_3.replay.yaml diff --git a/tests/gold_tests/remap/reload_4.replay.yaml b/tests/uranium_tests/remap/reload_4.replay.yaml similarity index 100% rename from tests/gold_tests/remap/reload_4.replay.yaml rename to tests/uranium_tests/remap/reload_4.replay.yaml diff --git a/tests/gold_tests/remap/reload_server.replay.yaml b/tests/uranium_tests/remap/reload_server.replay.yaml similarity index 100% rename from tests/gold_tests/remap/reload_server.replay.yaml rename to tests/uranium_tests/remap/reload_server.replay.yaml diff --git a/tests/gold_tests/remap/remap_acl_all_allowed.replay.yaml b/tests/uranium_tests/remap/remap_acl_all_allowed.replay.yaml similarity index 100% rename from tests/gold_tests/remap/remap_acl_all_allowed.replay.yaml rename to tests/uranium_tests/remap/remap_acl_all_allowed.replay.yaml diff --git a/tests/gold_tests/remap/remap_acl_all_denied.replay.yaml b/tests/uranium_tests/remap/remap_acl_all_denied.replay.yaml similarity index 100% rename from tests/gold_tests/remap/remap_acl_all_denied.replay.yaml rename to tests/uranium_tests/remap/remap_acl_all_denied.replay.yaml diff --git a/tests/gold_tests/remap/remap_acl_get_allowed.replay.yaml b/tests/uranium_tests/remap/remap_acl_get_allowed.replay.yaml similarity index 100% rename from tests/gold_tests/remap/remap_acl_get_allowed.replay.yaml rename to tests/uranium_tests/remap/remap_acl_get_allowed.replay.yaml diff --git a/tests/gold_tests/remap/remap_acl_get_post_allowed.replay.yaml b/tests/uranium_tests/remap/remap_acl_get_post_allowed.replay.yaml similarity index 100% rename from tests/gold_tests/remap/remap_acl_get_post_allowed.replay.yaml rename to tests/uranium_tests/remap/remap_acl_get_post_allowed.replay.yaml diff --git a/tests/gold_tests/remap/remap_acl_get_post_allowed_pp.replay.yaml b/tests/uranium_tests/remap/remap_acl_get_post_allowed_pp.replay.yaml similarity index 100% rename from tests/gold_tests/remap/remap_acl_get_post_allowed_pp.replay.yaml rename to tests/uranium_tests/remap/remap_acl_get_post_allowed_pp.replay.yaml diff --git a/tests/gold_tests/remap/remap_acl_get_post_denied.replay.yaml b/tests/uranium_tests/remap/remap_acl_get_post_denied.replay.yaml similarity index 100% rename from tests/gold_tests/remap/remap_acl_get_post_denied.replay.yaml rename to tests/uranium_tests/remap/remap_acl_get_post_denied.replay.yaml diff --git a/tests/gold_tests/remap/replay/regex_map_anchor.test.yaml b/tests/uranium_tests/remap/replay/regex_map_anchor.test.yaml similarity index 99% rename from tests/gold_tests/remap/replay/regex_map_anchor.test.yaml rename to tests/uranium_tests/remap/replay/regex_map_anchor.test.yaml index 9a61d92dba7..e80f69b7984 100644 --- a/tests/gold_tests/remap/replay/regex_map_anchor.test.yaml +++ b/tests/uranium_tests/remap/replay/regex_map_anchor.test.yaml @@ -17,7 +17,7 @@ meta: version: '1.0' -autest: +urtest: description: 'Verify regex_map performs full-hostname matching' dns: diff --git a/tests/uranium_tests/remap/test_basic_conf_remap_yaml.py b/tests/uranium_tests/remap/test_basic_conf_remap_yaml.py new file mode 100644 index 00000000000..07301df661d --- /dev/null +++ b/tests/uranium_tests/remap/test_basic_conf_remap_yaml.py @@ -0,0 +1,217 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_basic_conf_remap_yaml(urtest: UraniumTest) -> None: + ''' + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + urtest.Summary = ''' + Test conf_remap using a yaml file. + ''' + + urtest.ContinueOnFail = True + + class conf_remap_yaml_load_test: + """Test conf_remap using a yaml file.""" + + client_counter: int = 0 + ts_counter: int = 0 + server_counter: int = 0 + + def __init__(self, name: str, gold_file="", remap_filename="", remap_content=""): + """Initialize the test. + :param name: The name of the test. + :param gold_file: Gold file to be checked. + :param remap_filename: Remap yaml filename. + :param remap_content: remap yaml file content. + """ + self.name = name + self.gold_file = gold_file + self._remap_filename = remap_filename + self._remap_content = remap_content + + def _configure_server(self, tr: 'TestRun'): + """Configure the server. + + :param tr: The TestRun object to associate the server process with. + """ + server = urtest.MakeOriginServer(f"server-{conf_remap_yaml_load_test.ts_counter}", lookup_key="{%Host}{PATH}") + request_header2 = { + "headers": "GET /test HTTP/1.1\r\nHost: www.testexample.com\r\n\r\n", + "timestamp": "1469733493.993", + "body": "" + } + response_header2 = { + "headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", + "timestamp": "1469733493.993", + "body": "" + } + + server.addResponse("sessionfile.log", request_header2, response_header2) + conf_remap_yaml_load_test.server_counter += 1 + self._server = server + + def _configure_traffic_server(self, tr: 'TestRun'): + """Configure Traffic Server. + + :param tr: The TestRun object to associate the ts process with. + """ + ts = urtest.MakeATSProcess(f"ts-{conf_remap_yaml_load_test.ts_counter}") + + conf_remap_yaml_load_test.ts_counter += 1 + ts.Disk.records_config.update( + ''' + diags: + debug: + enabled: 1 + tags: conf_remap + dns: + resolv_conf: NULL + http: + referer_filter: 1 + url_remap: + pristine_host_hdr: 0 # make sure is 0 + + ''') + self._ts = ts + + def run(self, diags_fail_exp="", ts_retcode=0): + """Run the test. + :param diags_fail_exp: Text to be included to validate the error. + :param ts_retcode: Expected return code from TS. + """ + tr = urtest.AddTestRun(self.name) + self._configure_server(tr) + self._configure_traffic_server(tr) + + tr.Processes.Default.StartBefore(self._server) + tr.Processes.Default.StartBefore(self._ts) + + self._ts.ReturnCode = ts_retcode + + if ts_retcode > 0: # we could have errors logged and yet, we still want to move on. + self._ts.Ready = 0 + + if diags_fail_exp != "": + # some error logs will be written to the diags. + self._ts.Disk.diags_log.Content = Testers.IncludesExpression(diags_fail_exp, "Have a look.") + else: + tr.Processes.Default.ReturnCode = 0 + + if self.gold_file: + tr.Processes.Default.Streams.stderr = self.gold_file + + if self._remap_filename != "" and self._remap_content != "": + self._ts.Disk.MakeConfigFile(self._remap_filename).update(self._remap_content) + self._ts.Disk.remap_config.AddLine( + f'map http://www.testexample.com/ http://127.0.0.1:{self._server.Variables.Port} @plugin=conf_remap.so @pparam={self._remap_filename}' + ) + if Condition.CurlUsingUnixDomainSocket(): + tr.MakeCurlCommand( + '-H "Host: www.testexample.com" "http://127.0.0.1:{0}/test" --verbose'.format(self._ts.Variables.port), + ts=self._ts) + else: + tr.MakeCurlCommand( + '--proxy 127.0.0.1:{0} "http://www.testexample.com/test" -H "Host: www.testexample.com" --verbose'.format( + self._ts.Variables.port), + ts=self._ts) + conf_remap_yaml_load_test.client_counter += 1 + + gold_file = "gold/200OK_test.gold" + if Condition.CurlUsingUnixDomainSocket(): + gold_file = "gold/200OK_test_uds.gold" + + test0 = conf_remap_yaml_load_test( + "Test success", + gold_file=gold_file, + remap_filename="testexample_remap.yaml", + remap_content=''' + records: + url_remap: + pristine_host_hdr: 1 + ''') + test0.run() + + test1 = conf_remap_yaml_load_test( + "Test mismatch type", + remap_filename="mismatch_field_type_remap.yaml", + remap_content=''' + records: + url_remap: + pristine_host_hdr: !!float '1' + ''') + test1.run(diags_fail_exp="'proxy.config.url_remap.pristine_host_hdr' variable type mismatch", ts_retcode=33) + + test2 = conf_remap_yaml_load_test( + "Test invalid variable", + remap_filename="invalid1_field_type_remap.yaml", + remap_content=''' + records: + plugin: + dynamic_reload_mode: 1 + ''') + + test2.run( + diags_fail_exp="'proxy.config.plugin.dynamic_reload_mode' is not a configuration variable or cannot be overridden", + ts_retcode=33) + + # We let the conf_remap parse two fields, only one is valid, we expect ATS to start and the invalid fields ignored. + test3 = conf_remap_yaml_load_test( + "Test success", + gold_file=gold_file, + remap_filename="testexample2_remap.yaml", + remap_content=''' + records: + plugin: + dynamic_reload_mode: 1 + + url_remap: + pristine_host_hdr: 1 + ''') + test3.run(diags_fail_exp="'proxy.config.plugin.dynamic_reload_mode' is not a configuration variable or cannot be overridden") + + # Check null values + test4 = conf_remap_yaml_load_test( + "Test success - with NULL variable", + gold_file=gold_file, + remap_filename="testexample_remap.yaml", + remap_content=''' + records: + url_remap: + pristine_host_hdr: 1 + hostdb: + ip_resolve: "NULL" # We want to make sure this gets read as it should. "NULL" could be the value of this field. + ''') + test4.run() + urtest.execute() diff --git a/tests/uranium_tests/remap/test_conf_remap_float.py b/tests/uranium_tests/remap/test_conf_remap_float.py new file mode 100644 index 00000000000..045420dec7f --- /dev/null +++ b/tests/uranium_tests/remap/test_conf_remap_float.py @@ -0,0 +1,64 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_conf_remap_float(urtest: UraniumTest) -> None: + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + urtest.Summary = ''' + Test command: traffic_ctl config describe proxy.config.http.background_fill_completed_threshold (YTSATS-3309) + ''' + urtest.testName = 'Float in conf_remap Config Test' + + ts = urtest.MakeATSProcess("ts") + + ts.Disk.MakeConfigFile('conf_remap.yaml').update( + ''' + records: + http: + background_fill_completed_threshold: !!float '0.5' + ''') + + ts.Disk.remap_config.AddLine( + f"map http://cdn.example.com/ http://origin.example.com/ @plugin=conf_remap.so @pparam={urtest.RunDirectory}/ts/config/conf_remap.yaml" + ) + + tr = urtest.AddTestRun("traffic_ctl command") + tr.Env = ts.Env + tr.TimeOut = 5 + tr.StillRunningAfter = ts + + p = tr.Processes.Default + p.Command = f"traffic_ctl config describe proxy.config.http.background_fill_completed_threshold" + p.ReturnCode = 0 + p.StartBefore(urtest.Processes.ts) + urtest.execute() diff --git a/tests/uranium_tests/remap/test_map_with_recv_port.py b/tests/uranium_tests/remap/test_map_with_recv_port.py new file mode 100644 index 00000000000..413c6bb4250 --- /dev/null +++ b/tests/uranium_tests/remap/test_map_with_recv_port.py @@ -0,0 +1,115 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_map_with_recv_port(urtest: UraniumTest) -> None: + ''' + Verify correct behavior of regex_map in remap.config. + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + urtest.Summary = ''' + Verify correct behavior of map_with_recv_port in remap.config. + ''' + + urtest.ContinueOnFail = True + ts = urtest.MakeATSProcess("ts") + server = urtest.MakeOriginServer("server") + dns = urtest.MakeDNServer("dns", default='127.0.0.1') + + urtest.testName = "" + request_header_ip = { + "headers": "GET /ip HTTP/1.1\r\nHost: origin.example.com\r\n\r\n", + "timestamp": "1469733493.993", + "body": "" + } + response_header_ip = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": "ip"} + request_header_unix = { + "headers": "GET /unix HTTP/1.1\r\nHost: origin.example.com\r\n\r\n", + "timestamp": "1469733493.993", + "body": "" + } + response_header_unix = { + "headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", + "timestamp": "1469733493.993", + "body": "unix" + } + request_header_error = { + "headers": "GET /error HTTP/1.1\r\nHost: origin.example.com\r\n\r\n", + "timestamp": "1469733493.993", + "body": "" + } + response_header_error = { + "headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", + "timestamp": "1469733493.993", + "body": "error" + } + server.addResponse("sessionfile.log", request_header_ip, response_header_ip) + server.addResponse("sessionfile.log", request_header_unix, response_header_unix) + server.addResponse("sessionfile.log", request_header_error, response_header_error) + + ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'http|dns', + 'proxy.config.dns.nameservers': '127.0.0.1:{0}'.format(dns.Variables.Port), + 'proxy.config.dns.resolv_conf': 'NULL' + }) + + # This map rule should not match + ts.Disk.remap_config.AddLine( + r'map ' + r'http://test.example.com ' + r'http://origin.example.com:{}/error'.format(server.Variables.Port)) + # Rule for IP interface + ts.Disk.remap_config.AddLine( + r'map_with_recv_port ' + r'http://test.example.com:{}/ ' + r'http://origin.example.com:{}/ip'.format(ts.Variables.port, server.Variables.Port)) + # Rule for Unix Domain Socket + ts.Disk.remap_config.AddLine( + r'map_with_recv_port ' + r'http+unix://test.example.com ' + r'http://origin.example.com:{}/unix'.format(server.Variables.Port)) + + tr = urtest.AddTestRun() + tr.MakeCurlCommand('-H"Host: test.example.com" http://127.0.0.1:{0}/ --verbose'.format(ts.Variables.port), ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.StartBefore(server) + tr.Processes.Default.StartBefore(dns) + tr.Processes.Default.StartBefore(urtest.Processes.ts) + if Condition.CurlUsingUnixDomainSocket(): + tr.Processes.Default.Streams.stderr = "gold/map-with-recv-port-unix.gold" + else: + tr.Processes.Default.Streams.stderr = "gold/map-with-recv-port-ip.gold" + tr.StillRunningAfter = server + urtest.execute() diff --git a/tests/uranium_tests/remap/test_regex_map.py b/tests/uranium_tests/remap/test_regex_map.py new file mode 100644 index 00000000000..f1837e66412 --- /dev/null +++ b/tests/uranium_tests/remap/test_regex_map.py @@ -0,0 +1,84 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_regex_map(urtest: UraniumTest) -> None: + ''' + Verify correct behavior of regex_map in remap.config. + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + urtest.Summary = ''' + Verify correct behavior of regex_map in remap.config. + ''' + + urtest.ContinueOnFail = True + ts = urtest.MakeATSProcess("ts") + server = urtest.MakeOriginServer("server") + dns = urtest.MakeDNServer("dns", default='127.0.0.1') + + urtest.testName = "" + request_header = { + "headers": "GET / HTTP/1.1\r\nHost: zero.one.two.three.com\r\n\r\n", + "timestamp": "1469733493.993", + "body": "" + } + response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + server.addResponse("sessionfile.log", request_header, response_header) + + ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'http.*|dns|conf_remap', + 'proxy.config.http.referer_filter': 1, + 'proxy.config.dns.nameservers': '127.0.0.1:{0}'.format(dns.Variables.Port), + 'proxy.config.dns.resolv_conf': 'NULL' + }) + + ts.Disk.remap_config.AddLine( + r'regex_map ' + r'http://(.*)?one\.two\.three\.com/ ' + r'http://$1reactivate.four.five.six.com:{}/'.format(server.Variables.Port)) + ts.Disk.remap_config.AddLine( + r'regex_map ' + r'https://\b(?!(.*one|two|three|four|five|six)).+\b\.seven\.eight\.nine\.com/blah12345.html ' + r'https://www.example.com:{}/one/two/three/blah12345.html'.format(server.Variables.Port)) + + tr = urtest.AddTestRun() + tr.MakeCurlCommand('-H"Host: zero.one.two.three.com" http://127.0.0.1:{0}/ --verbose'.format(ts.Variables.port), ts=ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.StartBefore(server) + tr.Processes.Default.StartBefore(dns) + tr.Processes.Default.StartBefore(urtest.Processes.ts) + tr.Processes.Default.Streams.stderr = "gold/remap-zero-200.gold" + tr.StillRunningAfter = server + urtest.execute() diff --git a/tests/uranium_tests/remap/test_remap_acl.py b/tests/uranium_tests/remap/test_remap_acl.py new file mode 100644 index 00000000000..c96f55a8008 --- /dev/null +++ b/tests/uranium_tests/remap/test_remap_acl.py @@ -0,0 +1,643 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_remap_acl(urtest: UraniumTest) -> None: + ''' + Verify remap.config acl behavior. + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # # http://www.apache.org/licenses/LICENSE-2.0 # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import atexit + import os + import io + import re + import inspect + import shutil + import tempfile + from yaml import load, dump + from yaml import CLoader as Loader + from typing import List, Tuple + + from ports import get_port + + # Temporary directory for generated replay files. Cleaned up at process exit + # so the files don't accumulate in /tmp across test runs. + _replay_tmpdir = tempfile.mkdtemp(prefix='autest_replay_') + atexit.register(shutil.rmtree, _replay_tmpdir, True) + + urtest.Summary = ''' + Verify remap.config acl behavior. + ''' + + def update_config_file(path1: str, content1: str, path2: str, content2: str) -> None: + """Update two config files. + + This is used for some of the updates to the config files between test runs. + + :param path1: The path to the first config file. + :param content1: The content to write to the first config file. + :param path2: The path to the second config file. + :param content2: The content to write to the second config file. + """ + with open(path1, 'w') as f: + f.write(content1 + '\n') + with open(path2, 'w') as f: + f.write(content2 + '\n') + + class Test_remap_acl: + """Configure a test to verify remap.config acl behavior.""" + + _ts: 'TestProcess' = None + _ts_reload_counter: int = 0 + _ts_is_started: bool = False + + _server_counter: int = 0 + _client_counter: int = 0 + + def __init__( + self, name: str, replay_file: str, ip_allow_content: str, deactivate_ip_allow: bool, acl_behavior_policy: int, + acl_configuration: str, named_acls: List[Tuple[str, str]], expected_responses: List[int], proxy_protocol: bool): + """Initialize the test. + + :param name: The name of the test. + :param replay_file: The replay file to be used. + :param ip_allow_content: The ip_allow configuration to be used. + :param deactivate_ip_allow: Whether to deactivate the ip_allow filter. + :param acl_configuration: The ACL configuration to be used. + :param named_acls: The set of named ACLs to configure and use. + :param expect_responses: The in-order expected responses from the proxy. + """ + self._replay_file = replay_file + self._ip_allow_lines = ip_allow_content.split("\n") + self._deactivate_ip_allow = deactivate_ip_allow + self._acl_behavior_policy = acl_behavior_policy + self._acl_configuration = acl_configuration + self._named_acls = named_acls + self._expected_responses = expected_responses + + # Usually we configure the server first and use the server port to + # configure ATS to remap to it. In this case, though, we want a + # long-lived ATS process that spans TestRuns. So we let ATS choose an + # arbitrary availble server port, and then tell the TestRun-specific + # server to use that port. + server_port = self._configure_traffic_server() + tr = urtest.AddTestRun(name) + self._configure_server(tr, server_port) + self._configure_client(tr, proxy_protocol) + + def _configure_server(self, tr: 'TestRun', server_port: int) -> None: + """Configure the server. + """ + name = f"server-{Test_remap_acl._server_counter}" + server = tr.AddVerifierServerProcess(name, self._replay_file, http_ports=[server_port]) + Test_remap_acl._server_counter += 1 + self._server = server + + def _configure_traffic_server(self) -> int: + """Configure Traffic Server. + + :return: The listening port that the server should use. + """ + + call_reload: bool = False + if Test_remap_acl._ts is not None: + ts = Test_remap_acl._ts + call_reload = True + else: + ts = urtest.MakeATSProcess("ts", enable_cache=False, enable_proxy_protocol=True, enable_uds=False) + Test_remap_acl._ts = ts + self._ts = ts + port_name = f'ServerPort-{Test_remap_acl._ts_reload_counter}' + server_port: int = get_port(ts, port_name) + + remap_config_lines = [] + if self._deactivate_ip_allow: + remap_config_lines.append('.deactivatefilter ip_allow') + + # First, define the name ACLs (filters). + for name, definition in self._named_acls: + remap_config_lines.append(f'.definefilter {name} {definition}') + # Now activate them. + for name, _ in self._named_acls: + remap_config_lines.append(f'.activatefilter {name}') + + remap_config_lines.append(f'map / http://127.0.0.1:{server_port} {self._acl_configuration}') + + if call_reload: + # + # Update the ATS configuration. + # + tr = urtest.AddTestRun("Change the ATS configuration") + p = tr.Processes.Default + p.Command = ( + f'traffic_ctl config set proxy.config.http.connect_ports {server_port} && ' + f'traffic_ctl config set proxy.config.url_remap.acl_behavior_policy {self._acl_behavior_policy}') + + p.Env = ts.Env + tr.StillRunningAfter = ts + + remap_cfg_path = os.path.join(ts.Variables.CONFIGDIR, 'remap.config') + ip_allow_path = os.path.join(ts.Variables.CONFIGDIR, 'ip_allow.yaml') + p.Setup.Lambda( + lambda: update_config_file( + remap_cfg_path, '\n'.join(remap_config_lines), ip_allow_path, '\n'.join(self._ip_allow_lines))) + + # + # Kick off the ATS config reload and await completion. + # + Test_remap_acl._ts_reload_counter += 1 + tr = urtest.AddConfigReload(ts, expect_tasks=["remap.config"], description="Reload the ATS configuration") + tr.StillRunningAfter = ts + + else: + record_config = { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'http|url|remap|ip_allow|proxyprotocol', + 'proxy.config.http.push_method_enabled': 1, + 'proxy.config.http.connect_ports': server_port, + 'proxy.config.url_remap.acl_behavior_policy': self._acl_behavior_policy, + 'proxy.config.acl.subjects': 'PROXY,PEER', + } + + ts.Disk.records_config.update(record_config) + ts.Disk.remap_config.AddLines(remap_config_lines) + ts.Disk.ip_allow_yaml.AddLines(self._ip_allow_lines) + + return server_port + + def _configure_client(self, tr: 'TestRun', proxy_protocol: bool) -> None: + """Run the test. + + :param tr: The TestRun object to associate the client process with. + """ + + name = f"client-{Test_remap_acl._client_counter}" + ts = Test_remap_acl._ts + port = ts.Variables.port if proxy_protocol == False else ts.Variables.proxy_protocol_port + p = tr.AddVerifierClientProcess(name, self._replay_file, http_ports=[port]) + Test_remap_acl._client_counter += 1 + p.StartBefore(self._server) + if not Test_remap_acl._ts_is_started: + p.StartBefore(ts) + Test_remap_acl._ts_is_started = True + + if self._expected_responses == [None, None]: + # If there are no expected responses, expect the Warning about the rejected ip. + self._ts.Disk.diags_log.Content += Testers.ContainsExpression( + "client '127.0.0.1' prohibited by ip-allow policy", "Verify the client rejection warning message.") + + # Also, the client will complain about the broken connections. + p.ReturnCode = 1 + + else: + codes = [str(code) for code in self._expected_responses] + p.Streams.stdout += Testers.ContainsExpression( + '.*'.join(codes), "Verifying the expected order of responses", reflags=re.DOTALL | re.MULTILINE) + + class Test_old_action: + _ts_counter: int = 0 + + def __init__(self, name: str, acl_filter: str, ip_allow_content: str) -> None: + '''Test that ATS fails with a FATAL message if an old action is used with modern ACL filter policy. + + :param name: The name of the test run. + :param acl_filter: The ACL filter to use. + :param ip_allow_content: The ip_allow configuration to use. + ''' + + tr = urtest.AddTestRun(name) + ts = self._configure_traffic_server(tr, acl_filter, ip_allow_content) + + def _configure_traffic_server(self, tr: 'TestRun', acl_filter: str, ip_allow_content: str) -> 'Process': + '''Configure Traffic Server process + + :param tr: The TestRun object to associate the Traffic Server process with. + :param acl_filter: The ACL filter to configure in remap.config. + :param ip_allow_content: The ip_allow configuration to use. + :return: The Traffic Server process. + ''' + name = f"ts-old-action-{Test_old_action._ts_counter}" + Test_old_action._ts_counter += 1 + ts = tr.MakeATSProcess(name, enable_uds=False) + self._ts = ts + + ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'http|url|remap|ip_allow', + 'proxy.config.url_remap.acl_behavior_policy': 1, + }) + + ts.Disk.remap_config.AddLine(f'map / http://127.0.0.1:8080 {acl_filter}') + if ip_allow_content: + ts.Disk.ip_allow_yaml.AddLines(ip_allow_content.split("\n")) + + if acl_filter != '': + expected_error = '"allow" and "deny" are no longer valid.' + else: + expected_error = 'Legacy action name of' + + # We have to wait upon TS to emit the expected log message, but it cannot be + # the ts Ready criteria because autest might detect the process going away + # before it detects the log message. So we add a separate process that waits + # upon the log message. + watcher = tr.Processes.Process("watcher") + watcher.Command = "sleep 10" + watcher.Ready = When.FileContains(ts.Disk.diags_log.Name, expected_error) + watcher.StartBefore(ts) + + tr.Processes.Default.Command = 'printf "Fatal Shutdown Test"' + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.StartBefore(watcher) + + tr.Timeout = 5 + ts.ReturnCode = Any(33, 70) + ts.Ready = 0 + ts.Disk.diags_log.Content = Testers.IncludesExpression(expected_error, 'ATS should fatal with the old actions.') + + return ts + + IP_ALLOW_OLD_ACTION = f''' + ip_categories: + - name: ACME_LOCAL + ip_addrs: 127.0.0.1 + - name: ACME_EXTERNAL + ip_addrs: 5.6.7.8 + + ip_allow: + - apply: in + ip_addrs: 0/0 + action: allow + methods: + - GET + ''' + + IP_ALLOW_CONTENT = f''' + ip_categories: + - name: ACME_LOCAL + ip_addrs: 127.0.0.1 + - name: ACME_EXTERNAL + ip_addrs: 5.6.7.8 + + ip_allow: + - apply: in + ip_addrs: 0/0 + action: set_allow + methods: + - GET + ''' + + Test_old_action("Verify allow is reject in modern policy", "@action=allow @method=GET", IP_ALLOW_CONTENT) + Test_old_action("Verify deny is reject in modern policy", "@action=deny @method=GET", IP_ALLOW_CONTENT) + Test_old_action("Verify deny is reject in modern policy", "", IP_ALLOW_OLD_ACTION) + + test_ip_allow_optional_methods = Test_remap_acl( + "Verify non-allowed methods are blocked.", + replay_file='remap_acl_get_post_allowed.replay.yaml', + ip_allow_content=IP_ALLOW_CONTENT, + deactivate_ip_allow=False, + acl_behavior_policy=1, + acl_configuration='@action=set_allow @src_ip=127.0.0.1 @method=GET @method=POST', + named_acls=[], + expected_responses=[200, 200, 403, 403, 403], + proxy_protocol=False) + + test_ip_allow_optional_methods_pp = Test_remap_acl( + "Verify non-allowed methods are blocked (PP).", + replay_file='remap_acl_get_post_allowed_pp.replay.yaml', + ip_allow_content=IP_ALLOW_CONTENT, + deactivate_ip_allow=False, + acl_behavior_policy=1, + acl_configuration='@action=set_allow @src_ip=1.2.3.4 @method=GET @method=POST', + named_acls=[], + expected_responses=[200, 200, 403, 403, 403], + proxy_protocol=True) + + test_ip_allow_optional_methods = Test_remap_acl( + "Verify add_allow adds an allowed method.", + replay_file='remap_acl_get_post_allowed.replay.yaml', + ip_allow_content=IP_ALLOW_CONTENT, + deactivate_ip_allow=False, + acl_behavior_policy=1, + acl_configuration='@action=add_allow @src_ip=127.0.0.1 @method=POST', + named_acls=[], + expected_responses=[200, 200, 403, 403, 403], + proxy_protocol=False) + + test_ip_allow_optional_methods = Test_remap_acl( + "Verify add_allow adds allowed methods.", + replay_file='remap_acl_get_post_allowed.replay.yaml', + ip_allow_content=IP_ALLOW_CONTENT, + deactivate_ip_allow=False, + acl_behavior_policy=1, + acl_configuration='@action=add_allow @src_ip=127.0.0.1 @method=GET @method=POST', + named_acls=[], + expected_responses=[200, 200, 403, 403, 403], + proxy_protocol=False) + + test_ip_allow_optional_methods = Test_remap_acl( + "Verify if no ACLs match, ip_allow.yaml is used.", + replay_file='remap_acl_get_allowed.replay.yaml', + ip_allow_content=IP_ALLOW_CONTENT, + deactivate_ip_allow=False, + acl_behavior_policy=1, + acl_configuration='@action=set_allow @src_ip=1.2.3.4 @method=GET @method=POST', + named_acls=[], + expected_responses=[200, 403, 403, 403, 403], + proxy_protocol=False) + + test_ip_allow_optional_methods = Test_remap_acl( + "Verify @src_ip=all works.", + replay_file='remap_acl_get_post_allowed.replay.yaml', + ip_allow_content=IP_ALLOW_CONTENT, + deactivate_ip_allow=False, + acl_behavior_policy=1, + acl_configuration='@action=set_allow @src_ip=all @method=GET @method=POST', + named_acls=[], + expected_responses=[200, 200, 403, 403, 403], + proxy_protocol=False) + + test_ip_allow_optional_methods = Test_remap_acl( + "Verify @src_ip_category works.", + replay_file='remap_acl_get_post_allowed.replay.yaml', + ip_allow_content=IP_ALLOW_CONTENT, + deactivate_ip_allow=False, + acl_behavior_policy=1, + acl_configuration='@action=set_allow @src_ip_category=ACME_LOCAL @method=GET @method=POST', + named_acls=[], + expected_responses=[200, 200, 403, 403, 403], + proxy_protocol=False) + + test_ip_allow_optional_methods = Test_remap_acl( + "Verify no @src_ip implies all IP addresses.", + replay_file='remap_acl_get_post_allowed.replay.yaml', + ip_allow_content=IP_ALLOW_CONTENT, + deactivate_ip_allow=False, + acl_behavior_policy=1, + acl_configuration='@action=set_allow @method=GET @method=POST', + named_acls=[], + expected_responses=[200, 200, 403, 403, 403], + proxy_protocol=False) + + test_ip_allow_optional_methods = Test_remap_acl( + "Verify denied methods are blocked.", + replay_file='remap_acl_get_post_denied.replay.yaml', + ip_allow_content=IP_ALLOW_CONTENT, + deactivate_ip_allow=False, + acl_behavior_policy=1, + acl_configuration='@action=set_deny @src_ip=127.0.0.1 @method=GET @method=POST', + named_acls=[], + expected_responses=[403, 403, 200, 200, 400], + proxy_protocol=False) + + test_ip_allow_optional_methods = Test_remap_acl( + "Verify add_deny adds blocked methods.", + replay_file='remap_acl_all_denied.replay.yaml', + ip_allow_content=IP_ALLOW_CONTENT, + deactivate_ip_allow=False, + acl_behavior_policy=1, + acl_configuration='@action=add_deny @src_ip=127.0.0.1 @method=GET', + named_acls=[], + expected_responses=[403, 403, 403, 403, 403], + proxy_protocol=False) + + test_ip_allow_optional_methods = Test_remap_acl( + "Verify a default deny filter rule works.", + replay_file='remap_acl_all_denied.replay.yaml', + ip_allow_content=IP_ALLOW_CONTENT, + deactivate_ip_allow=False, + acl_behavior_policy=1, + acl_configuration='@action=set_allow @src_ip=1.2.3.4 @method=GET @method=POST', + named_acls=[('deny', '@action=set_deny')], + expected_responses=[403, 403, 403, 403, 403], + proxy_protocol=False) + + test_ip_allow_optional_methods = Test_remap_acl( + "Verify inverting @src_ip works.", + replay_file='remap_acl_all_denied.replay.yaml', + ip_allow_content=IP_ALLOW_CONTENT, + deactivate_ip_allow=False, + acl_behavior_policy=1, + acl_configuration='@action=set_allow @src_ip=~127.0.0.1 @method=GET @method=POST', + named_acls=[('deny', '@action=set_deny')], + expected_responses=[403, 403, 403, 403, 403], + proxy_protocol=False) + + test_ip_allow_optional_methods = Test_remap_acl( + "Verify inverting @src_ip works with the rule matching.", + replay_file='remap_acl_get_post_allowed.replay.yaml', + ip_allow_content=IP_ALLOW_CONTENT, + deactivate_ip_allow=False, + acl_behavior_policy=1, + acl_configuration='@action=set_allow @src_ip=~3.4.5.6 @method=GET @method=POST', + named_acls=[('deny', '@action=set_deny')], + expected_responses=[200, 200, 403, 403, 403], + proxy_protocol=False) + + test_ip_allow_optional_methods = Test_remap_acl( + "Verify inverting @src_ip_category works.", + replay_file='remap_acl_all_denied.replay.yaml', + ip_allow_content=IP_ALLOW_CONTENT, + deactivate_ip_allow=False, + acl_behavior_policy=1, + acl_configuration='@action=set_allow @src_ip_category=~ACME_LOCAL @method=GET @method=POST', + named_acls=[('deny', '@action=set_deny')], + expected_responses=[403, 403, 403, 403, 403], + proxy_protocol=False) + + test_ip_allow_optional_methods = Test_remap_acl( + "Verify inverting @src_ip_category works with the rule matching.", + replay_file='remap_acl_get_post_allowed.replay.yaml', + ip_allow_content=IP_ALLOW_CONTENT, + deactivate_ip_allow=False, + acl_behavior_policy=1, + acl_configuration='@action=set_allow @src_ip_category=~ACME_EXTERNAL @method=GET @method=POST', + named_acls=[('deny', '@action=set_deny')], + expected_responses=[200, 200, 403, 403, 403], + proxy_protocol=False) + + test_ip_allow_optional_methods = Test_remap_acl( + "Verify @src_ip and @src_ip_category AND together.", + replay_file='remap_acl_all_denied.replay.yaml', + ip_allow_content=IP_ALLOW_CONTENT, + deactivate_ip_allow=False, + acl_behavior_policy=1, + # The rule will not match because, while @src_ip matches, @src_ip_category does not. + acl_configuration='@action=set_allow @src_ip=127.0.0.1 @src_ip_category=ACME_EXTERNAL @method=GET @method=POST', + # Therefore, this named deny filter will block. + named_acls=[('deny', '@action=set_deny')], + expected_responses=[403, 403, 403, 403, 403], + proxy_protocol=False) + + test_ip_allow_optional_methods = Test_remap_acl( + "Verify defined in-line ACLS are evaluated before named ones.", + replay_file='remap_acl_get_post_allowed.replay.yaml', + ip_allow_content=IP_ALLOW_CONTENT, + deactivate_ip_allow=False, + acl_behavior_policy=1, + acl_configuration='@action=set_allow @src_ip=127.0.0.1 @method=GET @method=POST', + named_acls=[('deny', '@action=set_deny')], + expected_responses=[200, 200, 403, 403, 403], + proxy_protocol=False) + + test_ip_allow_optional_methods = Test_remap_acl( + "Verify remap.config line overrides ip_allow rule.", + replay_file='remap_acl_get_post_allowed.replay.yaml', + ip_allow_content=IP_ALLOW_CONTENT, + deactivate_ip_allow=False, + acl_behavior_policy=1, + acl_configuration='@action=set_allow @src_ip=127.0.0.1 @method=GET @method=POST', + named_acls=[], + expected_responses=[200, 200, 403, 403, 403], + proxy_protocol=False) + + test_ip_allow_optional_methods = Test_remap_acl( + "Verify we can deactivate the ip_allow filter.", + replay_file='remap_acl_all_allowed.replay.yaml', + ip_allow_content=IP_ALLOW_CONTENT, + deactivate_ip_allow=True, + acl_behavior_policy=1, + # This won't match, so nothing will match since ip_allow.yaml is off. + acl_configuration='@action=set_allow @src_ip=1.2.3.4 @method=GET @method=POST', + named_acls=[], + # Nothing will block the request since ip_allow.yaml is off. + expected_responses=[200, 200, 200, 200, 400], + proxy_protocol=False) + + test_ip_allow_optional_methods = Test_remap_acl( + "Verify in_ip matches on IP as expected.", + replay_file='remap_acl_get_post_allowed.replay.yaml', + ip_allow_content=IP_ALLOW_CONTENT, + deactivate_ip_allow=False, + acl_behavior_policy=1, + acl_configuration='@action=set_allow @in_ip=127.0.0.1 @method=GET @method=POST', + named_acls=[], + expected_responses=[200, 200, 403, 403, 403], + proxy_protocol=False) + + test_ip_allow_optional_methods = Test_remap_acl( + "Verify in_ip rules do not match on other IPs.", + replay_file='remap_acl_get_allowed.replay.yaml', + ip_allow_content=IP_ALLOW_CONTENT, + deactivate_ip_allow=False, + acl_behavior_policy=1, + acl_configuration='@action=set_allow @in_ip=3.4.5.6 @method=GET @method=POST', + named_acls=[], + expected_responses=[200, 403, 403, 403, 403], + proxy_protocol=False) + + test_named_acl_deny = Test_remap_acl( + "Verify a named ACL is applied if an in-line ACL is absent.", + replay_file='deny_head_post.replay.yaml', + ip_allow_content=IP_ALLOW_CONTENT, + deactivate_ip_allow=False, + acl_behavior_policy=1, + acl_configuration='', + named_acls=[('deny', '@action=set_deny @method=HEAD @method=POST')], + expected_responses=[200, 403, 403, 403], + proxy_protocol=False) + + def replay_proxy_response(filename, replay_file, get_proxy_response, post_proxy_response): + """ + replay_proxy_response writes the given replay file (which expects a single GET & POST client-request) + with the given proxy_response value. This is only used to support the tests in the combination table. + """ + + current_dir = os.path.dirname(inspect.getfile(inspect.currentframe())) + path = os.path.join(current_dir, filename) + data = None + with open(path) as f: + data = load(f, Loader=Loader) + for session in data["sessions"]: + for transaction in session["transactions"]: + method = transaction["client-request"]["method"] + if method == "GET": + transaction["proxy-response"]["status"] = 403 if get_proxy_response == None else get_proxy_response + elif method == "POST": + transaction["proxy-response"]["status"] = 403 if post_proxy_response == None else post_proxy_response + else: + raise Exception("Expected to find GET or POST request, found %s", method) + with open(replay_file, "w") as f: + f.write(dump(data)) + + from deactivate_ip_allow import all_deactivate_ip_allow_tests + from all_acl_combinations import all_acl_combination_tests + """ + Test all acl combinations + """ + for idx, test in enumerate(all_acl_combination_tests): + (fd, replay_file_name) = tempfile.mkstemp(suffix="acl_table_test_{}.replay".format(idx), dir=_replay_tmpdir) + os.close(fd) + replay_proxy_response( + "base.replay.yaml", + replay_file_name, + test["GET response"], + test["POST response"], + ) + Test_remap_acl( + "allcombo-{0} {1} {2} {3}".format(idx, test["inline"], test["named_acl"], test["ip_allow"]), + replay_file=replay_file_name, + ip_allow_content=test["ip_allow"], + deactivate_ip_allow=False, + acl_behavior_policy=0 if test["policy"] == "legacy" else 1, + acl_configuration=test["inline"], + named_acls=[("acl", test["named_acl"])] if test["named_acl"] != "" else [], + expected_responses=[test["GET response"], test["POST response"]], + proxy_protocol=False, + ) + """ + Test all ACL combinations + """ + for idx, test in enumerate(all_deactivate_ip_allow_tests): + try: + test["deactivate_ip_allow"] + except: + print(test) + (fd, replay_file_name) = tempfile.mkstemp(suffix="deactivate_ip_allow_table_test_{}.replay".format(idx), dir=_replay_tmpdir) + os.close(fd) + replay_proxy_response( + "base.replay.yaml", + replay_file_name, + test["GET response"], + test["POST response"], + ) + Test_remap_acl( + "ipallow-{0} {1} {2} {3}".format(idx, test["inline"], test["named_acl"], test["ip_allow"]), + replay_file=replay_file_name, + ip_allow_content=test["ip_allow"], + deactivate_ip_allow=test["deactivate_ip_allow"], + acl_behavior_policy=0 if test["policy"] == "legacy" else 1, + acl_configuration=test["inline"], + named_acls=[("acl", test["named_acl"])] if test["named_acl"] != "" else [], + expected_responses=[test["GET response"], test["POST response"]], + proxy_protocol=False, + ) + urtest.execute() diff --git a/tests/uranium_tests/remap/test_remap_http.py b/tests/uranium_tests/remap/test_remap_http.py new file mode 100644 index 00000000000..626c8ec692b --- /dev/null +++ b/tests/uranium_tests/remap/test_remap_http.py @@ -0,0 +1,190 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_remap_http(urtest: UraniumTest) -> None: + ''' + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + urtest.Summary = ''' + Test a basic remap of a http connection + ''' + + urtest.ContinueOnFail = True + # Define default ATS + ts = urtest.MakeATSProcess("ts") + server = urtest.MakeOriginServer("server") + server2 = urtest.MakeOriginServer("server2", lookup_key="{%Host}{PATH}") + dns = urtest.MakeDNServer("dns") + + urtest.testName = "" + request_header = {"headers": "GET / HTTP/1.1\r\nHost: www.example.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + # expected response from the origin server + response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + + request_header2 = { + "headers": "GET /test HTTP/1.1\r\nHost: www.testexample.com\r\n\r\n", + "timestamp": "1469733493.993", + "body": "" + } + # expected response from the origin server + response_header2 = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + + # add response to the server dictionary + server.addResponse("sessionfile.log", request_header, response_header) + server2.addResponse("sessionfile.log", request_header2, response_header2) + ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'http.*|dns|conf_remap', + 'proxy.config.http.referer_filter': 1, + 'proxy.config.dns.nameservers': '127.0.0.1:{0}'.format(dns.Variables.Port), + 'proxy.config.dns.resolv_conf': 'NULL' + }) + + ts.Disk.remap_config.AddLine('map http://www.example.com http://127.0.0.1:{0}'.format(server.Variables.Port)) + ts.Disk.remap_config.AddLine( + 'map_with_recv_port http://www.example2.com:{0} http://127.0.0.1:{1}'.format(ts.Variables.port, server.Variables.Port)) + ts.Disk.remap_config.AddLine('map http://www.example.com:8080 http://127.0.0.1:{0}'.format(server.Variables.Port)) + ts.Disk.remap_config.AddLine('redirect http://test3.com http://httpbin.org'.format(server.Variables.Port)) + ts.Disk.remap_config.AddLine( + 'map_with_referer http://test4.com http://127.0.0.1:{0} http://httpbin.org (.*[.])?persia[.]com'.format( + server.Variables.Port)) + ts.Disk.remap_config.AddLine('map http://testDNS.com http://audrey.hepburn.com:{0}'.format(server.Variables.Port)) + + ts.Disk.remap_config.AddLine( + 'map http://www.testexample.com http://127.0.0.1:{0} @plugin=conf_remap.so @pparam=proxy.config.url_remap.pristine_host_hdr=1' + .format(server2.Variables.Port)) + + dns.addRecords(records={"audrey.hepburn.com.": ["127.0.0.1"]}) + dns.addRecords(records={"whatever.com.": ["127.0.0.1"]}) + + # call localhost straight + tr = urtest.AddTestRun() + tr.MakeCurlCommand('"http://127.0.0.1:{0}/" --verbose'.format(ts.Variables.port), ts=ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.StartBefore(server) + tr.Processes.Default.StartBefore(dns) + tr.Processes.Default.StartBefore(urtest.Processes.ts) + tr.Processes.Default.Streams.stderr = "gold/remap-hitATS-404.gold" + tr.StillRunningAfter = server + + # www.example.com host + tr = urtest.AddTestRun() + tr.MakeCurlCommand( + '--proxy 127.0.0.1:{0} "http://www.example.com" -H "Proxy-Connection: keep-alive" --verbose'.format(ts.Variables.port), + ts=ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.stderr = "gold/remap-200.gold" + + # www.example2.com host (match on receive port) + # map_with_recv_port doesn't work with UDS + if not Condition.CurlUsingUnixDomainSocket(): + tr = urtest.AddTestRun() + tr.MakeCurlCommand( + '--proxy 127.0.0.1:{0} "http://www.example2.com" -H "Proxy-Connection: keep-alive" --verbose'.format( + ts.Variables.port), + ts=ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.stderr = "gold/remap2-200.gold" + + # www.example.com:80 host + tr = urtest.AddTestRun() + tr.MakeCurlCommand( + ' --proxy 127.0.0.1:{0} "http://www.example.com:80/" -H "Proxy-Connection: keep-alive" --verbose'.format( + ts.Variables.port), + ts=ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.stderr = "gold/remap-200.gold" + + # www.example.com:8080 host + tr = urtest.AddTestRun() + tr.MakeCurlCommand( + ' --proxy 127.0.0.1:{0} "http://www.example.com:8080" -H "Proxy-Connection: keep-alive" --verbose'.format( + ts.Variables.port), + ts=ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.stderr = "gold/remap-200.gold" + + # no rule for this + tr = urtest.AddTestRun() + tr.MakeCurlCommand( + ' --proxy 127.0.0.1:{0} "http://www.test.com/" -H "Proxy-Connection: keep-alive" --verbose'.format(ts.Variables.port), + ts=ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.stderr = "gold/remap-404.gold" + + # redirect result + tr = urtest.AddTestRun() + tr.MakeCurlCommand(' --proxy 127.0.0.1:{0} "http://test3.com" --verbose'.format(ts.Variables.port), ts=ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.stderr = "gold/remap-redirect.gold" + + # referer hit + tr = urtest.AddTestRun() + tr.MakeCurlCommand( + ' --proxy 127.0.0.1:{0} "http://test4.com" --header "Referer: persia.com" --verbose'.format(ts.Variables.port), ts=ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.stderr = "gold/remap-referer-hit.gold" + + # referer miss + tr = urtest.AddTestRun() + tr.MakeCurlCommand( + ' --proxy 127.0.0.1:{0} "http://test4.com" --header "Referer: monkey.com" --verbose'.format(ts.Variables.port), ts=ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.stderr = "gold/remap-referer-miss.gold" + + # referer hit + tr = urtest.AddTestRun() + tr.MakeCurlCommand( + ' --proxy 127.0.0.1:{0} "http://test4.com" --header "Referer: www.persia.com" --verbose'.format(ts.Variables.port), ts=ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.stderr = "gold/remap-referer-hit.gold" + + # DNS test + tr = urtest.AddTestRun() + tr.MakeCurlCommand(' --proxy 127.0.0.1:{0} "http://testDNS.com" --verbose'.format(ts.Variables.port), ts=ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.stderr = "gold/remap-DNS-200.gold" + + # microserver lookup test + tr = urtest.AddTestRun() + tr.MakeCurlCommand( + '--proxy 127.0.0.1:{0} "http://www.testexample.com/test" -H "Host: www.testexample.com" --verbose'.format( + ts.Variables.port), + ts=ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.StartBefore(server2) + tr.Processes.Default.Streams.stderr = "gold/lookupTest.gold" + tr.StillRunningAfter = server2 + urtest.execute() diff --git a/tests/uranium_tests/remap/test_remap_https.py b/tests/uranium_tests/remap/test_remap_https.py new file mode 100644 index 00000000000..a9bb834eeb6 --- /dev/null +++ b/tests/uranium_tests/remap/test_remap_https.py @@ -0,0 +1,148 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_remap_https(urtest: UraniumTest) -> None: + ''' + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + urtest.Summary = ''' + Test a basic remap of a http connection + ''' + + urtest.SkipIf(Condition.CurlUsingUnixDomainSocket()) + urtest.ContinueOnFail = True + # Define default ATS + ts = urtest.MakeATSProcess("ts", enable_tls=True) + server = urtest.MakeOriginServer("server") + server2 = urtest.MakeOriginServer("server2", ssl=True) + + # **testname is required** + testName = "" + request_header = {"headers": "GET / HTTP/1.1\r\nHost: www.example.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + # desired response form the origin server + response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + server.addResponse("sessionlog.json", request_header, response_header) + server2.addResponse("sessionlog.json", request_header, response_header) + + # add ssl materials like key, certificates for the server + ts.addDefaultSSLFiles() + + ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'lm|ssl', + 'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir), + 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts.Variables.SSLDir), + # enable ssl port + 'proxy.config.http.server_ports': '{0} {1}:proto=http2;http:ssl'.format(ts.Variables.port, ts.Variables.ssl_port), + 'proxy.config.ssl.client.verify.server.policy': 'PERMISSIVE', + }) + + ts.Disk.remap_config.AddLine('map https://www.example.com http://127.0.0.1:{0}'.format(server.Variables.Port)) + ts.Disk.remap_config.AddLine( + 'map https://www.example.com:{1} http://127.0.0.1:{0}'.format(server.Variables.Port, ts.Variables.ssl_port)) + ts.Disk.remap_config.AddLine( + 'map_with_recv_port https://www.example3.com:{1} http://127.0.0.1:{0}'.format(server.Variables.Port, ts.Variables.ssl_port)) + ts.Disk.remap_config.AddLine( + 'map https://www.anotherexample.com https://127.0.0.1:{0}'.format(server2.Variables.SSL_Port, ts.Variables.ssl_port)) + + ts.Disk.ssl_multicert_yaml.AddLines( + """ + ssl_multicert: + - dest_ip: "*" + ssl_cert_name: server.pem + ssl_key_name: server.key + """.split("\n")) + + # call localhost straight + tr = urtest.AddTestRun() + tr.MakeCurlCommand('--http1.1 -k https://127.0.0.1:{0} --verbose'.format(ts.Variables.ssl_port), ts=ts) + tr.Processes.Default.ReturnCode = 0 + + tr.Processes.Default.StartBefore(server) + tr.Processes.Default.StartBefore(server2) + tr.Processes.Default.StartBefore(urtest.Processes.ts) + tr.Processes.Default.Streams.stderr = "gold/remap-hitATS-404.gold" + tr.StillRunningAfter = server + tr.StillRunningAfter = ts + + # www.example.com host + tr = urtest.AddTestRun() + tr.MakeCurlCommand( + '--http1.1 -k https://127.0.0.1:{0} -H "Host: www.example.com" --verbose'.format(ts.Variables.ssl_port), ts=ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.stderr = "gold/remap-https-200.gold" + + # www.example.com:80 host + tr = urtest.AddTestRun() + tr.MakeCurlCommand( + '--http1.1 -k https://127.0.0.1:{0} -H "Host: www.example.com:443" --verbose'.format(ts.Variables.ssl_port), ts=ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.stderr = "gold/remap-https-200.gold" + + # www.example.com:8080 host + tr = urtest.AddTestRun() + tr.MakeCurlCommand( + '--http1.1 -k https://127.0.0.1:{0} -H "Host: www.example.com:{0}" --verbose'.format(ts.Variables.ssl_port), ts=ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.stderr = "gold/remap-https-200.gold" + + # www.example3.com (match on receive port) + tr = urtest.AddTestRun() + tr.MakeCurlCommand( + '--http1.1 -k https://127.0.0.1:{0} -H "Host: www.example3.com" --verbose'.format(ts.Variables.ssl_port), ts=ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.stderr = "gold/remap-https-200_3.gold" + + # no rule for this + tr = urtest.AddTestRun() + tr.MakeCurlCommand('--http1.1 -k https://127.0.0.1:{0} -H "Host: www.test.com" --verbose'.format(ts.Variables.ssl_port), ts=ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.stderr = "gold/remap-hitATS-404.gold" + + # bad port + tr = urtest.AddTestRun() + tr.MakeCurlCommand( + '--http1.1 -k https://127.0.0.1:{0} -H "Host: www.example.com:1234" --verbose'.format(ts.Variables.ssl_port), ts=ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.stderr = "gold/remap-hitATS-404.gold" + + # map www.anotherexample.com to https://.com + tr = urtest.AddTestRun() + tr.MakeCurlCommand( + '--http1.1 -k https://127.0.0.1:{0} -H "Host: www.anotherexample.com" --verbose'.format(ts.Variables.ssl_port), ts=ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.stderr = "gold/remap-https-200_2.gold" + tr.StillRunningAfter = server2 + urtest.execute() diff --git a/tests/uranium_tests/remap/test_remap_ip_resolve.py b/tests/uranium_tests/remap/test_remap_ip_resolve.py new file mode 100644 index 00000000000..c863e1851fe --- /dev/null +++ b/tests/uranium_tests/remap/test_remap_ip_resolve.py @@ -0,0 +1,94 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_remap_ip_resolve(urtest: UraniumTest) -> None: + ''' + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + urtest.Summary = ''' + Test a basic ip_resolve override using an ipv6 server + ''' + + urtest.ContinueOnFail = True + # Define default ATS + ts = urtest.MakeATSProcess("ts") + server = urtest.MakeOriginServer("server") + server_v6 = urtest.MakeOriginServer("server_v6", None, None, '::1', 0) + + dns = urtest.MakeDNServer("dns") + + urtest.testName = "" + request_header = {"headers": "GET / HTTP/1.1\r\nHost: www.example.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + # expected response from the origin server + response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + + # add response to the server dictionary + server.addResponse("sessionfile.log", request_header, response_header) + server_v6.addResponse("sessionfile.log", request_header, response_header) + ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'http.*|dns|conf_remap', + 'proxy.config.http.referer_filter': 1, + 'proxy.config.dns.nameservers': '127.0.0.1:{0}'.format(dns.Variables.Port), + 'proxy.config.dns.resolv_conf': 'NULL', + 'proxy.config.hostdb.ip_resolve': 'ipv4' + }) + + ts.Disk.remap_config.AddLine( + 'map http://testDNS.com http://test.ipv4.only.com:{0} @plugin=conf_remap.so @pparam=proxy.config.hostdb.ip_resolve=ipv6;ipv4;client' + .format(server.Variables.Port)) + ts.Disk.remap_config.AddLine( + 'map http://testDNS2.com http://test.ipv6.only.com:{0} @plugin=conf_remap.so @pparam=proxy.config.hostdb.ip_resolve=ipv6;only' + .format(server_v6.Variables.Port)) + + dns.addRecords(records={"test.ipv4.only.com.": ["127.0.0.1"]}) + dns.addRecords(records={"test.ipv6.only.com": ["127.0.0.1", "::1"]}) + + tr = urtest.AddTestRun() + tr.MakeCurlCommand(' --proxy 127.0.0.1:{0} "http://testDNS.com" --verbose'.format(ts.Variables.port), ts=ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.StartBefore(server) + tr.Processes.Default.StartBefore(dns) + tr.Processes.Default.StartBefore(urtest.Processes.ts) + tr.Processes.Default.Streams.stderr = "gold/remap-DNS-200.gold" + tr.StillRunningAfter = server + + tr = urtest.AddTestRun() + tr.MakeCurlCommand(' --proxy 127.0.0.1:{0} "http://testDNS2.com" --verbose'.format(ts.Variables.port), ts=ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.StartBefore(server_v6) + tr.Processes.Default.Streams.stderr = "gold/remap-DNS-ipv6-200.gold" + tr.StillRunningAfter = server_v6 + urtest.execute() diff --git a/tests/uranium_tests/remap/test_remap_load_empty_failure.py b/tests/uranium_tests/remap/test_remap_load_empty_failure.py new file mode 100644 index 00000000000..23855a6e973 --- /dev/null +++ b/tests/uranium_tests/remap/test_remap_load_empty_failure.py @@ -0,0 +1,63 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_remap_load_empty_failure(urtest: UraniumTest) -> None: + ''' + Verify correct behavior of regex_map in remap.config. + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + urtest.Summary = ''' + Test minimum rules on load - fail on missing file. + ''' + ts = urtest.MakeATSProcess("ts") + ts.Disk.remap_config.AddLine(f"") # empty file + ts.Disk.records_config.update({'proxy.config.url_remap.min_rules_required': 1}) + ts.ReturnCode = 33 # expect to Emergency fail due to empty "remap.config". + ts.Ready = 0 + + tr = urtest.AddTestRun("test") + + # We have to wait upon TS to emit the expected log message, but it cannot be + # the ts Ready criteria because autest might detect the process going away + # before it detects the log message. So we add a separate process that waits + # upon the log message. + watcher = urtest.Processes.Process("watcher") + watcher.Command = "sleep 10" + watcher.Ready = When.FileContains(ts.Disk.diags_log.Name, "remap.config failed to load") + watcher.StartBefore(ts) + + tr.Processes.Default.Command = "echo howdy" + tr.TimeOut = 5 + tr.Processes.Default.StartBefore(watcher) + urtest.execute() diff --git a/tests/uranium_tests/remap/test_remap_load_empty_success.py b/tests/uranium_tests/remap/test_remap_load_empty_success.py new file mode 100644 index 00000000000..8526be39cd9 --- /dev/null +++ b/tests/uranium_tests/remap/test_remap_load_empty_success.py @@ -0,0 +1,54 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_remap_load_empty_success(urtest: UraniumTest) -> None: + ''' + Verify correct behavior of regex_map in remap.config. + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + urtest.Summary = ''' + Test minimum rules on load - succeed on empty file. + ''' + + ts = urtest.MakeATSProcess("ts") + ts.Disk.remap_config.AddLine(f"") # empty file + ts.Disk.records_config.update({f'proxy.config.url_remap.min_rules_required': 0}) + + tr = urtest.AddTestRun("startup") + p = tr.Processes.Default + p.Command = "sh -c echo" + p.ReturnCode = 0 + p.StartBefore(urtest.Processes.ts) + tr.StillRunningAfter = ts + urtest.execute() diff --git a/tests/uranium_tests/remap/test_remap_load_missing_failure.py b/tests/uranium_tests/remap/test_remap_load_missing_failure.py new file mode 100644 index 00000000000..43f36038973 --- /dev/null +++ b/tests/uranium_tests/remap/test_remap_load_missing_failure.py @@ -0,0 +1,50 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_remap_load_missing_failure(urtest: UraniumTest) -> None: + ''' + Verify correct behavior of regex_map in remap.config. + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + urtest.Summary = ''' + Test minimum rules on load - fail on missing file. + ''' + ts = urtest.MakeATSProcess("ts") + ts.Disk.records_config.update({'proxy.config.url_remap.min_rules_required': 1}) + ts.ReturnCode = 33 # expect to Emergency fail due to empty "remap.config". + + tr = urtest.AddTestRun("test") + tr.Processes.Default.Command = "echo" + tr.Processes.Default.StartAfter(ts, ready=When.FileExists(ts.Disk.diags_log)) + urtest.execute() diff --git a/tests/uranium_tests/remap/test_remap_load_missing_success.py b/tests/uranium_tests/remap/test_remap_load_missing_success.py new file mode 100644 index 00000000000..fed3ab72059 --- /dev/null +++ b/tests/uranium_tests/remap/test_remap_load_missing_success.py @@ -0,0 +1,54 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_remap_load_missing_success(urtest: UraniumTest) -> None: + ''' + Verify correct behavior of regex_map in remap.config. + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + urtest.Summary = ''' + Test minimum rules on load - succeed on missing file. + ''' + + ts = urtest.MakeATSProcess("ts") + + ts.Disk.records_config.update({f'proxy.config.url_remap.min_rules_required': 0}) + + tr = urtest.AddTestRun("startup") + p = tr.Processes.Default + p.Command = "sh -c echo" + p.ReturnCode = 0 + p.StartBefore(urtest.Processes.ts) + tr.StillRunningAfter = ts + urtest.execute() diff --git a/tests/uranium_tests/remap/test_remap_reload.py b/tests/uranium_tests/remap/test_remap_reload.py new file mode 100644 index 00000000000..e62757c5832 --- /dev/null +++ b/tests/uranium_tests/remap/test_remap_reload.py @@ -0,0 +1,129 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_remap_reload(urtest: UraniumTest) -> None: + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + import os + + def update_remap_config(path: str, lines: list) -> None: + """Update the remap.config file. + + This is used to update the config file between test runs without + triggering framework warnings about overriding file objects. + + :param path: The path to the remap.config file. + :param lines: The list of lines to write to the file. + """ + with open(path, 'w') as f: + f.write('\n'.join(lines) + '\n') + + urtest.Summary = ''' + Test remap reloading + ''' + urtest.testName = 'remap_reload' + + replay_file_1 = "reload_1.replay.yaml" + replay_file_2 = "reload_2.replay.yaml" + replay_file_3 = "reload_3.replay.yaml" + replay_file_4 = "reload_4.replay.yaml" + + tm = urtest.MakeATSProcess("ts") + tm.Disk.diags_log.Content = Testers.ContainsExpression("remap.config failed to load", "Remap should fail to load") + remap_cfg_path = os.path.join(tm.Variables.CONFIGDIR, 'remap.config') + + pv = urtest.MakeVerifierServerProcess("pv", "reload_server.replay.yaml") + pv_port = pv.Variables.http_port + tm.Disk.remap_config.AddLines( + [ + f"map http://alpha.ex http://alpha.ex:{pv_port}", + f"map http://bravo.ex http://bravo.ex:{pv_port}", + f"map http://charlie.ex http://charlie.ex:{pv_port}", + f"map http://delta.ex http://delta.ex:{pv_port}", + ]) + tm.Disk.records_config.update({'proxy.config.url_remap.min_rules_required': 3}) + + nameserver = urtest.MakeDNServer("dns", default='127.0.0.1') + tm.Disk.records_config.update( + { + 'proxy.config.dns.nameservers': f"127.0.0.1:{nameserver.Variables.Port}", + 'proxy.config.dns.resolv_conf': 'NULL', + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'remap|config|file|rpc', + }) + + tr = urtest.AddTestRun("verify load") + tr.Processes.Default.StartBefore(pv) + tr.Processes.Default.StartBefore(nameserver) + tr.Processes.Default.StartBefore(tm) + tr.AddVerifierClientProcess("client", replay_file_1, http_ports=[tm.Variables.port]) + + tr = urtest.AddTestRun("Change remap.config to have only two lines") + p = tr.Processes.Default + p.Env = tm.Env + p.Command = 'echo "Change remap.config, two lines"' + p.Setup.Lambda( + lambda: update_remap_config( + remap_cfg_path, [ + f"map http://alpha.ex http://alpha.ex:{pv_port}", + f"map http://bravo.ex http://bravo.ex:{pv_port}", + ])) + + tr = urtest.AddConfigReload( + tm, expect="fail", expect_tasks=["remap.config"], delay_start=2, description="remap_config reload, fails") + + tr = urtest.AddTestRun("after first reload") + tr.AddVerifierClientProcess("client_2", replay_file_2, http_ports=[tm.Variables.port]) + + tr = urtest.AddTestRun("Change remap.config to have more than three lines") + p = tr.Processes.Default + p.Env = tm.Env + p.Command = 'echo "Change remap.config, more than three lines"' + p.Setup.Lambda( + lambda: update_remap_config( + remap_cfg_path, [ + f"map http://echo.ex http://echo.ex:{pv_port}", + f"map http://foxtrot.ex http://foxtrot.ex:{pv_port}", + f"map http://golf.ex http://golf.ex:{pv_port}", + f"map http://hotel.ex http://hotel.ex:{pv_port}", + f"map http://india.ex http://india.ex:{pv_port}", + ])) + + tr = urtest.AddConfigReload( + tm, expect="success", expect_tasks=["remap.config"], delay_start=2, description="remap_config reload, succeeds") + + tr = urtest.AddTestRun("post update charlie") + tr.AddVerifierClientProcess("client_3", replay_file_3, http_ports=[tm.Variables.port]) + + tr = urtest.AddTestRun("post update golf") + tr.AddVerifierClientProcess("client_4", replay_file_4, http_ports=[tm.Variables.port]) + urtest.execute() diff --git a/tests/uranium_tests/remap/test_remap_ws.py b/tests/uranium_tests/remap/test_remap_ws.py new file mode 100644 index 00000000000..c9777f4e67c --- /dev/null +++ b/tests/uranium_tests/remap/test_remap_ws.py @@ -0,0 +1,148 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_remap_ws(urtest: UraniumTest) -> None: + ''' + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + urtest.Summary = ''' + Test a basic remap of a websocket connections + ''' + + urtest.ContinueOnFail = True + + ts = urtest.MakeATSProcess("ts", enable_tls=True) + server = urtest.MakeOriginServer("server") + + testName = "Test WebSocket Remaps" + request_header = { + "headers": "GET /chat HTTP/1.1\r\nHost: www.example.com\r\nUpgrade: websocket\r\nConnection: Upgrade\r\n\r\n", + "body": None + } + response_header = { + "headers": + "HTTP/1.1 101 OK\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=\r\n\r\n", + "body": None + } + server.addResponse("sessionlog.json", request_header, response_header) + + ts.addDefaultSSLFiles() + + ts.Disk.records_config.update( + { + 'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir), + 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts.Variables.SSLDir), + }) + + ts.Disk.remap_config.AddLines( + [ + 'map ws://www.example.com:{1} ws://127.0.0.1:{0}'.format(server.Variables.Port, ts.Variables.port), + 'map wss://www.example.com:{1} ws://127.0.0.1:{0}'.format(server.Variables.Port, ts.Variables.ssl_port), + ]) + + ts.Disk.ssl_multicert_yaml.AddLines( + """ + ssl_multicert: + - dest_ip: "*" + ssl_cert_name: server.pem + ssl_key_name: server.key + """.split("\n")) + + if not Condition.CurlUsingUnixDomainSocket(): + # wss mapping + tr = urtest.AddTestRun() + tr.Processes.Default.StartBefore(server) + tr.Processes.Default.StartBefore(urtest.Processes.ts, ready=1) + tr.MakeCurlCommand( + '--max-time 2 -v -s -q -H "Connection: Upgrade" -H "Upgrade: websocket" -H "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==" -H "Sec-WebSocket-Version: 13" --http1.1 --resolve www.example.com:{0}:127.0.0.1 -k https://www.example.com:{0}/chat' + .format(ts.Variables.ssl_port), + ts=ts) + tr.Processes.Default.ReturnCode = 28 + tr.Processes.Default.Streams.stderr = "gold/remap-ws-upgrade.gold" + tr.StillRunningAfter = server + tr.StillRunningAfter = ts + + # ws mapping + tr = urtest.AddTestRun() + if Condition.CurlUsingUnixDomainSocket(): + tr.Processes.Default.StartBefore(server) + tr.Processes.Default.StartBefore(urtest.Processes.ts, ready=1) + tr.MakeCurlCommand( + '--max-time 2 -v -s -q -H "Connection: Upgrade" -H "Upgrade: websocket" -H "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==" -H "Sec-WebSocket-Version: 13" --http1.1 --resolve www.example.com:{0}:127.0.0.1 -k http://www.example.com:{0}/chat' + .format(ts.Variables.port), + ts=ts) + tr.Processes.Default.ReturnCode = 28 + tr.Processes.Default.Streams.stderr = "gold/remap-ws-upgrade.gold" + tr.StillRunningAfter = server + tr.StillRunningAfter = ts + + # Missing required headers (should result in 400) + tr = urtest.AddTestRun() + tr.MakeCurlCommand( + '--max-time 2 -v -s -q -H "Connection: Upgrade" -H "Upgrade: websocket" --http1.1 --resolve www.example.com:{0}:127.0.0.1 -k http://www.example.com:{0}/chat' + .format(ts.Variables.port), + ts=ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.stderr = "gold/remap-ws-upgrade-400.gold" + tr.StillRunningAfter = server + tr.StillRunningAfter = ts + + # Test metrics + tr = urtest.AddTestRun() + if Condition.CurlUsingUnixDomainSocket(): + metrics_gold_file = 'remap-ws-metrics-uds.gold' + else: + metrics_gold_file = 'remap-ws-metrics.gold' + tr.Processes.Default.Command = ( + f"{urtest.Variables.AtsTestToolsDir}/stdout_wait" + " 'traffic_ctl metric get" + + " proxy.process.http.total_incoming_connections" + " proxy.process.http.total_client_connections" + + " proxy.process.http.total_client_connections_ipv4" + " proxy.process.http.total_client_connections_ipv6" + + " proxy.process.http.total_server_connections" + " proxy.process.http2.total_client_connections" + + " proxy.process.http.connect_requests" + " proxy.process.tunnel.total_client_connections_blind_tcp" + + " proxy.process.tunnel.current_client_connections_blind_tcp" + " proxy.process.tunnel.total_server_connections_blind_tcp" + + " proxy.process.tunnel.current_server_connections_blind_tcp" + " proxy.process.tunnel.total_client_connections_tls_tunnel" + + " proxy.process.tunnel.current_client_connections_tls_tunnel" + + " proxy.process.tunnel.total_client_connections_tls_forward" + + " proxy.process.tunnel.current_client_connections_tls_forward" + + " proxy.process.tunnel.total_client_connections_tls_partial_blind" + + " proxy.process.tunnel.current_client_connections_tls_partial_blind" + + " proxy.process.tunnel.total_client_connections_tls_http" + " proxy.process.tunnel.current_client_connections_tls_http" + + " proxy.process.tunnel.total_server_connections_tls" + " proxy.process.tunnel.current_server_connections_tls'" + + f" {urtest.TestDirectory}/gold/{metrics_gold_file}") + # Need to copy over the environment so traffic_ctl knows where to find the unix domain socket + tr.Processes.Default.Env = ts.Env + tr.Processes.Default.ReturnCode = 0 + tr.StillRunningAfter = server + tr.StillRunningAfter = ts + urtest.execute() diff --git a/tests/gold_tests/remap_yaml/all_acl_combinations_yaml.py b/tests/uranium_tests/remap_yaml/all_acl_combinations_yaml.py similarity index 100% rename from tests/gold_tests/remap_yaml/all_acl_combinations_yaml.py rename to tests/uranium_tests/remap_yaml/all_acl_combinations_yaml.py diff --git a/tests/gold_tests/remap_yaml/base.replay.yaml b/tests/uranium_tests/remap_yaml/base.replay.yaml similarity index 100% rename from tests/gold_tests/remap_yaml/base.replay.yaml rename to tests/uranium_tests/remap_yaml/base.replay.yaml diff --git a/tests/gold_tests/remap_yaml/deactivate_ip_allow_yaml.py b/tests/uranium_tests/remap_yaml/deactivate_ip_allow_yaml.py similarity index 100% rename from tests/gold_tests/remap_yaml/deactivate_ip_allow_yaml.py rename to tests/uranium_tests/remap_yaml/deactivate_ip_allow_yaml.py diff --git a/tests/gold_tests/remap_yaml/deny_head_post.replay.yaml b/tests/uranium_tests/remap_yaml/deny_head_post.replay.yaml similarity index 100% rename from tests/gold_tests/remap_yaml/deny_head_post.replay.yaml rename to tests/uranium_tests/remap_yaml/deny_head_post.replay.yaml diff --git a/tests/gold_tests/remap_yaml/gold/200OK_test.gold b/tests/uranium_tests/remap_yaml/gold/200OK_test.gold similarity index 100% rename from tests/gold_tests/remap_yaml/gold/200OK_test.gold rename to tests/uranium_tests/remap_yaml/gold/200OK_test.gold diff --git a/tests/gold_tests/remap_yaml/gold/200OK_test_uds.gold b/tests/uranium_tests/remap_yaml/gold/200OK_test_uds.gold similarity index 100% rename from tests/gold_tests/remap_yaml/gold/200OK_test_uds.gold rename to tests/uranium_tests/remap_yaml/gold/200OK_test_uds.gold diff --git a/tests/gold_tests/remap_yaml/gold/lookupTest.gold b/tests/uranium_tests/remap_yaml/gold/lookupTest.gold similarity index 100% rename from tests/gold_tests/remap_yaml/gold/lookupTest.gold rename to tests/uranium_tests/remap_yaml/gold/lookupTest.gold diff --git a/tests/gold_tests/remap_yaml/gold/map-with-recv-port-ip.gold b/tests/uranium_tests/remap_yaml/gold/map-with-recv-port-ip.gold similarity index 100% rename from tests/gold_tests/remap_yaml/gold/map-with-recv-port-ip.gold rename to tests/uranium_tests/remap_yaml/gold/map-with-recv-port-ip.gold diff --git a/tests/gold_tests/remap_yaml/gold/map-with-recv-port-unix.gold b/tests/uranium_tests/remap_yaml/gold/map-with-recv-port-unix.gold similarity index 100% rename from tests/gold_tests/remap_yaml/gold/map-with-recv-port-unix.gold rename to tests/uranium_tests/remap_yaml/gold/map-with-recv-port-unix.gold diff --git a/tests/gold_tests/remap_yaml/gold/remap-200.gold b/tests/uranium_tests/remap_yaml/gold/remap-200.gold similarity index 100% rename from tests/gold_tests/remap_yaml/gold/remap-200.gold rename to tests/uranium_tests/remap_yaml/gold/remap-200.gold diff --git a/tests/gold_tests/remap_yaml/gold/remap-404.gold b/tests/uranium_tests/remap_yaml/gold/remap-404.gold similarity index 100% rename from tests/gold_tests/remap_yaml/gold/remap-404.gold rename to tests/uranium_tests/remap_yaml/gold/remap-404.gold diff --git a/tests/gold_tests/remap_yaml/gold/remap-DNS-200.gold b/tests/uranium_tests/remap_yaml/gold/remap-DNS-200.gold similarity index 100% rename from tests/gold_tests/remap_yaml/gold/remap-DNS-200.gold rename to tests/uranium_tests/remap_yaml/gold/remap-DNS-200.gold diff --git a/tests/gold_tests/remap_yaml/gold/remap-DNS-ipv6-200.gold b/tests/uranium_tests/remap_yaml/gold/remap-DNS-ipv6-200.gold similarity index 100% rename from tests/gold_tests/remap_yaml/gold/remap-DNS-ipv6-200.gold rename to tests/uranium_tests/remap_yaml/gold/remap-DNS-ipv6-200.gold diff --git a/tests/gold_tests/remap_yaml/gold/remap-hitATS-404.gold b/tests/uranium_tests/remap_yaml/gold/remap-hitATS-404.gold similarity index 100% rename from tests/gold_tests/remap_yaml/gold/remap-hitATS-404.gold rename to tests/uranium_tests/remap_yaml/gold/remap-hitATS-404.gold diff --git a/tests/gold_tests/remap_yaml/gold/remap-https-200.gold b/tests/uranium_tests/remap_yaml/gold/remap-https-200.gold similarity index 100% rename from tests/gold_tests/remap_yaml/gold/remap-https-200.gold rename to tests/uranium_tests/remap_yaml/gold/remap-https-200.gold diff --git a/tests/gold_tests/remap_yaml/gold/remap-https-200_2.gold b/tests/uranium_tests/remap_yaml/gold/remap-https-200_2.gold similarity index 100% rename from tests/gold_tests/remap_yaml/gold/remap-https-200_2.gold rename to tests/uranium_tests/remap_yaml/gold/remap-https-200_2.gold diff --git a/tests/gold_tests/remap_yaml/gold/remap-https-200_3.gold b/tests/uranium_tests/remap_yaml/gold/remap-https-200_3.gold similarity index 100% rename from tests/gold_tests/remap_yaml/gold/remap-https-200_3.gold rename to tests/uranium_tests/remap_yaml/gold/remap-https-200_3.gold diff --git a/tests/gold_tests/remap_yaml/gold/remap-ip-resolve.gold b/tests/uranium_tests/remap_yaml/gold/remap-ip-resolve.gold similarity index 100% rename from tests/gold_tests/remap_yaml/gold/remap-ip-resolve.gold rename to tests/uranium_tests/remap_yaml/gold/remap-ip-resolve.gold diff --git a/tests/gold_tests/remap_yaml/gold/remap-redirect.gold b/tests/uranium_tests/remap_yaml/gold/remap-redirect.gold similarity index 100% rename from tests/gold_tests/remap_yaml/gold/remap-redirect.gold rename to tests/uranium_tests/remap_yaml/gold/remap-redirect.gold diff --git a/tests/gold_tests/remap_yaml/gold/remap-referer-hit.gold b/tests/uranium_tests/remap_yaml/gold/remap-referer-hit.gold similarity index 100% rename from tests/gold_tests/remap_yaml/gold/remap-referer-hit.gold rename to tests/uranium_tests/remap_yaml/gold/remap-referer-hit.gold diff --git a/tests/gold_tests/remap_yaml/gold/remap-referer-miss.gold b/tests/uranium_tests/remap_yaml/gold/remap-referer-miss.gold similarity index 100% rename from tests/gold_tests/remap_yaml/gold/remap-referer-miss.gold rename to tests/uranium_tests/remap_yaml/gold/remap-referer-miss.gold diff --git a/tests/gold_tests/remap_yaml/gold/remap-ws-metrics-uds.gold b/tests/uranium_tests/remap_yaml/gold/remap-ws-metrics-uds.gold similarity index 100% rename from tests/gold_tests/remap_yaml/gold/remap-ws-metrics-uds.gold rename to tests/uranium_tests/remap_yaml/gold/remap-ws-metrics-uds.gold diff --git a/tests/gold_tests/remap_yaml/gold/remap-ws-metrics.gold b/tests/uranium_tests/remap_yaml/gold/remap-ws-metrics.gold similarity index 100% rename from tests/gold_tests/remap_yaml/gold/remap-ws-metrics.gold rename to tests/uranium_tests/remap_yaml/gold/remap-ws-metrics.gold diff --git a/tests/gold_tests/remap_yaml/gold/remap-ws-upgrade-400.gold b/tests/uranium_tests/remap_yaml/gold/remap-ws-upgrade-400.gold similarity index 100% rename from tests/gold_tests/remap_yaml/gold/remap-ws-upgrade-400.gold rename to tests/uranium_tests/remap_yaml/gold/remap-ws-upgrade-400.gold diff --git a/tests/gold_tests/remap_yaml/gold/remap-ws-upgrade.gold b/tests/uranium_tests/remap_yaml/gold/remap-ws-upgrade.gold similarity index 100% rename from tests/gold_tests/remap_yaml/gold/remap-ws-upgrade.gold rename to tests/uranium_tests/remap_yaml/gold/remap-ws-upgrade.gold diff --git a/tests/gold_tests/remap_yaml/gold/remap-zero-200.gold b/tests/uranium_tests/remap_yaml/gold/remap-zero-200.gold similarity index 100% rename from tests/gold_tests/remap_yaml/gold/remap-zero-200.gold rename to tests/uranium_tests/remap_yaml/gold/remap-zero-200.gold diff --git a/tests/gold_tests/remap_yaml/gold/remap2-200.gold b/tests/uranium_tests/remap_yaml/gold/remap2-200.gold similarity index 100% rename from tests/gold_tests/remap_yaml/gold/remap2-200.gold rename to tests/uranium_tests/remap_yaml/gold/remap2-200.gold diff --git a/tests/gold_tests/remap_yaml/reload_1.replay.yaml b/tests/uranium_tests/remap_yaml/reload_1.replay.yaml similarity index 100% rename from tests/gold_tests/remap_yaml/reload_1.replay.yaml rename to tests/uranium_tests/remap_yaml/reload_1.replay.yaml diff --git a/tests/gold_tests/remap_yaml/reload_2.replay.yaml b/tests/uranium_tests/remap_yaml/reload_2.replay.yaml similarity index 100% rename from tests/gold_tests/remap_yaml/reload_2.replay.yaml rename to tests/uranium_tests/remap_yaml/reload_2.replay.yaml diff --git a/tests/gold_tests/remap_yaml/reload_3.replay.yaml b/tests/uranium_tests/remap_yaml/reload_3.replay.yaml similarity index 100% rename from tests/gold_tests/remap_yaml/reload_3.replay.yaml rename to tests/uranium_tests/remap_yaml/reload_3.replay.yaml diff --git a/tests/gold_tests/remap_yaml/reload_4.replay.yaml b/tests/uranium_tests/remap_yaml/reload_4.replay.yaml similarity index 100% rename from tests/gold_tests/remap_yaml/reload_4.replay.yaml rename to tests/uranium_tests/remap_yaml/reload_4.replay.yaml diff --git a/tests/gold_tests/remap_yaml/reload_server.replay.yaml b/tests/uranium_tests/remap_yaml/reload_server.replay.yaml similarity index 100% rename from tests/gold_tests/remap_yaml/reload_server.replay.yaml rename to tests/uranium_tests/remap_yaml/reload_server.replay.yaml diff --git a/tests/gold_tests/remap_yaml/remap_acl_all_allowed.replay.yaml b/tests/uranium_tests/remap_yaml/remap_acl_all_allowed.replay.yaml similarity index 100% rename from tests/gold_tests/remap_yaml/remap_acl_all_allowed.replay.yaml rename to tests/uranium_tests/remap_yaml/remap_acl_all_allowed.replay.yaml diff --git a/tests/gold_tests/remap_yaml/remap_acl_all_denied.replay.yaml b/tests/uranium_tests/remap_yaml/remap_acl_all_denied.replay.yaml similarity index 100% rename from tests/gold_tests/remap_yaml/remap_acl_all_denied.replay.yaml rename to tests/uranium_tests/remap_yaml/remap_acl_all_denied.replay.yaml diff --git a/tests/gold_tests/remap_yaml/remap_acl_get_allowed.replay.yaml b/tests/uranium_tests/remap_yaml/remap_acl_get_allowed.replay.yaml similarity index 100% rename from tests/gold_tests/remap_yaml/remap_acl_get_allowed.replay.yaml rename to tests/uranium_tests/remap_yaml/remap_acl_get_allowed.replay.yaml diff --git a/tests/gold_tests/remap_yaml/remap_acl_get_post_allowed.replay.yaml b/tests/uranium_tests/remap_yaml/remap_acl_get_post_allowed.replay.yaml similarity index 100% rename from tests/gold_tests/remap_yaml/remap_acl_get_post_allowed.replay.yaml rename to tests/uranium_tests/remap_yaml/remap_acl_get_post_allowed.replay.yaml diff --git a/tests/gold_tests/remap_yaml/remap_acl_get_post_allowed_pp.replay.yaml b/tests/uranium_tests/remap_yaml/remap_acl_get_post_allowed_pp.replay.yaml similarity index 100% rename from tests/gold_tests/remap_yaml/remap_acl_get_post_allowed_pp.replay.yaml rename to tests/uranium_tests/remap_yaml/remap_acl_get_post_allowed_pp.replay.yaml diff --git a/tests/gold_tests/remap_yaml/remap_acl_get_post_denied.replay.yaml b/tests/uranium_tests/remap_yaml/remap_acl_get_post_denied.replay.yaml similarity index 100% rename from tests/gold_tests/remap_yaml/remap_acl_get_post_denied.replay.yaml rename to tests/uranium_tests/remap_yaml/remap_acl_get_post_denied.replay.yaml diff --git a/tests/uranium_tests/remap_yaml/test_basic_conf_remap_yaml_yaml.py b/tests/uranium_tests/remap_yaml/test_basic_conf_remap_yaml_yaml.py new file mode 100644 index 00000000000..e8c0764fa67 --- /dev/null +++ b/tests/uranium_tests/remap_yaml/test_basic_conf_remap_yaml_yaml.py @@ -0,0 +1,227 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_basic_conf_remap_yaml_yaml(urtest: UraniumTest) -> None: + ''' + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + urtest.Summary = ''' + Test conf_remap using a yaml file. + ''' + + urtest.ContinueOnFail = True + + class conf_remap_yaml_load_test: + """Test conf_remap using a yaml file.""" + + client_counter: int = 0 + ts_counter: int = 0 + server_counter: int = 0 + + def __init__(self, name: str, gold_file="", remap_filename="", remap_content=""): + """Initialize the test. + :param name: The name of the test. + :param gold_file: Gold file to be checked. + :param remap_filename: Remap yaml filename. + :param remap_content: remap yaml file content. + """ + self.name = name + self.gold_file = gold_file + self._remap_filename = remap_filename + self._remap_content = remap_content + + def _configure_server(self, tr: 'TestRun'): + """Configure the server. + + :param tr: The TestRun object to associate the server process with. + """ + server = urtest.MakeOriginServer(f"server-{conf_remap_yaml_load_test.ts_counter}", lookup_key="{%Host}{PATH}") + request_header2 = { + "headers": "GET /test HTTP/1.1\r\nHost: www.testexample.com\r\n\r\n", + "timestamp": "1469733493.993", + "body": "" + } + response_header2 = { + "headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", + "timestamp": "1469733493.993", + "body": "" + } + + server.addResponse("sessionfile.log", request_header2, response_header2) + conf_remap_yaml_load_test.server_counter += 1 + self._server = server + + def _configure_traffic_server(self, tr: 'TestRun'): + """Configure Traffic Server. + + :param tr: The TestRun object to associate the ts process with. + """ + ts = urtest.MakeATSProcess(f"ts-{conf_remap_yaml_load_test.ts_counter}") + + conf_remap_yaml_load_test.ts_counter += 1 + ts.Disk.records_config.update( + ''' + diags: + debug: + enabled: 1 + tags: conf_remap + dns: + resolv_conf: NULL + http: + referer_filter: 1 + url_remap: + pristine_host_hdr: 0 # make sure is 0 + + ''') + self._ts = ts + + def run(self, diags_fail_exp="", ts_retcode=0): + """Run the test. + :param diags_fail_exp: Text to be included to validate the error. + :param ts_retcode: Expected return code from TS. + """ + tr = urtest.AddTestRun(self.name) + self._configure_server(tr) + self._configure_traffic_server(tr) + + tr.Processes.Default.StartBefore(self._server) + tr.Processes.Default.StartBefore(self._ts) + + self._ts.ReturnCode = ts_retcode + + if ts_retcode > 0: # we could have errors logged and yet, we still want to move on. + self._ts.Ready = 0 + + if diags_fail_exp != "": + # some error logs will be written to the diags. + self._ts.Disk.diags_log.Content = Testers.IncludesExpression(diags_fail_exp, "Have a look.") + else: + tr.Processes.Default.ReturnCode = 0 + + if self.gold_file: + tr.Processes.Default.Streams.stderr = self.gold_file + + if self._remap_filename != "" and self._remap_content != "": + self._ts.Disk.MakeConfigFile(self._remap_filename).update(self._remap_content) + self._ts.Disk.remap_yaml.AddLines( + f''' + remap: + - type: map + from: + url: http://www.testexample.com/ + to: + url: http://127.0.0.1:{self._server.Variables.Port} + plugins: + - name: conf_remap.so + params: + - {self._remap_filename} + '''.split("\n")) + if Condition.CurlUsingUnixDomainSocket(): + tr.MakeCurlCommand( + '-H "Host: www.testexample.com" "http://127.0.0.1:{0}/test" --verbose'.format(self._ts.Variables.port), + ts=self._ts) + else: + tr.MakeCurlCommand( + '--proxy 127.0.0.1:{0} "http://www.testexample.com/test" -H "Host: www.testexample.com" --verbose'.format( + self._ts.Variables.port), + ts=self._ts) + conf_remap_yaml_load_test.client_counter += 1 + + gold_file = "gold/200OK_test.gold" + if Condition.CurlUsingUnixDomainSocket(): + gold_file = "gold/200OK_test_uds.gold" + + test0 = conf_remap_yaml_load_test( + "Test success", + gold_file=gold_file, + remap_filename="testexample_remap.yaml", + remap_content=''' + records: + url_remap: + pristine_host_hdr: 1 + ''') + test0.run() + + test1 = conf_remap_yaml_load_test( + "Test mismatch type", + remap_filename="mismatch_field_type_remap.yaml", + remap_content=''' + records: + url_remap: + pristine_host_hdr: !!float '1' + ''') + test1.run(diags_fail_exp="'proxy.config.url_remap.pristine_host_hdr' variable type mismatch", ts_retcode=33) + + test2 = conf_remap_yaml_load_test( + "Test invalid variable", + remap_filename="invalid1_field_type_remap.yaml", + remap_content=''' + records: + plugin: + dynamic_reload_mode: 1 + ''') + + test2.run( + diags_fail_exp="'proxy.config.plugin.dynamic_reload_mode' is not a configuration variable or cannot be overridden", + ts_retcode=33) + + # We let the conf_remap parse two fields, only one is valid, we expect ATS to start and the invalid fields ignored. + test3 = conf_remap_yaml_load_test( + "Test success", + gold_file=gold_file, + remap_filename="testexample2_remap.yaml", + remap_content=''' + records: + plugin: + dynamic_reload_mode: 1 + + url_remap: + pristine_host_hdr: 1 + ''') + test3.run(diags_fail_exp="'proxy.config.plugin.dynamic_reload_mode' is not a configuration variable or cannot be overridden") + + # Check null values + test4 = conf_remap_yaml_load_test( + "Test success - with NULL variable", + gold_file=gold_file, + remap_filename="testexample_remap.yaml", + remap_content=''' + records: + url_remap: + pristine_host_hdr: 1 + hostdb: + ip_resolve: "NULL" # We want to make sure this gets read as it should. "NULL" could be the value of this field. + ''') + test4.run() + urtest.execute() diff --git a/tests/uranium_tests/remap_yaml/test_conf_remap_float_yaml.py b/tests/uranium_tests/remap_yaml/test_conf_remap_float_yaml.py new file mode 100644 index 00000000000..11e5f4f188d --- /dev/null +++ b/tests/uranium_tests/remap_yaml/test_conf_remap_float_yaml.py @@ -0,0 +1,74 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_conf_remap_float_yaml(urtest: UraniumTest) -> None: + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + urtest.Summary = ''' + Test command: traffic_ctl config describe proxy.config.http.background_fill_completed_threshold (YTSATS-3309) + ''' + urtest.testName = 'Float in conf_remap Config Test' + + ts = urtest.MakeATSProcess("ts") + + ts.Disk.MakeConfigFile('conf_remap.yaml').update( + ''' + records: + http: + background_fill_completed_threshold: !!float '0.5' + ''') + + ts.Disk.remap_yaml.AddLines( + f''' + remap: + - type: map + from: + url: http://cdn.example.com/ + to: + url: http://origin.example.com/ + plugins: + - name: conf_remap.so + params: + - {urtest.RunDirectory}/ts/config/conf_remap.yaml + '''.split("\n")) + + tr = urtest.AddTestRun("traffic_ctl command") + tr.Env = ts.Env + tr.TimeOut = 5 + tr.StillRunningAfter = ts + + p = tr.Processes.Default + p.Command = f"traffic_ctl config describe proxy.config.http.background_fill_completed_threshold" + p.ReturnCode = 0 + p.StartBefore(urtest.Processes.ts) + urtest.execute() diff --git a/tests/uranium_tests/remap_yaml/test_map_with_recv_port_yaml.py b/tests/uranium_tests/remap_yaml/test_map_with_recv_port_yaml.py new file mode 100644 index 00000000000..eefa70a3a7b --- /dev/null +++ b/tests/uranium_tests/remap_yaml/test_map_with_recv_port_yaml.py @@ -0,0 +1,120 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_map_with_recv_port_yaml(urtest: UraniumTest) -> None: + ''' + Verify correct behavior of regex_map in remap.config. + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + urtest.Summary = ''' + Verify correct behavior of map_with_recv_port in remap.config. + ''' + + urtest.ContinueOnFail = True + ts = urtest.MakeATSProcess("ts") + server = urtest.MakeOriginServer("server") + dns = urtest.MakeDNServer("dns", default='127.0.0.1') + + urtest.testName = "" + request_header_ip = { + "headers": "GET /ip HTTP/1.1\r\nHost: origin.example.com\r\n\r\n", + "timestamp": "1469733493.993", + "body": "" + } + response_header_ip = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": "ip"} + request_header_unix = { + "headers": "GET /unix HTTP/1.1\r\nHost: origin.example.com\r\n\r\n", + "timestamp": "1469733493.993", + "body": "" + } + response_header_unix = { + "headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", + "timestamp": "1469733493.993", + "body": "unix" + } + request_header_error = { + "headers": "GET /error HTTP/1.1\r\nHost: origin.example.com\r\n\r\n", + "timestamp": "1469733493.993", + "body": "" + } + response_header_error = { + "headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", + "timestamp": "1469733493.993", + "body": "error" + } + server.addResponse("sessionfile.log", request_header_ip, response_header_ip) + server.addResponse("sessionfile.log", request_header_unix, response_header_unix) + server.addResponse("sessionfile.log", request_header_error, response_header_error) + + ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'http|dns', + 'proxy.config.dns.nameservers': '127.0.0.1:{0}'.format(dns.Variables.Port), + 'proxy.config.dns.resolv_conf': 'NULL' + }) + + # This map rule should not match + ts.Disk.remap_yaml.AddLines( + f''' + remap: + - type: map + from: + url: http://test.example.com + to: + url: http://origin.example.com:{server.Variables.Port}/error + - type: map_with_recv_port + from: + url: http://test.example.com:{ts.Variables.port}/ + to: + url: http://origin.example.com:{server.Variables.Port}/ip + - type: map_with_recv_port + from: + url: http+unix://test.example.com + to: + url: http://origin.example.com:{server.Variables.Port}/unix + '''.split("\n")) + + tr = urtest.AddTestRun() + tr.MakeCurlCommand('-H"Host: test.example.com" http://127.0.0.1:{0}/ --verbose'.format(ts.Variables.port), ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.StartBefore(server) + tr.Processes.Default.StartBefore(dns) + tr.Processes.Default.StartBefore(urtest.Processes.ts) + if Condition.CurlUsingUnixDomainSocket(): + tr.Processes.Default.Streams.stderr = "gold/map-with-recv-port-unix.gold" + else: + tr.Processes.Default.Streams.stderr = "gold/map-with-recv-port-ip.gold" + tr.StillRunningAfter = server + urtest.execute() diff --git a/tests/uranium_tests/remap_yaml/test_regex_map_yaml.py b/tests/uranium_tests/remap_yaml/test_regex_map_yaml.py new file mode 100644 index 00000000000..e3f595bf048 --- /dev/null +++ b/tests/uranium_tests/remap_yaml/test_regex_map_yaml.py @@ -0,0 +1,90 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_regex_map_yaml(urtest: UraniumTest) -> None: + ''' + Verify correct behavior of regex_map in remap.yaml. + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + urtest.Summary = ''' + Verify correct behavior of regex_map in remap.config. + ''' + + urtest.ContinueOnFail = True + ts = urtest.MakeATSProcess("ts") + server = urtest.MakeOriginServer("server") + dns = urtest.MakeDNServer("dns", default='127.0.0.1') + + urtest.testName = "" + request_header = { + "headers": "GET / HTTP/1.1\r\nHost: zero.one.two.three.com\r\n\r\n", + "timestamp": "1469733493.993", + "body": "" + } + response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + server.addResponse("sessionfile.log", request_header, response_header) + + ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'http.*|dns|conf_remap', + 'proxy.config.http.referer_filter': 1, + 'proxy.config.dns.nameservers': '127.0.0.1:{0}'.format(dns.Variables.Port), + 'proxy.config.dns.resolv_conf': 'NULL' + }) + + ts.Disk.remap_yaml.AddLines( + rf''' + remap: + - type: regex_map + from: + url: http://(.*)?one\.two\.three\.com/ + to: + url: http://$1reactivate.four.five.six.com:{server.Variables.Port}/ + - type: regex_map + from: + url: https://\b(?!(.*one|two|three|four|five|six)).+\b\.seven\.eight\.nine\.com/blah12345.html + to: + url: https://www.example.com:{server.Variables.Port}/one/two/three/blah12345.html + '''.split("\n")) + + tr = urtest.AddTestRun() + tr.MakeCurlCommand('-H"Host: zero.one.two.three.com" http://127.0.0.1:{0}/ --verbose'.format(ts.Variables.port), ts=ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.StartBefore(server) + tr.Processes.Default.StartBefore(dns) + tr.Processes.Default.StartBefore(urtest.Processes.ts) + tr.Processes.Default.Streams.stderr = "gold/remap-zero-200.gold" + tr.StillRunningAfter = server + urtest.execute() diff --git a/tests/uranium_tests/remap_yaml/test_remap_acl_yaml.py b/tests/uranium_tests/remap_yaml/test_remap_acl_yaml.py new file mode 100644 index 00000000000..3b3e4d65d3b --- /dev/null +++ b/tests/uranium_tests/remap_yaml/test_remap_acl_yaml.py @@ -0,0 +1,661 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_remap_acl_yaml(urtest: UraniumTest) -> None: + ''' + Verify remap.yaml acl behavior. + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # # http://www.apache.org/licenses/LICENSE-2.0 # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import os + import io + import re + import inspect + import tempfile + from yaml import load, dump + from yaml import CLoader as Loader + from typing import List, Tuple + + from ports import get_port + + urtest.Summary = ''' + Verify remap.yaml acl behavior. + ''' + + def update_config_file(path1: str, content1: str, path2: str, content2: str) -> None: + """Update two config files. + + This is used for some of the updates to the config files between test runs. + + :param path1: The path to the first config file. + :param content1: The content to write to the first config file. + :param path2: The path to the second config file. + :param content2: The content to write to the second config file. + """ + with open(path1, 'w') as f: + f.write(content1 + '\n') + with open(path2, 'w') as f: + f.write(content2 + '\n') + + class Test_remap_acl: + """Configure a test to verify remap.config acl behavior.""" + + _ts: 'TestProcess' = None + _ts_reload_counter: int = 0 + _ts_is_started: bool = False + + _server_counter: int = 0 + _client_counter: int = 0 + + def __init__( + self, name: str, replay_file: str, ip_allow_content: str, deactivate_ip_allow: bool, acl_behavior_policy: int, + acl_configuration: List[str], named_acls: List[Tuple[str, List[str]]], expected_responses: List[int], + proxy_protocol: bool): + """Initialize the test. + + :param name: The name of the test. + :param replay_file: The replay file to be used. + :param ip_allow_content: The ip_allow configuration to be used. + :param deactivate_ip_allow: Whether to deactivate the ip_allow filter. + :param acl_configuration: The ACL configuration to be used. + :param named_acls: The set of named ACLs to configure and use. + :param expect_responses: The in-order expected responses from the proxy. + """ + self._replay_file = replay_file + self._ip_allow_lines = ip_allow_content.split("\n") + self._deactivate_ip_allow = deactivate_ip_allow + self._acl_behavior_policy = acl_behavior_policy + self._acl_configuration = acl_configuration + self._named_acls = named_acls + self._expected_responses = expected_responses + + # Usually we configure the server first and use the server port to + # configure ATS to remap to it. In this case, though, we want a + # long-lived ATS process that spans TestRuns. So we let ATS choose an + # arbitrary availble server port, and then tell the TestRun-specific + # server to use that port. + server_port = self._configure_traffic_server() + tr = urtest.AddTestRun(name) + self._configure_server(tr, server_port) + self._configure_client(tr, proxy_protocol) + + def _configure_server(self, tr: 'TestRun', server_port: int) -> None: + """Configure the server. + """ + name = f"server-{Test_remap_acl._server_counter}" + server = tr.AddVerifierServerProcess(name, self._replay_file, http_ports=[server_port]) + Test_remap_acl._server_counter += 1 + self._server = server + + def _configure_traffic_server(self) -> int: + """Configure Traffic Server. + + :return: The listening port that the server should use. + """ + + call_reload: bool = False + if Test_remap_acl._ts is not None: + ts = Test_remap_acl._ts + call_reload = True + else: + ts = urtest.MakeATSProcess("ts", enable_cache=False, enable_proxy_protocol=True, enable_uds=False) + Test_remap_acl._ts = ts + self._ts = ts + port_name = f'ServerPort-{Test_remap_acl._ts_reload_counter}' + server_port: int = get_port(ts, port_name) + + remap_yaml_lines = ['remap:'] + if self._deactivate_ip_allow: + remap_yaml_lines.append('- deactivate_filter: ip_allow') + + # First, define the name ACLs (filters). + defined_filters = [] + for name, definition in self._named_acls: + if len(definition) > 0: + remap_yaml_lines.append('- define_filter:') + remap_yaml_lines.append(f' {name}:') + for d in definition: + remap_yaml_lines.append(f' {d}') + defined_filters.append(name) + # Now activate them. + for name in defined_filters: + remap_yaml_lines.append(f'- activate_filter: {name}') + + remap_yaml_lines.append('- type: map') + remap_yaml_lines.append(' from:') + remap_yaml_lines.append(' url: /') + remap_yaml_lines.append(' to:') + remap_yaml_lines.append(f' url: http://127.0.0.1:{server_port}') + + if len(self._acl_configuration) > 0: + remap_yaml_lines.append(' acl_filter:') + for f in self._acl_configuration: + remap_yaml_lines.append(f' {f}') + + if call_reload: + # + # Update the ATS configuration. + # + tr = urtest.AddTestRun("Change the ATS configuration") + p = tr.Processes.Default + p.Command = ( + f'traffic_ctl config set proxy.config.http.connect_ports {server_port} && ' + f'traffic_ctl config set proxy.config.url_remap.acl_behavior_policy {self._acl_behavior_policy}') + + p.Env = ts.Env + tr.StillRunningAfter = ts + + remap_yaml_path = os.path.join(ts.Variables.CONFIGDIR, 'remap.yaml') + ip_allow_path = os.path.join(ts.Variables.CONFIGDIR, 'ip_allow.yaml') + p.Setup.Lambda( + lambda: update_config_file( + remap_yaml_path, '\n'.join(remap_yaml_lines), ip_allow_path, '\n'.join(self._ip_allow_lines))) + + # + # Kick off the ATS config reload and await completion. + # + Test_remap_acl._ts_reload_counter += 1 + urtest.AddConfigReload(ts, expect_tasks=["remap.yaml"], description="Reload the ATS configuration") + + else: + record_config = { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'http|url|remap|ip_allow|proxyprotocol', + 'proxy.config.http.push_method_enabled': 1, + 'proxy.config.http.connect_ports': server_port, + 'proxy.config.url_remap.acl_behavior_policy': self._acl_behavior_policy, + 'proxy.config.acl.subjects': 'PROXY,PEER', + } + + ts.Disk.records_config.update(record_config) + ts.Disk.remap_yaml.AddLines(remap_yaml_lines) + ts.Disk.ip_allow_yaml.AddLines(self._ip_allow_lines) + + return server_port + + def _configure_client(self, tr: 'TestRun', proxy_protocol: bool) -> None: + """Run the test. + + :param tr: The TestRun object to associate the client process with. + """ + + name = f"client-{Test_remap_acl._client_counter}" + ts = Test_remap_acl._ts + port = ts.Variables.port if proxy_protocol == False else ts.Variables.proxy_protocol_port + p = tr.AddVerifierClientProcess(name, self._replay_file, http_ports=[port]) + Test_remap_acl._client_counter += 1 + p.StartBefore(self._server) + if not Test_remap_acl._ts_is_started: + p.StartBefore(ts) + Test_remap_acl._ts_is_started = True + + if self._expected_responses == [None, None]: + # If there are no expected responses, expect the Warning about the rejected ip. + self._ts.Disk.diags_log.Content += Testers.ContainsExpression( + "client '127.0.0.1' prohibited by ip-allow policy", "Verify the client rejection warning message.") + + # Also, the client will complain about the broken connections. + p.ReturnCode = 1 + + else: + codes = [str(code) for code in self._expected_responses] + p.Streams.stdout += Testers.ContainsExpression( + '.*'.join(codes), "Verifying the expected order of responses", reflags=re.DOTALL | re.MULTILINE) + + class Test_old_action: + _ts_counter: int = 0 + + def __init__(self, name: str, acl_filter: List[str], ip_allow_content: str) -> None: + '''Test that ATS fails with a FATAL message if an old action is used with modern ACL filter policy. + + :param name: The name of the test run. + :param acl_filter: The ACL filter to use. + :param ip_allow_content: The ip_allow configuration to use. + ''' + + tr = urtest.AddTestRun(name) + ts = self._configure_traffic_server(tr, acl_filter, ip_allow_content) + + def _configure_traffic_server(self, tr: 'TestRun', acl_filter: List[str], ip_allow_content: str) -> 'Process': + '''Configure Traffic Server process + + :param tr: The TestRun object to associate the Traffic Server process with. + :param acl_filter: The ACL filter to configure in remap.config. + :param ip_allow_content: The ip_allow configuration to use. + :return: The Traffic Server process. + ''' + name = f"ts-old-action-{Test_old_action._ts_counter}" + Test_old_action._ts_counter += 1 + ts = tr.MakeATSProcess(name, enable_uds=False) + self._ts = ts + + ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'http|url|remap|ip_allow', + 'proxy.config.url_remap.acl_behavior_policy': 1, + }) + + ts.Disk.remap_yaml.AddLines( + f''' + remap: + - type: map + from: + url: / + to: + url: http://127.0.0.1:8080 + acl_filter: '''.split("\n")) + + for f in acl_filter: + ts.Disk.remap_yaml.AddLine(f' {f}') + + if ip_allow_content: + ts.Disk.ip_allow_yaml.AddLines(ip_allow_content.split("\n")) + + if len(acl_filter) > 0: + expected_error = '"allow" and "deny" are no longer valid.' + else: + expected_error = 'Legacy action name of' + + # We have to wait upon TS to emit the expected log message, but it cannot be + # the ts Ready criteria because autest might detect the process going away + # before it detects the log message. So we add a separate process that waits + # upon the log message. + watcher = tr.Processes.Process("watcher") + watcher.Command = "sleep 10" + watcher.Ready = When.FileContains(ts.Disk.diags_log.Name, expected_error) + watcher.StartBefore(ts) + + tr.Processes.Default.Command = 'printf "Fatal Shutdown Test"' + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.StartBefore(watcher) + + tr.Timeout = 5 + ts.ReturnCode = Any(33, 70) + ts.Ready = 0 + ts.Disk.diags_log.Content = Testers.IncludesExpression(expected_error, 'ATS should fatal with the old actions.') + + return ts + + IP_ALLOW_OLD_ACTION = f''' + ip_categories: + - name: ACME_LOCAL + ip_addrs: 127.0.0.1 + - name: ACME_EXTERNAL + ip_addrs: 5.6.7.8 + + ip_allow: + - apply: in + ip_addrs: 0/0 + action: allow + methods: + - GET + ''' + + IP_ALLOW_CONTENT = f''' + ip_categories: + - name: ACME_LOCAL + ip_addrs: 127.0.0.1 + - name: ACME_EXTERNAL + ip_addrs: 5.6.7.8 + + ip_allow: + - apply: in + ip_addrs: 0/0 + action: set_allow + methods: + - GET + ''' + + Test_old_action("Verify allow is reject in modern policy", ['action: allow', 'method: GET'], IP_ALLOW_CONTENT) + Test_old_action("Verify deny is reject in modern policy", ['action: deny', 'method: GET'], IP_ALLOW_CONTENT) + Test_old_action("Verify deny is reject in modern policy", [], IP_ALLOW_OLD_ACTION) + + test_ip_allow_optional_methods = Test_remap_acl( + "Verify non-allowed methods are blocked.", + replay_file='remap_acl_get_post_allowed.replay.yaml', + ip_allow_content=IP_ALLOW_CONTENT, + deactivate_ip_allow=False, + acl_behavior_policy=1, + acl_configuration=['action: set_allow', 'src_ip: 127.0.0.1', 'method: [GET, POST]'], + named_acls=[], + expected_responses=[200, 200, 403, 403, 403], + proxy_protocol=False) + + test_ip_allow_optional_methods_pp = Test_remap_acl( + "Verify non-allowed methods are blocked (PP).", + replay_file='remap_acl_get_post_allowed_pp.replay.yaml', + ip_allow_content=IP_ALLOW_CONTENT, + deactivate_ip_allow=False, + acl_behavior_policy=1, + acl_configuration=['action: set_allow', 'src_ip: 1.2.3.4', 'method: [GET, POST]'], + named_acls=[], + expected_responses=[200, 200, 403, 403, 403], + proxy_protocol=True) + + test_ip_allow_optional_methods = Test_remap_acl( + "Verify add_allow adds an allowed method.", + replay_file='remap_acl_get_post_allowed.replay.yaml', + ip_allow_content=IP_ALLOW_CONTENT, + deactivate_ip_allow=False, + acl_behavior_policy=1, + acl_configuration=['action: add_allow', 'src_ip: 127.0.0.1', 'method: POST'], + named_acls=[], + expected_responses=[200, 200, 403, 403, 403], + proxy_protocol=False) + + test_ip_allow_optional_methods = Test_remap_acl( + "Verify add_allow adds allowed methods.", + replay_file='remap_acl_get_post_allowed.replay.yaml', + ip_allow_content=IP_ALLOW_CONTENT, + deactivate_ip_allow=False, + acl_behavior_policy=1, + acl_configuration=['action: add_allow', 'src_ip: 127.0.0.1', 'method: [GET, POST]'], + named_acls=[], + expected_responses=[200, 200, 403, 403, 403], + proxy_protocol=False) + + test_ip_allow_optional_methods = Test_remap_acl( + "Verify if no ACLs match, ip_allow.yaml is used.", + replay_file='remap_acl_get_allowed.replay.yaml', + ip_allow_content=IP_ALLOW_CONTENT, + deactivate_ip_allow=False, + acl_behavior_policy=1, + acl_configuration=['action: set_allow', 'src_ip: 1.2.3.4', 'method: [GET, POST]'], + named_acls=[], + expected_responses=[200, 403, 403, 403, 403], + proxy_protocol=False) + + test_ip_allow_optional_methods = Test_remap_acl( + "Verify @src_ip=all works.", + replay_file='remap_acl_get_post_allowed.replay.yaml', + ip_allow_content=IP_ALLOW_CONTENT, + deactivate_ip_allow=False, + acl_behavior_policy=1, + acl_configuration=['action: set_allow', 'src_ip: all', 'method: [GET, POST]'], + named_acls=[], + expected_responses=[200, 200, 403, 403, 403], + proxy_protocol=False) + + test_ip_allow_optional_methods = Test_remap_acl( + "Verify @src_ip_category works.", + replay_file='remap_acl_get_post_allowed.replay.yaml', + ip_allow_content=IP_ALLOW_CONTENT, + deactivate_ip_allow=False, + acl_behavior_policy=1, + acl_configuration=['action: set_allow', 'src_ip_category: ACME_LOCAL', 'method: [GET, POST]'], + named_acls=[], + expected_responses=[200, 200, 403, 403, 403], + proxy_protocol=False) + + test_ip_allow_optional_methods = Test_remap_acl( + "Verify no @src_ip implies all IP addresses.", + replay_file='remap_acl_get_post_allowed.replay.yaml', + ip_allow_content=IP_ALLOW_CONTENT, + deactivate_ip_allow=False, + acl_behavior_policy=1, + acl_configuration=['action: set_allow', 'method: [GET, POST]'], + named_acls=[], + expected_responses=[200, 200, 403, 403, 403], + proxy_protocol=False) + + test_ip_allow_optional_methods = Test_remap_acl( + "Verify denied methods are blocked.", + replay_file='remap_acl_get_post_denied.replay.yaml', + ip_allow_content=IP_ALLOW_CONTENT, + deactivate_ip_allow=False, + acl_behavior_policy=1, + acl_configuration=['action: set_deny', 'src_ip: 127.0.0.1', 'method: [GET, POST]'], + named_acls=[], + expected_responses=[403, 403, 200, 200, 400], + proxy_protocol=False) + + test_ip_allow_optional_methods = Test_remap_acl( + "Verify add_deny adds blocked methods.", + replay_file='remap_acl_all_denied.replay.yaml', + ip_allow_content=IP_ALLOW_CONTENT, + deactivate_ip_allow=False, + acl_behavior_policy=1, + acl_configuration=['action: add_deny', 'src_ip: 127.0.0.1', 'method: GET'], + named_acls=[], + expected_responses=[403, 403, 403, 403, 403], + proxy_protocol=False) + + test_ip_allow_optional_methods = Test_remap_acl( + "Verify a default deny filter rule works.", + replay_file='remap_acl_all_denied.replay.yaml', + ip_allow_content=IP_ALLOW_CONTENT, + deactivate_ip_allow=False, + acl_behavior_policy=1, + acl_configuration=['action: set_allow', 'src_ip: 1.2.3.4', 'method: [GET, POST]'], + named_acls=[('deny', ['action: set_deny'])], + expected_responses=[403, 403, 403, 403, 403], + proxy_protocol=False) + + test_ip_allow_optional_methods = Test_remap_acl( + "Verify inverting @src_ip works.", + replay_file='remap_acl_all_denied.replay.yaml', + ip_allow_content=IP_ALLOW_CONTENT, + deactivate_ip_allow=False, + acl_behavior_policy=1, + acl_configuration=['action: set_allow', 'src_ip_invert: 127.0.0.1', 'method: [GET, POST]'], + named_acls=[('deny', ['action: set_deny'])], + expected_responses=[403, 403, 403, 403, 403], + proxy_protocol=False) + + test_ip_allow_optional_methods = Test_remap_acl( + "Verify inverting @src_ip works with the rule matching.", + replay_file='remap_acl_get_post_allowed.replay.yaml', + ip_allow_content=IP_ALLOW_CONTENT, + deactivate_ip_allow=False, + acl_behavior_policy=1, + acl_configuration=['action: set_allow', 'src_ip_invert: 3.4.5.6', 'method: [GET, POST]'], + named_acls=[('deny', ['action: set_deny'])], + expected_responses=[200, 200, 403, 403, 403], + proxy_protocol=False) + + test_ip_allow_optional_methods = Test_remap_acl( + "Verify inverting @src_ip_category works.", + replay_file='remap_acl_all_denied.replay.yaml', + ip_allow_content=IP_ALLOW_CONTENT, + deactivate_ip_allow=False, + acl_behavior_policy=1, + acl_configuration=['action: set_allow', 'src_ip_category_invert: ACME_LOCAL', 'method: [GET, POST]'], + named_acls=[('deny', ['action: set_deny'])], + expected_responses=[403, 403, 403, 403, 403], + proxy_protocol=False) + + test_ip_allow_optional_methods = Test_remap_acl( + "Verify inverting @src_ip_category works with the rule matching.", + replay_file='remap_acl_get_post_allowed.replay.yaml', + ip_allow_content=IP_ALLOW_CONTENT, + deactivate_ip_allow=False, + acl_behavior_policy=1, + acl_configuration=['action: set_allow', 'src_ip_category_invert: ACME_EXTERNAL', 'method: [GET, POST]'], + named_acls=[('deny', ['action: set_deny'])], + expected_responses=[200, 200, 403, 403, 403], + proxy_protocol=False) + + test_ip_allow_optional_methods = Test_remap_acl( + "Verify @src_ip and @src_ip_category AND together.", + replay_file='remap_acl_all_denied.replay.yaml', + ip_allow_content=IP_ALLOW_CONTENT, + deactivate_ip_allow=False, + acl_behavior_policy=1, + # The rule will not match because, while @src_ip matches, @src_ip_category does not. + acl_configuration=['action: set_allow', 'src_ip: 127.0.0.1', 'src_ip_category: ACME_EXTERNAL', 'method: [GET, POST]'], + # Therefore, this named deny filter will block. + named_acls=[('deny', ['action: set_deny'])], + expected_responses=[403, 403, 403, 403, 403], + proxy_protocol=False) + + test_ip_allow_optional_methods = Test_remap_acl( + "Verify defined in-line ACLS are evaluated before named ones.", + replay_file='remap_acl_get_post_allowed.replay.yaml', + ip_allow_content=IP_ALLOW_CONTENT, + deactivate_ip_allow=False, + acl_behavior_policy=1, + acl_configuration=['action: set_allow', 'src_ip: 127.0.0.1', 'method: [GET, POST]'], + named_acls=[('deny', ['action: set_deny'])], + expected_responses=[200, 200, 403, 403, 403], + proxy_protocol=False) + + test_ip_allow_optional_methods = Test_remap_acl( + "Verify remap.config line overrides ip_allow rule.", + replay_file='remap_acl_get_post_allowed.replay.yaml', + ip_allow_content=IP_ALLOW_CONTENT, + deactivate_ip_allow=False, + acl_behavior_policy=1, + acl_configuration=['action: set_allow', 'src_ip: 127.0.0.1', 'method: [GET, POST]'], + named_acls=[], + expected_responses=[200, 200, 403, 403, 403], + proxy_protocol=False) + + test_ip_allow_optional_methods = Test_remap_acl( + "Verify we can deactivate the ip_allow filter.", + replay_file='remap_acl_all_allowed.replay.yaml', + ip_allow_content=IP_ALLOW_CONTENT, + deactivate_ip_allow=True, + acl_behavior_policy=1, + # This won't match, so nothing will match since ip_allow.yaml is off. + acl_configuration=['action: set_allow', 'src_ip: 1.2.3.4', 'method: [GET, POST]'], + named_acls=[], + # Nothing will block the request since ip_allow.yaml is off. + expected_responses=[200, 200, 200, 200, 400], + proxy_protocol=False) + + test_ip_allow_optional_methods = Test_remap_acl( + "Verify in_ip matches on IP as expected.", + replay_file='remap_acl_get_post_allowed.replay.yaml', + ip_allow_content=IP_ALLOW_CONTENT, + deactivate_ip_allow=False, + acl_behavior_policy=1, + acl_configuration=['action: set_allow', 'in_ip: 127.0.0.1', 'method: [GET, POST]'], + named_acls=[], + expected_responses=[200, 200, 403, 403, 403], + proxy_protocol=False) + + test_ip_allow_optional_methods = Test_remap_acl( + "Verify in_ip rules do not match on other IPs.", + replay_file='remap_acl_get_allowed.replay.yaml', + ip_allow_content=IP_ALLOW_CONTENT, + deactivate_ip_allow=False, + acl_behavior_policy=1, + acl_configuration=['action: set_allow', 'in_ip: 3.4.5.6', 'method: [GET, POST]'], + named_acls=[], + expected_responses=[200, 403, 403, 403, 403], + proxy_protocol=False) + + test_named_acl_deny = Test_remap_acl( + "Verify a named ACL is applied if an in-line ACL is absent.", + replay_file='deny_head_post.replay.yaml', + ip_allow_content=IP_ALLOW_CONTENT, + deactivate_ip_allow=False, + acl_behavior_policy=1, + acl_configuration=[], + named_acls=[('deny', ['action: set_deny', 'method: [HEAD, POST]'])], + expected_responses=[200, 403, 403, 403], + proxy_protocol=False) + + def replay_proxy_response(filename, replay_file, get_proxy_response, post_proxy_response): + """ + replay_proxy_response writes the given replay file (which expects a single GET & POST client-request) + with the given proxy_response value. This is only used to support the tests in the combination table. + """ + + current_dir = os.path.dirname(inspect.getfile(inspect.currentframe())) + path = os.path.join(current_dir, filename) + data = None + with open(path) as f: + data = load(f, Loader=Loader) + for session in data["sessions"]: + for transaction in session["transactions"]: + method = transaction["client-request"]["method"] + if method == "GET": + transaction["proxy-response"]["status"] = 403 if get_proxy_response == None else get_proxy_response + elif method == "POST": + transaction["proxy-response"]["status"] = 403 if post_proxy_response == None else post_proxy_response + else: + raise Exception("Expected to find GET or POST request, found %s", method) + with open(replay_file, "w") as f: + f.write(dump(data)) + + from deactivate_ip_allow_yaml import all_deactivate_ip_allow_tests_yaml + from all_acl_combinations_yaml import all_acl_combination_tests_yaml + """ + Test all acl combinations + """ + for idx, test in enumerate(all_acl_combination_tests_yaml): + (_, replay_file_name) = tempfile.mkstemp(suffix="acl_table_test_{}.replay".format(idx)) + replay_proxy_response( + "base.replay.yaml", + replay_file_name, + test["GET response"], + test["POST response"], + ) + Test_remap_acl( + "allcombo-{0} {1} {2} {3}".format(idx, test["inline"], test["named_acl"], test["ip_allow"]), + replay_file=replay_file_name, + ip_allow_content=test["ip_allow"], + deactivate_ip_allow=False, + acl_behavior_policy=0 if test["policy"] == "legacy" else 1, + acl_configuration=test["inline"], + named_acls=[("acl", test["named_acl"])] if test["named_acl"] != "" else [], + expected_responses=[test["GET response"], test["POST response"]], + proxy_protocol=False, + ) + """ + Test all ACL combinations + """ + for idx, test in enumerate(all_deactivate_ip_allow_tests_yaml): + try: + test["deactivate_ip_allow"] + except: + print(test) + (_, replay_file_name) = tempfile.mkstemp(suffix="deactivate_ip_allow_table_test_{}.replay".format(idx)) + replay_proxy_response( + "base.replay.yaml", + replay_file_name, + test["GET response"], + test["POST response"], + ) + Test_remap_acl( + "ipallow-{0} {1} {2} {3}".format(idx, test["inline"], test["named_acl"], test["ip_allow"]), + replay_file=replay_file_name, + ip_allow_content=test["ip_allow"], + deactivate_ip_allow=test["deactivate_ip_allow"], + acl_behavior_policy=0 if test["policy"] == "legacy" else 1, + acl_configuration=test["inline"], + named_acls=[("acl", test["named_acl"])] if test["named_acl"] != "" else [], + expected_responses=[test["GET response"], test["POST response"]], + proxy_protocol=False, + ) + urtest.execute() diff --git a/tests/uranium_tests/remap_yaml/test_remap_http_yaml.py b/tests/uranium_tests/remap_yaml/test_remap_http_yaml.py new file mode 100644 index 00000000000..6c5de58c9e9 --- /dev/null +++ b/tests/uranium_tests/remap_yaml/test_remap_http_yaml.py @@ -0,0 +1,224 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_remap_http_yaml(urtest: UraniumTest) -> None: + ''' + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + urtest.Summary = ''' + Test a basic remap of a http connection + ''' + + urtest.ContinueOnFail = True + # Define default ATS + ts = urtest.MakeATSProcess("ts") + server = urtest.MakeOriginServer("server") + server2 = urtest.MakeOriginServer("server2", lookup_key="{%Host}{PATH}") + dns = urtest.MakeDNServer("dns") + + urtest.testName = "" + request_header = {"headers": "GET / HTTP/1.1\r\nHost: www.example.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + # expected response from the origin server + response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + + request_header2 = { + "headers": "GET /test HTTP/1.1\r\nHost: www.testexample.com\r\n\r\n", + "timestamp": "1469733493.993", + "body": "" + } + # expected response from the origin server + response_header2 = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + + # add response to the server dictionary + server.addResponse("sessionfile.log", request_header, response_header) + server2.addResponse("sessionfile.log", request_header2, response_header2) + ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'http.*|dns|conf_remap|remap_yaml', + 'proxy.config.http.referer_filter': 1, + 'proxy.config.dns.nameservers': '127.0.0.1:{0}'.format(dns.Variables.Port), + 'proxy.config.dns.resolv_conf': 'NULL' + }) + + ts.Disk.remap_yaml.AddLines( + f''' + remap: + - type: map + from: + url: http://www.example.com + to: + url: http://127.0.0.1:{server.Variables.Port} + - type: map_with_recv_port + from: + url: http://www.example2.com:{ts.Variables.port} + to: + url: http://127.0.0.1:{server.Variables.Port} + - type: map + from: + url: http://www.example.com:8080 + to: + url: http://127.0.0.1:{server.Variables.Port} + - type: redirect + from: + url: http://test3.com + to: + url: http://httpbin.org + - type: map_with_referer + from: + url: http://test4.com + to: + url: http://127.0.0.1:{server.Variables.Port} + redirect: + url: http://httpbin.org + regex: + - (.*[.])?persia[.]com + - type: map + from: + url: http://testDNS.com + to: + url: http://audrey.hepburn.com:{server.Variables.Port} + - type: map + from: + url: http://www.testexample.com + to: + url: http://127.0.0.1:{server2.Variables.Port} + plugins: + - name: conf_remap.so + params: + - proxy.config.url_remap.pristine_host_hdr=1 + '''.split("\n")) + + dns.addRecords(records={"audrey.hepburn.com.": ["127.0.0.1"]}) + dns.addRecords(records={"whatever.com.": ["127.0.0.1"]}) + + # call localhost straight + tr = urtest.AddTestRun() + tr.MakeCurlCommand('"http://127.0.0.1:{0}/" --verbose'.format(ts.Variables.port), ts=ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.StartBefore(server) + tr.Processes.Default.StartBefore(dns) + tr.Processes.Default.StartBefore(urtest.Processes.ts) + tr.Processes.Default.Streams.stderr = "gold/remap-hitATS-404.gold" + tr.StillRunningAfter = server + + # www.example.com host + tr = urtest.AddTestRun() + tr.MakeCurlCommand( + '--proxy 127.0.0.1:{0} "http://www.example.com" -H "Proxy-Connection: keep-alive" --verbose'.format(ts.Variables.port), + ts=ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.stderr = "gold/remap-200.gold" + + # www.example2.com host (match on receive port) + # map_with_recv_port doesn't work with UDS + if not Condition.CurlUsingUnixDomainSocket(): + tr = urtest.AddTestRun() + tr.MakeCurlCommand( + '--proxy 127.0.0.1:{0} "http://www.example2.com" -H "Proxy-Connection: keep-alive" --verbose'.format( + ts.Variables.port), + ts=ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.stderr = "gold/remap2-200.gold" + + # www.example.com:80 host + tr = urtest.AddTestRun() + tr.MakeCurlCommand( + ' --proxy 127.0.0.1:{0} "http://www.example.com:80/" -H "Proxy-Connection: keep-alive" --verbose'.format( + ts.Variables.port), + ts=ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.stderr = "gold/remap-200.gold" + + # www.example.com:8080 host + tr = urtest.AddTestRun() + tr.MakeCurlCommand( + ' --proxy 127.0.0.1:{0} "http://www.example.com:8080" -H "Proxy-Connection: keep-alive" --verbose'.format( + ts.Variables.port), + ts=ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.stderr = "gold/remap-200.gold" + + # no rule for this + tr = urtest.AddTestRun() + tr.MakeCurlCommand( + ' --proxy 127.0.0.1:{0} "http://www.test.com/" -H "Proxy-Connection: keep-alive" --verbose'.format(ts.Variables.port), + ts=ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.stderr = "gold/remap-404.gold" + + # redirect result + tr = urtest.AddTestRun() + tr.MakeCurlCommand(' --proxy 127.0.0.1:{0} "http://test3.com" --verbose'.format(ts.Variables.port), ts=ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.stderr = "gold/remap-redirect.gold" + + # referer hit + tr = urtest.AddTestRun() + tr.MakeCurlCommand( + ' --proxy 127.0.0.1:{0} "http://test4.com" --header "Referer: persia.com" --verbose'.format(ts.Variables.port), ts=ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.stderr = "gold/remap-referer-hit.gold" + + # referer miss + tr = urtest.AddTestRun() + tr.MakeCurlCommand( + ' --proxy 127.0.0.1:{0} "http://test4.com" --header "Referer: monkey.com" --verbose'.format(ts.Variables.port), ts=ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.stderr = "gold/remap-referer-miss.gold" + + # referer hit + tr = urtest.AddTestRun() + tr.MakeCurlCommand( + ' --proxy 127.0.0.1:{0} "http://test4.com" --header "Referer: www.persia.com" --verbose'.format(ts.Variables.port), ts=ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.stderr = "gold/remap-referer-hit.gold" + + # DNS test + tr = urtest.AddTestRun() + tr.MakeCurlCommand(' --proxy 127.0.0.1:{0} "http://testDNS.com" --verbose'.format(ts.Variables.port), ts=ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.stderr = "gold/remap-DNS-200.gold" + + # microserver lookup test + tr = urtest.AddTestRun() + tr.MakeCurlCommand( + '--proxy 127.0.0.1:{0} "http://www.testexample.com/test" -H "Host: www.testexample.com" --verbose'.format( + ts.Variables.port), + ts=ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.StartBefore(server2) + tr.Processes.Default.Streams.stderr = "gold/lookupTest.gold" + tr.StillRunningAfter = server2 + urtest.execute() diff --git a/tests/uranium_tests/remap_yaml/test_remap_https_yaml.py b/tests/uranium_tests/remap_yaml/test_remap_https_yaml.py new file mode 100644 index 00000000000..268a66fcf26 --- /dev/null +++ b/tests/uranium_tests/remap_yaml/test_remap_https_yaml.py @@ -0,0 +1,165 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_remap_https_yaml(urtest: UraniumTest) -> None: + ''' + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + urtest.Summary = ''' + Test a basic remap of a http connection + ''' + + urtest.SkipIf(Condition.CurlUsingUnixDomainSocket()) + urtest.ContinueOnFail = True + # Define default ATS + ts = urtest.MakeATSProcess("ts", enable_tls=True) + server = urtest.MakeOriginServer("server") + server2 = urtest.MakeOriginServer("server2", ssl=True) + + # **testname is required** + testName = "" + request_header = {"headers": "GET / HTTP/1.1\r\nHost: www.example.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + # desired response form the origin server + response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + server.addResponse("sessionlog.json", request_header, response_header) + server2.addResponse("sessionlog.json", request_header, response_header) + + # add ssl materials like key, certificates for the server + ts.addDefaultSSLFiles() + + ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'lm|ssl', + 'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir), + 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts.Variables.SSLDir), + # enable ssl port + 'proxy.config.http.server_ports': '{0} {1}:proto=http2;http:ssl'.format(ts.Variables.port, ts.Variables.ssl_port), + 'proxy.config.ssl.client.verify.server.policy': 'PERMISSIVE', + }) + + ts.Disk.remap_yaml.AddLines( + f''' + remap: + - type: map + from: + url: https://www.example.com + to: + url: http://127.0.0.1:{server.Variables.Port} + - type: map + from: + url: https://www.example.com:{ts.Variables.ssl_port} + to: + url: http://127.0.0.1:{server.Variables.Port} + - type: map_with_recv_port + from: + url: https://www.example3.com:{ts.Variables.ssl_port} + to: + url: http://127.0.0.1:{server.Variables.Port} + - type: map + from: + url: https://www.anotherexample.com + to: + url: https://127.0.0.1:{server2.Variables.SSL_Port} + '''.split("\n")) + + ts.Disk.ssl_multicert_yaml.AddLines( + """ + ssl_multicert: + - dest_ip: "*" + ssl_cert_name: server.pem + ssl_key_name: server.key + """.split("\n")) + + # call localhost straight + tr = urtest.AddTestRun() + tr.MakeCurlCommand('--http1.1 -k https://127.0.0.1:{0} --verbose'.format(ts.Variables.ssl_port), ts=ts) + tr.Processes.Default.ReturnCode = 0 + + tr.Processes.Default.StartBefore(server) + tr.Processes.Default.StartBefore(server2) + tr.Processes.Default.StartBefore(urtest.Processes.ts) + tr.Processes.Default.Streams.stderr = "gold/remap-hitATS-404.gold" + tr.StillRunningAfter = server + tr.StillRunningAfter = ts + + # www.example.com host + tr = urtest.AddTestRun() + tr.MakeCurlCommand( + '--http1.1 -k https://127.0.0.1:{0} -H "Host: www.example.com" --verbose'.format(ts.Variables.ssl_port), ts=ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.stderr = "gold/remap-https-200.gold" + + # www.example.com:80 host + tr = urtest.AddTestRun() + tr.MakeCurlCommand( + '--http1.1 -k https://127.0.0.1:{0} -H "Host: www.example.com:443" --verbose'.format(ts.Variables.ssl_port), ts=ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.stderr = "gold/remap-https-200.gold" + + # www.example.com:8080 host + tr = urtest.AddTestRun() + tr.MakeCurlCommand( + '--http1.1 -k https://127.0.0.1:{0} -H "Host: www.example.com:{0}" --verbose'.format(ts.Variables.ssl_port), ts=ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.stderr = "gold/remap-https-200.gold" + + # www.example3.com (match on receive port) + tr = urtest.AddTestRun() + tr.MakeCurlCommand( + '--http1.1 -k https://127.0.0.1:{0} -H "Host: www.example3.com" --verbose'.format(ts.Variables.ssl_port), ts=ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.stderr = "gold/remap-https-200_3.gold" + + # no rule for this + tr = urtest.AddTestRun() + tr.MakeCurlCommand('--http1.1 -k https://127.0.0.1:{0} -H "Host: www.test.com" --verbose'.format(ts.Variables.ssl_port), ts=ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.stderr = "gold/remap-hitATS-404.gold" + + # bad port + tr = urtest.AddTestRun() + tr.MakeCurlCommand( + '--http1.1 -k https://127.0.0.1:{0} -H "Host: www.example.com:1234" --verbose'.format(ts.Variables.ssl_port), ts=ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.stderr = "gold/remap-hitATS-404.gold" + + # map www.anotherexample.com to https://.com + tr = urtest.AddTestRun() + tr.MakeCurlCommand( + '--http1.1 -k https://127.0.0.1:{0} -H "Host: www.anotherexample.com" --verbose'.format(ts.Variables.ssl_port), ts=ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.stderr = "gold/remap-https-200_2.gold" + tr.StillRunningAfter = server2 + urtest.execute() diff --git a/tests/uranium_tests/remap_yaml/test_remap_ip_resolve_yaml.py b/tests/uranium_tests/remap_yaml/test_remap_ip_resolve_yaml.py new file mode 100644 index 00000000000..8c2efe46c91 --- /dev/null +++ b/tests/uranium_tests/remap_yaml/test_remap_ip_resolve_yaml.py @@ -0,0 +1,110 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_remap_ip_resolve_yaml(urtest: UraniumTest) -> None: + ''' + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + urtest.Summary = ''' + Test a basic ip_resolve override using an ipv6 server + ''' + + urtest.ContinueOnFail = True + # Define default ATS + ts = urtest.MakeATSProcess("ts") + server = urtest.MakeOriginServer("server") + server_v6 = urtest.MakeOriginServer("server_v6", None, None, '::1', 0) + + dns = urtest.MakeDNServer("dns") + + urtest.testName = "" + request_header = {"headers": "GET / HTTP/1.1\r\nHost: www.example.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + # expected response from the origin server + response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + + # add response to the server dictionary + server.addResponse("sessionfile.log", request_header, response_header) + server_v6.addResponse("sessionfile.log", request_header, response_header) + ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'http.*|dns|conf_remap', + 'proxy.config.http.referer_filter': 1, + 'proxy.config.dns.nameservers': '127.0.0.1:{0}'.format(dns.Variables.Port), + 'proxy.config.dns.resolv_conf': 'NULL', + 'proxy.config.hostdb.ip_resolve': 'ipv4' + }) + + ts.Disk.remap_yaml.AddLines( + f''' + remap: + - type: map + from: + url: http://testDNS.com + to: + url: http://test.ipv4.only.com:{server.Variables.Port} + plugins: + - name: conf_remap.so + params: + - proxy.config.hostdb.ip_resolve=ipv6;ipv4;client + - type: map + from: + url: http://testDNS2.com + to: + url: http://test.ipv6.only.com:{server_v6.Variables.Port} + plugins: + - name: conf_remap.so + params: + - proxy.config.hostdb.ip_resolve=ipv6;only + '''.split("\n")) + + dns.addRecords(records={"test.ipv4.only.com.": ["127.0.0.1"]}) + dns.addRecords(records={"test.ipv6.only.com": ["127.0.0.1", "::1"]}) + + tr = urtest.AddTestRun() + tr.MakeCurlCommand(' --proxy 127.0.0.1:{0} "http://testDNS.com" --verbose'.format(ts.Variables.port), ts=ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.StartBefore(server) + tr.Processes.Default.StartBefore(dns) + tr.Processes.Default.StartBefore(urtest.Processes.ts) + tr.Processes.Default.Streams.stderr = "gold/remap-DNS-200.gold" + tr.StillRunningAfter = server + + tr = urtest.AddTestRun() + tr.MakeCurlCommand(' --proxy 127.0.0.1:{0} "http://testDNS2.com" --verbose'.format(ts.Variables.port), ts=ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.StartBefore(server_v6) + tr.Processes.Default.Streams.stderr = "gold/remap-DNS-ipv6-200.gold" + tr.StillRunningAfter = server_v6 + urtest.execute() diff --git a/tests/uranium_tests/remap_yaml/test_remap_load_empty_failure_yaml.py b/tests/uranium_tests/remap_yaml/test_remap_load_empty_failure_yaml.py new file mode 100644 index 00000000000..868c2d28eac --- /dev/null +++ b/tests/uranium_tests/remap_yaml/test_remap_load_empty_failure_yaml.py @@ -0,0 +1,63 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_remap_load_empty_failure_yaml(urtest: UraniumTest) -> None: + ''' + Verify correct behavior of regex_map in remap.yaml. + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + urtest.Summary = ''' + Test minimum rules on load - fail on missing file. + ''' + ts = urtest.MakeATSProcess("ts") + ts.Disk.remap_yaml.AddLine(f"") # empty file + ts.Disk.records_config.update({'proxy.config.url_remap.min_rules_required': 1}) + ts.ReturnCode = 33 # expect to Emergency fail due to empty "remap.config". + ts.Ready = 0 + + tr = urtest.AddTestRun("test") + + # We have to wait upon TS to emit the expected log message, but it cannot be + # the ts Ready criteria because autest might detect the process going away + # before it detects the log message. So we add a separate process that waits + # upon the log message. + watcher = urtest.Processes.Process("watcher") + watcher.Command = "sleep 10" + watcher.Ready = When.FileContains(ts.Disk.diags_log.Name, "remap.yaml failed to load") + watcher.StartBefore(ts) + + tr.Processes.Default.Command = "echo howdy" + tr.TimeOut = 5 + tr.Processes.Default.StartBefore(watcher) + urtest.execute() diff --git a/tests/uranium_tests/remap_yaml/test_remap_load_empty_success_yaml.py b/tests/uranium_tests/remap_yaml/test_remap_load_empty_success_yaml.py new file mode 100644 index 00000000000..25d79ed82d0 --- /dev/null +++ b/tests/uranium_tests/remap_yaml/test_remap_load_empty_success_yaml.py @@ -0,0 +1,54 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_remap_load_empty_success_yaml(urtest: UraniumTest) -> None: + ''' + Verify correct behavior of regex_map in remap.yaml. + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + urtest.Summary = ''' + Test minimum rules on load - succeed on empty file. + ''' + + ts = urtest.MakeATSProcess("ts") + ts.Disk.remap_yaml.AddLine(f"") # empty file + ts.Disk.records_config.update({f'proxy.config.url_remap.min_rules_required': 0}) + + tr = urtest.AddTestRun("startup") + p = tr.Processes.Default + p.Command = "sh -c echo" + p.ReturnCode = 0 + p.StartBefore(urtest.Processes.ts) + tr.StillRunningAfter = ts + urtest.execute() diff --git a/tests/uranium_tests/remap_yaml/test_remap_load_missing_failure_yaml.py b/tests/uranium_tests/remap_yaml/test_remap_load_missing_failure_yaml.py new file mode 100644 index 00000000000..09298107f2e --- /dev/null +++ b/tests/uranium_tests/remap_yaml/test_remap_load_missing_failure_yaml.py @@ -0,0 +1,50 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_remap_load_missing_failure_yaml(urtest: UraniumTest) -> None: + ''' + Verify correct behavior of regex_map in remap.yaml. + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + urtest.Summary = ''' + Test minimum rules on load - fail on missing file. + ''' + ts = urtest.MakeATSProcess("ts") + ts.Disk.records_config.update({'proxy.config.url_remap.min_rules_required': 1}) + ts.ReturnCode = 33 # expect to Emergency fail due to empty "remap.config". + + tr = urtest.AddTestRun("test") + tr.Processes.Default.Command = "echo" + tr.Processes.Default.StartAfter(ts, ready=When.FileExists(ts.Disk.diags_log)) + urtest.execute() diff --git a/tests/uranium_tests/remap_yaml/test_remap_load_missing_success_yaml.py b/tests/uranium_tests/remap_yaml/test_remap_load_missing_success_yaml.py new file mode 100644 index 00000000000..9ea672abde1 --- /dev/null +++ b/tests/uranium_tests/remap_yaml/test_remap_load_missing_success_yaml.py @@ -0,0 +1,54 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_remap_load_missing_success_yaml(urtest: UraniumTest) -> None: + ''' + Verify correct behavior of regex_map in remap.yaml. + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + urtest.Summary = ''' + Test minimum rules on load - succeed on missing file. + ''' + + ts = urtest.MakeATSProcess("ts") + + ts.Disk.records_config.update({f'proxy.config.url_remap.min_rules_required': 0}) + + tr = urtest.AddTestRun("startup") + p = tr.Processes.Default + p.Command = "sh -c echo" + p.ReturnCode = 0 + p.StartBefore(urtest.Processes.ts) + tr.StillRunningAfter = ts + urtest.execute() diff --git a/tests/uranium_tests/remap_yaml/test_remap_reload_yaml.py b/tests/uranium_tests/remap_yaml/test_remap_reload_yaml.py new file mode 100644 index 00000000000..b6ad248daa0 --- /dev/null +++ b/tests/uranium_tests/remap_yaml/test_remap_reload_yaml.py @@ -0,0 +1,175 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_remap_reload_yaml(urtest: UraniumTest) -> None: + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + import os + + def update_remap_yaml(path: str, lines: list) -> None: + """Update the remap.yaml file. + + This is used to update the config file between test runs without + triggering framework warnings about overriding file objects. + + :param path: The path to the remap.config file. + :param lines: The list of lines to write to the file. + """ + with open(path, 'w') as f: + f.write('\n'.join(lines) + '\n') + + urtest.Summary = ''' + Test remap reloading + ''' + urtest.testName = 'remap_reload' + + replay_file_1 = "reload_1.replay.yaml" + replay_file_2 = "reload_2.replay.yaml" + replay_file_3 = "reload_3.replay.yaml" + replay_file_4 = "reload_4.replay.yaml" + + tm = urtest.MakeATSProcess("ts", disable_log_checks=True) + remap_cfg_path = os.path.join(tm.Variables.CONFIGDIR, 'remap.yaml') + + pv = urtest.MakeVerifierServerProcess("pv", "reload_server.replay.yaml") + pv_port = pv.Variables.http_port + + tm.Disk.remap_yaml.AddLines( + f''' + remap: + - type: map + from: + url: http://alpha.ex + to: + url: http://alpha.ex:{pv_port} + - type: map + from: + url: http://bravo.ex + to: + url: http://bravo.ex:{pv_port} + - type: map + from: + url: http://charlie.ex + to: + url: http://charlie.ex:{pv_port} + - type: map + from: + url: http://delta.ex + to: + url: http://delta.ex:{pv_port} + '''.split("\n")) + + tm.Disk.records_config.update({'proxy.config.url_remap.min_rules_required': 3}) + + nameserver = urtest.MakeDNServer("dns", default='127.0.0.1') + tm.Disk.records_config.update( + { + 'proxy.config.dns.nameservers': f"127.0.0.1:{nameserver.Variables.Port}", + 'proxy.config.dns.resolv_conf': 'NULL' + }) + + tr = urtest.AddTestRun("verify load") + tr.Processes.Default.StartBefore(pv) + tr.Processes.Default.StartBefore(nameserver) + tr.Processes.Default.StartBefore(tm) + tr.AddVerifierClientProcess("client", replay_file_1, http_ports=[tm.Variables.port]) + + tr = urtest.AddTestRun("Change remap.yaml to have only two remap rules") + p = tr.Processes.Default + p.Env = tm.Env + p.Command = 'echo "Change remap.yaml, two lines"' + p.Setup.Lambda( + lambda: update_remap_yaml( + remap_cfg_path, f''' + remap: + - type: map + from: + url: http://alpha.ex + to: + url: http://alpha.ex:{pv_port} + - type: map + from: + url: http://bravo.ex + to: + url: http://bravo.ex:{pv_port} + '''.split("\n"))) + + tr = urtest.AddConfigReload( + tm, expect="fail", expect_tasks=["remap.yaml"], delay_start=2, description="remap_yaml reload, fails") + + tr = urtest.AddTestRun("after first reload") + tr.AddVerifierClientProcess("client_2", replay_file_2, http_ports=[tm.Variables.port]) + + tr = urtest.AddTestRun("Change remap.yaml to have more than three remap rules") + p = tr.Processes.Default + p.Env = tm.Env + p.Command = 'echo "Change remap.yaml, more than three lines"' + p.Setup.Lambda( + lambda: update_remap_yaml( + remap_cfg_path, f''' + remap: + - type: map + from: + url: http://echo.ex + to: + url: http://echo.ex:{pv_port} + - type: map + from: + url: http://foxtrot.ex + to: + url: http://foxtrot.ex:{pv_port} + - type: map + from: + url: http://golf.ex + to: + url: http://golf.ex:{pv_port} + - type: map + from: + url: http://hotel.ex + to: + url: http://hotel.ex:{pv_port} + - type: map + from: + url: http://india.ex + to: + url: http://india.ex:{pv_port} + '''.split("\n"))) + + tr = urtest.AddConfigReload( + tm, expect="success", expect_tasks=["remap.yaml"], delay_start=2, description="remap_yaml reload, succeeds") + + tr = urtest.AddTestRun("post update charlie") + tr.AddVerifierClientProcess("client_3", replay_file_3, http_ports=[tm.Variables.port]) + + tr = urtest.AddTestRun("post update golf") + tr.AddVerifierClientProcess("client_4", replay_file_4, http_ports=[tm.Variables.port]) + urtest.execute() diff --git a/tests/uranium_tests/remap_yaml/test_remap_ws_yaml.py b/tests/uranium_tests/remap_yaml/test_remap_ws_yaml.py new file mode 100644 index 00000000000..6f9706679a2 --- /dev/null +++ b/tests/uranium_tests/remap_yaml/test_remap_ws_yaml.py @@ -0,0 +1,157 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_remap_ws_yaml(urtest: UraniumTest) -> None: + ''' + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + urtest.Summary = ''' + Test a basic remap of a websocket connections + ''' + + urtest.ContinueOnFail = True + + ts = urtest.MakeATSProcess("ts", enable_tls=True) + server = urtest.MakeOriginServer("server") + + testName = "Test WebSocket Remaps" + request_header = { + "headers": "GET /chat HTTP/1.1\r\nHost: www.example.com\r\nUpgrade: websocket\r\nConnection: Upgrade\r\n\r\n", + "body": None + } + response_header = { + "headers": + "HTTP/1.1 101 OK\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=\r\n\r\n", + "body": None + } + server.addResponse("sessionlog.json", request_header, response_header) + + ts.addDefaultSSLFiles() + + ts.Disk.records_config.update( + { + 'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir), + 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts.Variables.SSLDir), + }) + + ts.Disk.remap_yaml.AddLines( + f''' + remap: + - type: map + from: + url: ws://www.example.com:{ts.Variables.port} + to: + url: ws://127.0.0.1:{server.Variables.Port} + - type: map + from: + url: wss://www.example.com:{ts.Variables.ssl_port} + to: + url: ws://127.0.0.1:{server.Variables.Port} + '''.split("\n")) + + ts.Disk.ssl_multicert_yaml.AddLines( + """ + ssl_multicert: + - dest_ip: "*" + ssl_cert_name: server.pem + ssl_key_name: server.key + """.split("\n")) + + if not Condition.CurlUsingUnixDomainSocket(): + # wss mapping + tr = urtest.AddTestRun() + tr.Processes.Default.StartBefore(server) + tr.Processes.Default.StartBefore(urtest.Processes.ts, ready=1) + tr.MakeCurlCommand( + '--max-time 2 -v -s -q -H "Connection: Upgrade" -H "Upgrade: websocket" -H "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==" -H "Sec-WebSocket-Version: 13" --http1.1 --resolve www.example.com:{0}:127.0.0.1 -k https://www.example.com:{0}/chat' + .format(ts.Variables.ssl_port), + ts=ts) + tr.Processes.Default.ReturnCode = 28 + tr.Processes.Default.Streams.stderr = "gold/remap-ws-upgrade.gold" + tr.StillRunningAfter = server + tr.StillRunningAfter = ts + + # ws mapping + tr = urtest.AddTestRun() + if Condition.CurlUsingUnixDomainSocket(): + tr.Processes.Default.StartBefore(server) + tr.Processes.Default.StartBefore(urtest.Processes.ts, ready=1) + tr.MakeCurlCommand( + '--max-time 2 -v -s -q -H "Connection: Upgrade" -H "Upgrade: websocket" -H "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==" -H "Sec-WebSocket-Version: 13" --http1.1 --resolve www.example.com:{0}:127.0.0.1 -k http://www.example.com:{0}/chat' + .format(ts.Variables.port), + ts=ts) + tr.Processes.Default.ReturnCode = 28 + tr.Processes.Default.Streams.stderr = "gold/remap-ws-upgrade.gold" + tr.StillRunningAfter = server + tr.StillRunningAfter = ts + + # Missing required headers (should result in 400) + tr = urtest.AddTestRun() + tr.MakeCurlCommand( + '--max-time 2 -v -s -q -H "Connection: Upgrade" -H "Upgrade: websocket" --http1.1 --resolve www.example.com:{0}:127.0.0.1 -k http://www.example.com:{0}/chat' + .format(ts.Variables.port), + ts=ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.stderr = "gold/remap-ws-upgrade-400.gold" + tr.StillRunningAfter = server + tr.StillRunningAfter = ts + + # Test metrics + tr = urtest.AddTestRun() + if Condition.CurlUsingUnixDomainSocket(): + metrics_gold_file = 'remap-ws-metrics-uds.gold' + else: + metrics_gold_file = 'remap-ws-metrics.gold' + tr.Processes.Default.Command = ( + f"{urtest.Variables.AtsTestToolsDir}/stdout_wait" + " 'traffic_ctl metric get" + + " proxy.process.http.total_incoming_connections" + " proxy.process.http.total_client_connections" + + " proxy.process.http.total_client_connections_ipv4" + " proxy.process.http.total_client_connections_ipv6" + + " proxy.process.http.total_server_connections" + " proxy.process.http2.total_client_connections" + + " proxy.process.http.connect_requests" + " proxy.process.tunnel.total_client_connections_blind_tcp" + + " proxy.process.tunnel.current_client_connections_blind_tcp" + " proxy.process.tunnel.total_server_connections_blind_tcp" + + " proxy.process.tunnel.current_server_connections_blind_tcp" + " proxy.process.tunnel.total_client_connections_tls_tunnel" + + " proxy.process.tunnel.current_client_connections_tls_tunnel" + + " proxy.process.tunnel.total_client_connections_tls_forward" + + " proxy.process.tunnel.current_client_connections_tls_forward" + + " proxy.process.tunnel.total_client_connections_tls_partial_blind" + + " proxy.process.tunnel.current_client_connections_tls_partial_blind" + + " proxy.process.tunnel.total_client_connections_tls_http" + " proxy.process.tunnel.current_client_connections_tls_http" + + " proxy.process.tunnel.total_server_connections_tls" + " proxy.process.tunnel.current_server_connections_tls'" + + f" {urtest.TestDirectory}/gold/{metrics_gold_file}") + # Need to copy over the environment so traffic_ctl knows where to find the unix domain socket + tr.Processes.Default.Env = ts.Env + tr.Processes.Default.ReturnCode = 0 + tr.StillRunningAfter = server + tr.StillRunningAfter = ts + urtest.execute() diff --git a/tests/uranium_tests/runroot/test_runroot_error.py b/tests/uranium_tests/runroot/test_runroot_error.py new file mode 100644 index 00000000000..ec48c3ccad4 --- /dev/null +++ b/tests/uranium_tests/runroot/test_runroot_error.py @@ -0,0 +1,79 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_runroot_error(urtest: UraniumTest) -> None: + ''' + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import os + + urtest.Summary = ''' + Test for expected error and failure of runroot from traffic_layout. + ''' + urtest.ContinueOnFail = True + + # create runroot + path = os.path.join(urtest.RunDirectory, "runroot") + tr = urtest.AddTestRun() + tr.Processes.Default.Command = "$ATS_BIN/traffic_layout init --path " + path + f = tr.Disk.File(os.path.join(path, "runroot.yaml")) + f.Exists = True + + # use existing runroot + tr = urtest.AddTestRun("using existing runroot") + tr.Processes.Default.Command = "$ATS_BIN/traffic_layout init --path " + path + tr.Processes.Default.Streams.All = Testers.ContainsExpression("Using existing runroot", "init incorrect usage") + f = tr.Disk.File(os.path.join(path, "runroot.yaml")) + f.Exists = True + + # create runroot inside another + path_inside = os.path.join(path, "runroot") + tr = urtest.AddTestRun("create runroot inside runroot") + tr.Processes.Default.Command = "$ATS_BIN/traffic_layout init --path " + path_inside + tr.Processes.Default.Streams.All = Testers.ContainsExpression( + "Cannot create runroot inside another runroot", "init incorrect usage") + f = tr.Disk.File(os.path.join(path_inside, "runroot.yaml")) + f.Exists = False + + # remove invalid runroot + path_invalid = os.path.join(urtest.RunDirectory, "tmp") + tr = urtest.AddTestRun("remove invalid runroot") + tr.Processes.Default.Command = "$ATS_BIN/traffic_layout remove --path " + path_invalid + tr.Processes.Default.Streams.All = Testers.ContainsExpression("Unable to read", "remove incorrect usage") + + # verify invalid runroot + tr = urtest.AddTestRun("verify invalid runroot") + tr.Processes.Default.Command = "$ATS_BIN/traffic_layout verify --path " + path_invalid + tr.Processes.Default.Streams.All = Testers.ContainsExpression("Unable to read", "verify incorrect usage") + urtest.execute() diff --git a/tests/uranium_tests/runroot/test_runroot_init.py b/tests/uranium_tests/runroot/test_runroot_init.py new file mode 100644 index 00000000000..5a7fd6174a5 --- /dev/null +++ b/tests/uranium_tests/runroot/test_runroot_init.py @@ -0,0 +1,99 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_runroot_init(urtest: UraniumTest) -> None: + ''' + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import os + + urtest.Summary = ''' + Test for init of runroot from traffic_layout. + ''' + urtest.ContinueOnFail = True + urtest.SkipUnless( + urtest.Variables.BINDIR.startswith(urtest.Variables.PREFIX), "need to guarantee bin path starts with prefix for runroot") + + # init from pass in path + path1 = os.path.join(urtest.RunDirectory, "runroot1") + tr = urtest.AddTestRun("Test traffic_layout init #1") + tr.Processes.Default.Command = "$ATS_BIN/traffic_layout init --path " + path1 + tr.Processes.Default.ReturnCode = 0 + f = tr.Disk.File(os.path.join(path1, "runroot.yaml")) + f.Exists = True + + # init to relative directory + path2 = os.path.join(urtest.RunDirectory, "runroot2") + tr = urtest.AddTestRun("Test traffic_layout init #2") + tr.Processes.Default.Command = "cd " + urtest.RunDirectory + ";$ATS_BIN/traffic_layout init --path runroot2" + tr.Processes.Default.ReturnCode = 0 + f = tr.Disk.File(os.path.join(path2, "runroot.yaml")) + f.Exists = True + + # init to cwd + path3 = os.path.join(urtest.RunDirectory, "runroot3") + tr = urtest.AddTestRun("Test traffic_layout init #3") + tr.Processes.Default.Command = "mkdir " + path3 + ";cd " + path3 + ";$ATS_BIN/traffic_layout init" + tr.Processes.Default.ReturnCode = 0 + f = tr.Disk.File(os.path.join(path3, "runroot.yaml")) + f.Exists = True + + # --force init to an non-empty directory + path4 = os.path.join(urtest.RunDirectory, "runroot4") + tr = urtest.AddTestRun("Test traffic_layout init #4") + randomfile = os.path.join(path4, "foo") + tr.Processes.Default.Command = "mkdir " + path4 + ";touch " + randomfile + ";$ATS_BIN/traffic_layout init --force --path " + path4 + tr.Processes.Default.ReturnCode = 0 + f = tr.Disk.File(os.path.join(path4, "runroot.yaml")) + f.Exists = True + + # create runroot with junk to guarantee only traffic server related files are copied + bin_path = urtest.Variables.BINDIR[urtest.Variables.BINDIR.find(urtest.Variables.PREFIX) + len(urtest.Variables.PREFIX) + 1:] + path5 = os.path.join(urtest.RunDirectory, "runroot5") + exe_path = os.path.join(bin_path, "traffic_layout") + junk_file = os.path.join(bin_path, "junk") + junk = os.path.join(path1, junk_file) + + tr = urtest.AddTestRun("Test traffic_layout init #5") + # create the junk files in runroot1 and create(init and copy) runroot5 from runroot 1 + tr.Processes.Default.Command = "touch " + junk + ";" + os.path.join(path1, exe_path) + " init --path " + path5 + tr.Processes.Default.ReturnCode = 0 + f = tr.Disk.File(os.path.join(path5, "runroot.yaml")) + f.Exists = True + # check if the junk file is created and not copied to the new runroot + f = tr.Disk.File(junk) + f.Exists = True + f = tr.Disk.File(os.path.join(path5, junk_file)) + f.Exists = False + urtest.execute() diff --git a/tests/uranium_tests/runroot/test_runroot_remove.py b/tests/uranium_tests/runroot/test_runroot_remove.py new file mode 100644 index 00000000000..58483ed4c7e --- /dev/null +++ b/tests/uranium_tests/runroot/test_runroot_remove.py @@ -0,0 +1,91 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_runroot_remove(urtest: UraniumTest) -> None: + ''' + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import os + + urtest.Summary = ''' + Test for remove of runroot from traffic_layout. + ''' + urtest.ContinueOnFail = True + + # create three runroot for removing testing + path1 = os.path.join(urtest.RunDirectory, "runroot1") + tr = urtest.AddTestRun() + tr.Processes.Default.Command = "$ATS_BIN/traffic_layout init --path " + path1 + f = tr.Disk.File(os.path.join(path1, "runroot.yaml")) + f.Exists = True + + path2 = os.path.join(urtest.RunDirectory, "runroot2") + tr = urtest.AddTestRun() + tr.Processes.Default.Command = "$ATS_BIN/traffic_layout init --path " + path2 + f = tr.Disk.File(os.path.join(path2, "runroot.yaml")) + f.Exists = True + + path3 = os.path.join(urtest.RunDirectory, "runroot3") + tr = urtest.AddTestRun() + tr.Processes.Default.Command = "$ATS_BIN/traffic_layout init --path " + path3 + f = tr.Disk.File(os.path.join(path3, "runroot.yaml")) + f.Exists = True + + # normal remove from pass in path + tr = urtest.AddTestRun("Test traffic_layout remove #1") + tr.Processes.Default.Command = "$ATS_BIN/traffic_layout remove --path " + path1 + tr.Processes.Default.ReturnCode = 0 + f = tr.Disk.File(os.path.join(path1, "runroot.yaml")) + f.Exists = False + d = tr.Disk.Directory(path1) + d.Exists = False + + # remove of relative path + tr = urtest.AddTestRun("Test traffic_layout remove #2") + tr.Processes.Default.Command = "cd " + urtest.RunDirectory + ";$ATS_BIN/traffic_layout remove --path runroot2" + tr.Processes.Default.ReturnCode = 0 + f = tr.Disk.File(os.path.join(path2, "runroot.yaml")) + f.Exists = False + d = tr.Disk.Directory(path2) + d.Exists = False + + # remove cwd + tr = urtest.AddTestRun("Test traffic_layout remove #3") + tr.Processes.Default.Command = "mkdir " + path3 + ";cd " + path3 + ";$ATS_BIN/traffic_layout remove" + tr.Processes.Default.ReturnCode = 0 + f = tr.Disk.File(os.path.join(path3, "runroot.yaml")) + f.Exists = False + d = tr.Disk.Directory(path3) + d.Exists = True + urtest.execute() diff --git a/tests/uranium_tests/runroot/test_runroot_use.py b/tests/uranium_tests/runroot/test_runroot_use.py new file mode 100644 index 00000000000..a76837e1a1f --- /dev/null +++ b/tests/uranium_tests/runroot/test_runroot_use.py @@ -0,0 +1,86 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_runroot_use(urtest: UraniumTest) -> None: + ''' + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import os + + urtest.Summary = ''' + Test for using of runroot from traffic_layout. + ''' + urtest.ContinueOnFail = True + urtest.SkipUnless( + urtest.Variables.BINDIR.startswith(urtest.Variables.PREFIX), "need to guarantee bin path starts with prefix for runroot") + + # create two runroot for testing + path = os.path.join(urtest.RunDirectory, "runroot") + tr = urtest.AddTestRun() + tr.Processes.Default.Command = "$ATS_BIN/traffic_layout init --path " + path + f = tr.Disk.File(os.path.join(path, "runroot.yaml")) + f.Exists = True + + path2 = os.path.join(urtest.RunDirectory, "runroot2") + tr = urtest.AddTestRun() + tr.Processes.Default.Command = "$ATS_BIN/traffic_layout init --path " + path2 + f = tr.Disk.File(os.path.join(path2, "runroot.yaml")) + f.Exists = True + + # 1. --run-root use path cmd + tr = urtest.AddTestRun("use runroot via commandline") + tr.Processes.Default.Command = "$ATS_BIN/traffic_layout info --run-root=" + path + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.All = Testers.ContainsExpression("PREFIX: " + path, "commandline runroot path") + + # 2. use cwd as runroot + tr = urtest.AddTestRun("use runroot via cwd") + tr.Processes.Default.Command = "cd " + path + ";" + "$ATS_BIN/traffic_layout info" + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.All = Testers.ContainsExpression("PREFIX: " + path, "cwd runroot path") + + # 4. use path directly bin + bin_path = urtest.Variables.BINDIR[urtest.Variables.BINDIR.find(urtest.Variables.PREFIX) + len(urtest.Variables.PREFIX) + 1:] + tr = urtest.AddTestRun("use runroot via bin executable") + tr.Processes.Default.Command = os.path.join(path, os.path.join(bin_path, "traffic_layout") + " info") + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.All = Testers.ContainsExpression("PREFIX: " + path, "bin path") + + # 3. TS_RUNROOT ENV variable + tr = urtest.AddTestRun("use runroot via TS_RUNROOT") + tr.Processes.Default.Env["TS_RUNROOT"] = path2 + tr.Processes.Default.Command = "$ATS_BIN/traffic_layout info" + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.All = Testers.ContainsExpression("PREFIX: " + path2, "$TS_RUNROOT Env path") + urtest.execute() diff --git a/tests/uranium_tests/runroot/test_runroot_verify.py b/tests/uranium_tests/runroot/test_runroot_verify.py new file mode 100644 index 00000000000..3e6a4cb2469 --- /dev/null +++ b/tests/uranium_tests/runroot/test_runroot_verify.py @@ -0,0 +1,86 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_runroot_verify(urtest: UraniumTest) -> None: + ''' + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import os + + urtest.Summary = ''' + Test for verify of runroot from traffic_layout. + ''' + + prefix = urtest.Variables["PREFIX"] + bindir = urtest.Variables["BINDIR"] + logdir = urtest.Variables["LOGDIR"] + + if bindir.startswith(prefix): + # get the bin directory based on removing the common prefix + binsuffix = bindir[len(prefix) + 1:] + else: + # given a custom setup this might work.. or it might not + binsuffix = bindir + + if logdir.startswith(prefix): + # get the bin directory based on removing the common prefix + logsuffix = bindir[len(prefix) + 1:] + else: + # given a custom setup this might work.. or it might not + logsuffix = logdir + + # create runroot + path = os.path.join(urtest.RunDirectory, "runroot") + tr = urtest.AddTestRun("Create runroot") + tr.Processes.Default.Command = "$ATS_BIN/traffic_layout init --path " + path + f = tr.Disk.File(os.path.join(path, "runroot.yaml")) + f.Exists = True + + # verify test #1 + tr = urtest.AddTestRun("verify runroot test1") + tr.Processes.Default.Command = "$ATS_BIN/traffic_layout verify --path " + path + + tr.Processes.Default.Streams.All = Testers.ContainsExpression(os.path.join(path, binsuffix), "example bindir output") + tr.Processes.Default.Streams.All = Testers.ContainsExpression(os.path.join(path, logsuffix), "example logdir output") + tr.Processes.Default.Streams.All = Testers.ContainsExpression("PASSED", "contain passed message") + + # verify test #2 + bin_path = urtest.Variables.BINDIR[urtest.Variables.BINDIR.find(urtest.Variables.PREFIX) + len(urtest.Variables.PREFIX) + 1:] + tr = urtest.AddTestRun("verify runroot test2") + tr.Processes.Default.Command = "cd " + path + ";" + os.path.join(bin_path, "traffic_layout") + " verify --path " + path + + tr.Processes.Default.Streams.All = Testers.ContainsExpression(os.path.join(path, binsuffix), "example bindir output") + tr.Processes.Default.Streams.All = Testers.ContainsExpression(os.path.join(path, logsuffix), "example logdir output") + tr.Processes.Default.Streams.All = Testers.ContainsExpression("PASSED", "contain passed message") + urtest.execute() diff --git a/tests/gold_tests/session_sharing/gold/200.gold b/tests/uranium_tests/session_sharing/gold/200.gold similarity index 100% rename from tests/gold_tests/session_sharing/gold/200.gold rename to tests/uranium_tests/session_sharing/gold/200.gold diff --git a/tests/uranium_tests/session_sharing/test_session_match.py b/tests/uranium_tests/session_sharing/test_session_match.py new file mode 100644 index 00000000000..a96553b1577 --- /dev/null +++ b/tests/uranium_tests/session_sharing/test_session_match.py @@ -0,0 +1,143 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_session_match(urtest: UraniumTest) -> None: + ''' + Test that a plugin can modify server session sharing. + ''' + + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + class SessionMatchTest: + TestCounter = 0 + + def __init__(self, TestSummary, sharingMatchValue): + SessionMatchTest.TestCounter += 1 + self._MyTestCount = SessionMatchTest.TestCounter + urtest.Summary = TestSummary + self._tr = urtest.AddTestRun() + self._sharingMatchValue = sharingMatchValue + self.setupOriginServer() + self.setupTS() + + def setupOriginServer(self): + self._server = urtest.MakeOriginServer("server{counter}".format(counter=self._MyTestCount)) + request_header = { + "headers": "GET /one HTTP/1.1\r\nHost: www.example.com\r\nContent-Length: 0\r\n\r\n", + "timestamp": "1469733493.993", + "body": "" + } + response_header = { + "headers": "HTTP/1.1 200 OK\r\nServer: microserver\r\n" + "Content-Length: 0\r\n\r\n", + "timestamp": "1469733493.993", + "body": "" + } + self._server.addResponse("sessionlog.json", request_header, response_header) + + request_header2 = { + "headers": "GET /two HTTP/1.1\r\nContent-Length: 0\r\n" + "Host: www.example.com\r\n\r\n", + "timestamp": "1469733493.993", + "body": "a\r\na\r\na\r\n\r\n" + } + response_header2 = { + "headers": "HTTP/1.1 200 OK\r\nServer: microserver\r\n" + "Content-Length: 0\r\n\r\n", + "timestamp": "1469733493.993", + "body": "" + } + self._server.addResponse("sessionlog.json", request_header2, response_header2) + + request_header3 = { + "headers": "GET /three HTTP/1.1\r\nContent-Length: 0\r\n" + "Host: www.example.com\r\nConnection: close\r\n\r\n", + "timestamp": "1469733493.993", + "body": "a\r\na\r\na\r\n\r\n" + } + response_header3 = { + "headers": "HTTP/1.1 200 OK\r\nServer: microserver\r\n" + "Connection: close\r\nContent-Length: 0\r\n\r\n", + "timestamp": "1469733493.993", + "body": "" + } + self._server.addResponse("sessionlog.json", request_header3, response_header3) + + def setupTS(self): + self._ts = urtest.MakeATSProcess("ts{counter}".format(counter=self._MyTestCount)) + self._ts.Disk.remap_config.AddLine('map / http://127.0.0.1:{0}'.format(self._server.Variables.Port)) + self._ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'http', + 'proxy.config.http.auth_server_session_private': 1, + 'proxy.config.http.server_session_sharing.pool': 'global', + 'proxy.config.http.server_session_sharing.match': self._sharingMatchValue, + }) + + def _runTraffic(self): + self._tr.MakeCurlCommandMulti( + '{{curl}} -v -H\'Host: www.example.com\' -H\'Connection: close\' http://127.0.0.1:{port}/one &&' + '{{curl}} -v -H\'Host: www.example.com\' -H\'Connection: close\' http://127.0.0.1:{port}/two &&' + '{{curl}} -v -H\'Host: www.example.com\' -H\'Connection: close\' http://127.0.0.1:{port}/three'.format( + port=self._ts.Variables.port), + ts=self._ts) + self._tr.Processes.Default.ReturnCode = 0 + self._tr.Processes.Default.StartBefore(self._server) + self._tr.Processes.Default.StartBefore(self._ts) + self._tr.Processes.Default.Streams.stderr = "gold/200.gold" + + def runAndExpectSharing(self): + self._runTraffic() + self._ts.Disk.traffic_out.Content = Testers.ContainsExpression( + "global pool search successful", "Verify that sessions got shared") + + def runAndExpectNoSharing(self): + self._runTraffic() + self._ts.Disk.traffic_out.Content = Testers.ExcludesExpression( + "global pool search successful", "Verify that sessions did not get shared") + + sessionMatchTest = SessionMatchTest(TestSummary='Test that session sharing works with host matching', sharingMatchValue='host') + sessionMatchTest.runAndExpectSharing() + + sessionMatchTest = SessionMatchTest(TestSummary='Test that session sharing works with ip matching', sharingMatchValue='ip') + sessionMatchTest.runAndExpectSharing() + + sessionMatchTest = SessionMatchTest( + TestSummary='Test that session sharing works with matching both ip and host', sharingMatchValue='both') + sessionMatchTest.runAndExpectSharing() + + sessionMatchTest = SessionMatchTest( + TestSummary='Test that session sharing is disabled when matching is set to none', sharingMatchValue='none') + sessionMatchTest.runAndExpectNoSharing() + urtest.execute() diff --git a/tests/uranium_tests/shutdown/test_crashlog_no_false_positive.py b/tests/uranium_tests/shutdown/test_crashlog_no_false_positive.py new file mode 100644 index 00000000000..7aafa2b388f --- /dev/null +++ b/tests/uranium_tests/shutdown/test_crashlog_no_false_positive.py @@ -0,0 +1,94 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_crashlog_no_false_positive(urtest: UraniumTest) -> None: + ''' + Verify that a normal (SIGTERM) shutdown does not spuriously fire traffic_crashlog. + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import os + + urtest.Summary = ''' + Verify that gracefully stopping traffic_server (SIGTERM) does not cause the crash + log helper (traffic_crashlog) to emit a bogus crash log. + + Regression test: the crash log helper is parked with SIGSTOP and arms a + parent-death signal (PR_SET_PDEATHSIG=SIGCONT). When traffic_server exits + normally -- e.g. a routine SIGTERM shutdown -- the kernel resumes the helper via + that death signal. The helper used to decide "did I wake because of a real crash + or a plain exit?" with a racy getppid() check, and would lose the race and write + an empty crash log against the already-exiting process. The fix replaces that + guess with a handshake: crash_logger_invoke() writes the signal number to the + helper, and a plain exit closes the socket so the helper reads EOF and stays + quiet. + + Note: PR_SET_PDEATHSIG is a no-op on Darwin, so this false positive only occurs + on Linux. On other platforms this test still confirms that the helper is armed + and does not emit a crash log on a clean shutdown. + ''' + + ts = urtest.MakeATSProcess('ts') + + ts.Disk.records_config.update( + { + # Enable the "server" debug tag so traffic_server emits the "received exit signal, + # shutting down" DIAG line (the shutdown anchor checked below) to traffic.out. + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'server', + # Force the crash log helper on regardless of the build's TS_USE_REMOTE_UNWINDING + # default (it is NULL on platforms without remote unwinding, e.g. Darwin). + 'proxy.config.crash_log_helper': os.path.join(ts.Variables.BINDIR, 'traffic_crashlog'), + }) + + tr = urtest.AddTestRun('Start traffic_server with the crash log helper armed, then let AuTest SIGTERM it') + tr.Processes.Default.Command = 'printf "crash log helper armed"' + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.StartBefore(ts) + tr.StillRunningAfter = ts + + # Anchor the scenario: confirm traffic_server actually ran the graceful (SIGTERM) shutdown + # path -- the exact trigger that woke the crash log helper in production. Without this the + # ExcludesExpression checks below could pass without ever exercising shutdown. (The crash + # log helper itself is forced on via proxy.config.crash_log_helper above, and debug/DIAG + # output is emitted to traffic.out.) + ts.Disk.traffic_out.Content = Testers.ContainsExpression( + 'received exit signal, shutting down', 'traffic_server should have run the graceful shutdown path') + + # The crash log helper shares traffic_server's stderr (bound to traffic.out). On a clean + # shutdown it must neither wake nor write a crash log. + ts.Disk.traffic_out.Content += Testers.ExcludesExpression( + 'crashlog started', 'the crash log helper must not wake on a clean SIGTERM shutdown') + ts.Disk.traffic_out.Content += Testers.ExcludesExpression( + 'wrote crash log', 'a clean SIGTERM shutdown must not produce a crash log') + urtest.execute() diff --git a/tests/uranium_tests/shutdown/test_emergency.py b/tests/uranium_tests/shutdown/test_emergency.py new file mode 100644 index 00000000000..63084f2c3c2 --- /dev/null +++ b/tests/uranium_tests/shutdown/test_emergency.py @@ -0,0 +1,84 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_emergency(urtest: UraniumTest) -> None: + ''' + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import os + + urtest.Summary = 'Test TSEmergency API' + urtest.ContinueOnFail = True + + # Define default ATS + ts = urtest.MakeATSProcess('ts') + + urtest.testName = 'Emergency Shutdown Test' + + ts.Disk.records_config.update( + { + 'proxy.config.exec_thread.autoconfig.enabled': 0, + 'proxy.config.exec_thread.autoconfig.scale': 1.5, + 'proxy.config.exec_thread.limit': 16, + 'proxy.config.accept_threads': 1, + 'proxy.config.task_threads': 2, + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'TSEmergency_test' + }) + + # Load plugin + urtest.PrepareTestPlugin(os.path.join(urtest.Variables.AtsTestPluginsDir, 'emergency_shutdown.so'), ts) + + tr = urtest.AddTestRun() + + # We have to wait upon TS to emit the expected log message, but it cannot be + # the ts Ready criteria because autest might detect the process going away + # before it detects the log message. So we add a separate process that waits + # upon the log message. + watcher = urtest.Processes.Process("watcher") + watcher.Command = "sleep 10" + watcher.Ready = When.FileContains(ts.Disk.diags_log.Name, "testing emergency shutdown") + watcher.StartBefore(ts) + + tr.Processes.Default.Command = 'printf "Emergency Shutdown Test"' + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.StartBefore(watcher) + + tr.Timeout = 5 + ts.ReturnCode = 33 + ts.Ready = 0 + ts.Disk.traffic_out.Content = Testers.ExcludesExpression('failed to shutdown', 'should NOT contain "failed to shutdown"') + ts.Disk.diags_log.Content = Testers.IncludesExpression( + 'testing emergency shutdown', 'should contain "testing emergency shutdown"') + urtest.execute() diff --git a/tests/uranium_tests/shutdown/test_fatal.py b/tests/uranium_tests/shutdown/test_fatal.py new file mode 100644 index 00000000000..a48fb824c07 --- /dev/null +++ b/tests/uranium_tests/shutdown/test_fatal.py @@ -0,0 +1,83 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_fatal(urtest: UraniumTest) -> None: + ''' + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import os + + urtest.Summary = 'Test TSFatal API' + urtest.ContinueOnFail = True + + # Define default ATS + ts = urtest.MakeATSProcess('ts') + + urtest.testName = 'Fatal Shutdown Test' + + ts.Disk.records_config.update( + { + 'proxy.config.exec_thread.autoconfig.enabled': 0, + 'proxy.config.exec_thread.autoconfig.scale': 1.5, + 'proxy.config.exec_thread.limit': 16, + 'proxy.config.accept_threads': 1, + 'proxy.config.task_threads': 2, + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'TSFatal_test' + }) + + # Load plugin + urtest.PrepareTestPlugin(os.path.join(urtest.Variables.AtsTestPluginsDir, 'fatal_shutdown.so'), ts) + + tr = urtest.AddTestRun() + + # We have to wait upon TS to emit the expected log message, but it cannot be + # the ts Ready criteria because autest might detect the process going away + # before it detects the log message. So we add a separate process that waits + # upon the log message. + watcher = urtest.Processes.Process("watcher") + watcher.Command = "sleep 10" + watcher.Ready = When.FileContains(ts.Disk.diags_log.Name, "testing fatal shutdown") + watcher.StartBefore(ts) + + tr.Processes.Default.Command = 'printf "Fatal Shutdown Test"' + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.StartBefore(watcher) + + tr.Timeout = 5 + ts.ReturnCode = 70 + ts.Ready = 0 + ts.Disk.traffic_out.Content = Testers.ExcludesExpression('failed to shutdown', 'should NOT contain "failed to shutdown"') + ts.Disk.diags_log.Content = Testers.IncludesExpression('testing fatal shutdown', 'should contain "testing fatal shutdown"') + urtest.execute() diff --git a/tests/gold_tests/slow_post/__init__.py b/tests/uranium_tests/slow_post/__init__.py similarity index 100% rename from tests/gold_tests/slow_post/__init__.py rename to tests/uranium_tests/slow_post/__init__.py diff --git a/tests/gold_tests/slow_post/gold/200.gold b/tests/uranium_tests/slow_post/gold/200.gold similarity index 100% rename from tests/gold_tests/slow_post/gold/200.gold rename to tests/uranium_tests/slow_post/gold/200.gold diff --git a/tests/gold_tests/slow_post/quick_server.py b/tests/uranium_tests/slow_post/quick_server.py similarity index 100% rename from tests/gold_tests/slow_post/quick_server.py rename to tests/uranium_tests/slow_post/quick_server.py diff --git a/tests/gold_tests/slow_post/slow_post_client.py b/tests/uranium_tests/slow_post/slow_post_client.py similarity index 100% rename from tests/gold_tests/slow_post/slow_post_client.py rename to tests/uranium_tests/slow_post/slow_post_client.py diff --git a/tests/gold_tests/slow_post/slow_post_clients.py b/tests/uranium_tests/slow_post/slow_post_clients.py similarity index 100% rename from tests/gold_tests/slow_post/slow_post_clients.py rename to tests/uranium_tests/slow_post/slow_post_clients.py diff --git a/tests/uranium_tests/slow_post/test_quick_server.py b/tests/uranium_tests/slow_post/test_quick_server.py new file mode 100644 index 00000000000..9c9b41ff6d3 --- /dev/null +++ b/tests/uranium_tests/slow_post/test_quick_server.py @@ -0,0 +1,153 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_quick_server(urtest: UraniumTest) -> None: + """Verify ATS handles a server that replies before receiving a full request.""" + + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import os + from ports import get_port + import sys + + urtest.Summary = __doc__ + + class QuickServerTest: + """Verify that ATS doesn't delay responses behind slow posts.""" + + _init_file = '__init__.py' + _http_utils = 'http_utils.py' + _slow_post_client = 'slow_post_client.py' + _quick_server = 'quick_server.py' + + _dns_counter = 0 + _server_counter = 0 + _ts_counter = 0 + + def __init__(self, abort_request: bool, drain_request: bool, abort_response_headers: bool): + """Initialize the test. + + :param drain_request: Whether the server should drain the request body. + :param abort_request: Whether the client should abort the request body. + before disconnecting. + """ + self._should_drain_request = drain_request + self._should_abort_request = abort_request + self._should_abort_response_headers = abort_response_headers + + def _configure_dns(self, tr: 'TestRun') -> None: + """Configure the DNS. + + :param tr: The test run to associate with the DNS process with. + """ + self._dns = tr.MakeDNServer(f'dns-{QuickServerTest._dns_counter}', default='127.0.0.1') + QuickServerTest._dns_counter += 1 + + def _configure_server(self, tr: 'TestRun'): + """Configure the origin server. + + This server replies with a response immediately after receiving the + request headers. + + :param tr: The test run to associate with the server process with. + """ + server = tr.Processes.Process(f'server-{QuickServerTest._server_counter}') + QuickServerTest._server_counter += 1 + port = get_port(server, "http_port") + server.Command = \ + f'{sys.executable} {self._quick_server} 127.0.0.1 {port} ' + if self._should_drain_request: + server.Command += '--drain-request ' + if self._should_abort_response_headers: + server.Command += '--abort-response-headers ' + server.Ready = When.PortOpenv4(port) + self._server = server + + def _configure_traffic_server(self, tr: 'TestRun'): + """Configure ATS. + + :param tr: The test run to associate with the ATS process with. + """ + self._ts = tr.MakeATSProcess(f'ts-{QuickServerTest._ts_counter}') + QuickServerTest._ts_counter += 1 + self._ts.Disk.remap_config.AddLine(f'map / http://quick.server.com:{self._server.Variables.http_port}') + self._ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'http|dns|hostdb', + 'proxy.config.dns.nameservers': f'127.0.0.1:{self._dns.Variables.Port}', + 'proxy.config.dns.resolv_conf': 'NULL', + }) + + def run(self): + """Run the test.""" + tr = urtest.AddTestRun( + f'Aborting request: {self._should_abort_request}, ' + f'Draining request: {self._should_drain_request}, ' + f'Aborting response headers: {self._should_abort_response_headers}') + + self._configure_dns(tr) + self._configure_server(tr) + self._configure_traffic_server(tr) + + tools_dir = self._ts.Variables.AtsTestToolsDir + http_utils = os.path.join(tools_dir, 'http_utils.py') + tr.Setup.CopyAs(self._init_file, urtest.RunDirectory) + tr.Setup.CopyAs(http_utils, urtest.RunDirectory) + tr.Setup.CopyAs(self._slow_post_client, urtest.RunDirectory) + tr.Setup.CopyAs(self._quick_server, urtest.RunDirectory) + + client_command = (f'{sys.executable} {self._slow_post_client} ' + '127.0.0.1 ' + f'{self._ts.Variables.port} ') + if not self._should_abort_request: + client_command += '--finish-request ' + p = tr.Processes.Default + p.Command = client_command + if self._should_abort_request or self._should_abort_response_headers: + p.Streams.All += Testers.ExcludesExpression('HTTP/1.1 200 OK', 'Verify response was received') + else: + p.Streams.All += Testers.ContainsExpression('HTTP/1.1 200 OK', 'Verify response was received') + + p.ReturnCode = 0 + self._ts.StartBefore(self._dns) + self._ts.StartBefore(self._server) + p.StartBefore(self._ts) + tr.Timeout = 10 + + for abort_request in [True, False]: + for drain_request in [True, False]: + for abort_response_headers in [True, False]: + test = QuickServerTest(abort_request, drain_request, abort_response_headers) + test.run() + urtest.execute() diff --git a/tests/gold_tests/slow_post/test_secrets/aaa-signed.key b/tests/uranium_tests/slow_post/test_secrets/aaa-signed.key similarity index 100% rename from tests/gold_tests/slow_post/test_secrets/aaa-signed.key rename to tests/uranium_tests/slow_post/test_secrets/aaa-signed.key diff --git a/tests/gold_tests/slow_post/test_secrets/aaa-signed.pem b/tests/uranium_tests/slow_post/test_secrets/aaa-signed.pem similarity index 100% rename from tests/gold_tests/slow_post/test_secrets/aaa-signed.pem rename to tests/uranium_tests/slow_post/test_secrets/aaa-signed.pem diff --git a/tests/uranium_tests/slow_post/test_server_abort.py b/tests/uranium_tests/slow_post/test_server_abort.py new file mode 100644 index 00000000000..e243101373c --- /dev/null +++ b/tests/uranium_tests/slow_post/test_server_abort.py @@ -0,0 +1,78 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_server_abort(urtest: UraniumTest) -> None: + ''' + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + import re + + urtest.Summary = ''' + AuTest with bad configuration of microserver to simulate server aborting the connection unexpectedly + ''' + urtest.SkipIf(Condition.CurlUsingUnixDomainSocket()) + ts = urtest.MakeATSProcess("ts", enable_tls=True) + # note the microserver by default is not configured to use ssl + server = urtest.MakeOriginServer("server") + ts.Disk.remap_config.AddLine( + # The following config tells ATS to do tls with the origin server on a + # non-tls port. This is misconfigured intentionally to trigger an exception + # on the origin server so that it aborts the connection upon receiving a + # request + 'map / https://127.0.0.1:{0}'.format(server.Variables.Port)) + ts.Disk.ssl_multicert_yaml.AddLines( + """ + ssl_multicert: + - dest_ip: "*" + ssl_cert_name: aaa-signed.pem + ssl_key_name: aaa-signed.key + """.split("\n")) + ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.tags': 'http|dns', + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.ssl.server.cert.path': f'{urtest.TestDirectory}/test_secrets', + 'proxy.config.ssl.server.private_key.path': f'{urtest.TestDirectory}/test_secrets', + }) + + tr = urtest.AddTestRun() + tr.Processes.Default.StartBefore(server) + tr.Processes.Default.StartBefore(ts) + tr.MakeCurlCommand("-v -k -H \")host: foo.com\" https://127.0.0.1:{0}".format(ts.Variables.ssl_port), ts=ts) + tr.ReturnCode = 0 + tr.StillRunningAfter = server + tr.StillRunningAfter = ts + server.Streams.stderr += Testers.ContainsExpression( + "(UnicodeDecodeError|IndexError: list index out of range)", + "Verify that the server raises an exception when processing the request.") + urtest.execute() diff --git a/tests/uranium_tests/slow_post/test_slow_post.py b/tests/uranium_tests/slow_post/test_slow_post.py new file mode 100644 index 00000000000..3dd30e2ae09 --- /dev/null +++ b/tests/uranium_tests/slow_post/test_slow_post.py @@ -0,0 +1,103 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_slow_post(urtest: UraniumTest) -> None: + ''' + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import sys + + urtest.SkipUnless(Condition.PluginExists('request_buffer.so')) + + class SlowPostAttack: + + def __init__(cls): + urtest.Summary = 'Test how ATS handles the slow-post attack' + cls._origin_max_connections = 3 + cls._slow_post_client = 'slow_post_clients.py' + cls.setupOriginServer() + cls.setupTS() + cls._ts.Setup.CopyAs(cls._slow_post_client, urtest.RunDirectory) + + def setupOriginServer(self): + self._server = urtest.MakeOriginServer("server") + request_header = { + "headers": "GET / HTTP/1.1\r\nHost: www.example.com\r\n\r\n", + "timestamp": "1469733493.993", + "body": "" + } + response_header = { + "headers": "HTTP/1.1 200 OK\r\nServer: microserver\r\nConnection: close\r\n\r\n", + "timestamp": "1469733493.993", + "body": "" + } + self._server.addResponse("sessionlog.json", request_header, response_header) + request_header2 = { + "headers": + "POST / HTTP/1.1\r\nTransfer-Encoding: chunked\r\nHost: www.example.com\r\nConnection: keep-alive\r\n\r\n", + "timestamp": "1469733493.993", + "body": "a\r\na\r\na\r\n\r\n" + } + response_header2 = { + "headers": "HTTP/1.1 200 OK\r\nServer: microserver\r\nConnection: close\r\n\r\n", + "timestamp": "1469733493.993", + "body": "" + } + self._server.addResponse("sessionlog.json", request_header2, response_header2) + + def setupTS(self): + self._ts = urtest.MakeATSProcess("ts") + self._ts.Disk.remap_config.AddLine('map / http://127.0.0.1:{0}'.format(self._server.Variables.Port)) + # This plugin can enable request buffer for POST. + urtest.PrepareInstalledPlugin('request_buffer.so', self._ts) + self._ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'http', + 'proxy.config.http.per_server.connection.max': self._origin_max_connections, + }) + + def run(self): + tr = urtest.AddTestRun() + tr.Processes.Default.Command = \ + f'{sys.executable} {self._slow_post_client} -p {self._ts.Variables.port} -c {self._origin_max_connections}' + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.StartBefore(self._server) + tr.Processes.Default.StartBefore(urtest.Processes.ts) + tr.Processes.Default.Streams.stdout = "gold/200.gold" + + urtest.Summary = 'Test how ATS handles the slow-post attack' + slowPostAttack = SlowPostAttack() + slowPostAttack.run() + urtest.execute() diff --git a/tests/gold_tests/statistics/abort_client.py b/tests/uranium_tests/statistics/abort_client.py similarity index 100% rename from tests/gold_tests/statistics/abort_client.py rename to tests/uranium_tests/statistics/abort_client.py diff --git a/tests/gold_tests/statistics/replay/metric_response_429.test.yaml b/tests/uranium_tests/statistics/replay/metric_response_429.test.yaml similarity index 99% rename from tests/gold_tests/statistics/replay/metric_response_429.test.yaml rename to tests/uranium_tests/statistics/replay/metric_response_429.test.yaml index b326bdac06c..a6ac81c137d 100644 --- a/tests/gold_tests/statistics/replay/metric_response_429.test.yaml +++ b/tests/uranium_tests/statistics/replay/metric_response_429.test.yaml @@ -17,7 +17,7 @@ meta: version: "1.0" -autest: +urtest: description: 'Verify proxy.process.http.429_responses metric is incremented' dns: diff --git a/tests/uranium_tests/statistics/test_metric_response_000.py b/tests/uranium_tests/statistics/test_metric_response_000.py new file mode 100644 index 00000000000..189229ea6da --- /dev/null +++ b/tests/uranium_tests/statistics/test_metric_response_000.py @@ -0,0 +1,130 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_metric_response_000(urtest: UraniumTest) -> None: + """Verify the proxy.process.http.000_responses stat is incremented for client aborts.""" + + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import os + import sys + + urtest.Summary = __doc__ + + class MetricResponse000Test: + """Verify that the 000_responses stat is incremented when a client aborts.""" + + _abort_client = 'abort_client.py' + _server_counter = 0 + _ts_counter = 0 + + def __init__(self): + """Configure and run the test.""" + self._configure_server() + self._configure_traffic_server() + self._configure_abort_client() + self._configure_successful_request() + self._verify_000_metric() + + def _configure_server(self) -> None: + """Configure the origin server.""" + self._server = urtest.MakeOriginServer(f'server-{MetricResponse000Test._server_counter}') + MetricResponse000Test._server_counter += 1 + + request_header = { + "headers": "GET / HTTP/1.1\r\nHost: www.example.com\r\n\r\n", + "timestamp": "1469733493.993", + "body": "" + } + response_header = { + "headers": "HTTP/1.1 200 OK\r\nConnection: close\r\nContent-Length: 0\r\n\r\n", + "timestamp": "1469733493.993", + "body": "" + } + self._server.addResponse("sessionlog.json", request_header, response_header) + + def _configure_traffic_server(self) -> None: + """Configure ATS.""" + self._ts = urtest.MakeATSProcess(f'ts-{MetricResponse000Test._ts_counter}', enable_cache=False) + MetricResponse000Test._ts_counter += 1 + + self._ts.Disk.remap_config.AddLine(f'map / http://127.0.0.1:{self._server.Variables.Port}/') + self._ts.Disk.records_config.update({ + 'proxy.config.diags.debug.enabled': 0, + 'proxy.config.diags.debug.tags': 'http', + }) + + def _configure_abort_client(self) -> None: + """Configure a client to send a partial request and abort.""" + tr = urtest.AddTestRun('Trigger a client abort with partial request') + + tr.Setup.CopyAs(os.path.join(urtest.TestDirectory, self._abort_client), urtest.RunDirectory) + + p = tr.Processes.Default + p.Command = f'{sys.executable} {self._abort_client} 127.0.0.1 {self._ts.Variables.port}' + p.ReturnCode = 0 + + self._ts.StartBefore(self._server) + p.StartBefore(self._ts) + + tr.StillRunningAfter = self._ts + tr.StillRunningAfter = self._server + + def _configure_successful_request(self) -> None: + """Send a successful request to verify it doesn't increment 000 stat.""" + tr = urtest.AddTestRun('Send a successful request') + tr.Processes.Default.Command = f'curl -s -o /dev/null -w "%{{http_code}}" http://127.0.0.1:{self._ts.Variables.port}/' + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.All = Testers.ContainsExpression('200', 'Expected 200 response') + tr.StillRunningAfter = self._ts + tr.StillRunningAfter = self._server + + def _verify_000_metric(self) -> None: + """Verify the 000_responses stat is incremented.""" + # Wait for stats to propagate. + tr = urtest.AddTestRun('Wait for stats') + tr.Processes.Default.Command = 'sleep 2' + tr.Processes.Default.ReturnCode = 0 + tr.StillRunningAfter = self._ts + + # Verify the 000_responses stat is non-zero. + tr = urtest.AddTestRun('Check 000_responses stat') + tr.Processes.Default.Command = 'traffic_ctl metric get proxy.process.http.000_responses' + tr.Processes.Default.Env = self._ts.Env + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.All = Testers.ContainsExpression( + 'proxy.process.http.000_responses 1', 'The 000_responses stat should be 1') + tr.StillRunningAfter = self._ts + + MetricResponse000Test() + urtest.execute() diff --git a/tests/gold_tests/thread_config/check_threads.py b/tests/uranium_tests/thread_config/check_threads.py similarity index 100% rename from tests/gold_tests/thread_config/check_threads.py rename to tests/uranium_tests/thread_config/check_threads.py diff --git a/tests/uranium_tests/thread_config/test_thread_config.py b/tests/uranium_tests/thread_config/test_thread_config.py new file mode 100644 index 00000000000..4364787e0fc --- /dev/null +++ b/tests/uranium_tests/thread_config/test_thread_config.py @@ -0,0 +1,273 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_thread_config(urtest: UraniumTest) -> None: + ''' + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import sys + + urtest.Summary = 'Test that Trafficserver starts with different thread configurations.' + urtest.ContinueOnFail = True + urtest.SkipUnless(Condition.IsPlatform("linux")) + + ts = urtest.MakeATSProcess('ts-1_exec-0_accept-1_task-1_aio') + ts.Disk.records_config.update( + { + 'proxy.config.exec_thread.autoconfig.enabled': 0, + 'proxy.config.exec_thread.autoconfig.scale': 1.5, + 'proxy.config.exec_thread.limit': 1, + 'proxy.config.accept_threads': 0, + 'proxy.config.task_threads': 1, + 'proxy.config.cache.threads_per_disk': 1, + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'iocore_thread_start|iocore_net_accept_start' + }) + ts.Setup.CopyAs('check_threads.py', urtest.RunDirectory) + + tr = urtest.AddTestRun() + TS_ROOT = ts.Env['TS_ROOT'] + tr.Processes.Default.Command = f'{sys.executable} check_threads.py -p {TS_ROOT} -e 1 -a 0 -t 1 -c 1' + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.StartBefore(ts) + + ts = urtest.MakeATSProcess('ts-1_exec-1_accept-2_task-8_aio') + ts.Disk.records_config.update( + { + 'proxy.config.exec_thread.autoconfig.enabled': 0, + 'proxy.config.exec_thread.autoconfig.scale': 1.5, + 'proxy.config.exec_thread.limit': 1, + 'proxy.config.accept_threads': 1, + 'proxy.config.task_threads': 2, + 'proxy.config.cache.threads_per_disk': 8, + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'iocore_thread_start|iocore_net_accept_start' + }) + + tr = urtest.AddTestRun() + TS_ROOT = ts.Env['TS_ROOT'] + tr.Processes.Default.Command = f'{sys.executable} check_threads.py -p {TS_ROOT} -e 1 -a 1 -t 2 -c 8' + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.StartBefore(ts) + + ts = urtest.MakeATSProcess('ts-1_exec-10_accept-10_task-32_aio') + ts.Disk.records_config.update( + { + 'proxy.config.exec_thread.autoconfig.enabled': 0, + 'proxy.config.exec_thread.autoconfig.scale': 1.5, + 'proxy.config.exec_thread.limit': 1, + 'proxy.config.accept_threads': 10, + 'proxy.config.task_threads': 10, + 'proxy.config.cache.threads_per_disk': 32, + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'iocore_thread_start|iocore_net_accept_start' + }) + + tr = urtest.AddTestRun() + TS_ROOT = ts.Env['TS_ROOT'] + tr.Processes.Default.Command = f'{sys.executable} check_threads.py -p {TS_ROOT} -e 1 -a 10 -t 10 -c 32' + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.StartBefore(ts) + + ts = urtest.MakeATSProcess('ts-2_exec-0_accept-1_task-1_aio') + ts.Disk.records_config.update( + { + 'proxy.config.exec_thread.autoconfig.enabled': 0, + 'proxy.config.exec_thread.autoconfig.scale': 1.5, + 'proxy.config.exec_thread.limit': 2, + 'proxy.config.accept_threads': 0, + 'proxy.config.task_threads': 1, + 'proxy.config.cache.threads_per_disk': 1, + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'iocore_thread_start|iocore_net_accept_start' + }) + + tr = urtest.AddTestRun() + TS_ROOT = ts.Env['TS_ROOT'] + tr.Processes.Default.Command = f'{sys.executable} check_threads.py -p {TS_ROOT} -e 2 -a 0 -t 1 -c 1' + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.StartBefore(ts) + + ts = urtest.MakeATSProcess('ts-2_exec-1_accept-2_task-8_aio') + ts.Disk.records_config.update( + { + 'proxy.config.exec_thread.autoconfig.enabled': 0, + 'proxy.config.exec_thread.autoconfig.scale': 1.5, + 'proxy.config.exec_thread.limit': 2, + 'proxy.config.accept_threads': 1, + 'proxy.config.task_threads': 2, + 'proxy.config.cache.threads_per_disk': 8, + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'iocore_thread_start|iocore_net_accept_start' + }) + + tr = urtest.AddTestRun() + TS_ROOT = ts.Env['TS_ROOT'] + tr.Processes.Default.Command = f'{sys.executable} check_threads.py -p {TS_ROOT} -e 2 -a 1 -t 2 -c 8' + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.StartBefore(ts) + + ts = urtest.MakeATSProcess('ts-2_exec-10_accept-10_task-32_aio') + ts.Disk.records_config.update( + { + 'proxy.config.exec_thread.autoconfig.enabled': 0, + 'proxy.config.exec_thread.autoconfig.scale': 1.5, + 'proxy.config.exec_thread.limit': 2, + 'proxy.config.accept_threads': 10, + 'proxy.config.task_threads': 10, + 'proxy.config.cache.threads_per_disk': 32, + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'iocore_thread_start|iocore_net_accept_start' + }) + + tr = urtest.AddTestRun() + TS_ROOT = ts.Env['TS_ROOT'] + tr.Processes.Default.Command = f'{sys.executable} check_threads.py -p {TS_ROOT} -e 2 -a 10 -t 10 -c 32' + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.StartBefore(ts) + + ts = urtest.MakeATSProcess('ts-32_exec-0_accept-1_task-1_aio') + ts.Disk.records_config.update( + { + 'proxy.config.exec_thread.autoconfig.enabled': 0, + 'proxy.config.exec_thread.autoconfig.scale': 1.5, + 'proxy.config.exec_thread.limit': 32, + 'proxy.config.accept_threads': 0, + 'proxy.config.task_threads': 1, + 'proxy.config.cache.threads_per_disk': 1, + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'iocore_thread_start|iocore_net_accept_start' + }) + + tr = urtest.AddTestRun() + TS_ROOT = ts.Env['TS_ROOT'] + tr.Processes.Default.Command = f'{sys.executable} check_threads.py -p {TS_ROOT} -e 32 -a 0 -t 1 -c 1' + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.StartBefore(ts) + + ts = urtest.MakeATSProcess('ts-32_exec-1_accept-2_task-8_aio') + ts.Disk.records_config.update( + { + 'proxy.config.exec_thread.autoconfig.enabled': 0, + 'proxy.config.exec_thread.autoconfig.scale': 1.5, + 'proxy.config.exec_thread.limit': 32, + 'proxy.config.accept_threads': 1, + 'proxy.config.task_threads': 2, + 'proxy.config.cache.threads_per_disk': 8, + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'iocore_thread_start|iocore_net_accept_start' + }) + + tr = urtest.AddTestRun() + TS_ROOT = ts.Env['TS_ROOT'] + tr.Processes.Default.Command = f'{sys.executable} check_threads.py -p {TS_ROOT} -e 32 -a 1 -t 2 -c 8' + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.StartBefore(ts) + + ts = urtest.MakeATSProcess('ts-32_exec-10_accept-10_task-32_aio') + ts.Disk.records_config.update( + { + 'proxy.config.exec_thread.autoconfig.enabled': 0, + 'proxy.config.exec_thread.autoconfig.scale': 1.5, + 'proxy.config.exec_thread.limit': 32, + 'proxy.config.accept_threads': 10, + 'proxy.config.task_threads': 10, + 'proxy.config.cache.threads_per_disk': 32, + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'iocore_thread_start|iocore_net_accept_start' + }) + + tr = urtest.AddTestRun() + TS_ROOT = ts.Env['TS_ROOT'] + tr.Processes.Default.Command = f'{sys.executable} check_threads.py -p {TS_ROOT} -e 32 -a 10 -t 10 -c 32' + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.StartBefore(ts) + + ts = urtest.MakeATSProcess('ts-100_exec-0_accept-1_task-1_aio') + ts.Disk.records_config.update( + { + 'proxy.config.exec_thread.autoconfig.enabled': 0, + 'proxy.config.exec_thread.autoconfig.scale': 1.5, + 'proxy.config.exec_thread.limit': 100, + 'proxy.config.accept_threads': 0, + 'proxy.config.task_threads': 1, + 'proxy.config.cache.threads_per_disk': 1, + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'iocore_thread_start|iocore_net_accept_start' + }) + + tr = urtest.AddTestRun() + TS_ROOT = ts.Env['TS_ROOT'] + tr.Processes.Default.Command = f'{sys.executable} check_threads.py -p {TS_ROOT} -e 100 -a 0 -t 1 -c 1' + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.StartBefore(ts) + + ts = urtest.MakeATSProcess('ts-100_exec-1_accept-2_task-8_aio') + ts.Disk.records_config.update( + { + 'proxy.config.exec_thread.autoconfig.enabled': 0, + 'proxy.config.exec_thread.autoconfig.scale': 1.5, + 'proxy.config.exec_thread.limit': 100, + 'proxy.config.accept_threads': 1, + 'proxy.config.task_threads': 2, + 'proxy.config.cache.threads_per_disk': 8, + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'iocore_thread_start|iocore_net_accept_start' + }) + + tr = urtest.AddTestRun() + TS_ROOT = ts.Env['TS_ROOT'] + tr.Processes.Default.Command = f'{sys.executable} check_threads.py -p {TS_ROOT} -e 100 -a 1 -t 2 -c 8' + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.StartBefore(ts) + + ts = urtest.MakeATSProcess('ts-100_exec-10_accept-10_task-32_aio') + ts.Disk.records_config.update( + { + 'proxy.config.exec_thread.autoconfig.enabled': 0, + 'proxy.config.exec_thread.autoconfig.scale': 1.5, + 'proxy.config.exec_thread.limit': 100, + 'proxy.config.accept_threads': 10, + 'proxy.config.task_threads': 10, + 'proxy.config.cache.threads_per_disk': 32, + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'iocore_thread_start|iocore_net_accept_start' + }) + + tr = urtest.AddTestRun() + TS_ROOT = ts.Env['TS_ROOT'] + tr.Processes.Default.Command = f'{sys.executable} check_threads.py -p {TS_ROOT} -e 100 -a 10 -t 10 -c 32' + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.StartBefore(ts) + urtest.execute() diff --git a/tests/gold_tests/timeout/.gitignore b/tests/uranium_tests/timeout/.gitignore similarity index 100% rename from tests/gold_tests/timeout/.gitignore rename to tests/uranium_tests/timeout/.gitignore diff --git a/tests/gold_tests/timeout/CMakeLists.txt b/tests/uranium_tests/timeout/CMakeLists.txt similarity index 100% rename from tests/gold_tests/timeout/CMakeLists.txt rename to tests/uranium_tests/timeout/CMakeLists.txt diff --git a/tests/gold_tests/timeout/case1.sh b/tests/uranium_tests/timeout/case1.sh similarity index 96% rename from tests/gold_tests/timeout/case1.sh rename to tests/uranium_tests/timeout/case1.sh index 126b5931b8b..4cf2df69fa3 100755 --- a/tests/gold_tests/timeout/case1.sh +++ b/tests/uranium_tests/timeout/case1.sh @@ -16,7 +16,7 @@ # This is funky delaying and backgrounding the client request, but I just # could not get the command executing in the network space to go to background -# without blocking the autest. +# without blocking the Uranium test runner. (sleep 1; curl -i http://127.0.0.1:${1}/not-blocked ) & diff --git a/tests/gold_tests/timeout/create_request.sh b/tests/uranium_tests/timeout/create_request.sh similarity index 100% rename from tests/gold_tests/timeout/create_request.sh rename to tests/uranium_tests/timeout/create_request.sh diff --git a/tests/gold_tests/timeout/delay-server.sh b/tests/uranium_tests/timeout/delay-server.sh similarity index 100% rename from tests/gold_tests/timeout/delay-server.sh rename to tests/uranium_tests/timeout/delay-server.sh diff --git a/tests/gold_tests/timeout/gold/client_default_inactivity_timeout.gold b/tests/uranium_tests/timeout/gold/client_default_inactivity_timeout.gold similarity index 100% rename from tests/gold_tests/timeout/gold/client_default_inactivity_timeout.gold rename to tests/uranium_tests/timeout/gold/client_default_inactivity_timeout.gold diff --git a/tests/gold_tests/timeout/gold/quic_no_activity_timeout.gold b/tests/uranium_tests/timeout/gold/quic_no_activity_timeout.gold similarity index 100% rename from tests/gold_tests/timeout/gold/quic_no_activity_timeout.gold rename to tests/uranium_tests/timeout/gold/quic_no_activity_timeout.gold diff --git a/tests/gold_tests/timeout/http2_incomplete_header_client.py b/tests/uranium_tests/timeout/http2_incomplete_header_client.py similarity index 100% rename from tests/gold_tests/timeout/http2_incomplete_header_client.py rename to tests/uranium_tests/timeout/http2_incomplete_header_client.py diff --git a/tests/gold_tests/timeout/replay/default_inactivity_timeout.replay.yaml b/tests/uranium_tests/timeout/replay/default_inactivity_timeout.replay.yaml similarity index 100% rename from tests/gold_tests/timeout/replay/default_inactivity_timeout.replay.yaml rename to tests/uranium_tests/timeout/replay/default_inactivity_timeout.replay.yaml diff --git a/tests/gold_tests/timeout/replay/http2_incomplete_header_timeout.replay.yaml b/tests/uranium_tests/timeout/replay/http2_incomplete_header_timeout.replay.yaml similarity index 100% rename from tests/gold_tests/timeout/replay/http2_incomplete_header_timeout.replay.yaml rename to tests/uranium_tests/timeout/replay/http2_incomplete_header_timeout.replay.yaml diff --git a/tests/gold_tests/timeout/replay/http2_no_activity_timeout.replay.yaml b/tests/uranium_tests/timeout/replay/http2_no_activity_timeout.replay.yaml similarity index 100% rename from tests/gold_tests/timeout/replay/http2_no_activity_timeout.replay.yaml rename to tests/uranium_tests/timeout/replay/http2_no_activity_timeout.replay.yaml diff --git a/tests/gold_tests/timeout/replay/quic_no_activity_timeout.replay.yaml b/tests/uranium_tests/timeout/replay/quic_no_activity_timeout.replay.yaml similarity index 100% rename from tests/gold_tests/timeout/replay/quic_no_activity_timeout.replay.yaml rename to tests/uranium_tests/timeout/replay/quic_no_activity_timeout.replay.yaml diff --git a/tests/gold_tests/timeout/setupnetns.sh b/tests/uranium_tests/timeout/setupnetns.sh similarity index 100% rename from tests/gold_tests/timeout/setupnetns.sh rename to tests/uranium_tests/timeout/setupnetns.sh diff --git a/tests/gold_tests/timeout/slow_server.yaml b/tests/uranium_tests/timeout/slow_server.yaml similarity index 100% rename from tests/gold_tests/timeout/slow_server.yaml rename to tests/uranium_tests/timeout/slow_server.yaml diff --git a/tests/gold_tests/timeout/slow_server_uds.yaml b/tests/uranium_tests/timeout/slow_server_uds.yaml similarity index 100% rename from tests/gold_tests/timeout/slow_server_uds.yaml rename to tests/uranium_tests/timeout/slow_server_uds.yaml diff --git a/tests/gold_tests/timeout/ssl-delay-server.cc b/tests/uranium_tests/timeout/ssl-delay-server.cc similarity index 100% rename from tests/gold_tests/timeout/ssl-delay-server.cc rename to tests/uranium_tests/timeout/ssl-delay-server.cc diff --git a/tests/gold_tests/timeout/ssl/cert.crt b/tests/uranium_tests/timeout/ssl/cert.crt similarity index 100% rename from tests/gold_tests/timeout/ssl/cert.crt rename to tests/uranium_tests/timeout/ssl/cert.crt diff --git a/tests/gold_tests/timeout/ssl/private-key.key b/tests/uranium_tests/timeout/ssl/private-key.key similarity index 100% rename from tests/gold_tests/timeout/ssl/private-key.key rename to tests/uranium_tests/timeout/ssl/private-key.key diff --git a/tests/uranium_tests/timeout/test_accept_timeout.py b/tests/uranium_tests/timeout/test_accept_timeout.py new file mode 100644 index 00000000000..1597a71033d --- /dev/null +++ b/tests/uranium_tests/timeout/test_accept_timeout.py @@ -0,0 +1,97 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_accept_timeout(urtest: UraniumTest) -> None: + ''' + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + urtest.Summary = 'Testing ATS inactivity timeout' + + urtest.SkipUnless( + Condition.HasCurlFeature('http2'), Condition.HasProgram("telnet", "Need telnet to shutdown when server shuts down tcp"), + Condition.HasProgram("nc", "Need nc to send data to server")) + urtest.SkipIf(Condition.CurlUsingUnixDomainSocket()) + + ts = urtest.MakeATSProcess("ts", enable_tls=True) + + ts.addSSLfile("../tls/ssl/server.pem") + ts.addSSLfile("../tls/ssl/server.key") + + ts.Disk.records_config.update( + { + 'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir), + 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts.Variables.SSLDir), + 'proxy.config.http.transaction_no_activity_timeout_in': 6, + 'proxy.config.http.accept_no_activity_timeout': 2, + 'proxy.config.net.default_inactivity_timeout': 10, + 'proxy.config.net.defer_accept': 0 # Must turn off defer accept to test the raw TCP case + }) + + ts.Disk.ssl_multicert_yaml.AddLines( + """ + ssl_multicert: + - dest_ip: "*" + ssl_cert_name: server.pem + ssl_key_name: server.key + """.split("\n")) + + # case 1 TLS with no data + tr = urtest.AddTestRun("tr") + tr.Setup.Copy("time_client.sh") + tr.Processes.Default.StartBefore(ts) + tr.Processes.Default.Command = 'sh ./time_client.sh \'openssl s_client -ign_eof -connect 127.0.0.1:{0}\''.format( + ts.Variables.ssl_port) + tr.Processes.Default.Streams.stdout = Testers.ContainsExpression( + "Accept timeout", "Request should fail from the accept timeout") + + # case 2 TLS with incomplete request header + tr2 = urtest.AddTestRun("tr") + tr2.Processes.Default.Command = 'echo "GET /.html HTTP/1.1" | sh ./time_client.sh \'openssl s_client -ign_eof -connect 127.0.0.1:{0}\''.format( + ts.Variables.ssl_port) + tr2.Processes.Default.Streams.stdout = Testers.ContainsExpression( + "Transaction inactivity timeout", "Request should fail with transaction inactivity timeout") + + # case 3 TCP with no data + tr3 = urtest.AddTestRun("tr") + tr3.Processes.Default.Command = 'sh ./time_client.sh \'telnet 127.0.0.1 {0}\''.format(ts.Variables.port) + tr3.Processes.Default.Streams.stdout = Testers.ContainsExpression( + "Accept timeout", "Request should fail from the accept timeout") + + # case 4 TCP with incomplete request header + tr4 = urtest.AddTestRun("tr") + tr4.Setup.Copy("create_request.sh") + tr4.Processes.Default.Command = 'sh ./time_client.sh \'nc -c ./create_request.sh 127.0.0.1 {0}\''.format(ts.Variables.port) + tr4.Processes.Default.Streams.stdout = Testers.ContainsExpression( + "Transaction inactivity timeout", "Request should fail with transaction inactivity timeout") + urtest.execute() diff --git a/tests/uranium_tests/timeout/test_active_timeout.py b/tests/uranium_tests/timeout/test_active_timeout.py new file mode 100644 index 00000000000..017adc0ab88 --- /dev/null +++ b/tests/uranium_tests/timeout/test_active_timeout.py @@ -0,0 +1,96 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_active_timeout(urtest: UraniumTest) -> None: + ''' + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + urtest.Summary = 'Testing ATS active timeout' + + urtest.SkipUnless(Condition.HasCurlFeature('http2')) + + if Condition.HasATSFeature('TS_USE_QUIC') and Condition.HasCurlFeature('http3'): + ts = urtest.MakeATSProcess("ts", enable_tls=True, enable_quic=True) + else: + ts = urtest.MakeATSProcess("ts", enable_tls=True) + server = urtest.MakeOriginServer("server", delay=8) + + request_header = {"headers": "GET /file HTTP/1.1\r\nHost: *\r\n\r\n", "timestamp": "5678", "body": ""} + response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "5678", "body": ""} + + server.addResponse("sessionfile.log", request_header, response_header) + + ts.addDefaultSSLFiles() + + ts.Disk.records_config.update( + { + 'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir), + 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts.Variables.SSLDir), + 'proxy.config.url_remap.remap_required': 1, + 'proxy.config.http.transaction_active_timeout_out': 2, + }) + + ts.Disk.remap_config.AddLine('map / http://127.0.0.1:{0}/'.format(server.Variables.Port)) + + ts.Disk.ssl_multicert_yaml.AddLines( + """ + ssl_multicert: + - dest_ip: "*" + ssl_cert_name: server.pem + ssl_key_name: server.key + """.split("\n")) + + tr = urtest.AddTestRun("tr") + tr.Processes.Default.StartBefore(server) + tr.Processes.Default.StartBefore(ts) + tr.MakeCurlCommand('-i http://127.0.0.1:{0}/file'.format(ts.Variables.port), ts=ts) + tr.Processes.Default.Streams.stdout = Testers.ContainsExpression("Activity Timeout", "Request should fail with active timeout") + + if not Condition.CurlUsingUnixDomainSocket(): + tr2 = urtest.AddTestRun("tr") + tr2.MakeCurlCommand('-k -i --http1.1 https://127.0.0.1:{0}/file'.format(ts.Variables.ssl_port), ts=ts) + tr2.Processes.Default.Streams.stdout = Testers.ContainsExpression( + "Activity Timeout", "Request should fail with active timeout") + + tr3 = urtest.AddTestRun("tr") + tr3.MakeCurlCommand('-k -i --http2 https://127.0.0.1:{0}/file'.format(ts.Variables.ssl_port), ts=ts) + tr3.Processes.Default.Streams.stdout = Testers.ContainsExpression( + "Activity Timeout", "Request should fail with active timeout") + + if Condition.HasATSFeature('TS_USE_QUIC') and Condition.HasCurlFeature('http3'): + tr4 = urtest.AddTestRun("tr") + tr4.MakeCurlCommand('-k -i --http3 https://localhost:{0}/file'.format(ts.Variables.ssl_port), ts=ts) + tr4.Processes.Default.Streams.stdout = Testers.ContainsExpression( + "Activity Timeout", "Request should fail with active timeout") + urtest.execute() diff --git a/tests/uranium_tests/timeout/test_conn_timeout.py b/tests/uranium_tests/timeout/test_conn_timeout.py new file mode 100644 index 00000000000..69c33aa60e2 --- /dev/null +++ b/tests/uranium_tests/timeout/test_conn_timeout.py @@ -0,0 +1,120 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_conn_timeout(urtest: UraniumTest) -> None: + ''' + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + urtest.Summary = 'Testing ATS TCP handshake timeout' + + # Skipping this in the normal CI because it requires privilege. + # Comment out to run in your privileged environment + urtest.SkipIf(Condition.true("Test requires privilege")) + + ts = urtest.MakeATSProcess("ts") + + urtest.ContinueOnFail = True + urtest.GetTcpPort("blocked_upstream_port") + urtest.GetTcpPort("upstream_port") + + server4 = urtest.MakeOriginServer("server4") + + ts.Disk.records_config.update( + { + 'proxy.config.url_remap.remap_required': 1, + 'proxy.config.http.connect_attempts_timeout': 2, + 'proxy.config.http.connect_attempts_max_retries': 0, + 'proxy.config.http.transaction_no_activity_timeout_out': 5, + 'proxy.config.diags.debug.enabled': 0, + 'proxy.config.diags.debug.tags': 'http', + }) + + ts.Disk.remap_config.AddLine('map /blocked http://10.1.1.1:{0}'.format(urtest.Variables.blocked_upstream_port)) + ts.Disk.remap_config.AddLine('map /not-blocked http://10.1.1.1:{0}'.format(urtest.Variables.upstream_port)) + + ts.Disk.logging_yaml.AddLines( + ''' + logging: + formats: + - name: testformat + format: '% % % %' + logs: + - mode: ascii + format: testformat + filename: squid + '''.split("\n")) + + # Set up the network name space. Requires privilege + tr = urtest.AddTestRun("tr-ns-setup") + tr.Processes.Default.StartBefore(ts) + tr.Processes.Default.TimeOut = 2 + tr.Setup.Copy('setupnetns.sh') + tr.Processes.Default.Command = 'echo start; sudo sh -x ./setupnetns.sh {0} {1}'.format( + urtest.Variables.blocked_upstream_port, urtest.Variables.upstream_port) + + # Request to the port that is blocked in the network ns. The SYN should never be responded to + # and the connect timeout should trigger with a 50x return. If the SYN handshake occurs, the + # no activity timeout would trigger, but not before the test timeout expires + tr = urtest.AddTestRun("tr-blocking") + tr.MakeCurlCommand('-i http://127.0.0.1:{0}/blocked {0}'.format(ts.Variables.port), ts=ts) + tr.Processes.Default.TimeOut = 4 + tr.Processes.Default.Streams.All = Testers.ContainsExpression( + "HTTP/1.1 502 internal error - server connection terminated", "Connect failed") + + tr = urtest.AddTestRun("tr-blocking-post") + tr.MakeCurlCommand('-d "stuff" -i http://127.0.0.1:{0}/blocked {0}'.format(ts.Variables.port), ts=ts) + tr.Processes.Default.TimeOut = 4 + tr.Processes.Default.Streams.All = Testers.ContainsExpression( + "HTTP/1.1 502 internal error - server connection terminated", "Connect failed") + + # Should not catch the connect timeout. Even though the first bytes are not sent until after the 2 second connect timeout + # But before the no-activity timeout + tr = urtest.AddTestRun("tr-delayed") + tr.Setup.Copy('delay-server.sh') + tr.Setup.Copy('case1.sh') + tr.Processes.Default.Command = 'sh ./case1.sh {0} {1}'.format(ts.Variables.port, ts.Variables.upstream_port) + tr.Processes.Default.TimeOut = 7 + tr.Processes.Default.Streams.All = Testers.ContainsExpression("HTTP/1.1 200", "Connect succeeded") + + # cleanup the network namespace and virtual network + tr = urtest.AddTestRun("tr-cleanup") + tr.Processes.Default.Command = 'sudo ip netns del testserver; sudo ip link del veth0 type veth peer name veth1' + tr.Processes.Default.TimeOut = 4 + + tr = urtest.AddTestRun("Wait for the access log to write out") + tr.Processes.Default.StartBefore(server4, ready=When.FileExists(ts.Disk.squid_log)) + tr.StillRunningAfter = ts + tr.Processes.Default.Command = 'echo "log file exists"' + tr.Processes.Default.ReturnCode = 0 + urtest.execute() diff --git a/tests/uranium_tests/timeout/test_default_inactivity_timeout.py b/tests/uranium_tests/timeout/test_default_inactivity_timeout.py new file mode 100644 index 00000000000..9c790f04dc2 --- /dev/null +++ b/tests/uranium_tests/timeout/test_default_inactivity_timeout.py @@ -0,0 +1,122 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_default_inactivity_timeout(urtest: UraniumTest) -> None: + ''' + Verify correct behavior for default_inactivity_timeout. + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + urtest.Summary = __doc__ + + class TestDefaultInactivityTimeout: + """Configure a test for default_inactivity_test.""" + + replay_file = "replay/default_inactivity_timeout.replay.yaml" + client_gold_file = 'gold/client_default_inactivity_timeout.gold' + client_counter: int = 0 + ts_counter: int = 0 + server_counter: int = 0 + + def __init__(self, name: str, use_override: bool): + """Initialize the test. + + :param name: The name of the test. + :param use_override: Whether to use the override rather than the global + configuration. + """ + self.name = name + self.use_override = use_override + + def _configure_server(self, tr: 'TestRun') -> None: + """Configure the server. + + :param tr: The TestRun object to associate the server process with. + """ + server = tr.AddVerifierServerProcess(f"server_{TestDefaultInactivityTimeout.server_counter}", self.replay_file) + TestDefaultInactivityTimeout.server_counter += 1 + self._server = server + + def _configure_traffic_server(self, tr: 'TestRun') -> None: + """Configure Traffic Server. + + :param tr: The TestRun object to associate the ts process with. + :return: A Traffic Server Test process. + """ + ts = tr.MakeATSProcess(f"ts-{TestDefaultInactivityTimeout.ts_counter}") + TestDefaultInactivityTimeout.ts_counter += 1 + self._ts = ts + + debug_tags = 'http|cache|socket|net_queue|inactivity_cop|conf_remap' + ts.Disk.records_config.update({ + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': debug_tags, + }) + + origin_port = self._server.Variables.http_port + remap_line = f'map / http://127.0.0.1:{origin_port}' + + if self.use_override: + remap_line += (' @plugin=conf_remap.so ' + '@pparam=proxy.config.net.default_inactivity_timeout=2') + else: + ts.Disk.records_config.update({ + 'proxy.config.net.default_inactivity_timeout': 2, + }) + + ts.Disk.remap_config.AddLine(remap_line) + ts.Disk.traffic_out.Content += Testers.ContainsExpression( + 'timed out due to default inactivity timeout', 'Verify that the default inactivity timeout was triggered.') + + def run(self) -> None: + """Run the test.""" + tr = urtest.AddTestRun(self.name) + self._configure_server(tr) + self._configure_traffic_server(tr) + + tr.Processes.Default.StartBefore(self._server) + tr.Processes.Default.StartBefore(self._ts) + tr.AddVerifierClientProcess( + f'client-{TestDefaultInactivityTimeout.client_counter}', self.replay_file, http_ports=[self._ts.Variables.port]) + TestDefaultInactivityTimeout.client_counter += 1 + + # Set up expectectations for the timeout closing the connection. + tr.Processes.Default.ReturnCode = 1 + tr.Processes.Default.Streams.all = self.client_gold_file + + test = TestDefaultInactivityTimeout("global config", use_override=False) + test.run() + + test = TestDefaultInactivityTimeout("override config", use_override=True) + test.run() + urtest.execute() diff --git a/tests/uranium_tests/timeout/test_http2_incomplete_header_timeout.py b/tests/uranium_tests/timeout/test_http2_incomplete_header_timeout.py new file mode 100644 index 00000000000..b3d1b508f98 --- /dev/null +++ b/tests/uranium_tests/timeout/test_http2_incomplete_header_timeout.py @@ -0,0 +1,226 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_http2_incomplete_header_timeout(urtest: UraniumTest) -> None: + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import sys + from typing import Optional + + urtest.Summary = 'Verify http2.incomplete_header_timeout_in' + + # VC_EVENT_ACTIVE_TIMEOUT. The incomplete header timeout is an active timeout on + # the stream, so ATS reports this event rather than VC_EVENT_INACTIVITY_TIMEOUT + # (105). + VC_EVENT_ACTIVE_TIMEOUT = 106 + + # Http2ErrorCode::HTTP2_ERROR_COMPRESSION_ERROR. ATS tears down the connection + # rather than just the stream because the undelivered CONTINUATION frame leaves + # its HPACK dynamic table out of sync. + HTTP2_ERROR_COMPRESSION_ERROR = 9 + + class TestHttp2IncompleteHeaderTimeout: + """Configure a test for http2.incomplete_header_timeout_in. + + The client sends a HEADERS frame without the END_HEADERS flag, which leaves + ATS waiting for a CONTINUATION frame. incomplete_header_timeout_in bounds + how long ATS waits. + """ + + client_script: str = 'http2_incomplete_header_client.py' + replay_file: str = 'replay/http2_incomplete_header_timeout.replay.yaml' + counter: int = 0 + + def __init__( + self, + name: str, + incomplete_header_timeout_in: int, + path: str, + uuid: str, + end_headers: bool = False, + continuation_delay: Optional[float] = None, + min_elapsed: Optional[float] = None, + max_elapsed: Optional[float] = None, + expect_timeout: bool = False): + """Initialize the test. + + :param name: The name of the test run. + :param incomplete_header_timeout_in: The value to configure for + proxy.config.http2.incomplete_header_timeout_in. + :param path: The :path pseudo header the client requests. + :param uuid: The uuid of the replay file transaction to request. + :param end_headers: Whether the client sends the complete header block + with END_HEADERS in a single HEADERS frame. + :param continuation_delay: How long after the HEADERS frame the client + waits before sending the END_HEADERS CONTINUATION frame. None means + that the client never sends it. + :param min_elapsed: Fail if ATS ends the stream sooner than this. + :param max_elapsed: Fail if ATS ends the stream later than this. + :param expect_timeout: Whether ATS is expected to time out the stream. + """ + self._name = name + self._incomplete_header_timeout_in = incomplete_header_timeout_in + self._path = path + self._uuid = uuid + self._end_headers = end_headers + self._continuation_delay = continuation_delay + self._min_elapsed = min_elapsed + self._max_elapsed = max_elapsed + self._expect_timeout = expect_timeout + + def _configure_server(self, tr: 'TestRun') -> None: + """Configure the origin server. + + :param tr: The TestRun object to associate the server process with. + """ + self._server = tr.AddVerifierServerProcess(f'server-{self.counter}', self.replay_file) + + def _configure_traffic_server(self, tr: 'TestRun') -> None: + """Configure Traffic Server. + + :param tr: The TestRun object to associate the ts process with. + """ + ts = tr.MakeATSProcess(f'ts-{self.counter}', enable_tls=True, enable_cache=False) + self._ts = ts + + ts.addSSLfile('ssl/cert.crt') + ts.addSSLfile('ssl/private-key.key') + ts.Disk.ssl_multicert_yaml.AddLines( + f""" + ssl_multicert: + - dest_ip: "*" + ssl_cert_name: {ts.Variables.SSLDir}/cert.crt + ssl_key_name: {ts.Variables.SSLDir}/private-key.key + """.split('\n')) + + ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'http2', + 'proxy.config.ssl.server.cert.path': ts.Variables.SSLDir, + 'proxy.config.ssl.server.private_key.path': ts.Variables.SSLDir, + 'proxy.config.http2.incomplete_header_timeout_in': self._incomplete_header_timeout_in, + }) + + ts.Disk.remap_config.AddLine(f'map / http://127.0.0.1:{self._server.Variables.http_port}') + + def _configure_client(self, tr: 'TestRun') -> None: + """Configure the ad hoc HTTP/2 client. + + :param tr: The TestRun object to associate the client process with. + """ + tr.Setup.Copy(self.client_script) + command = ( + f'{sys.executable} {self.client_script} {self._ts.Variables.ssl_port} ' + f'--path {self._path} --uuid {self._uuid}') + if self._end_headers: + command += ' --end-headers' + if self._continuation_delay is not None: + command += f' --continuation-delay {self._continuation_delay}' + if self._min_elapsed is not None: + command += f' --min-elapsed {self._min_elapsed}' + if self._max_elapsed is not None: + command += f' --max-elapsed {self._max_elapsed}' + + tr.Processes.Default.Command = command + tr.Processes.Default.ReturnCode = 0 + + def _configure_expectations(self, tr: 'TestRun') -> None: + """Configure the testers for the test run. + + :param tr: The TestRun object to associate the testers with. + """ + timeout_error = 'ERROR: HTTP/2 stream error timeout' + if self._expect_timeout: + tr.Processes.Default.Streams.stdout += Testers.ContainsExpression( + f'GOAWAY error_code={HTTP2_ERROR_COMPRESSION_ERROR} last_stream_id=1', + 'ATS should close the connection with COMPRESSION_ERROR because the HPACK table is out of sync.') + self._ts.Disk.traffic_out.Content += Testers.ContainsExpression( + f'timeout event={VC_EVENT_ACTIVE_TIMEOUT}', + 'ATS should time out the incomplete header with the stream active timeout.') + # The default diags.log check rejects any ERROR:, but this test + # expects the incomplete header timeout to be reported. + self._ts.Disk.diags_log.Content = Testers.ContainsExpression( + timeout_error, 'ATS should log the incomplete header timeout.') + else: + tr.Processes.Default.Streams.stdout += Testers.ContainsExpression( + 'stream 1: status=200', 'ATS should proxy the request rather than time out the stream.') + tr.Processes.Default.Streams.stdout += Testers.ExcludesExpression('GOAWAY', 'ATS should not close the connection.') + self._ts.Disk.diags_log.Content += Testers.ExcludesExpression( + timeout_error, 'ATS should not report an incomplete header timeout.') + + def run(self) -> None: + """Run the test.""" + tr = urtest.AddTestRun(self._name) + self._configure_server(tr) + self._configure_traffic_server(tr) + self._configure_client(tr) + self._configure_expectations(tr) + + tr.Processes.Default.StartBefore(self._server) + tr.Processes.Default.StartBefore(self._ts) + TestHttp2IncompleteHeaderTimeout.counter += 1 + + # A client that never completes the header block should be timed out at the + # configured timeout, well before the 10 second default. + TestHttp2IncompleteHeaderTimeout( + 'Incomplete header block times out', + incomplete_header_timeout_in=3, + path='/incomplete', + uuid='incomplete_header', + continuation_delay=None, + min_elapsed=2.5, + max_elapsed=7, + expect_timeout=True).run() + + # A client that completes the header block before the timeout expires should be + # proxied normally. + TestHttp2IncompleteHeaderTimeout( + 'CONTINUATION frame arrives before the timeout', + incomplete_header_timeout_in=5, + path='/incomplete', + uuid='incomplete_header', + continuation_delay=1, + expect_timeout=False).run() + + # The timeout is canceled when the transaction starts, so a slow origin + # response must not be mistaken for an incomplete header block. + TestHttp2IncompleteHeaderTimeout( + 'Timeout is canceled once the transaction starts', + incomplete_header_timeout_in=2, + path='/delayed', + uuid='delayed_response', + end_headers=True, + min_elapsed=4.5, + expect_timeout=False).run() + urtest.execute() diff --git a/tests/uranium_tests/timeout/test_http2_no_activity_timeout.py b/tests/uranium_tests/timeout/test_http2_no_activity_timeout.py new file mode 100644 index 00000000000..a98f95c8004 --- /dev/null +++ b/tests/uranium_tests/timeout/test_http2_no_activity_timeout.py @@ -0,0 +1,176 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_http2_no_activity_timeout(urtest: UraniumTest) -> None: + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + from typing import Optional + import re + + urtest.Summary = 'Verify http2.no_activity_timeout_(in|out)' + + class Test_http2_no_activity_timeout: + """Configure a test for http2 no_activity_timeout_(in|out).""" + + replay_file = "replay/http2_no_activity_timeout.replay.yaml" + client_counter: int = 0 + ts_counter: int = 0 + server_counter: int = 0 + + def __init__( + self, + name: str, + replay_keys: Optional[str] = None, + no_activity_timeout_in: Optional[int] = None, + expect_in_timeout=False, + no_activity_timeout_out: Optional[int] = None, + expect_out_timeout=False): + """Initialize the test. + + :param name: The name of the test. + :param replay_keys: The set of keys to replay from the replay file. If + not provided, all keys will be replayed. + :param no_activity_timeout_in: Configuration value for + ts.http2.no_activity_timeout_in. None means that the default value will + be used. + :param expect_in_timeout: True if there is an expected inbound timeout. + :param no_activity_timeout_out: Configuration value for + ts.http2.no_activity_timeout_out. None means that the default value will + be used. + :param expect_out_timeout: True if there is an expected outbound timeout. + """ + self._name = name + self._replay_keys = replay_keys + self._no_activity_timeout_in = no_activity_timeout_in + self._expect_in_timeout = expect_in_timeout + self._no_activity_timeout_out = no_activity_timeout_out + self._expect_out_timeout = expect_out_timeout + + def _configure_server(self, tr: 'TestRun'): + """Configure the server. + + :param tr: The TestRun object to associate the server process with. + """ + self._server = tr.AddVerifierServerProcess(f"server_{Test_http2_no_activity_timeout.server_counter}", self.replay_file) + Test_http2_no_activity_timeout.server_counter += 1 + + def _configure_traffic_server(self, tr: 'TestRun'): + """Configure Traffic Server. + + :param tr: The TestRun object to associate the ts process with. + """ + self._ts = tr.MakeATSProcess(f"ts-{Test_http2_no_activity_timeout.ts_counter}", enable_tls=True, enable_cache=False) + Test_http2_no_activity_timeout.ts_counter += 1 + + self._ts.addSSLfile("ssl/cert.crt") + self._ts.addSSLfile("ssl/private-key.key") + self._ts.Disk.ssl_multicert_yaml.AddLines( + f""" + ssl_multicert: + - dest_ip: "*" + ssl_cert_name: {self._ts.Variables.SSLDir}/cert.crt + ssl_key_name: {self._ts.Variables.SSLDir}/private-key.key + """.split("\n")) + + self._ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'http|socket|inactivity_cop', + 'proxy.config.ssl.server.cert.path': self._ts.Variables.SSLDir, + 'proxy.config.ssl.server.private_key.path': self._ts.Variables.SSLDir, + "proxy.config.ssl.client.verify.server.policy": 'PERMISSIVE', + 'proxy.config.ssl.client.alpn_protocols': 'h2,http/1.1', + }) + + if self._no_activity_timeout_in is not None: + self._ts.Disk.records_config.update({ + 'proxy.config.http2.no_activity_timeout_in': self._no_activity_timeout_in, + }) + if self._no_activity_timeout_out is not None: + self._ts.Disk.records_config.update({ + 'proxy.config.http2.no_activity_timeout_out': self._no_activity_timeout_out, + }) + + self._ts.Disk.remap_config.AddLine(f'map / https://127.0.0.1:{self._server.Variables.https_port}') + + def run(self): + """Run the test.""" + tr = urtest.AddTestRun(self._name) + self._configure_server(tr) + self._configure_traffic_server(tr) + + tr.Processes.Default.StartBefore(self._server) + tr.Processes.Default.StartBefore(self._ts) + + tr.AddVerifierClientProcess( + f'client-{Test_http2_no_activity_timeout.client_counter}', + self.replay_file, + https_ports=[self._ts.Variables.ssl_port], + keys=self._replay_keys) + Test_http2_no_activity_timeout.client_counter += 1 + + if self._expect_in_timeout: + tr.Processes.Default.Streams.All += Testers.IncludesExpression( + "SSL_read error", "The client should have a read error due to an ATS timeout.") + self._ts.Disk.traffic_out.Content += Testers.IncludesExpression( + "http2_cs.*Closing event:.*TIMEOUT", "We should detect a client side timeout.") + elif self._expect_out_timeout: + # There should be two origin connections: + # 1. For the first no delay transaction. + # 2. Another after a timeout, closing the first. + self._server.Streams.All += Testers.IncludesExpression( + "Negotiated ALPN from client ALPN.*Negotiated ALPN from client ALPN", + "A second server side connection should be needed after the first times out.", + reflags=re.MULTILINE | re.DOTALL) + self._ts.Disk.traffic_out.Content += Testers.IncludesExpression( + "http2_cs.*Closing event:.*TIMEOUT", "We should detect a server side timeout.") + + test0 = Test_http2_no_activity_timeout("Default no activity timeout", expect_in_timeout=False, expect_out_timeout=False) + test0.run() + + test1 = Test_http2_no_activity_timeout( + "Client (in) side inactivity timeout", + no_activity_timeout_in=1, + replay_keys="no_delay 3_second_request_delay", + expect_in_timeout=True, + expect_out_timeout=False) + test1.run() + + test2 = Test_http2_no_activity_timeout( + "Server (out) side inactivity timeout", + no_activity_timeout_out=1, + replay_keys="no_delay 3_second_request_delay", + expect_in_timeout=False, + expect_out_timeout=True) + test2.run() + urtest.execute() diff --git a/tests/uranium_tests/timeout/test_inactive_client_timeout.py b/tests/uranium_tests/timeout/test_inactive_client_timeout.py new file mode 100644 index 00000000000..a2ba3800862 --- /dev/null +++ b/tests/uranium_tests/timeout/test_inactive_client_timeout.py @@ -0,0 +1,99 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_inactive_client_timeout(urtest: UraniumTest) -> None: + ''' + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + urtest.Summary = 'Testing ATS client inactivity timeout' + + ts = urtest.MakeATSProcess("ts", enable_tls=True) + if Condition.CurlUsingUnixDomainSocket(): + replay_file = "slow_server_uds.yaml" + else: + replay_file = "slow_server.yaml" + server = urtest.MakeVerifierServerProcess("server", replay_file) + + urtest.ContinueOnFail = True + + ts.addDefaultSSLFiles() + + ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'http', + 'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir), + 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts.Variables.SSLDir), + 'proxy.config.url_remap.remap_required': 1, + 'proxy.config.http.transaction_no_activity_timeout_in': 2, + 'proxy.config.ssl.client.verify.server.policy': 'PERMISSIVE', + }) + + ts.Disk.remap_config.AddLines( + [ + 'map https://www.tls.com/ https://127.0.0.1:{0}'.format(server.Variables.https_port), + 'map / http://127.0.0.1:{0}'.format(server.Variables.http_port), + ]) + + ts.Disk.ssl_multicert_yaml.AddLines( + """ + ssl_multicert: + - dest_ip: "*" + ssl_cert_name: server.pem + ssl_key_name: server.key + """.split("\n")) + + # + # Test 1: Verify that server delay does not trigger client activity timeout. + # + + # The Proxy Verifier server will delay for 3 seconds before returning a response. This is more than + # the 2 second proxy.config.http.transaction_no_activity_timeout_in (the client inactivity timeout), + # but less than the default 30 second proxy.config.http.transaction_no_activity_timeout_out (server + # inactivity timeout). These tests therefore exercise that the client inactivity timeout does not + # get applied after the request is sent. In other words, a slow to respond server should not + # trigger the client inactivity timeout. + tr = urtest.AddTestRun("Verify that server delay does not trigger client activity timeout.") + client = tr.AddVerifierClientProcess("client", replay_file, http_ports=[ts.Variables.port], https_ports=[ts.Variables.ssl_port]) + tr.Processes.Default.StartBefore(ts) + tr.Processes.Default.StartBefore(server) + + client.Streams.All += Testers.ContainsExpression('x-response: 1', 'Verify that the first response is received') + client.Streams.All += Testers.ContainsExpression('x-response: 2', 'Verify that the second response is received') + if not Condition.CurlUsingUnixDomainSocket(): + client.Streams.All += Testers.ContainsExpression('x-response: 3', 'Verify that the third response is received') + client.Streams.All += Testers.ContainsExpression('x-response: 4', 'Verify that the fourth response is received') + client.Streams.All += Testers.ContainsExpression('x-response: 5', 'Verify that the fifth response is received') + client.Streams.All += Testers.ContainsExpression('x-response: 6', 'Verify that the sixth response is received') + urtest.execute() diff --git a/tests/uranium_tests/timeout/test_inactive_timeout.py b/tests/uranium_tests/timeout/test_inactive_timeout.py new file mode 100644 index 00000000000..1c9a79a891f --- /dev/null +++ b/tests/uranium_tests/timeout/test_inactive_timeout.py @@ -0,0 +1,88 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_inactive_timeout(urtest: UraniumTest) -> None: + ''' + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + urtest.Summary = 'Testing ATS inactivity timeout' + + urtest.SkipUnless(Condition.HasCurlFeature('http2')) + + ts = urtest.MakeATSProcess("ts", enable_tls=True) + server = urtest.MakeOriginServer("server", delay=8) + + request_header = {"headers": "GET /file HTTP/1.1\r\nHost: *\r\n\r\n", "timestamp": "5678", "body": ""} + response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "5678", "body": ""} + + server.addResponse("sessionfile.log", request_header, response_header) + + ts.addDefaultSSLFiles() + + ts.Disk.records_config.update( + { + 'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir), + 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts.Variables.SSLDir), + 'proxy.config.url_remap.remap_required': 1, + 'proxy.config.http.transaction_no_activity_timeout_out': 2, + }) + + ts.Disk.remap_config.AddLine('map / http://127.0.0.1:{0}/'.format(server.Variables.Port)) + + ts.Disk.ssl_multicert_yaml.AddLines( + """ + ssl_multicert: + - dest_ip: "*" + ssl_cert_name: server.pem + ssl_key_name: server.key + """.split("\n")) + + tr = urtest.AddTestRun("tr") + tr.Processes.Default.StartBefore(server) + tr.Processes.Default.StartBefore(ts) + tr.MakeCurlCommand('-i http://127.0.0.1:{0}/file'.format(ts.Variables.port), ts=ts) + tr.Processes.Default.Streams.stdout = Testers.ContainsExpression( + "Inactivity Timeout", "Request should fail with inactivity timeout") + + if not Condition.CurlUsingUnixDomainSocket(): + tr2 = urtest.AddTestRun("tr") + tr2.MakeCurlCommand('-k -i --http1.1 https://127.0.0.1:{0}/file'.format(ts.Variables.ssl_port), ts=ts) + tr2.Processes.Default.Streams.stdout = Testers.ContainsExpression( + "Inactivity Timeout", "Request should fail with inactivity timeout") + + tr3 = urtest.AddTestRun("tr") + tr3.MakeCurlCommand('-k -i --http2 https://127.0.0.1:{0}/file'.format(ts.Variables.ssl_port), ts=ts) + tr3.Processes.Default.Streams.stdout = Testers.ContainsExpression( + "Inactivity Timeout", "Request should fail with inactivity timeout") + urtest.execute() diff --git a/tests/uranium_tests/timeout/test_quic_no_activity_timeout.py b/tests/uranium_tests/timeout/test_quic_no_activity_timeout.py new file mode 100644 index 00000000000..9c3038cb59a --- /dev/null +++ b/tests/uranium_tests/timeout/test_quic_no_activity_timeout.py @@ -0,0 +1,154 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_quic_no_activity_timeout(urtest: UraniumTest) -> None: + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + urtest.Summary = 'Basic checks on QUIC max_idle_timeout set by ts.quic.no_activity_timeout_in' + + urtest.SkipUnless(Condition.HasATSFeature('TS_USE_QUIC')) + + class Test_quic_no_activity_timeout: + """Configure a test for QUIC no_activity_timeout_in.""" + + replay_file = "replay/quic_no_activity_timeout.replay.yaml" + client_counter: int = 0 + ts_counter: int = 0 + server_counter: int = 0 + + def __init__(self, name: str, no_activity_timeout_in=1000, gold_file="", replay_keys="", extra_recs=None): + """Initialize the test. + + :param name: The name of the test. + :param no_activity_timeout_in: Configuration value for ts.quic.no_activity_timeout_in + :param gold_file: Gold file to be checked. + :param replay_keys: Keys to be used by pv + :param extra_recs: Any additional records to be set, either a yaml string or a dict. + """ + self.name = name + self.no_activity_timeout_in = no_activity_timeout_in + self.gold_file = gold_file + self.replay_keys = replay_keys + self.extra_recs = extra_recs + + def _configure_server(self, tr: 'TestRun'): + """Configure the server. + + :param tr: The TestRun object to associate the server process with. + """ + server = tr.AddVerifierServerProcess(f"server_{Test_quic_no_activity_timeout.server_counter}", self.replay_file) + Test_quic_no_activity_timeout.server_counter += 1 + self._server = server + + def _configure_traffic_server(self, tr: 'TestRun'): + """Configure Traffic Server. + + :param tr: The TestRun object to associate the ts process with. + """ + ts = tr.MakeATSProcess(f"ts-{Test_quic_no_activity_timeout.ts_counter}", enable_quic=True, enable_tls=True) + + Test_quic_no_activity_timeout.ts_counter += 1 + self._ts = ts + self._ts.addSSLfile("ssl/cert.crt") + self._ts.addSSLfile("ssl/private-key.key") + self._ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'ssl|net|v_quic|quic|http|socket|inactivity_cop', + 'proxy.config.quic.no_activity_timeout_in': self.no_activity_timeout_in, + 'proxy.config.quic.qlog.file_base': f'log/qlog_{Test_quic_no_activity_timeout.server_counter}', + }) + + if self.extra_recs: + self._ts.Disk.records_config.update(self.extra_recs) + + self._ts.Disk.remap_config.AddLine(f'map / http://127.0.0.1:{self._server.Variables.http_port}') + self._ts.Disk.ssl_multicert_yaml.AddLines( + f""" + ssl_multicert: + - dest_ip: "*" + ssl_cert_name: {self._ts.Variables.SSLDir}/cert.crt + ssl_key_name: {self._ts.Variables.SSLDir}/private-key.key + """.split("\n")) + + def run(self, check_for_max_idle_timeout=False): + """Run the test.""" + tr = urtest.AddTestRun(self.name) + self._configure_server(tr) + self._configure_traffic_server(tr) + + tr.Processes.Default.StartBefore(self._server) + tr.Processes.Default.StartBefore(self._ts) + + tr.AddVerifierClientProcess( + f'client-{Test_quic_no_activity_timeout.client_counter}', + self.replay_file, + http3_ports=[self._ts.Variables.ssl_port], + keys=self.replay_keys) + Test_quic_no_activity_timeout.client_counter += 1 + + if check_for_max_idle_timeout: + tr.Processes.Default.ReturnCode = 1 # timeout + self._ts.Disk.traffic_out.Content += Testers.IncludesExpression( + "QUIC Idle timeout detected", "We should detect the timeout.") + else: + tr.Processes.Default.ReturnCode = 0 + + if self.gold_file: + tr.Processes.Default.Streams.all = self.gold_file + + # Tests start. + + test0 = Test_quic_no_activity_timeout( + "Test ts.quic.no_activity_timeout_in(quic max_idle_timeout), no delays", + no_activity_timeout_in=0, # no timeout `max_idle_timeout` + replay_keys="nodelays") + test0.run() + + if Condition.HasATSFeature('TS_HAS_QUICHE'): + test1 = Test_quic_no_activity_timeout( + "Test ts.quic.no_activity_timeout_in(quic max_idle_timeout) with a 5s delay", + no_activity_timeout_in=3000, # 3s `max_idle_timeout` + replay_keys="delay5s", + gold_file="gold/quic_no_activity_timeout.gold") + test1.run(check_for_max_idle_timeout=True) + + # QUIC Ignores the default_inactivity_timeout config, so the ts.quic.no_activity_timeout_in + # should be honored + test2 = Test_quic_no_activity_timeout( + "Ignoring default_inactivity_timeout and use the ts.quic.no_activity_timeout_in instead", + replay_keys="delay5s", + no_activity_timeout_in=3000, + extra_recs={'proxy.config.net.default_inactivity_timeout': 1}) + test2.run(check_for_max_idle_timeout=True) + urtest.execute() diff --git a/tests/uranium_tests/timeout/test_quic_poll_timeout.py b/tests/uranium_tests/timeout/test_quic_poll_timeout.py new file mode 100644 index 00000000000..894ee3b6dbd --- /dev/null +++ b/tests/uranium_tests/timeout/test_quic_poll_timeout.py @@ -0,0 +1,96 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_quic_poll_timeout(urtest: UraniumTest) -> None: + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + from typing import Optional + + urtest.Summary = 'Checks records.proxy.config.udp.poll_timeout' + + urtest.SkipUnless(Condition.HasATSFeature('TS_HAS_QUICHE')) + + class TestPollTimeout: + """Configure a test for poll_timeout.""" + + ts_counter: int = 0 + + def __init__(self, name: str, udp_poll_timeout_in: Optional[int] = None) -> None: + """Initialize the test. + + :param name: The name of the test. + :param udp_poll_timeout_in: Configuration value for proxy.config.udp.poll_timeout + """ + self.name = name + self.udp_poll_timeout_in = udp_poll_timeout_in + self.expected_udp_poll_timeout = 100 + if udp_poll_timeout_in is not None: + self.expected_udp_poll_timeout = udp_poll_timeout_in + + def _configure_traffic_server(self, tr: 'TestRun'): + """Configure Traffic Server. + + :param tr: The TestRun object to associate the ts process with. + """ + ts = tr.MakeATSProcess(f"ts-{TestPollTimeout.ts_counter}", enable_quic=True, enable_tls=True) + + TestPollTimeout.ts_counter += 1 + self._ts = ts + self._ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'net|v_quic|quic|socket|inactivity_cop|v_iocore_net_poll', + }) + + if self.udp_poll_timeout_in is not None: + self._ts.Disk.records_config.update({'proxy.config.udp.poll_timeout': self.udp_poll_timeout_in}) + + def run(self): + """Run the test.""" + tr = urtest.AddTestRun(self.name) + self._configure_traffic_server(tr) + + tr.Processes.Default.Command = "echo 'testing records.proxy.config.udp.poll_timeout'" + tr.Processes.Default.StartBefore(self._ts) + + self._ts.Disk.traffic_out.Content += Testers.IncludesExpression( + f"ET_UDP.*timeout: {self.expected_udp_poll_timeout},", "Verify UDP poll timeout.") + + # Tests start. + + test0 = TestPollTimeout("Test records.proxy.config.udp.poll_timeout with default value.") + test0.run() + + test1 = TestPollTimeout("Test records.proxy.config.udp.poll_timeout with value of 10.", udp_poll_timeout_in=10) + test1.run() + urtest.execute() diff --git a/tests/uranium_tests/timeout/test_tls_conn_timeout.py b/tests/uranium_tests/timeout/test_tls_conn_timeout.py new file mode 100644 index 00000000000..972e26b5bfa --- /dev/null +++ b/tests/uranium_tests/timeout/test_tls_conn_timeout.py @@ -0,0 +1,140 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_tls_conn_timeout(urtest: UraniumTest) -> None: + ''' + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import os + + urtest.Summary = 'Testing ATS TLS handshake timeout' + + ts = urtest.MakeATSProcess("ts") + + urtest.Setup.Copy(os.path.join(urtest.Variables.AtsBuildUraniumTestsDir, 'timeout', 'ssl-delay-server')) + + urtest.ContinueOnFail = True + urtest.GetTcpPort("block_connect_port") + urtest.GetTcpPort("block_ttfb_port") + urtest.GetTcpPort("get_block_connect_port") + urtest.GetTcpPort("get_block_ttfb_port") + + delay_post_connect = urtest.Processes.Process( + "delay post connect", './ssl-delay-server {0} 3 0 server.pem'.format(urtest.Variables.block_connect_port)) + delay_post_ttfb = urtest.Processes.Process( + "delay post ttfb", './ssl-delay-server {0} 0 6 server.pem'.format(urtest.Variables.block_ttfb_port)) + + delay_get_connect = urtest.Processes.Process( + "delay get connect", './ssl-delay-server {0} 3 0 server.pem'.format(urtest.Variables.get_block_connect_port)) + delay_get_ttfb = urtest.Processes.Process( + "delay get ttfb", './ssl-delay-server {0} 0 6 server.pem'.format(urtest.Variables.get_block_ttfb_port)) + + ts.Disk.records_config.update( + { + 'proxy.config.url_remap.remap_required': 1, + 'proxy.config.http.connect_attempts_timeout': 1, + 'proxy.config.http.connect_attempts_max_retries': 1, + 'proxy.config.http.transaction_no_activity_timeout_out': 4, + 'proxy.config.diags.debug.enabled': 0, + 'proxy.config.diags.debug.tags': 'http|ssl', + 'proxy.config.ssl.client.verify.server.policy': 'PERMISSIVE', + }) + + ts.Disk.remap_config.AddLine('map /connect_blocked https://127.0.0.1:{0}'.format(urtest.Variables.block_connect_port)) + ts.Disk.remap_config.AddLine('map /ttfb_blocked https://127.0.0.1:{0}'.format(urtest.Variables.block_ttfb_port)) + ts.Disk.remap_config.AddLine('map /get_connect_blocked https://127.0.0.1:{0}'.format(urtest.Variables.get_block_connect_port)) + ts.Disk.remap_config.AddLine('map /get_ttfb_blocked https://127.0.0.1:{0}'.format(urtest.Variables.get_block_ttfb_port)) + + # Commenting out the per test case timeouts. In the CI, there is too big of a risk that we hit those timeouts. Had hoped to use + # The test case timeouts to differentiate between a good origin timeout and a too long origin timeout + + # Run the connection and TTFB timeout tests first with POST. + + # Request the port that should timeout in the handshake + # Should retry once + tr = urtest.AddTestRun("tr-blocking-post") + tr.Setup.Copy(os.path.join(urtest.Variables.AtsTestToolsDir, "ssl", "server.pem")) + tr.Processes.Default.StartBefore(urtest.Processes.ts) + tr.Processes.Default.StartBefore(delay_post_connect, ready=When.PortOpen(urtest.Variables.block_connect_port)) + tr.MakeCurlCommand( + '-H"Connection:close" -d "bob" -i http://127.0.0.1:{0}/connect_blocked --tlsv1.2'.format(ts.Variables.port), ts=ts) + tr.Processes.Default.Streams.All = Testers.ContainsExpression("HTTP/1.1 502 Connection timed out", "Connect failed") + tr.Processes.Default.ReturnCode = 0 + tr.StillRunningAfter = delay_post_connect + tr.StillRunningAfter = urtest.Processes.ts + + # Should not catch the connect timeout. Even though the first bytes are not sent until after the 2 second connect timeout + # Should not retry the connection + tr = urtest.AddTestRun("tr-delayed-post") + tr.Processes.Default.StartBefore(delay_post_ttfb, ready=When.PortOpen(urtest.Variables.block_ttfb_port)) + tr.MakeCurlCommand( + '-H"Connection:close" -d "bob" -i http://127.0.0.1:{0}/ttfb_blocked --tlsv1.2'.format(ts.Variables.port), ts=ts) + tr.Processes.Default.Streams.All = Testers.ContainsExpression("504 Connection Timed Out", "Connect timeout") + tr.Processes.Default.ReturnCode = 0 + tr.StillRunningAfter = delay_post_ttfb + + # Run the connection and TTFB timeout tests again with GET + + # Request the port that should timeout in the handshake + # Should retry once + tr = urtest.AddTestRun("tr-blocking-get") + tr.Processes.Default.StartBefore(delay_get_connect, ready=When.PortOpen(urtest.Variables.get_block_connect_port)) + tr.MakeCurlCommand( + '-H"Connection:close" -i http://127.0.0.1:{0}/get_connect_blocked --tlsv1.2'.format(ts.Variables.port), ts=ts) + tr.Processes.Default.Streams.All = Testers.ContainsExpression("HTTP/1.1 502 Connection timed out", "Connect failed") + tr.Processes.Default.ReturnCode = 0 + tr.StillRunningAfter = delay_get_connect + + # Should not catch the connect timeout. Even though the first bytes are not sent until after the 2 second connect timeout + # Since get is idempotent, It will try to connect again even though the GET request had been sent + tr = urtest.AddTestRun("tr-delayed-get") + tr.Processes.Default.StartBefore(delay_get_ttfb, ready=When.PortOpen(urtest.Variables.get_block_ttfb_port)) + tr.MakeCurlCommand('-H"Connection:close" -i http://127.0.0.1:{0}/get_ttfb_blocked --tlsv1.2'.format(ts.Variables.port), ts=ts) + tr.Processes.Default.Streams.All = Testers.ContainsExpression("504 Connection Timed Out", "Connect timeout") + tr.Processes.Default.ReturnCode = 0 + tr.StillRunningAfter = delay_get_ttfb + + delay_post_connect.Streams.All = Testers.ContainsExpression( + "Accept try", "Should appear at least two times (may be an extra one due to port ready test)") + delay_post_connect.Streams.All += Testers.ExcludesExpression("TTFB delay", "Should not reach the TTFB delay logic") + delay_post_ttfb.Streams.All = Testers.ContainsExpression("Accept try", "Should appear one time") + delay_post_ttfb.Streams.All += Testers.ContainsExpression("TTFB delay", "Should reach the TTFB delay logic") + + delay_get_connect.Streams.All = Testers.ContainsExpression( + "Accept try", "Should appear at least two times (may be an extra one due to port ready test)") + delay_get_connect.Streams.All += Testers.ExcludesExpression("TTFB delay", "Should not reach the TTFB delay logic") + delay_get_ttfb.Streams.All = Testers.ContainsExpression( + "Accept try", "Should appear at least two times (may be an extra one due to the port ready test)") + delay_get_ttfb.Streams.All += Testers.ContainsExpression("TTFB delay", "Should reach the TTFB delay logic") + urtest.execute() diff --git a/tests/uranium_tests/timeout/test_tunnel_active_timeout.py b/tests/uranium_tests/timeout/test_tunnel_active_timeout.py new file mode 100644 index 00000000000..6470dbab4b4 --- /dev/null +++ b/tests/uranium_tests/timeout/test_tunnel_active_timeout.py @@ -0,0 +1,121 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_tunnel_active_timeout(urtest: UraniumTest) -> None: + ''' + Verify that tunnel active timeout produces ERR_TUN_ACTIVE_TIMEOUT squid code. + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import os + import sys + + urtest.Summary = ''' + Verify that tunnel active timeout produces ERR_TUN_ACTIVE_TIMEOUT squid code. + ''' + + ts = urtest.MakeATSProcess("ts", enable_tls=True) + server = urtest.MakeOriginServer("server", ssl=True) + + # Simple response from origin + request_header = {"headers": "GET / HTTP/1.1\r\nHost: server\r\n\r\n", "timestamp": "1234", "body": ""} + response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1234", "body": "hello"} + server.addResponse("sessionlog.json", request_header, response_header) + + ts.addDefaultSSLFiles() + + ts.Disk.ssl_multicert_yaml.AddLines( + f""" + ssl_multicert: + - dest_ip: "*" + ssl_cert_name: server.pem + ssl_key_name: server.key + """.split('\n')) + + ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'http|ssl|tunnel', + 'proxy.config.ssl.server.cert.path': ts.Variables.SSLDir, + 'proxy.config.ssl.server.private_key.path': ts.Variables.SSLDir, + 'proxy.config.ssl.client.verify.server.policy': 'PERMISSIVE', + 'proxy.config.http.connect_ports': f'{server.Variables.SSL_Port}', + # Set a short active timeout for tunnels (2 seconds) + 'proxy.config.http.transaction_active_timeout_in': 2, + # Force log flush every second for test reliability + 'proxy.config.log.max_secs_per_buffer': 1, + }) + + ts.Disk.remap_config.AddLine(f'map / https://127.0.0.1:{server.Variables.SSL_Port}') + ts.addPrivateConnectAllowYaml() + + # Configure custom log format to capture squid code + ts.Disk.logging_yaml.AddLines( + ''' + logging: + formats: + - name: custom + format: '% % %' + logs: + - filename: squid.log + format: custom + '''.split("\n")) + + # Test: Perform a CONNECT request that will time out + tr = urtest.AddTestRun("Tunnel active timeout test") + tr.Processes.Default.StartBefore(server) + tr.Processes.Default.StartBefore(ts) + + # Use the tunnel_timeout_client.py script to establish a CONNECT tunnel and then + # just hold the connection until ATS times it out + tr.Setup.Copy('tunnel_timeout_client.py') + + # Connect, establish tunnel, then sleep to trigger active timeout + tr.Processes.Default.Command = ( + f'{sys.executable} tunnel_timeout_client.py 127.0.0.1 {ts.Variables.port} ' + f'127.0.0.1 {server.Variables.SSL_Port} 5') + # The connection will be closed by ATS due to timeout + tr.Processes.Default.ReturnCode = 0 + tr.StillRunningAfter = ts + + # Wait for the access log to be written + tr = urtest.AddTestRun("Wait for the access log to write out") + tr.DelayStart = 3 + tr.StillRunningAfter = ts + tr.Processes.Default.Command = 'echo "waiting for log flush"' + tr.Processes.Default.ReturnCode = 0 + + # Verify the squid code in the access log + ts.Disk.File(os.path.join(ts.Variables.LOGDIR, 'squid.log')).Content = Testers.ContainsExpression( + 'ERR_TUN_ACTIVE_TIMEOUT.*CONNECT', 'Verify the tunnel timeout squid code is logged') + urtest.execute() diff --git a/tests/gold_tests/timeout/time_client.sh b/tests/uranium_tests/timeout/time_client.sh similarity index 100% rename from tests/gold_tests/timeout/time_client.sh rename to tests/uranium_tests/timeout/time_client.sh diff --git a/tests/gold_tests/timeout/tunnel_timeout_client.py b/tests/uranium_tests/timeout/tunnel_timeout_client.py similarity index 100% rename from tests/gold_tests/timeout/tunnel_timeout_client.py rename to tests/uranium_tests/timeout/tunnel_timeout_client.py diff --git a/tests/gold_tests/tls/CMakeLists.txt b/tests/uranium_tests/tls/CMakeLists.txt similarity index 100% rename from tests/gold_tests/tls/CMakeLists.txt rename to tests/uranium_tests/tls/CMakeLists.txt diff --git a/tests/gold_tests/tls/early_h1_get.txt b/tests/uranium_tests/tls/early_h1_get.txt similarity index 100% rename from tests/gold_tests/tls/early_h1_get.txt rename to tests/uranium_tests/tls/early_h1_get.txt diff --git a/tests/gold_tests/tls/early_h1_post.txt b/tests/uranium_tests/tls/early_h1_post.txt similarity index 100% rename from tests/gold_tests/tls/early_h1_post.txt rename to tests/uranium_tests/tls/early_h1_post.txt diff --git a/tests/gold_tests/tls/early_h2_get.txt b/tests/uranium_tests/tls/early_h2_get.txt similarity index 100% rename from tests/gold_tests/tls/early_h2_get.txt rename to tests/uranium_tests/tls/early_h2_get.txt diff --git a/tests/gold_tests/tls/early_h2_multi1.txt b/tests/uranium_tests/tls/early_h2_multi1.txt similarity index 100% rename from tests/gold_tests/tls/early_h2_multi1.txt rename to tests/uranium_tests/tls/early_h2_multi1.txt diff --git a/tests/gold_tests/tls/early_h2_multi2.txt b/tests/uranium_tests/tls/early_h2_multi2.txt similarity index 100% rename from tests/gold_tests/tls/early_h2_multi2.txt rename to tests/uranium_tests/tls/early_h2_multi2.txt diff --git a/tests/gold_tests/tls/early_h2_post.txt b/tests/uranium_tests/tls/early_h2_post.txt similarity index 100% rename from tests/gold_tests/tls/early_h2_post.txt rename to tests/uranium_tests/tls/early_h2_post.txt diff --git a/tests/gold_tests/tls/file.ticket b/tests/uranium_tests/tls/file.ticket similarity index 100% rename from tests/gold_tests/tls/file.ticket rename to tests/uranium_tests/tls/file.ticket diff --git a/tests/gold_tests/tls/gold/accesslog.gold b/tests/uranium_tests/tls/gold/accesslog.gold similarity index 100% rename from tests/gold_tests/tls/gold/accesslog.gold rename to tests/uranium_tests/tls/gold/accesslog.gold diff --git a/tests/gold_tests/tls/gold/clientcert-accesslog.gold b/tests/uranium_tests/tls/gold/clientcert-accesslog.gold similarity index 100% rename from tests/gold_tests/tls/gold/clientcert-accesslog.gold rename to tests/uranium_tests/tls/gold/clientcert-accesslog.gold diff --git a/tests/gold_tests/tls/gold/proxycert-accesslog.gold b/tests/uranium_tests/tls/gold/proxycert-accesslog.gold similarity index 100% rename from tests/gold_tests/tls/gold/proxycert-accesslog.gold rename to tests/uranium_tests/tls/gold/proxycert-accesslog.gold diff --git a/tests/gold_tests/tls/gold/proxycert2-accesslog.gold b/tests/uranium_tests/tls/gold/proxycert2-accesslog.gold similarity index 100% rename from tests/gold_tests/tls/gold/proxycert2-accesslog.gold rename to tests/uranium_tests/tls/gold/proxycert2-accesslog.gold diff --git a/tests/gold_tests/tls/gold/ssl-post.gold b/tests/uranium_tests/tls/gold/ssl-post.gold similarity index 100% rename from tests/gold_tests/tls/gold/ssl-post.gold rename to tests/uranium_tests/tls/gold/ssl-post.gold diff --git a/tests/gold_tests/tls/gold/tls-partial-blind-tunnel-metrics.gold b/tests/uranium_tests/tls/gold/tls-partial-blind-tunnel-metrics.gold similarity index 100% rename from tests/gold_tests/tls/gold/tls-partial-blind-tunnel-metrics.gold rename to tests/uranium_tests/tls/gold/tls-partial-blind-tunnel-metrics.gold diff --git a/tests/gold_tests/tls/gold/tls-tunnel-forward-metrics.gold b/tests/uranium_tests/tls/gold/tls-tunnel-forward-metrics.gold similarity index 100% rename from tests/gold_tests/tls/gold/tls-tunnel-forward-metrics.gold rename to tests/uranium_tests/tls/gold/tls-tunnel-forward-metrics.gold diff --git a/tests/gold_tests/tls/gold/tls-tunnel-metrics.gold b/tests/uranium_tests/tls/gold/tls-tunnel-metrics.gold similarity index 100% rename from tests/gold_tests/tls/gold/tls-tunnel-metrics.gold rename to tests/uranium_tests/tls/gold/tls-tunnel-metrics.gold diff --git a/tests/gold_tests/tls/h2_early_decode.py b/tests/uranium_tests/tls/h2_early_decode.py similarity index 99% rename from tests/gold_tests/tls/h2_early_decode.py rename to tests/uranium_tests/tls/h2_early_decode.py index 9247dd2a34f..27ffc0824e9 100755 --- a/tests/gold_tests/tls/h2_early_decode.py +++ b/tests/uranium_tests/tls/h2_early_decode.py @@ -234,7 +234,7 @@ def main(): # sample command to get this output: # openssl s_client -bind 127.0.0.1:61991 -connect 127.0.0.1:61992 -tls1_3 # -quiet -sess_out /home/duke/Dev/ats-test/sess.dat -sess_in - # /home/duke/Dev/ats-test/sess.dat -early_data ./gold_tests/tls/early2.txt + # /home/duke/Dev/ats-test/sess.dat -early_data ./uranium_tests/tls/early2.txt # >! _sandbox/tls_0rtt_server/early2_out.txt 2>&1 if len(sys.argv) < 2: diff --git a/tests/gold_tests/tls/h2_early_gen.py b/tests/uranium_tests/tls/h2_early_gen.py similarity index 100% rename from tests/gold_tests/tls/h2_early_gen.py rename to tests/uranium_tests/tls/h2_early_gen.py diff --git a/tests/gold_tests/tls/proxy_protocol_client.py b/tests/uranium_tests/tls/proxy_protocol_client.py similarity index 100% rename from tests/gold_tests/tls/proxy_protocol_client.py rename to tests/uranium_tests/tls/proxy_protocol_client.py diff --git a/tests/gold_tests/tls/receive_split_client_hello.py b/tests/uranium_tests/tls/receive_split_client_hello.py similarity index 100% rename from tests/gold_tests/tls/receive_split_client_hello.py rename to tests/uranium_tests/tls/receive_split_client_hello.py diff --git a/tests/gold_tests/tls/replay/allow-plain.replay.yaml b/tests/uranium_tests/tls/replay/allow-plain.replay.yaml similarity index 100% rename from tests/gold_tests/tls/replay/allow-plain.replay.yaml rename to tests/uranium_tests/tls/replay/allow-plain.replay.yaml diff --git a/tests/gold_tests/tls/replay/ip_allow.replay.yaml b/tests/uranium_tests/tls/replay/ip_allow.replay.yaml similarity index 100% rename from tests/gold_tests/tls/replay/ip_allow.replay.yaml rename to tests/uranium_tests/tls/replay/ip_allow.replay.yaml diff --git a/tests/gold_tests/tls/replay/ip_allow_proxy.replay.yaml b/tests/uranium_tests/tls/replay/ip_allow_proxy.replay.yaml similarity index 100% rename from tests/gold_tests/tls/replay/ip_allow_proxy.replay.yaml rename to tests/uranium_tests/tls/replay/ip_allow_proxy.replay.yaml diff --git a/tests/gold_tests/tls/replay/ip_allow_tunnel.replay.yaml b/tests/uranium_tests/tls/replay/ip_allow_tunnel.replay.yaml similarity index 100% rename from tests/gold_tests/tls/replay/ip_allow_tunnel.replay.yaml rename to tests/uranium_tests/tls/replay/ip_allow_tunnel.replay.yaml diff --git a/tests/gold_tests/tls/replay/outbound_sni_server_name_plain.test.yaml b/tests/uranium_tests/tls/replay/outbound_sni_server_name_plain.test.yaml similarity index 99% rename from tests/gold_tests/tls/replay/outbound_sni_server_name_plain.test.yaml rename to tests/uranium_tests/tls/replay/outbound_sni_server_name_plain.test.yaml index b13506b0ed6..621b87fb519 100644 --- a/tests/gold_tests/tls/replay/outbound_sni_server_name_plain.test.yaml +++ b/tests/uranium_tests/tls/replay/outbound_sni_server_name_plain.test.yaml @@ -17,7 +17,7 @@ meta: version: "1.0" -autest: +urtest: description: "Plain inbound transaction with outbound SNI server_name policy" dns: name: "dns" diff --git a/tests/gold_tests/tls/replay/tls_cert_compression.replay.yaml b/tests/uranium_tests/tls/replay/tls_cert_compression.replay.yaml similarity index 100% rename from tests/gold_tests/tls/replay/tls_cert_compression.replay.yaml rename to tests/uranium_tests/tls/replay/tls_cert_compression.replay.yaml diff --git a/tests/gold_tests/tls/replay/tls_cert_compression_cache.replay.yaml b/tests/uranium_tests/tls/replay/tls_cert_compression_cache.replay.yaml similarity index 100% rename from tests/gold_tests/tls/replay/tls_cert_compression_cache.replay.yaml rename to tests/uranium_tests/tls/replay/tls_cert_compression_cache.replay.yaml diff --git a/tests/gold_tests/tls/split_client_hello.py b/tests/uranium_tests/tls/split_client_hello.py similarity index 100% rename from tests/gold_tests/tls/split_client_hello.py rename to tests/uranium_tests/tls/split_client_hello.py diff --git a/tests/gold_tests/tls/ssl-post.c b/tests/uranium_tests/tls/ssl-post.c similarity index 100% rename from tests/gold_tests/tls/ssl-post.c rename to tests/uranium_tests/tls/ssl-post.c diff --git a/tests/gold_tests/tls/ssl/aaa-ca.key b/tests/uranium_tests/tls/ssl/aaa-ca.key similarity index 100% rename from tests/gold_tests/tls/ssl/aaa-ca.key rename to tests/uranium_tests/tls/ssl/aaa-ca.key diff --git a/tests/gold_tests/tls/ssl/aaa-ca.pem b/tests/uranium_tests/tls/ssl/aaa-ca.pem similarity index 100% rename from tests/gold_tests/tls/ssl/aaa-ca.pem rename to tests/uranium_tests/tls/ssl/aaa-ca.pem diff --git a/tests/gold_tests/tls/ssl/aaa-signed.key b/tests/uranium_tests/tls/ssl/aaa-signed.key similarity index 100% rename from tests/gold_tests/tls/ssl/aaa-signed.key rename to tests/uranium_tests/tls/ssl/aaa-signed.key diff --git a/tests/gold_tests/tls/ssl/aaa-signed.pem b/tests/uranium_tests/tls/ssl/aaa-signed.pem similarity index 100% rename from tests/gold_tests/tls/ssl/aaa-signed.pem rename to tests/uranium_tests/tls/ssl/aaa-signed.pem diff --git a/tests/gold_tests/tls/ssl/bbb-ca.key b/tests/uranium_tests/tls/ssl/bbb-ca.key similarity index 100% rename from tests/gold_tests/tls/ssl/bbb-ca.key rename to tests/uranium_tests/tls/ssl/bbb-ca.key diff --git a/tests/gold_tests/tls/ssl/bbb-ca.pem b/tests/uranium_tests/tls/ssl/bbb-ca.pem similarity index 100% rename from tests/gold_tests/tls/ssl/bbb-ca.pem rename to tests/uranium_tests/tls/ssl/bbb-ca.pem diff --git a/tests/gold_tests/tls/ssl/bbb-signed.key b/tests/uranium_tests/tls/ssl/bbb-signed.key similarity index 100% rename from tests/gold_tests/tls/ssl/bbb-signed.key rename to tests/uranium_tests/tls/ssl/bbb-signed.key diff --git a/tests/gold_tests/tls/ssl/bbb-signed.pem b/tests/uranium_tests/tls/ssl/bbb-signed.pem similarity index 100% rename from tests/gold_tests/tls/ssl/bbb-signed.pem rename to tests/uranium_tests/tls/ssl/bbb-signed.pem diff --git a/tests/gold_tests/tls/ssl/ca.ocsp.key b/tests/uranium_tests/tls/ssl/ca.ocsp.key similarity index 100% rename from tests/gold_tests/tls/ssl/ca.ocsp.key rename to tests/uranium_tests/tls/ssl/ca.ocsp.key diff --git a/tests/gold_tests/tls/ssl/ca.ocsp.pem b/tests/uranium_tests/tls/ssl/ca.ocsp.pem similarity index 100% rename from tests/gold_tests/tls/ssl/ca.ocsp.pem rename to tests/uranium_tests/tls/ssl/ca.ocsp.pem diff --git a/tests/gold_tests/tls/ssl/ccc-ca.key b/tests/uranium_tests/tls/ssl/ccc-ca.key similarity index 100% rename from tests/gold_tests/tls/ssl/ccc-ca.key rename to tests/uranium_tests/tls/ssl/ccc-ca.key diff --git a/tests/gold_tests/tls/ssl/ccc-ca.pem b/tests/uranium_tests/tls/ssl/ccc-ca.pem similarity index 100% rename from tests/gold_tests/tls/ssl/ccc-ca.pem rename to tests/uranium_tests/tls/ssl/ccc-ca.pem diff --git a/tests/gold_tests/tls/ssl/ccc-signed.key b/tests/uranium_tests/tls/ssl/ccc-signed.key similarity index 100% rename from tests/gold_tests/tls/ssl/ccc-signed.key rename to tests/uranium_tests/tls/ssl/ccc-signed.key diff --git a/tests/gold_tests/tls/ssl/ccc-signed.pem b/tests/uranium_tests/tls/ssl/ccc-signed.pem similarity index 100% rename from tests/gold_tests/tls/ssl/ccc-signed.pem rename to tests/uranium_tests/tls/ssl/ccc-signed.pem diff --git a/tests/gold_tests/tls/ssl/combined-ec.pem b/tests/uranium_tests/tls/ssl/combined-ec.pem similarity index 100% rename from tests/gold_tests/tls/ssl/combined-ec.pem rename to tests/uranium_tests/tls/ssl/combined-ec.pem diff --git a/tests/gold_tests/tls/ssl/combined.pem b/tests/uranium_tests/tls/ssl/combined.pem similarity index 100% rename from tests/gold_tests/tls/ssl/combined.pem rename to tests/uranium_tests/tls/ssl/combined.pem diff --git a/tests/gold_tests/tls/ssl/combo-signed-foo.pem b/tests/uranium_tests/tls/ssl/combo-signed-foo.pem similarity index 100% rename from tests/gold_tests/tls/ssl/combo-signed-foo.pem rename to tests/uranium_tests/tls/ssl/combo-signed-foo.pem diff --git a/tests/gold_tests/tls/ssl/combo.pem b/tests/uranium_tests/tls/ssl/combo.pem similarity index 100% rename from tests/gold_tests/tls/ssl/combo.pem rename to tests/uranium_tests/tls/ssl/combo.pem diff --git a/tests/gold_tests/tls/ssl/gen_client_certs.sh b/tests/uranium_tests/tls/ssl/gen_client_certs.sh similarity index 100% rename from tests/gold_tests/tls/ssl/gen_client_certs.sh rename to tests/uranium_tests/tls/ssl/gen_client_certs.sh diff --git a/tests/gold_tests/tls/ssl/gen_foobar_certs.sh b/tests/uranium_tests/tls/ssl/gen_foobar_certs.sh similarity index 100% rename from tests/gold_tests/tls/ssl/gen_foobar_certs.sh rename to tests/uranium_tests/tls/ssl/gen_foobar_certs.sh diff --git a/tests/gold_tests/tls/ssl/ocsp_response.der b/tests/uranium_tests/tls/ssl/ocsp_response.der similarity index 100% rename from tests/gold_tests/tls/ssl/ocsp_response.der rename to tests/uranium_tests/tls/ssl/ocsp_response.der diff --git a/tests/gold_tests/tls/ssl/openssl.cnf b/tests/uranium_tests/tls/ssl/openssl.cnf similarity index 100% rename from tests/gold_tests/tls/ssl/openssl.cnf rename to tests/uranium_tests/tls/ssl/openssl.cnf diff --git a/tests/gold_tests/tls/ssl/passphrase.key b/tests/uranium_tests/tls/ssl/passphrase.key similarity index 100% rename from tests/gold_tests/tls/ssl/passphrase.key rename to tests/uranium_tests/tls/ssl/passphrase.key diff --git a/tests/gold_tests/tls/ssl/passphrase.pem b/tests/uranium_tests/tls/ssl/passphrase.pem similarity index 100% rename from tests/gold_tests/tls/ssl/passphrase.pem rename to tests/uranium_tests/tls/ssl/passphrase.pem diff --git a/tests/gold_tests/tls/ssl/passphrase2.key b/tests/uranium_tests/tls/ssl/passphrase2.key similarity index 100% rename from tests/gold_tests/tls/ssl/passphrase2.key rename to tests/uranium_tests/tls/ssl/passphrase2.key diff --git a/tests/gold_tests/tls/ssl/passphrase2.pem b/tests/uranium_tests/tls/ssl/passphrase2.pem similarity index 100% rename from tests/gold_tests/tls/ssl/passphrase2.pem rename to tests/uranium_tests/tls/ssl/passphrase2.pem diff --git a/tests/gold_tests/tls/ssl/responder.ocsp.key b/tests/uranium_tests/tls/ssl/responder.ocsp.key similarity index 100% rename from tests/gold_tests/tls/ssl/responder.ocsp.key rename to tests/uranium_tests/tls/ssl/responder.ocsp.key diff --git a/tests/gold_tests/tls/ssl/responder.ocsp.pem b/tests/uranium_tests/tls/ssl/responder.ocsp.pem similarity index 100% rename from tests/gold_tests/tls/ssl/responder.ocsp.pem rename to tests/uranium_tests/tls/ssl/responder.ocsp.pem diff --git a/tests/gold_tests/tls/ssl/server-bar.key b/tests/uranium_tests/tls/ssl/server-bar.key similarity index 100% rename from tests/gold_tests/tls/ssl/server-bar.key rename to tests/uranium_tests/tls/ssl/server-bar.key diff --git a/tests/gold_tests/tls/ssl/server-bar.pem b/tests/uranium_tests/tls/ssl/server-bar.pem similarity index 100% rename from tests/gold_tests/tls/ssl/server-bar.pem rename to tests/uranium_tests/tls/ssl/server-bar.pem diff --git a/tests/gold_tests/tls/ssl/server-foo.key b/tests/uranium_tests/tls/ssl/server-foo.key similarity index 100% rename from tests/gold_tests/tls/ssl/server-foo.key rename to tests/uranium_tests/tls/ssl/server-foo.key diff --git a/tests/gold_tests/tls/ssl/server-foo.pem b/tests/uranium_tests/tls/ssl/server-foo.pem similarity index 100% rename from tests/gold_tests/tls/ssl/server-foo.pem rename to tests/uranium_tests/tls/ssl/server-foo.pem diff --git a/tests/gold_tests/tls/ssl/server.key b/tests/uranium_tests/tls/ssl/server.key similarity index 100% rename from tests/gold_tests/tls/ssl/server.key rename to tests/uranium_tests/tls/ssl/server.key diff --git a/tests/gold_tests/tls/ssl/server.ocsp.key b/tests/uranium_tests/tls/ssl/server.ocsp.key similarity index 100% rename from tests/gold_tests/tls/ssl/server.ocsp.key rename to tests/uranium_tests/tls/ssl/server.ocsp.key diff --git a/tests/gold_tests/tls/ssl/server.ocsp.pem b/tests/uranium_tests/tls/ssl/server.ocsp.pem similarity index 100% rename from tests/gold_tests/tls/ssl/server.ocsp.pem rename to tests/uranium_tests/tls/ssl/server.ocsp.pem diff --git a/tests/gold_tests/tls/ssl/server.pem b/tests/uranium_tests/tls/ssl/server.pem similarity index 100% rename from tests/gold_tests/tls/ssl/server.pem rename to tests/uranium_tests/tls/ssl/server.pem diff --git a/tests/gold_tests/tls/ssl/signed-bar.key b/tests/uranium_tests/tls/ssl/signed-bar.key similarity index 100% rename from tests/gold_tests/tls/ssl/signed-bar.key rename to tests/uranium_tests/tls/ssl/signed-bar.key diff --git a/tests/gold_tests/tls/ssl/signed-bar.pem b/tests/uranium_tests/tls/ssl/signed-bar.pem similarity index 100% rename from tests/gold_tests/tls/ssl/signed-bar.pem rename to tests/uranium_tests/tls/ssl/signed-bar.pem diff --git a/tests/gold_tests/tls/ssl/signed-bob-bar.pem b/tests/uranium_tests/tls/ssl/signed-bob-bar.pem similarity index 100% rename from tests/gold_tests/tls/ssl/signed-bob-bar.pem rename to tests/uranium_tests/tls/ssl/signed-bob-bar.pem diff --git a/tests/gold_tests/tls/ssl/signed-bob-foo.pem b/tests/uranium_tests/tls/ssl/signed-bob-foo.pem similarity index 100% rename from tests/gold_tests/tls/ssl/signed-bob-foo.pem rename to tests/uranium_tests/tls/ssl/signed-bob-foo.pem diff --git a/tests/gold_tests/tls/ssl/signed-foo-ec.key b/tests/uranium_tests/tls/ssl/signed-foo-ec.key similarity index 100% rename from tests/gold_tests/tls/ssl/signed-foo-ec.key rename to tests/uranium_tests/tls/ssl/signed-foo-ec.key diff --git a/tests/gold_tests/tls/ssl/signed-foo-ec.pem b/tests/uranium_tests/tls/ssl/signed-foo-ec.pem similarity index 100% rename from tests/gold_tests/tls/ssl/signed-foo-ec.pem rename to tests/uranium_tests/tls/ssl/signed-foo-ec.pem diff --git a/tests/gold_tests/tls/ssl/signed-foo.key b/tests/uranium_tests/tls/ssl/signed-foo.key similarity index 100% rename from tests/gold_tests/tls/ssl/signed-foo.key rename to tests/uranium_tests/tls/ssl/signed-foo.key diff --git a/tests/gold_tests/tls/ssl/signed-foo.pem b/tests/uranium_tests/tls/ssl/signed-foo.pem similarity index 100% rename from tests/gold_tests/tls/ssl/signed-foo.pem rename to tests/uranium_tests/tls/ssl/signed-foo.pem diff --git a/tests/gold_tests/tls/ssl/signed-san-ec.key b/tests/uranium_tests/tls/ssl/signed-san-ec.key similarity index 100% rename from tests/gold_tests/tls/ssl/signed-san-ec.key rename to tests/uranium_tests/tls/ssl/signed-san-ec.key diff --git a/tests/gold_tests/tls/ssl/signed-san-ec.pem b/tests/uranium_tests/tls/ssl/signed-san-ec.pem similarity index 100% rename from tests/gold_tests/tls/ssl/signed-san-ec.pem rename to tests/uranium_tests/tls/ssl/signed-san-ec.pem diff --git a/tests/gold_tests/tls/ssl/signed-san.key b/tests/uranium_tests/tls/ssl/signed-san.key similarity index 100% rename from tests/gold_tests/tls/ssl/signed-san.key rename to tests/uranium_tests/tls/ssl/signed-san.key diff --git a/tests/gold_tests/tls/ssl/signed-san.pem b/tests/uranium_tests/tls/ssl/signed-san.pem similarity index 100% rename from tests/gold_tests/tls/ssl/signed-san.pem rename to tests/uranium_tests/tls/ssl/signed-san.pem diff --git a/tests/gold_tests/tls/ssl/signed-wild.key b/tests/uranium_tests/tls/ssl/signed-wild.key similarity index 100% rename from tests/gold_tests/tls/ssl/signed-wild.key rename to tests/uranium_tests/tls/ssl/signed-wild.key diff --git a/tests/gold_tests/tls/ssl/signed-wild.pem b/tests/uranium_tests/tls/ssl/signed-wild.pem similarity index 100% rename from tests/gold_tests/tls/ssl/signed-wild.pem rename to tests/uranium_tests/tls/ssl/signed-wild.pem diff --git a/tests/gold_tests/tls/ssl/signed2-bar.pem b/tests/uranium_tests/tls/ssl/signed2-bar.pem similarity index 100% rename from tests/gold_tests/tls/ssl/signed2-bar.pem rename to tests/uranium_tests/tls/ssl/signed2-bar.pem diff --git a/tests/gold_tests/tls/ssl/signed2-foo.pem b/tests/uranium_tests/tls/ssl/signed2-foo.pem similarity index 100% rename from tests/gold_tests/tls/ssl/signed2-foo.pem rename to tests/uranium_tests/tls/ssl/signed2-foo.pem diff --git a/tests/gold_tests/tls/ssl/signer.key b/tests/uranium_tests/tls/ssl/signer.key similarity index 100% rename from tests/gold_tests/tls/ssl/signer.key rename to tests/uranium_tests/tls/ssl/signer.key diff --git a/tests/gold_tests/tls/ssl/signer.pem b/tests/uranium_tests/tls/ssl/signer.pem similarity index 100% rename from tests/gold_tests/tls/ssl/signer.pem rename to tests/uranium_tests/tls/ssl/signer.pem diff --git a/tests/gold_tests/tls/ssl/signer2.key b/tests/uranium_tests/tls/ssl/signer2.key similarity index 100% rename from tests/gold_tests/tls/ssl/signer2.key rename to tests/uranium_tests/tls/ssl/signer2.key diff --git a/tests/gold_tests/tls/ssl/signer2.pem b/tests/uranium_tests/tls/ssl/signer2.pem similarity index 100% rename from tests/gold_tests/tls/ssl/signer2.pem rename to tests/uranium_tests/tls/ssl/signer2.pem diff --git a/tests/gold_tests/tls/test-0rtt-s_client.py b/tests/uranium_tests/tls/test-0rtt-s_client.py similarity index 100% rename from tests/gold_tests/tls/test-0rtt-s_client.py rename to tests/uranium_tests/tls/test-0rtt-s_client.py diff --git a/tests/gold_tests/tls/test-nc-s_client.sh b/tests/uranium_tests/tls/test-nc-s_client.sh similarity index 100% rename from tests/gold_tests/tls/test-nc-s_client.sh rename to tests/uranium_tests/tls/test-nc-s_client.sh diff --git a/tests/uranium_tests/tls/test_allow_plain.py b/tests/uranium_tests/tls/test_allow_plain.py new file mode 100644 index 00000000000..4ab7062b456 --- /dev/null +++ b/tests/uranium_tests/tls/test_allow_plain.py @@ -0,0 +1,127 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_allow_plain(urtest: UraniumTest) -> None: + ''' + Test the allow-plain attribute of the ssl port + Clients sending non-tls request to ssl port should get passed to + non-tls processing in ATS if the allow-plain attibute is present + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import os + + urtest.Summary = ''' + Test allow-plain attributed + ''' + + urtest.ContinueOnFail = False + + # Define default ATS + ts = urtest.MakeATSProcess("ts", enable_tls=True) + + # ---- + # Setup Origin Server + # ---- + replay_file = "replay/allow-plain.replay.yaml" + server = urtest.MakeVerifierServerProcess("server", replay_file) + + ts.addDefaultSSLFiles() + + ts.Disk.records_config.update( + { + 'proxy.config.http.server_ports': '{0}:ssl:allow-plain'.format(ts.Variables.ssl_port), + 'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir), + 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts.Variables.SSLDir), + 'proxy.config.diags.debug.enabled': 0, + 'proxy.config.diags.debug.tags': 'ssl|http', + }) + + ts.Disk.remap_config.AddLines( + [ + 'map / http://127.0.0.1:{0}'.format(server.Variables.http_port), + 'map /post http://127.0.0.1:{0}/post'.format(server.Variables.http_port), + ]) + + ts.Disk.ssl_multicert_yaml.AddLines( + """ + ssl_multicert: + - dest_ip: "*" + ssl_cert_name: server.pem + ssl_key_name: server.key + """.split("\n")) + + big_post_body = "0123456789" * 50000 + big_post_body_file = open(os.path.join(urtest.RunDirectory, "big_post_body"), "w") + big_post_body_file.write(big_post_body) + big_post_body_file.close() + + # TLS curl should work of course + tr = urtest.AddTestRun() + # Wait for the micro server + tr.Processes.Default.StartBefore(server, ready=When.PortOpen(server.Variables.http_port)) + # Delay on readiness of our ssl ports + tr.Processes.Default.StartBefore(urtest.Processes.ts) + + tr.MakeCurlCommand( + '-o /dev/null -k --verbose -H "uuid: get" --ipv4 --http1.1 --resolve www.example.com:{}:127.0.0.1 https://www.example.com:{}/' + .format(ts.Variables.ssl_port, ts.Variables.ssl_port), + ts=ts) + tr.Processes.Default.ReturnCode = 0 + tr.StillRunningAfter = server + tr.StillRunningAfter = ts + tr.Processes.Default.Streams.all = Testers.ContainsExpression("TLS", "Should negiotiate TLS") + + # non-TLS curl should also work to the same port + tr2 = urtest.AddTestRun() + tr2.MakeCurlCommand( + '--verbose --ipv4 --http1.1 -H "uuid: get" --resolve www.example.com:{}:127.0.0.1 http://www.example.com:{}'.format( + ts.Variables.ssl_port, ts.Variables.ssl_port), + ts=ts) + tr2.Processes.Default.ReturnCode = 0 + tr2.StillRunningAfter = server + tr2.StillRunningAfter = ts + tr2.Processes.Default.Streams.all = Testers.ExcludesExpression("TLS", "Should not negiotiate TLS") + + # Make sure a post > 32K works. Early version forgot to free a reader which caused a stall once the initial buffer filled + # Seems like we needed to make a second resquest to trigger the issue + tr3 = urtest.AddTestRun() + tr3.MakeCurlCommand( + '--verbose -d @big_post_body -H "uuid: post" --ipv4 --http1.1 --resolve www.example.com:{}:127.0.0.1 http://www.example.com:{}/post http://www.example.com:{}/post' + .format(ts.Variables.ssl_port, ts.Variables.ssl_port, ts.Variables.ssl_port), + ts=ts) + tr3.Processes.Default.ReturnCode = 0 + tr3.StillRunningAfter = server + tr3.StillRunningAfter = ts + tr3.Processes.Default.Streams.all = Testers.ExcludesExpression("TLS", "Should not negiotiate TLS") + urtest.execute() diff --git a/tests/uranium_tests/tls/test_exit_on_cert_load_fail.py b/tests/uranium_tests/tls/test_exit_on_cert_load_fail.py new file mode 100644 index 00000000000..2102595b372 --- /dev/null +++ b/tests/uranium_tests/tls/test_exit_on_cert_load_fail.py @@ -0,0 +1,144 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_exit_on_cert_load_fail(urtest: UraniumTest) -> None: + ''' + Test the exit_on_load_fail behavior for the SSL cert loading. + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import os + + urtest.Summary = ''' + Test the exit_on_load_fail behavior for the SSL cert loading. + ''' + + class Test_exit_on_cert_load_fail: + """Configure a test to verify behavior of SSL cert load failures.""" + + ts_counter: int = 0 + + def __init__(self, name: str, is_testing_server_side: bool, enable_exit_on_load=False): + """ + Initialize the test. + :param name: The name of the test. + :param is_testing_server_side: Whether to test the server side or client side cert loading. + enable_exit_on_load: Whether to enable the exit_on_load_fail config. + """ + self.name = name + self.is_testing_server_side = is_testing_server_side + self.enable_exit_on_load = enable_exit_on_load + + def _configure_traffic_server(self, tr: 'TestRun'): + """Configure Traffic Server. + + :param tr: The TestRun object to associate the ts process with. + """ + ts = tr.MakeATSProcess(f"ts-{Test_exit_on_cert_load_fail.ts_counter}", enable_tls=True) + Test_exit_on_cert_load_fail.ts_counter += 1 + self._ts = ts + client_cert_path = 'NULL' + enable_exit_on_server_cert_load_failure = 1 if self.enable_exit_on_load and self.is_testing_server_side else 0 + enable_exit_on_client_cert_load_failure = 1 if self.enable_exit_on_load and not self.is_testing_server_side else 0 + + if not self.is_testing_server_side: + # Point to a non-existent cert to force a load failure. + client_cert_path = f'{ts.Variables.SSLDir}/non-existent-cert.pem' + # Also setup the server certs so that issues are limited to client + # cert loading. + self._ts.addDefaultSSLFiles() + self._ts.Disk.ssl_multicert_yaml.AddLines( + """ + ssl_multicert: + - dest_ip: "*" + ssl_cert_name: server.pem + ssl_key_name: server.key + """.split("\n")) + self._ts.Disk.records_config.update( + { + 'proxy.config.ssl.server.cert.path': f'{ts.Variables.SSLDir}', + 'proxy.config.ssl.server.private_key.path': f'{ts.Variables.SSLDir}', + 'proxy.config.ssl.client.cert.filename': client_cert_path, + 'proxy.config.ssl.server.multicert.exit_on_load_fail': enable_exit_on_server_cert_load_failure, + 'proxy.config.ssl.client.cert.exit_on_load_fail': enable_exit_on_client_cert_load_failure, + }) + # The cert loading happen on startup, so the remap rule is not + # triggered. + RANDOM_PORT = 12345 + self._ts.Disk.remap_config.AddLine(f'map / https://127.0.0.1:{RANDOM_PORT}/') + + def run(self): + """Run the test.""" + tr = urtest.AddTestRun(self.name) + self._configure_traffic_server(tr) + + # The client process can be anything. + tr.Processes.Default.Command = "echo" + tr.Processes.Default.StartAfter(self._ts, ready=When.FileExists(self._ts.Disk.diags_log)) + + # Override the default exclusion of error log. + self._ts.Disk.diags_log.Content = Testers.ContainsExpression("ERROR:", "These tests should have error logs.") + + if self.enable_exit_on_load: + self._ts.ReturnCode = 33 + self._ts.Disk.diags_log.Content += Testers.ContainsExpression( + "EMERGENCY: ", "Failure loading the certs results in an emergency error.") + self._ts.Disk.diags_log.Content += Testers.ExcludesExpression( + "Traffic Server is fully initialized", "Traffic Server should exit upon the load failure.") + else: + self._ts.ReturnCode = 0 + self._ts.Disk.diags_log.Content += Testers.ContainsExpression( + "Traffic Server is fully initialized", "Traffic Server should start up successfully.") + + if self.is_testing_server_side: + self._ts.Disk.diags_log.Content += Testers.ContainsExpression( + "ERROR:.*failed to load", "Verify that there is a cert loading issue.") + else: + self._ts.Disk.diags_log.Content += Testers.ContainsExpression( + "ERROR: failed to access cert", "Verify that there is a cert loading issue.") + self._ts.Disk.diags_log.Content += Testers.ContainsExpression( + "Can't initialize the SSL client, HTTPS in remap rules will not function", + "There should be an error loading the cert.") + + # Test server cert loading. + Test_exit_on_cert_load_fail( + "load server cert with exit on load disabled", is_testing_server_side=True, enable_exit_on_load=False).run() + Test_exit_on_cert_load_fail( + "load server cert with exit on load enabled", is_testing_server_side=True, enable_exit_on_load=True).run() + + # Test client cert loading. + Test_exit_on_cert_load_fail( + "load client cert with exit on load disabled", is_testing_server_side=False, enable_exit_on_load=False).run() + Test_exit_on_cert_load_fail( + "load client cert with exit on load enabled", is_testing_server_side=False, enable_exit_on_load=True).run() + urtest.execute() diff --git a/tests/uranium_tests/tls/test_proxy_protocol_addressless.py b/tests/uranium_tests/tls/test_proxy_protocol_addressless.py new file mode 100644 index 00000000000..e5f075de5cc --- /dev/null +++ b/tests/uranium_tests/tls/test_proxy_protocol_addressless.py @@ -0,0 +1,108 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_proxy_protocol_addressless(urtest: UraniumTest) -> None: + ''' + Verify handling of addressless PROXY protocol headers. + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import sys + + urtest.Summary = ''' + Verify listeners handle addressless PROXY protocol headers. + ''' + + host = 'addressless.proxy.protocol.test' + + def add_addressless_proxy_protocol_run(protocol_version: int, description: str, use_tls: bool) -> None: + """Add a test run for an addressless PROXY header.""" + mode = 'tls' if use_tls else 'http' + ts = urtest.MakeATSProcess( + f'ts_{mode}_v{protocol_version}', enable_tls=use_tls, enable_cache=False, enable_proxy_protocol=True) + server = urtest.MakeOriginServer(f'server_{mode}_v{protocol_version}') + server.ReturnCode = 0 + + request_header = { + "headers": f"GET /proxy_protocol HTTP/1.1\r\nHost: {host}\r\nConnection: close\r\n\r\n", + "timestamp": "1469733493.993", + "body": "" + } + response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": "ok"} + server.addResponse("sessionlog.json", request_header, response_header) + + if use_tls: + ts.addDefaultSSLFiles() + ts.Disk.ssl_multicert_yaml.AddLines( + """ + ssl_multicert: + - dest_ip: "*" + ssl_cert_name: server.pem + ssl_key_name: server.key + """.split("\n")) + + ts.Disk.remap_config.AddLine(f'map / http://127.0.0.1:{server.Variables.Port}/') + records_config = { + 'proxy.config.http.proxy_protocol_allowlist': '127.0.0.1', + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'proxyprotocol', + } + if use_tls: + records_config.update( + { + 'proxy.config.ssl.server.cert.path': ts.Variables.SSLDir, + 'proxy.config.ssl.server.private_key.path': ts.Variables.SSLDir, + }) + ts.Disk.records_config.update(records_config) + + tr = urtest.AddTestRun(description) + tr.TimeOut = 10 + tr.Setup.Copy('proxy_protocol_client.py') + port = ts.Variables.proxy_protocol_ssl_port if use_tls else ts.Variables.proxy_protocol_port + tr.Processes.Default.Command = ( + f'{sys.executable} proxy_protocol_client.py 127.0.0.1 {port} {host} ' + f'127.0.0.1 127.0.0.1 60123 {server.Variables.Port} ' + f'{protocol_version} --addressless') + if use_tls: + tr.Processes.Default.Command += ' --https' + tr.Processes.Default.StartBefore(server) + tr.Processes.Default.StartBefore(ts) + tr.ReturnCode = 0 + tr.Processes.Default.Streams.All += Testers.ContainsExpression( + "HTTP/1.1 200 OK", "Verify a successful response is received") + + add_addressless_proxy_protocol_run(1, 'PROXY v1 UNKNOWN before HTTP/1', use_tls=False) + add_addressless_proxy_protocol_run(2, 'PROXY v2 LOCAL before HTTP/1', use_tls=False) + add_addressless_proxy_protocol_run(1, 'PROXY v1 UNKNOWN before TLS', use_tls=True) + add_addressless_proxy_protocol_run(2, 'PROXY v2 LOCAL before TLS', use_tls=True) + urtest.execute() diff --git a/tests/uranium_tests/tls/test_ssl_key_dialog.py b/tests/uranium_tests/tls/test_ssl_key_dialog.py new file mode 100644 index 00000000000..60282890b2f --- /dev/null +++ b/tests/uranium_tests/tls/test_ssl_key_dialog.py @@ -0,0 +1,120 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_ssl_key_dialog(urtest: UraniumTest) -> None: + ''' + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding right ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + urtest.Summary = ''' + Test that Trafficserver starts with default configurations. + ''' + + ts = urtest.MakeATSProcess("ts", enable_tls=True) + server = urtest.MakeOriginServer("server") + + ts.addSSLfile("ssl/passphrase.pem") + ts.addSSLfile("ssl/passphrase.key") + + ts.addSSLfile("ssl/passphrase2.pem") + ts.addSSLfile("ssl/passphrase2.key") + + ts.Disk.remap_config.AddLine(f"map https://passphrase:{ts.Variables.ssl_port}/ http://127.0.0.1:{server.Variables.Port}") + ts.Disk.remap_config.AddLine(f"map https://passphrase2:{ts.Variables.ssl_port}/ http://127.0.0.1:{server.Variables.Port}") + ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'ssl_load|http', + 'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir), + 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts.Variables.SSLDir), + }) + + ts.Disk.ssl_multicert_yaml.AddLines( + """ + ssl_multicert: + - dest_ip: "*" + ssl_cert_name: passphrase.pem + ssl_key_name: passphrase.key + ssl_key_dialog: "exec:/bin/bash -c 'echo -n passphrase'" + """.split("\n")) + + request_header = {"headers": "GET / HTTP/1.1\r\nHost: bogus\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": "success!"} + server.addResponse("sessionlog.json", request_header, response_header) + + tr = urtest.AddTestRun("use a key with passphrase") + tr.Setup.Copy("ssl/signer.pem") + tr.MakeCurlCommand( + f"-v --cacert ./signer.pem --resolve 'passphrase:{ts.Variables.ssl_port}:127.0.0.1' https://passphrase:{ts.Variables.ssl_port}/", + ts=ts) + tr.ReturnCode = 0 + tr.Processes.Default.StartBefore(server) + tr.Processes.Default.StartBefore(urtest.Processes.ts) + tr.Processes.Default.Streams.stderr.Content = Testers.ContainsExpression("200", "expected 200 OK response") + tr.Processes.Default.Streams.stdout.Content = Testers.ContainsExpression("success!", "expected success") + tr.StillRunningAfter = server + tr.StillRunningAfter = ts + + tr2 = urtest.AddTestRun("Update config files") + # Update the multicert config + sslcertpath = ts.Disk.ssl_multicert_yaml.AbsPath + tr2.Disk.File(sslcertpath, id="ssl_multicert_config", typename="ats:config"), + tr2.Disk.ssl_multicert_config.AddLines( + """ + ssl_multicert: + - dest_ip: "*" + ssl_cert_name: passphrase2.pem + ssl_key_name: passphrase2.key + ssl_key_dialog: "exec:/bin/bash -c 'echo -n passphrase'" + """.split("\n")) + tr2.StillRunningAfter = ts + tr2.StillRunningAfter = server + tr2.Processes.Default.Command = 'echo Updated configs' + # Need to copy over the environment so traffic_ctl knows where to find the unix domain socket + tr2.Processes.Default.Env = ts.Env + tr2.Processes.Default.ReturnCode = 0 + + tr2reload = urtest.AddConfigReload(ts, expect_tasks=["ssl_multicert.yaml"], description="Reload config") + tr2reload.StillRunningAfter = server + + tr3 = urtest.AddTestRun("use a key with passphrase") + tr3.Setup.Copy("ssl/signer.pem") + tr3.MakeCurlCommand( + f"-v --cacert ./signer.pem --resolve 'passphrase2:{ts.Variables.ssl_port}:127.0.0.1' https://passphrase2:{ts.Variables.ssl_port}/", + ts=ts) + tr3.ReturnCode = 0 + tr3.Processes.Default.Streams.stderr.Content = Testers.ContainsExpression("200", "expected 200 OK response") + tr3.Processes.Default.Streams.stdout.Content = Testers.ContainsExpression("success!", "expected success") + tr3.StillRunningAfter = server + tr3.StillRunningAfter = ts + urtest.execute() diff --git a/tests/uranium_tests/tls/test_ssl_multicert_loader.py b/tests/uranium_tests/tls/test_ssl_multicert_loader.py new file mode 100644 index 00000000000..13f5a022270 --- /dev/null +++ b/tests/uranium_tests/tls/test_ssl_multicert_loader.py @@ -0,0 +1,186 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_ssl_multicert_loader(urtest: UraniumTest) -> None: + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + urtest.Summary = ''' + Test reloading ssl_multicert.yaml with errors and keeping around the old ssl config structure + ''' + + sni_domain = 'example.com' + + ts = urtest.MakeATSProcess("ts", enable_tls=True, disable_log_checks=True) + server = urtest.MakeOriginServer("server") + server2 = urtest.MakeOriginServer("server2") + request_header = {"headers": f"GET / HTTP/1.1\r\nHost: {sni_domain}\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + + response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + server.addResponse("sessionlog.json", request_header, response_header) + + ts.Disk.records_config.update( + { + 'proxy.config.ssl.server.cert.path': f'{ts.Variables.SSLDir}', + 'proxy.config.ssl.server.private_key.path': f'{ts.Variables.SSLDir}', + 'proxy.config.ssl.server.multicert.exit_on_load_fail': 0, + }) + + ts.addDefaultSSLFiles() + + ts.Disk.remap_config.AddLine(f'map / http://127.0.0.1:{server.Variables.Port}') + + ts.Disk.ssl_multicert_yaml.AddLines( + """ + ssl_multicert: + - dest_ip: "*" + ssl_cert_name: server.pem + ssl_key_name: server.key + """.split("\n")) + + tr = urtest.AddTestRun("ensure we can connect for SNI $sni_domain") + tr.Processes.Default.StartBefore(urtest.Processes.ts) + tr.Processes.Default.StartBefore(server) + tr.StillRunningAfter = ts + tr.StillRunningAfter = server + tr.MakeCurlCommand( + f"-q -s -v -k --resolve '{sni_domain}:{ts.Variables.ssl_port}:127.0.0.1' https://{sni_domain}:{ts.Variables.ssl_port}", + ts=ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.stdout = Testers.ExcludesExpression("Could Not Connect", "Check response") + tr.Processes.Default.Streams.stderr = Testers.IncludesExpression(f"CN={sni_domain}", "Check response") + + tr2 = urtest.AddTestRun("Update config files") + # Update the configs - overwrite the ssl_multicert.yaml file with an invalid config + sslcertpath = ts.Disk.ssl_multicert_yaml.AbsPath + + tr2.Disk.File(sslcertpath, id="ssl_multicert_update", typename="ats:config") + tr2.Disk.ssl_multicert_update.AddLines( + """ + ssl_multicert: + - ssl_cert_name: server_does_not_exist.pem + ssl_key_name: server_does_not_exist.key + - dest_ip: "*" + ssl_cert_name: server.pem_doesnotexist + ssl_key_name: server.key + """.split("\n")) + tr2.StillRunningAfter = ts + tr2.StillRunningAfter = server + tr2.Processes.Default.Command = 'echo Updated configs' + tr2.Processes.Default.Env = ts.Env + tr2.Processes.Default.ReturnCode = 0 + + tr2reload = urtest.AddConfigReload(ts, expect="fail", expect_tasks=["ssl_multicert.yaml"], description="Reload config") + tr2reload.StillRunningAfter = server + + # Reload of ssl_multicert.yaml should fail, BUT the old config structure + # should be in place to successfully answer for the test domain + tr3 = urtest.AddTestRun("Make request again for $sni_domain") + tr3.Processes.Default.StartBefore(server2) + tr3.StillRunningAfter = ts + tr3.StillRunningAfter = server + tr3.MakeCurlCommand( + f"-q -s -v -k --resolve '{sni_domain}:{ts.Variables.ssl_port}:127.0.0.1' https://{sni_domain}:{ts.Variables.ssl_port}", + ts=ts) + tr3.Processes.Default.ReturnCode = 0 + tr3.Processes.Default.Streams.stdout = Testers.ExcludesExpression("Could Not Connect", "Check response") + tr3.Processes.Default.Streams.stderr = Testers.IncludesExpression(f"CN={sni_domain}", "Check response") + ts.Disk.diags_log.Content = Testers.ExcludesExpression( + r'\(quic\).*ssl_multicert', 'QUIC certificates should not load without a configured QUIC listener') + + ########################################################################## + # Ensure ATS fails/exits when non-existent cert is specified + # Also, not explicitly setting proxy.config.ssl.server.multicert.exit_on_load_fail + # to catch if the current default (1) changes in the future + + ts2 = urtest.MakeATSProcess("ts2", enable_tls=True) + ts2.Disk.ssl_multicert_yaml.AddLines( + """ + ssl_multicert: + - dest_ip: "*" + ssl_cert_name: server.pem_doesnotexist + ssl_key_name: server.key + """.split("\n")) + + tr4 = urtest.AddTestRun() + tr4.Processes.Default.Command = 'echo Waiting' + tr4.Processes.Default.ReturnCode = 0 + tr4.Processes.Default.StartBefore(ts2) + + ts2.ReturnCode = 33 # ink_emergency will exit with UNRECOVERABLE_EXIT. + ts2.Ready = 0 # Need this to be 0 because we are testing shutdown, this is to make autest not think ats went away for a bad reason. + ts2.Disk.traffic_out.Content = Testers.ExcludesExpression( + 'Traffic Server is fully initialized', 'process should fail when invalid certificate specified') + ts2.Disk.diags_log.Content = Testers.IncludesExpression('EMERGENCY: failed to load SSL certificate file', 'check diags.log"') + + ########################################################################## + # Verify parallel cert loading on startup (firstLoad uses hardware_concurrency, + # not the configured concurrency value, so the thread count is host-dependent) + + ts3 = urtest.MakeATSProcess("ts3", enable_tls=True) + server3 = urtest.MakeOriginServer("server3") + server3.addResponse("sessionlog.json", request_header, response_header) + + ts3.Disk.records_config.update( + { + 'proxy.config.ssl.server.cert.path': f'{ts3.Variables.SSLDir}', + 'proxy.config.ssl.server.private_key.path': f'{ts3.Variables.SSLDir}', + }) + + ts3.addDefaultSSLFiles() + + ts3.Disk.remap_config.AddLine(f'map / http://127.0.0.1:{server3.Variables.Port}') + + # Need at least 2 certs for multi-threading to kick in + ts3.Disk.ssl_multicert_yaml.AddLines( + """ + ssl_multicert: + - dest_ip: "*" + ssl_cert_name: server.pem + ssl_key_name: server.key + - ssl_cert_name: server.pem + ssl_key_name: server.key + """.split("\n")) + + tr5 = urtest.AddTestRun("Verify parallel cert loading") + tr5.Processes.Default.StartBefore(ts3) + tr5.Processes.Default.StartBefore(server3) + tr5.StillRunningAfter = ts3 + tr5.StillRunningAfter = server3 + tr5.MakeCurlCommand( + f"-q -s -v -k --resolve '{sni_domain}:{ts3.Variables.ssl_port}:127.0.0.1' https://{sni_domain}:{ts3.Variables.ssl_port}", + ts=ts3) + tr5.Processes.Default.ReturnCode = 0 + tr5.Processes.Default.Streams.stdout = Testers.ExcludesExpression("Could Not Connect", "Check response") + tr5.Processes.Default.Streams.stderr = Testers.IncludesExpression(f"CN={sni_domain}", "Check response") + ts3.Disk.diags_log.Content = Testers.IncludesExpression('loaded 2 certs', 'verify certs were loaded successfully') + urtest.execute() diff --git a/tests/uranium_tests/tls/test_tls.py b/tests/uranium_tests/tls/test_tls.py new file mode 100644 index 00000000000..89629c78859 --- /dev/null +++ b/tests/uranium_tests/tls/test_tls.py @@ -0,0 +1,111 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_tls(urtest: UraniumTest) -> None: + ''' + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import os + + urtest.Summary = ''' + Test tls + ''' + + # Define default ATS + ts = urtest.MakeATSProcess("ts", enable_tls=True) + server = urtest.MakeOriginServer("server") + + # ssl-post is built via `make`. Here we copy the built binary down to the test + # directory so that the test runs in this file can use it. + urtest.Setup.Copy(os.path.join(urtest.Variables.AtsBuildUraniumTestsDir, 'tls', 'ssl-post')) + + requestLocation = "test2" + reHost = "www.example.com" + + testName = "" + + header_count = 378 + + header_string = "POST /post HTTP/1.1\r\nHost: www.example.com\r\nContent-Length:1000\r\n" + + for i in range(0, header_count): + header_string = "{1}header{0}:{0}\r\n".format(i, header_string) + header_string = "{0}\r\n".format(header_string) + + post_body = "" + for i in range(0, 1000): + post_body = "{0}0".format(post_body) + + # Add info the origin server responses + server.addResponse( + "sessionlog.json", { + "headers": header_string, + "timestamp": "1469733493.993", + "body": post_body + }, { + "headers": + "HTTP/1.1 200 OK\r\nServer: microserver\r\nConnection: close\r\nCache-Control: max-age=3600\r\nContent-Length: 2\r\n\r\n", + "timestamp": "1469733493.993", + "body": "ok" + }) + + # add ssl materials like key, certificates for the server + ts.addDefaultSSLFiles() + + ts.Disk.remap_config.AddLine('map / http://127.0.0.1:{0}'.format(server.Variables.Port)) + + ts.Disk.ssl_multicert_yaml.AddLines( + """ + ssl_multicert: + - dest_ip: "*" + ssl_cert_name: server.pem + ssl_key_name: server.key + """.split("\n")) + ts.Disk.records_config.update( + { + 'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir), + 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts.Variables.SSLDir), + 'proxy.config.exec_thread.autoconfig.scale': 1.0, + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'ssl', + }) + + tr = urtest.AddTestRun("Run-Test") + tr.Command = './ssl-post 127.0.0.1 40 {0} {1}'.format(header_count, ts.Variables.ssl_port) + tr.ReturnCode = 0 + tr.Processes.Default.StartBefore(server) + tr.Processes.Default.StartBefore(urtest.Processes.ts) + tr.Processes.Default.Streams.stdout = "gold/ssl-post.gold" + tr.StillRunningAfter = server + urtest.execute() diff --git a/tests/uranium_tests/tls/test_tls_0rtt_server.py b/tests/uranium_tests/tls/test_tls_0rtt_server.py new file mode 100644 index 00000000000..23fc282cd1f --- /dev/null +++ b/tests/uranium_tests/tls/test_tls_0rtt_server.py @@ -0,0 +1,290 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_tls_0rtt_server(urtest: UraniumTest) -> None: + ''' + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import sys + + urtest.Summary = ''' + Test ATS TLSv1.3 0-RTT support + ''' + + # Checking only OpenSSL version allows you to run this test with BoringSSL (and it should pass). + urtest.SkipUnless(Condition.HasOpenSSLVersion('1.1.1'),) + + ts1 = urtest.MakeATSProcess('ts1', enable_tls=True) + ts2 = urtest.MakeATSProcess('ts2', enable_tls=True) + server = urtest.MakeOriginServer('server') + + request_header1 = {'headers': 'GET / HTTP/1.1\r\nHost: www.example.com\r\n\r\n', 'timestamp': '1469733493.993', 'body': ''} + response_header1 = { + 'headers': 'HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n', + 'timestamp': '1469733493.993', + 'body': 'curl test' + } + request_header2 = { + 'headers': 'GET /early_get HTTP/1.1\r\nHost: www.example.com\r\n\r\n', + 'timestamp': '1469733493.993', + 'body': '' + } + response_header2 = { + 'headers': 'HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n', + 'timestamp': '1469733493.993', + 'body': 'early data accepted' + } + request_header3 = { + 'headers': 'POST /early_post HTTP/1.1\r\nHost: www.example.com\r\nContent-Length: 11\r\n\r\n', + 'timestamp': '1469733493.993', + 'body': 'knock knock' + } + response_header3 = { + 'headers': 'HTTP/1.1 200 OK\r\nServer: uServer\r\nConnection: close\r\nTransfer-Encoding: chunked\r\n\r\n', + 'timestamp': '1415926535.898', + 'body': '' + } + request_header4 = { + 'headers': 'GET /early_multi_1 HTTP/1.1\r\nHost: www.example.com\r\n\r\n', + 'timestamp': '1469733493.993', + 'body': '' + } + response_header4 = { + 'headers': 'HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n', + 'timestamp': '1469733493.993', + 'body': 'early data accepted multi_1' + } + request_header5 = { + 'headers': 'GET /early_multi_2 HTTP/1.1\r\nHost: www.example.com\r\n\r\n', + 'timestamp': '1469733493.993', + 'body': '' + } + response_header5 = { + 'headers': 'HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n', + 'timestamp': '1469733493.993', + 'body': 'early data accepted multi_2' + } + request_header6 = { + 'headers': 'GET /early_multi_3 HTTP/1.1\r\nHost: www.example.com\r\n\r\n', + 'timestamp': '1469733493.993', + 'body': '' + } + response_header6 = { + 'headers': 'HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n', + 'timestamp': '1469733493.993', + 'body': 'early data accepted multi_3' + } + server.addResponse('sessionlog.json', request_header1, response_header1) + server.addResponse('sessionlog.json', request_header2, response_header2) + server.addResponse('sessionlog.json', request_header3, response_header3) + server.addResponse('sessionlog.json', request_header4, response_header4) + server.addResponse('sessionlog.json', request_header5, response_header5) + server.addResponse('sessionlog.json', request_header6, response_header6) + + ts1.addSSLfile('ssl/server.pem') + ts1.addSSLfile('ssl/server.key') + + ts1.Setup.Copy('test-0rtt-s_client.py') + ts1.Setup.Copy('h2_early_decode.py') + ts1.Setup.Copy('early_h1_get.txt') + ts1.Setup.Copy('early_h1_post.txt') + ts1.Setup.Copy('early_h2_get.txt') + ts1.Setup.Copy('early_h2_post.txt') + ts1.Setup.Copy('early_h2_multi1.txt') + ts1.Setup.Copy('early_h2_multi2.txt') + + ts1.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'http|ssl_early_data|ssl', + 'proxy.config.exec_thread.autoconfig.enabled': 0, + 'proxy.config.exec_thread.limit': 8, + 'proxy.config.ssl.server.cert.path': '{0}'.format(ts1.Variables.SSLDir), + 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts1.Variables.SSLDir), + 'proxy.config.ssl.server.session_ticket.enable': 1, + 'proxy.config.ssl.server.max_early_data': 16384, + 'proxy.config.ssl.server.allow_early_data_params': 0, + 'proxy.config.ssl.server.cipher_suite': + 'ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-DSS-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-DSS-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA256:DHE-RSA-AES128-SHA256:DHE-DSS-AES128-SHA256:DHE-RSA-AES256-SHA:DHE-DSS-AES256-SHA:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!aECDH:!EDH-DSS-DES-CBC3-SHA:!EDH-RSA-DES-CBC3-SHA:!KRB5-DES-CBC3-SHA' + }) + + ts1.Disk.ssl_multicert_yaml.AddLines( + """ + ssl_multicert: + - dest_ip: "*" + ssl_cert_name: server.pem + ssl_key_name: server.key + """.split("\n")) + + ts1.Disk.remap_config.AddLine('map / http://127.0.0.1:{0}'.format(server.Variables.Port)) + + ts1.Disk.sni_yaml.AddLines([ + 'sni:', + '- fqdn: example-no.com', + ' server_max_early_data: 0', + ]) + + ts2.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'http|ssl_early_data|ssl', + 'proxy.config.exec_thread.autoconfig.enabled': 0, + 'proxy.config.exec_thread.limit': 8, + 'proxy.config.ssl.server.cert.path': '{0}'.format(ts1.Variables.SSLDir), + 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts1.Variables.SSLDir), + 'proxy.config.ssl.server.session_ticket.enable': 1, + 'proxy.config.ssl.server.max_early_data': 0, + 'proxy.config.ssl.server.allow_early_data_params': 0, + 'proxy.config.ssl.server.cipher_suite': + 'ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-DSS-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-DSS-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA256:DHE-RSA-AES128-SHA256:DHE-DSS-AES128-SHA256:DHE-RSA-AES256-SHA:DHE-DSS-AES256-SHA:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!aECDH:!EDH-DSS-DES-CBC3-SHA:!EDH-RSA-DES-CBC3-SHA:!KRB5-DES-CBC3-SHA' + }) + + ts2.Disk.ssl_multicert_yaml.AddLines( + """ + ssl_multicert: + - dest_ip: "*" + ssl_cert_name: server.pem + ssl_key_name: server.key + """.split("\n")) + + ts2.Disk.remap_config.AddLine('map / http://127.0.0.1:{0}'.format(server.Variables.Port)) + + ts2.Disk.sni_yaml.AddLines([ + 'sni:', + '- fqdn: example-yes.com', + ' server_max_early_data: 16384', + ]) + + tr = urtest.AddTestRun('Basic Curl Test') + tr.MakeCurlCommand('-k --resolve example.com:{0}:127.0.0.1 https://example.com:{0}'.format(ts1.Variables.ssl_port), ts=ts1) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.StartBefore(server) + tr.Processes.Default.StartBefore(ts1) + tr.Processes.Default.Streams.All = Testers.ContainsExpression('curl test', 'Making sure the basics still work') + tr.Processes.Default.Streams.All += Testers.ExcludesExpression('early data accepted', '') + tr.StillRunningAfter = server + tr.StillRunningAfter += ts1 + + tr = urtest.AddTestRun('TLSv1.3 0-RTT Support (HTTP/1.1 GET)') + tr.Processes.Default.Command = f'{sys.executable} {urtest.RunDirectory}/test-0rtt-s_client.py -p {ts1.Variables.ssl_port} -v h1 -t get -r {urtest.RunDirectory}' + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.All = Testers.ContainsExpression('early data accepted', '') + tr.Processes.Default.Streams.All += Testers.ExcludesExpression('curl test', '') + tr.StillRunningAfter = server + tr.StillRunningAfter += ts1 + + tr = urtest.AddTestRun('TLSv1.3 0-RTT Support (HTTP/1.1 POST)') + tr.Processes.Default.Command = f'{sys.executable} {urtest.RunDirectory}/test-0rtt-s_client.py -p {ts1.Variables.ssl_port} -v h1 -t post -r {urtest.RunDirectory}' + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.All = Testers.ContainsExpression('HTTP/1.1 425 Too Early', '') + tr.Processes.Default.Streams.All += Testers.ExcludesExpression('curl test', '') + tr.Processes.Default.Streams.All += Testers.ExcludesExpression('early data accepted', '') + tr.StillRunningAfter = server + tr.StillRunningAfter += ts1 + + tr = urtest.AddTestRun('TLSv1.3 0-RTT Support (HTTP/2 GET)') + tr.Processes.Default.Command = f'{sys.executable} {urtest.RunDirectory}/test-0rtt-s_client.py -p {ts1.Variables.ssl_port} -v h2 -t get -r {urtest.RunDirectory}' + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.All = Testers.ContainsExpression('early data accepted', '') + tr.Processes.Default.Streams.All += Testers.ExcludesExpression('curl test', '') + tr.StillRunningAfter = server + tr.StillRunningAfter += ts1 + + tr = urtest.AddTestRun('TLSv1.3 0-RTT Support (HTTP/2 POST)') + tr.Processes.Default.Command = f'{sys.executable} {urtest.RunDirectory}/test-0rtt-s_client.py -p {ts1.Variables.ssl_port} -v h2 -t post -r {urtest.RunDirectory}' + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.All = Testers.ContainsExpression(':status 425', 'Only safe methods are allowed') + tr.Processes.Default.Streams.All += Testers.ExcludesExpression('curl test', '') + tr.Processes.Default.Streams.All += Testers.ExcludesExpression('early data accepted', '') + tr.StillRunningAfter = server + tr.StillRunningAfter += ts1 + + tr = urtest.AddTestRun('TLSv1.3 0-RTT Support (HTTP/2 Multiplex)') + tr.Processes.Default.Command = f'{sys.executable} {urtest.RunDirectory}/test-0rtt-s_client.py -p {ts1.Variables.ssl_port} -v h2 -t multi1 -r {urtest.RunDirectory}' + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.All = Testers.ContainsExpression('early data accepted multi_1', '') + tr.Processes.Default.Streams.All += Testers.ContainsExpression('early data accepted multi_2', '') + tr.Processes.Default.Streams.All += Testers.ContainsExpression('early data accepted multi_3', '') + tr.Processes.Default.Streams.All += Testers.ExcludesExpression('curl test', '') + tr.StillRunningAfter = server + tr.StillRunningAfter += ts1 + + tr = urtest.AddTestRun('TLSv1.3 0-RTT Support (HTTP/2 Multiplex with POST)') + tr.Processes.Default.Command = f'{sys.executable} {urtest.RunDirectory}/test-0rtt-s_client.py -p {ts1.Variables.ssl_port} -v h2 -t multi2 -r {urtest.RunDirectory}' + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.All = Testers.ContainsExpression('early data accepted multi_1', '') + tr.Processes.Default.Streams.All += Testers.ContainsExpression(':status 425', 'Only safe methods are allowed') + tr.Processes.Default.Streams.All += Testers.ContainsExpression('early data accepted multi_3', '') + tr.Processes.Default.Streams.All += Testers.ExcludesExpression('curl test', '') + tr.StillRunningAfter = server + tr.StillRunningAfter += ts1 + + tr = urtest.AddTestRun('TLSv1.3 0-RTT Support (HTTP/1.1 GET) SNI Provided') + tr.Processes.Default.Command = f'{sys.executable} {urtest.RunDirectory}/test-0rtt-s_client.py -p {ts1.Variables.ssl_port} -v h1 -t get -r {urtest.RunDirectory} -s example.com' + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.All = Testers.ContainsExpression('early data accepted', '') + tr.Processes.Default.Streams.All += Testers.ExcludesExpression('curl test', '') + tr.StillRunningAfter = server + tr.StillRunningAfter += ts1 + + tr = urtest.AddTestRun('TLSv1.3 0-RTT Support (HTTP/1.1 GET) Disabled By SNI Config') + tr.Processes.Default.Command = f'{sys.executable} {urtest.RunDirectory}/test-0rtt-s_client.py -p {ts1.Variables.ssl_port} -v h1 -t get -r {urtest.RunDirectory} -s example-no.com' + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.All = Testers.ExcludesExpression('early data accepted', '') + tr.Processes.Default.Streams.All += Testers.ExcludesExpression('curl test', '') + tr.StillRunningAfter = server + + tr = urtest.AddTestRun('TLSv1.3 0-RTT Support (HTTP/1.1 GET) Disabled In General') + tr.Processes.Default.Command = f'{sys.executable} {urtest.RunDirectory}/test-0rtt-s_client.py -p {ts2.Variables.ssl_port} -v h1 -t get -r {urtest.RunDirectory}' + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.StartBefore(ts2) + tr.Processes.Default.Streams.All = Testers.ExcludesExpression('early data accepted', '') + tr.Processes.Default.Streams.All += Testers.ExcludesExpression('curl test', '') + tr.StillRunningAfter = server + tr.StillRunningAfter += ts2 + + tr = urtest.AddTestRun('TLSv1.3 0-RTT Support (HTTP/1.1 GET) Disabled In General SNI Provided') + tr.Processes.Default.Command = f'{sys.executable} {urtest.RunDirectory}/test-0rtt-s_client.py -p {ts2.Variables.ssl_port} -v h1 -t get -r {urtest.RunDirectory} -s example.com' + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.All = Testers.ExcludesExpression('early data accepted', '') + tr.Processes.Default.Streams.All += Testers.ExcludesExpression('curl test', '') + tr.StillRunningAfter = server + tr.StillRunningAfter += ts2 + + tr = urtest.AddTestRun('TLSv1.3 0-RTT Support (HTTP/1.1 GET) Enabled By SNI Config') + tr.Processes.Default.Command = f'{sys.executable} {urtest.RunDirectory}/test-0rtt-s_client.py -p {ts2.Variables.ssl_port} -v h1 -t get -r {urtest.RunDirectory} -s example-yes.com' + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.All = Testers.ContainsExpression('early data accepted', '') + tr.Processes.Default.Streams.All += Testers.ExcludesExpression('curl test', '') + urtest.execute() diff --git a/tests/uranium_tests/tls/test_tls_accept_timeout_crash.py b/tests/uranium_tests/tls/test_tls_accept_timeout_crash.py new file mode 100644 index 00000000000..6cf2ea89b24 --- /dev/null +++ b/tests/uranium_tests/tls/test_tls_accept_timeout_crash.py @@ -0,0 +1,102 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_tls_accept_timeout_crash(urtest: UraniumTest) -> None: + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + ''' + Regression test: stale TLS accept-timeout events must not crash the session acceptors. + + With a pending TS_HTTP_SSN_START_HOOK whose callout defers, a stale TLS + handshake or accept-no-activity inactivity timer could fire after the read VIO + had been reassigned to a session acceptor but before the session took + ownership, triggering a release_assert in the acceptor's mainEvent and aborting + traffic_server. + + Reproduction: set the TLS handshake and accept-no-activity timeouts to 1 + second, load a plugin that delays the SSN_START hook by 3 seconds, then drive + TLS requests over HTTP/1.1 and HTTP/2. Before the fix, traffic_server aborts + during the delay window. After the fix, the client receives normal 200 + responses. + ''' + + import os + + urtest.Summary = 'TLS accept timeout must not crash HttpSessionAccept when SSN_START hook defers.' + + replay_file = "tls_accept_timeout_crash.replay.yaml" + + server = urtest.MakeVerifierServerProcess("server", replay_file) + + ts = urtest.MakeATSProcess("ts", enable_tls=True, enable_cache=False) + ts.addDefaultSSLFiles() + + ts.Disk.records_config.update( + { + 'proxy.config.ssl.server.cert.path': f'{ts.Variables.SSLDir}', + 'proxy.config.ssl.server.private_key.path': f'{ts.Variables.SSLDir}', + 'proxy.config.ssl.handshake_timeout_in': 1, + 'proxy.config.http.accept_no_activity_timeout': 1, + 'proxy.config.http2.accept_no_activity_timeout': 1, + 'proxy.config.url_remap.remap_required': 0, + }) + + ts.Disk.ssl_multicert_yaml.AddLines( + """ + ssl_multicert: + - dest_ip: "*" + ssl_cert_name: server.pem + ssl_key_name: server.key + """.split("\n")) + + ts.Disk.remap_config.AddLine(f'map / http://127.0.0.1:{server.Variables.http_port}/') + + # Delay TS_HTTP_SSN_START_HOOK reenable by 3s — longer than both + # handshake_timeout_in and accept_no_activity_timeout (1s each), shorter than + # the verifier's 5s read timeout. Either timer firing during the hook window + # lands a VC_EVENT_INACTIVITY_TIMEOUT on the session acceptor's VIO cont. + urtest.PrepareTestPlugin(os.path.join(urtest.Variables.AtsTestPluginsDir, 'hook_add_plugin.so'), ts, '-delay-ms=3000') + + # Assert traffic_server never hits the release_assert landmine in the session + # acceptors when an inactivity timer fires during the SSN_START hook window. + ts.Disk.traffic_out.Content += Testers.ExcludesExpression( + r'failed assertion `event == NET_EVENT_ACCEPT', 'session acceptors must not abort on stale inactivity timeouts') + ts.Disk.diags_log.Content += Testers.ExcludesExpression( + r'FATAL.*Assertion', 'traffic_server must not abort on a stale accept-path inactivity timeout') + + tr = urtest.AddTestRun("TLS request through delayed SSN_START hook") + tr.Processes.Default.StartBefore(server) + tr.Processes.Default.StartBefore(ts) + tr.AddVerifierClientProcess("client", replay_file, https_ports=[ts.Variables.ssl_port]) + tr.StillRunningAfter = ts + tr.StillRunningAfter = server + urtest.execute() diff --git a/tests/uranium_tests/tls/test_tls_async_handshake.py b/tests/uranium_tests/tls/test_tls_async_handshake.py new file mode 100644 index 00000000000..aa870ecabba --- /dev/null +++ b/tests/uranium_tests/tls/test_tls_async_handshake.py @@ -0,0 +1,103 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_tls_async_handshake(urtest: UraniumTest) -> None: + ''' + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import os + + urtest.Summary = ''' + Test TLS handshakes through OpenSSL's async job interface. + ''' + + async_handshake = os.path.join(urtest.Variables.AtsTestPluginsDir, 'async_handshake.so') + + urtest.SkipUnless( + Condition.HasOpenSSLVersion('1.1.1'), + Condition.IsOpenSSL(), + Condition(lambda: os.path.isfile(async_handshake), async_handshake + " not found."), + ) + + ts = urtest.MakeATSProcess("ts", enable_tls=True) + server = urtest.MakeOriginServer("server") + + if os.path.isfile(async_handshake): + urtest.PrepareTestPlugin(async_handshake, ts) + + server.addResponse( + "sessionlog.json", { + "headers": "GET / HTTP/1.1\r\nuuid: basic\r\n\r\n", + "timestamp": "1469733493.993", + "body": "" + }, { + "headers": + "HTTP/1.1 200 OK\r\nServer: microserver\r\nConnection: close\r\n" + "Cache-Control: max-age=3600\r\nContent-Length: 2\r\n\r\n", + "timestamp": "1469733493.993", + "body": "ok" + }) + + ts.addSSLfile("ssl/server.pem") + ts.addSSLfile("ssl/server.key") + + ts.Disk.remap_config.AddLine('map / http://127.0.0.1:{0}'.format(server.Variables.Port)) + + ts.Disk.ssl_multicert_yaml.AddLines( + """ + ssl_multicert: + - dest_ip: "*" + ssl_cert_name: server.pem + ssl_key_name: server.key + """.split("\n")) + ts.Disk.records_config.update( + { + 'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir), + 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts.Variables.SSLDir), + 'proxy.config.exec_thread.autoconfig.scale': 1.0, + 'proxy.config.ssl.async.handshake.enabled': 1, + 'proxy.config.diags.debug.enabled': 0, + 'proxy.config.diags.debug.tags': 'ssl|http' + }) + + tr = urtest.AddTestRun("Run-Test") + tr.MakeCurlCommand("-k -v -H uuid:basic -H host:example.com https://127.0.0.1:{0}/".format(ts.Variables.ssl_port), ts=ts) + tr.ReturnCode = 0 + tr.Processes.Default.StartBefore(server) + tr.Processes.Default.StartBefore(urtest.Processes.ts, ready=When.PortOpen(ts.Variables.ssl_port)) + tr.Processes.Default.Streams.All = Testers.ContainsExpression(r"HTTP/(2|1\.1) 200", "Request succeeds") + tr.StillRunningAfter = server + + ts.Disk.traffic_out.Content += Testers.ContainsExpression("resumed OpenSSL async job", "The OpenSSL async job resumes") + urtest.execute() diff --git a/tests/uranium_tests/tls/test_tls_bad_alpn.py b/tests/uranium_tests/tls/test_tls_bad_alpn.py new file mode 100644 index 00000000000..092fb0d7a4e --- /dev/null +++ b/tests/uranium_tests/tls/test_tls_bad_alpn.py @@ -0,0 +1,102 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_tls_bad_alpn(urtest: UraniumTest) -> None: + ''' + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import os + import ports + + urtest.Summary = ''' + Ensure that handshake fails if invalid alpn string is offered + ''' + + # Only later versions of openssl support the `-alpn` option. + urtest.SkipUnless(Condition.HasOpenSSLVersion('1.1.1')) + + # Define default ATS + ts = urtest.MakeATSProcess("ts", enable_tls=True) + + # add ssl materials like key, certificates for the server + ts.addSSLfile("ssl/server.pem") + ts.addSSLfile("ssl/server.key") + + # Make sure the TS server certs are different from the origin certs + ts.Disk.ssl_multicert_yaml.AddLines( + """ + ssl_multicert: + - dest_ip: "*" + ssl_cert_name: server.pem + ssl_key_name: server.key + """.split("\n")) + + # Case 1, global config policy=permissive properties=signature + # override for foo.com policy=enforced properties=all + ts.Disk.records_config.update( + { + 'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir), + 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts.Variables.SSLDir), + }) + + tr = urtest.AddTestRun("alpn banana") + tr.Processes.Default.Command = "openssl s_client -ign_eof -alpn=banana -connect 127.0.0.1:{}".format(ts.Variables.ssl_port) + tr.ReturnCode = 1 # Should fail + tr.Processes.Default.StartBefore(urtest.Processes.ts) + tr.StillRunningAfter = ts + tr.Processes.Default.Streams.All += Testers.IncludesExpression("No ALPN negotiated", "Banana should not match a negotiation") + + tr = urtest.AddTestRun("alpn http/1.1") + tr.Processes.Default.Command = "printf 'GET / HTTP/1.1\r\n\r\n' | openssl s_client -ign_eof -alpn=http/1.1 -connect 127.0.0.1:{}".format( + ts.Variables.ssl_port) + tr.ReturnCode = 0 + tr.StillRunningAfter = ts + tr.Processes.Default.Streams.All += Testers.IncludesExpression("ALPN protocol: http/1.1", "Successful ALPN") + tr.Processes.Default.Streams.All += Testers.IncludesExpression("HTTP/1.1 400 Host Header Required", "Processed the request") + + tr = urtest.AddTestRun("no alpn") + tr.Processes.Default.Command = "printf 'GET / HTTP/1.1\r\n\r\n' | openssl s_client -ign_eof -connect 127.0.0.1:{}".format( + ts.Variables.ssl_port) + tr.ReturnCode = 0 + tr.StillRunningAfter = ts + tr.Processes.Default.Streams.All += Testers.IncludesExpression("No ALPN negotiated", "No ALPN offered, none negotiated") + tr.Processes.Default.Streams.All += Testers.IncludesExpression("HTTP/1.1 400 Host Header Required", "Processed the request") + + tr = urtest.AddTestRun("alpn h2") + tr.MakeCurlCommand("-k --http2 -v -o /dev/null https://127.0.0.1:{}".format(ts.Variables.ssl_port), ts=ts) + tr.ReturnCode = 0 + tr.StillRunningAfter = ts + tr.Processes.Default.Streams.All += Testers.IncludesExpression("ALPN. server accepted.*h2", "negotiated h2") + tr.Processes.Default.Streams.All += Testers.IncludesExpression("HTTP/2 404 ", "Good response") + urtest.execute() diff --git a/tests/uranium_tests/tls/test_tls_cert_comp.py b/tests/uranium_tests/tls/test_tls_cert_comp.py new file mode 100644 index 00000000000..e06f5ee42ec --- /dev/null +++ b/tests/uranium_tests/tls/test_tls_cert_comp.py @@ -0,0 +1,197 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_tls_cert_comp(urtest: UraniumTest) -> None: + ''' + Verify TLS Certificate Compression (RFC 8879) between two ATS processes. + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + urtest.Summary = ''' + Verify TLS Certificate Compression (RFC 8879) works between two ATS + instances. An edge ATS (client) connects via HTTPS to a mid ATS (server) + with cert compression enabled. The test verifies compression and + decompression succeed by checking the ssl cert compression metrics. + ''' + + urtest.SkipUnless(Condition.HasATSFeature('TS_HAS_CERT_COMPRESSION_CALLBACKS')) + + REPLAY_FILE = 'replay/tls_cert_compression.replay.yaml' + + class TestCertCompression: + server_counter: int = 0 + ts_counter: int = 0 + client_counter: int = 0 + + def __init__(self, algorithm: str) -> None: + self._algorithm = algorithm + self._server = self._configure_server() + self._ts_mid = self._configure_ts_mid() + self._ts_edge = self._configure_ts_edge() + + def _configure_server(self) -> 'Process': + name = f'server-{TestCertCompression.server_counter}' + TestCertCompression.server_counter += 1 + server = urtest.MakeVerifierServerProcess(name, REPLAY_FILE) + return server + + def _configure_ts_mid(self) -> 'Process': + """Mid-tier ATS that terminates TLS and forwards to origin.""" + name = f'm{TestCertCompression.ts_counter}' + TestCertCompression.ts_counter += 1 + ts = urtest.MakeATSProcess(name, enable_tls=True, enable_cache=False) + + ts.addDefaultSSLFiles() + ts.Disk.ssl_multicert_yaml.AddLines( + """ + ssl_multicert: + - dest_ip: "*" + ssl_cert_name: server.pem + ssl_key_name: server.key + """.split("\n")) + + ts.Disk.remap_config.AddLine(f'map / http://127.0.0.1:{self._server.Variables.http_port}/') + + ts.Disk.records_config.update( + { + 'proxy.config.ssl.server.cert.path': ts.Variables.SSLDir, + 'proxy.config.ssl.server.private_key.path': ts.Variables.SSLDir, + 'proxy.config.ssl.server.cert_compression.algorithms': self._algorithm, + 'proxy.config.ssl.server.cert_compression.cache': 0, + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'ssl_cert_compress', + }) + + return ts + + def _configure_ts_edge(self) -> 'Process': + """Edge ATS that connects to mid-tier via HTTPS.""" + name = f'e{TestCertCompression.ts_counter}' + TestCertCompression.ts_counter += 1 + ts = urtest.MakeATSProcess(name, enable_tls=True, enable_cache=False) + + ts.addDefaultSSLFiles() + ts.Disk.ssl_multicert_yaml.AddLines( + """ + ssl_multicert: + - dest_ip: "*" + ssl_cert_name: server.pem + ssl_key_name: server.key + """.split("\n")) + + ts.Disk.remap_config.AddLine(f'map / https://127.0.0.1:{self._ts_mid.Variables.ssl_port}/') + + ts.Disk.records_config.update( + { + 'proxy.config.ssl.server.cert.path': ts.Variables.SSLDir, + 'proxy.config.ssl.server.private_key.path': ts.Variables.SSLDir, + 'proxy.config.ssl.client.verify.server.policy': 'PERMISSIVE', + 'proxy.config.ssl.client.cert_compression.algorithms': self._algorithm, + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'ssl_cert_compress', + }) + + return ts + + def run(self) -> None: + # Test run 1: Send traffic through the proxy chain. + tr = urtest.AddTestRun(f'Send request through edge->mid with {self._algorithm} cert compression') + tr.Processes.Default.StartBefore(self._server) + tr.Processes.Default.StartBefore(self._ts_mid) + tr.Processes.Default.StartBefore(self._ts_edge) + + name = f'client-{TestCertCompression.client_counter}' + TestCertCompression.client_counter += 1 + tr.AddVerifierClientProcess(name, REPLAY_FILE, http_ports=[self._ts_edge.Variables.port]) + + # Test run 2: Check compression metric on the mid-tier (server side). + tr = urtest.AddTestRun(f'Verify {self._algorithm} compression metric on mid-tier') + tr.Processes.Default.Command = (f'traffic_ctl metric get' + f' proxy.process.ssl.cert_compress.{self._algorithm}') + tr.Processes.Default.Env = self._ts_mid.Env + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.All = Testers.ContainsExpression( + f'proxy.process.ssl.cert_compress.{self._algorithm} 1', + f'Certificate should have been compressed with {self._algorithm}') + tr.StillRunningAfter = self._ts_mid + tr.StillRunningAfter = self._ts_edge + tr.StillRunningAfter = self._server + + # Test run 3: Check decompression metric on the edge (client side). + tr = urtest.AddTestRun(f'Verify {self._algorithm} decompression metric on edge') + tr.Processes.Default.Command = (f'traffic_ctl metric get' + f' proxy.process.ssl.cert_decompress.{self._algorithm}') + tr.Processes.Default.Env = self._ts_edge.Env + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.All = Testers.ContainsExpression( + f'proxy.process.ssl.cert_decompress.{self._algorithm} 1', + f'Certificate should have been decompressed with {self._algorithm}') + tr.StillRunningAfter = self._ts_mid + tr.StillRunningAfter = self._ts_edge + tr.StillRunningAfter = self._server + + # Test run 4: Verify no failures on either side. + tr = urtest.AddTestRun(f'Verify no {self._algorithm} compression failures on mid-tier') + tr.Processes.Default.Command = ( + f'traffic_ctl metric get' + f' proxy.process.ssl.cert_compress.{self._algorithm}_failure') + tr.Processes.Default.Env = self._ts_mid.Env + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.All = Testers.ContainsExpression( + f'proxy.process.ssl.cert_compress.{self._algorithm}_failure 0', + f'There should be no {self._algorithm} compression failures') + tr.StillRunningAfter = self._ts_mid + tr.StillRunningAfter = self._ts_edge + tr.StillRunningAfter = self._server + + tr = urtest.AddTestRun(f'Verify no {self._algorithm} decompression failures on edge') + tr.Processes.Default.Command = ( + f'traffic_ctl metric get' + f' proxy.process.ssl.cert_decompress.{self._algorithm}_failure') + tr.Processes.Default.Env = self._ts_edge.Env + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.All = Testers.ContainsExpression( + f'proxy.process.ssl.cert_decompress.{self._algorithm}_failure 0', + f'There should be no {self._algorithm} decompression failures') + tr.StillRunningAfter = self._ts_mid + tr.StillRunningAfter = self._ts_edge + tr.StillRunningAfter = self._server + + algorithms = ['zlib'] + if Condition.HasATSFeature('TS_HAS_BROTLI'): + algorithms.append('brotli') + if Condition.HasATSFeature('TS_HAS_ZSTD'): + algorithms.append('zstd') + for algorithm in algorithms: + TestCertCompression(algorithm).run() + urtest.execute() diff --git a/tests/uranium_tests/tls/test_tls_cert_comp_cache.py b/tests/uranium_tests/tls/test_tls_cert_comp_cache.py new file mode 100644 index 00000000000..f5e6b2a1820 --- /dev/null +++ b/tests/uranium_tests/tls/test_tls_cert_comp_cache.py @@ -0,0 +1,203 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_tls_cert_comp_cache(urtest: UraniumTest) -> None: + ''' + Verify TLS Certificate Compression cache behavior. + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + urtest.Summary = ''' + Verify that the certificate compression cache works correctly. When caching + is enabled, the compressed result is reused across handshakes. When disabled, + each handshake compresses independently and the cache_hit metric stays at 0. + ''' + + urtest.SkipUnless(Condition.HasATSFeature('TS_HAS_CERT_COMPRESSION_CALLBACKS')) + + REPLAY_FILE = 'replay/tls_cert_compression_cache.replay.yaml' + + class TestCertCompressionCache: + server_counter: int = 0 + ts_counter: int = 0 + client_counter: int = 0 + + def __init__(self, cache_enabled: bool) -> None: + self._cache_enabled = cache_enabled + self._algorithm = 'zlib' + self._server = self._configure_server() + self._ts_mid = self._configure_ts_mid() + self._ts_edge = self._configure_ts_edge() + + def _configure_server(self) -> 'Process': + name = f'server-{TestCertCompressionCache.server_counter}' + TestCertCompressionCache.server_counter += 1 + server = urtest.MakeVerifierServerProcess(name, REPLAY_FILE) + return server + + def _configure_ts_mid(self) -> 'Process': + """Mid-tier ATS that terminates TLS and forwards to origin.""" + name = f'm{TestCertCompressionCache.ts_counter}' + TestCertCompressionCache.ts_counter += 1 + ts = urtest.MakeATSProcess(name, enable_tls=True, enable_cache=False) + + ts.addDefaultSSLFiles() + ts.Disk.ssl_multicert_yaml.AddLines( + """ + ssl_multicert: + - dest_ip: "*" + ssl_cert_name: server.pem + ssl_key_name: server.key + """.split("\n")) + + ts.Disk.remap_config.AddLine(f'map / http://127.0.0.1:{self._server.Variables.http_port}/') + + ts.Disk.records_config.update( + { + 'proxy.config.ssl.server.cert.path': ts.Variables.SSLDir, + 'proxy.config.ssl.server.private_key.path': ts.Variables.SSLDir, + 'proxy.config.ssl.server.cert_compression.algorithms': self._algorithm, + 'proxy.config.ssl.server.cert_compression.cache': 1 if self._cache_enabled else 0, + 'proxy.config.ssl.server.session_ticket.enable': 0, + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'ssl_cert_compress', + }) + + return ts + + def _configure_ts_edge(self) -> 'Process': + """Edge ATS that connects to mid-tier via HTTPS.""" + name = f'e{TestCertCompressionCache.ts_counter}' + TestCertCompressionCache.ts_counter += 1 + ts = urtest.MakeATSProcess(name, enable_tls=True, enable_cache=False) + + ts.addDefaultSSLFiles() + ts.Disk.ssl_multicert_yaml.AddLines( + """ + ssl_multicert: + - dest_ip: "*" + ssl_cert_name: server.pem + ssl_key_name: server.key + """.split("\n")) + + ts.Disk.remap_config.AddLine(f'map / https://127.0.0.1:{self._ts_mid.Variables.ssl_port}/') + + ts.Disk.records_config.update( + { + 'proxy.config.ssl.server.cert.path': ts.Variables.SSLDir, + 'proxy.config.ssl.server.private_key.path': ts.Variables.SSLDir, + 'proxy.config.ssl.client.verify.server.policy': 'PERMISSIVE', + 'proxy.config.ssl.client.cert_compression.algorithms': self._algorithm, + 'proxy.config.http.keep_alive_enabled_out': 0, + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'ssl_cert_compress', + }) + + return ts + + def run(self) -> None: + cache_label = 'enabled' if self._cache_enabled else 'disabled' + + # Test run 1: Send 2 requests over 2 separate TLS connections. + tr = urtest.AddTestRun(f'Send 2 requests with cache {cache_label}') + tr.Processes.Default.StartBefore(self._server) + tr.Processes.Default.StartBefore(self._ts_mid) + tr.Processes.Default.StartBefore(self._ts_edge) + + name = f'client-{TestCertCompressionCache.client_counter}' + TestCertCompressionCache.client_counter += 1 + tr.AddVerifierClientProcess(name, REPLAY_FILE, http_ports=[self._ts_edge.Variables.port]) + + # Test run 2: Check compression count on mid-tier — should be 2. + tr = urtest.AddTestRun(f'Verify compression count with cache {cache_label}') + tr.Processes.Default.Command = (f'traffic_ctl metric get' + f' proxy.process.ssl.cert_compress.{self._algorithm}') + tr.Processes.Default.Env = self._ts_mid.Env + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.All = Testers.ContainsExpression( + f'proxy.process.ssl.cert_compress.{self._algorithm} 2', f'Should have 2 {self._algorithm} compressions') + tr.StillRunningAfter = self._ts_mid + tr.StillRunningAfter = self._ts_edge + tr.StillRunningAfter = self._server + + # Test run 3: Check decompression count on edge — should be 2. + tr = urtest.AddTestRun(f'Verify decompression count with cache {cache_label}') + tr.Processes.Default.Command = (f'traffic_ctl metric get' + f' proxy.process.ssl.cert_decompress.{self._algorithm}') + tr.Processes.Default.Env = self._ts_edge.Env + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.All = Testers.ContainsExpression( + f'proxy.process.ssl.cert_decompress.{self._algorithm} 2', f'Should have 2 {self._algorithm} decompressions') + tr.StillRunningAfter = self._ts_mid + tr.StillRunningAfter = self._ts_edge + tr.StillRunningAfter = self._server + + # Test run 4: Verify no compression failures. + tr = urtest.AddTestRun(f'Verify no compression failures with cache {cache_label}') + tr.Processes.Default.Command = ( + f'traffic_ctl metric get' + f' proxy.process.ssl.cert_compress.{self._algorithm}_failure') + tr.Processes.Default.Env = self._ts_mid.Env + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.All = Testers.ContainsExpression( + f'proxy.process.ssl.cert_compress.{self._algorithm}_failure 0', + f'Should have no {self._algorithm} compression failures') + tr.StillRunningAfter = self._ts_mid + tr.StillRunningAfter = self._ts_edge + tr.StillRunningAfter = self._server + + # Test run 5: Check cache_hit metric. + if self._cache_enabled: + tr = urtest.AddTestRun(f'Verify cache_hit is 1 with cache {cache_label}') + tr.Processes.Default.Command = 'traffic_ctl metric get proxy.process.ssl.cert_compress.cache_hit' + tr.Processes.Default.Env = self._ts_mid.Env + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.All = Testers.ContainsExpression( + 'proxy.process.ssl.cert_compress.cache_hit 1', 'cache_hit should be 1 when caching is enabled (1 miss + 1 hit)') + tr.StillRunningAfter = self._ts_mid + tr.StillRunningAfter = self._ts_edge + tr.StillRunningAfter = self._server + else: + tr = urtest.AddTestRun(f'Verify cache_hit is 0 with cache {cache_label}') + tr.Processes.Default.Command = 'traffic_ctl metric get proxy.process.ssl.cert_compress.cache_hit' + tr.Processes.Default.Env = self._ts_mid.Env + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.All = Testers.ContainsExpression( + 'proxy.process.ssl.cert_compress.cache_hit 0', 'cache_hit should be 0 when caching is disabled') + tr.StillRunningAfter = self._ts_mid + tr.StillRunningAfter = self._ts_edge + tr.StillRunningAfter = self._server + + TestCertCompressionCache(cache_enabled=True).run() + TestCertCompressionCache(cache_enabled=False).run() + urtest.execute() diff --git a/tests/uranium_tests/tls/test_tls_check_cert_select_plugin.py b/tests/uranium_tests/tls/test_tls_check_cert_select_plugin.py new file mode 100644 index 00000000000..50fa4646aab --- /dev/null +++ b/tests/uranium_tests/tls/test_tls_check_cert_select_plugin.py @@ -0,0 +1,207 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_tls_check_cert_select_plugin(urtest: UraniumTest) -> None: + ''' + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import os + + urtest.Summary = ''' + Test ATS offering different certificates based on SNI. Load via plugin + ''' + + # The origin only has a response registered for a request with no Host header, so with + # pristine_host_hdr every request 404s at the origin. A 404 therefore proves the exchange + # happened. Match the response status line rather than a bare "404", which also matches the + # ephemeral port number curl prints (a port such as 62404 made this test fail at random). + # Covers both HTTP/1.1 ("HTTP/1.1 404 Not Found") and HTTP/2 ("HTTP/2 404"). + HTTP_404 = r"HTTP/[\d.]+ 404" + + # Define default ATS + ts = urtest.MakeATSProcess("ts", enable_tls=True) + server = urtest.MakeOriginServer("server", ssl=True) + dns = urtest.MakeDNServer("dns") + + request_header = {"headers": "GET / HTTP/1.1\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + server.addResponse("sessionlog.json", request_header, response_header) + + # add ssl materials like key, certificates for the server + ts.addSSLfile("ssl/signed-foo.pem") + ts.addSSLfile("ssl/signed-foo.key") + ts.addSSLfile("ssl/signed-bar.pem") + ts.addSSLfile("ssl/signed2-bar.pem") + ts.addSSLfile("ssl/signed-bar.key") + ts.addSSLfile("ssl/server.pem") + ts.addSSLfile("ssl/server.key") + ts.addSSLfile("ssl/signer.pem") + ts.addSSLfile("ssl/signer.key") + + ts.Disk.remap_config.AddLine('map / https://foo.com:{1}'.format(ts.Variables.ssl_port, server.Variables.SSL_Port)) + + ts.Disk.ssl_multicert_yaml.AddLines( + """ + ssl_multicert: + - dest_ip: 127.0.0.1 + ssl_cert_name: signed-foo.pem + ssl_key_name: signed-foo.key + - ssl_cert_name: signed2-bar.pem + ssl_key_name: signed-bar.key + - dest_ip: "*" + ssl_cert_name: server.pem + ssl_key_name: server.key + """.split("\n")) + + urtest.PrepareTestPlugin(os.path.join(urtest.Variables.AtsTestPluginsDir, 'ssl_secret_load_test.so'), ts) + + # Case 1, global config policy=permissive properties=signature + # override for foo.com policy=enforced properties=all + ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.tags': 'ssl_secret_load_test|ssl', + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.ssl.server.cert.path': '{0}/../'.format(ts.Variables.SSLDir), + 'proxy.config.ssl.server.private_key.path': '{0}/../'.format(ts.Variables.SSLDir), + 'proxy.config.ssl.server.cipher_suite': + 'ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-SHA384:AES128-GCM-SHA256:AES256-GCM-SHA384:ECDHE-RSA-RC4-SHA:ECDHE-RSA-AES128-SHA:ECDHE-RSA-AES256-SHA:RC4-SHA:RC4-MD5:AES128-SHA:AES256-SHA:DES-CBC3-SHA!SRP:!DSS:!PSK:!aNULL:!eNULL:!SSLv2', + 'proxy.config.url_remap.pristine_host_hdr': 1, + 'proxy.config.dns.nameservers': '127.0.0.1:{0}'.format(dns.Variables.Port), + 'proxy.config.exec_thread.autoconfig.scale': 1.0, + 'proxy.config.dns.resolv_conf': 'NULL', + 'proxy.config.ssl.client.verify.server.policy': 'PERMISSIVE', + }) + + dns.addRecords(records={"foo.com.": ["127.0.0.1"]}) + dns.addRecords(records={"bar.com.": ["127.0.0.1"]}) + + # Should receive a bar.com cert + tr = urtest.AddTestRun("bar.com cert") + tr.Setup.Copy("ssl/signer.pem") + tr.Setup.Copy("ssl/signer2.pem") + tr.MakeCurlCommand( + "-v --cacert ./signer2.pem --resolve 'bar.com:{0}:127.0.0.1' https://bar.com:{0}".format(ts.Variables.ssl_port), ts=ts) + tr.ReturnCode = 0 + tr.Processes.Default.StartBefore(server) + tr.Processes.Default.StartBefore(dns) + tr.Processes.Default.StartBefore(urtest.Processes.ts, ready=When.PortOpen(ts.Variables.ssl_port)) + tr.StillRunningAfter = server + tr.StillRunningAfter = ts + tr.Processes.Default.Streams.All = Testers.ExcludesExpression("Could Not Connect", "Curl attempt should have succeeded") + tr.Processes.Default.Streams.All += Testers.ContainsExpression("CN=bar.com", "Cert should contain bar.com") + tr.Processes.Default.Streams.All += Testers.ExcludesExpression("CN=foo.com", "Cert should not contain foo.com") + tr.Processes.Default.Streams.All += Testers.ContainsExpression(HTTP_404, "Should make an exchange") + + # Should receive a foo.com cert + tr2 = urtest.AddTestRun("foo.com cert") + tr2.MakeCurlCommand( + "-v --cacert ./signer.pem --resolve 'foo.com:{0}:127.0.0.1' https://foo.com:{0}".format(ts.Variables.ssl_port), ts=ts) + tr2.ReturnCode = 0 + tr2.StillRunningAfter = server + tr2.StillRunningAfter = ts + tr2.Processes.Default.Streams.All = Testers.ExcludesExpression("Could Not Connect", "Curl attempt should have succeeded") + tr2.Processes.Default.Streams.All += Testers.ContainsExpression("CN=foo.com", "Cert should contain foo.com") + tr2.Processes.Default.Streams.All += Testers.ExcludesExpression("CN=bar.com", "Cert should not contain bar.com") + tr2.Processes.Default.Streams.All += Testers.ContainsExpression(HTTP_404, "Should make an exchange") + + # Should receive random.server.com + tr2 = urtest.AddTestRun("random.server.com cert") + tr2.MakeCurlCommand( + "-v -k --resolve 'random.server.com:{0}:127.0.0.1' https://random.server.com:{0}".format(ts.Variables.ssl_port), ts=ts) + tr2.ReturnCode = 0 + tr2.StillRunningAfter = server + tr2.StillRunningAfter = ts + tr2.Processes.Default.Streams.All = Testers.ExcludesExpression("Could Not Connect", "Curl attempt should have succeeded") + tr2.Processes.Default.Streams.All += Testers.ContainsExpression("CN=random.server.com", "Cert should contain random.server.com") + tr2.Processes.Default.Streams.All += Testers.ExcludesExpression("CN=foo.com", "Cert should not contain foo.com") + tr2.Processes.Default.Streams.All += Testers.ExcludesExpression("CN=bar.com", "Cert should not contain bar.com") + tr2.Processes.Default.Streams.All += Testers.ContainsExpression(HTTP_404, "Should make an exchange") + + # No SNI match should match specific IP address, foo.com + # SNI name and returned cert name will not match, so must use -k to avoid cert verification + tr2 = urtest.AddTestRun("Bad SNI") + tr2.MakeCurlCommand( + "-v -k --cacert ./signer.pem --resolve 'bad.sni.com:{0}:127.0.0.1' https://bad.sni.com:{0}".format(ts.Variables.ssl_port), + ts=ts) + tr2.ReturnCode = 0 + tr2.StillRunningAfter = server + tr2.StillRunningAfter = ts + tr2.Processes.Default.Streams.All = Testers.ExcludesExpression("Could Not Connect", "Curl attempt should have succeeded") + tr2.Processes.Default.Streams.All += Testers.ContainsExpression("CN=foo.com", "Cert should contain foo.com") + tr2.Processes.Default.Streams.All += Testers.ExcludesExpression("CN=bar.com", "Cert should not contain bar.com") + tr2.Processes.Default.Streams.All += Testers.ContainsExpression(HTTP_404, "Should make an exchange") + + # Copy in a new version of the bar.com cert. Replace it with the version + # signed by signer 1. Wait at least a second to sure the file update time + # differs + trupdate = urtest.AddTestRun("Update server bar cert file in place") + trupdate.StillRunningAfter = ts + trupdate.StillRunningAfter = server + trupdate.Setup.CopyAs("ssl/signed-bar.pem", ".", "{0}/signed2-bar.pem".format(ts.Variables.SSLDir)) + # For some reason the Setup.CopyAs does not change the modification time, so we touch + trupdate.Processes.Default.Command = 'touch {0}/signed2-bar.pem'.format(ts.Variables.SSLDir) + # Need to copy over the environment so traffic_ctl knows where to find the unix domain socket + trupdate.Processes.Default.Env = ts.Env + trupdate.Processes.Default.ReturnCode = 0 + + # The plugin will pull every 3 seconds. So wait 4 seconds and test again. + # Request with CA=signer.pem should work. Request with CA=signer2.pem + # should fail + tr = urtest.AddTestRun("Test new version of bar cert with good CA") + tr.DelayStart = 4 + tr.MakeCurlCommandMulti( + "date; {{curl}} -v --cacert ./signer.pem --resolve 'bar.com:{0}:127.0.0.1' https://bar.com:{0}".format( + ts.Variables.ssl_port), + ts=ts) + tr.ReturnCode = 0 + tr.StillRunningAfter = server + tr.StillRunningAfter = ts + tr.Processes.Default.Streams.All = Testers.ExcludesExpression("Could Not Connect", "Curl attempt should have succeeded") + tr.Processes.Default.Streams.All += Testers.ContainsExpression("CN=bar.com", "Cert should contain bar.com") + tr.Processes.Default.Streams.All += Testers.ExcludesExpression("CN=foo.com", "Cert should not contain foo.com") + tr.Processes.Default.Streams.All += Testers.ContainsExpression(HTTP_404, "Should make an exchange") + + tr = urtest.AddTestRun("Test new version of bar cert with bad CA") + tr.MakeCurlCommand( + "-v --cacert ./signer2.pem --resolve 'bar.com:{0}:127.0.0.1' https://bar.com:{0}".format(ts.Variables.ssl_port), ts=ts) + tr.ReturnCode = 60 + tr.StillRunningAfter = server + tr.StillRunningAfter = ts + tr.Processes.Default.Streams.All = Testers.ContainsExpression( + r"curl: \(60\) SSL certificate", "Failed certificate verification") + # Older versions of curl do not print certificate subject details when + # certificate verification fails. + tr.Processes.Default.Streams.All += Testers.ExcludesExpression("CN=foo.com", "Cert should not contain foo.com") + tr.Processes.Default.Streams.All += Testers.ExcludesExpression(HTTP_404, "Should make an exchange") + urtest.execute() diff --git a/tests/uranium_tests/tls/test_tls_check_cert_selection.py b/tests/uranium_tests/tls/test_tls_check_cert_selection.py new file mode 100644 index 00000000000..7c731317076 --- /dev/null +++ b/tests/uranium_tests/tls/test_tls_check_cert_selection.py @@ -0,0 +1,147 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_tls_check_cert_selection(urtest: UraniumTest) -> None: + ''' + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + urtest.Summary = ''' + Test ATS offering different certificates based on SNI + ''' + + # Define default ATS + ts = urtest.MakeATSProcess("ts", enable_tls=True) + server = urtest.MakeOriginServer("server", ssl=True) + dns = urtest.MakeDNServer("dns") + + request_header = {"headers": "GET / HTTP/1.1\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + server.addResponse("sessionlog.json", request_header, response_header) + + # add ssl materials like key, certificates for the server + ts.addSSLfile("ssl/signed-foo.pem") + ts.addSSLfile("ssl/signed-foo.key") + ts.addSSLfile("ssl/signed-bar.pem") + ts.addSSLfile("ssl/signed2-bar.pem") + ts.addSSLfile("ssl/signed-bar.key") + ts.addSSLfile("ssl/signer.pem") + ts.addSSLfile("ssl/signer.key") + ts.addSSLfile("ssl/combo.pem") + + ts.Disk.remap_config.AddLine('map / https://foo.com:{1}'.format(ts.Variables.ssl_port, server.Variables.SSL_Port)) + + ts.Disk.ssl_multicert_yaml.AddLines( + """ + ssl_multicert: + - dest_ip: "127.0.0.1" + ssl_cert_name: signed-foo.pem + ssl_key_name: signed-foo.key + - ssl_cert_name: signed2-bar.pem + ssl_key_name: signed-bar.key + - dest_ip: "*" + ssl_cert_name: combo.pem + """.split("\n")) + + # Case 1, global config policy=permissive properties=signature + # override for foo.com policy=enforced properties=all + ts.Disk.records_config.update( + { + 'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir), + 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts.Variables.SSLDir), + 'proxy.config.url_remap.pristine_host_hdr': 1, + 'proxy.config.dns.nameservers': '127.0.0.1:{0}'.format(dns.Variables.Port), + 'proxy.config.exec_thread.autoconfig.scale': 1.0, + 'proxy.config.dns.resolv_conf': 'NULL', + 'proxy.config.ssl.client.verify.server.policy': 'PERMISSIVE', + }) + + dns.addRecords(records={"foo.com.": ["127.0.0.1"]}) + dns.addRecords(records={"bar.com.": ["127.0.0.1"]}) + + # Should receive a bar.com cert + tr = urtest.AddTestRun("bar.com cert") + tr.Setup.Copy("ssl/signer.pem") + tr.Setup.Copy("ssl/signer2.pem") + tr.MakeCurlCommand( + "-v --cacert ./signer2.pem --resolve 'bar.com:{0}:127.0.0.1' https://bar.com:{0}".format(ts.Variables.ssl_port), ts=ts) + tr.ReturnCode = 0 + tr.Processes.Default.StartBefore(server) + tr.Processes.Default.StartBefore(dns) + tr.Processes.Default.StartBefore(urtest.Processes.ts) + tr.StillRunningAfter = server + tr.StillRunningAfter = ts + tr.Processes.Default.Streams.All = Testers.ExcludesExpression("Could Not Connect", "Curl attempt should have succeeded") + tr.Processes.Default.Streams.All += Testers.ContainsExpression("CN=bar.com", "Cert should contain bar.com") + tr.Processes.Default.Streams.All += Testers.ExcludesExpression("CN=foo.com", "Cert should not contain foo.com") + tr.Processes.Default.Streams.All += Testers.ContainsExpression("404", "Should make an exchange") + + # Should receive a foo.com cert + tr2 = urtest.AddTestRun("foo.com cert") + tr2.MakeCurlCommand( + "-v --cacert ./signer.pem --resolve 'foo.com:{0}:127.0.0.1' https://foo.com:{0}".format(ts.Variables.ssl_port), ts=ts) + tr2.ReturnCode = 0 + tr2.StillRunningAfter = server + tr2.StillRunningAfter = ts + tr2.Processes.Default.Streams.All = Testers.ExcludesExpression("Could Not Connect", "Curl attempt should have succeeded") + tr2.Processes.Default.Streams.All += Testers.ContainsExpression("CN=foo.com", "Cert should contain foo.com") + tr2.Processes.Default.Streams.All += Testers.ExcludesExpression("CN=bar.com", "Cert should not contain bar.com") + tr.Processes.Default.Streams.All += Testers.ContainsExpression("404", "Should make an exchange") + + # Should receive random.server.com + tr2 = urtest.AddTestRun("random.server.com cert") + tr2.MakeCurlCommand( + "-v -k --resolve 'random.server.com:{0}:127.0.0.1' https://random.server.com:{0}".format(ts.Variables.ssl_port), ts=ts) + tr2.ReturnCode = 0 + tr2.StillRunningAfter = server + tr2.StillRunningAfter = ts + tr2.Processes.Default.Streams.All = Testers.ExcludesExpression("Could Not Connect", "Curl attempt should have succeeded") + tr2.Processes.Default.Streams.All += Testers.ContainsExpression("CN=random.server.com", "Cert should contain random.server.com") + tr2.Processes.Default.Streams.All += Testers.ExcludesExpression("CN=foo.com", "Cert should not contain foo.com") + tr2.Processes.Default.Streams.All += Testers.ExcludesExpression("CN=bar.com", "Cert should not contain bar.com") + tr.Processes.Default.Streams.All += Testers.ContainsExpression("404", "Should make an exchange") + + # No SNI match should match specific IP address, foo.com + # SNI name and returned cert name will not match, so must use -k to avoid cert verification + tr2 = urtest.AddTestRun("Bad SNI") + tr2.MakeCurlCommand( + "-v -k --cacert ./signer.pem --resolve 'bad.sni.com:{0}:127.0.0.1' https://bad.sni.com:{0}".format(ts.Variables.ssl_port), + ts=ts) + tr2.ReturnCode = 0 + tr2.StillRunningAfter = server + tr2.StillRunningAfter = ts + tr2.Processes.Default.Streams.All = Testers.ExcludesExpression("Could Not Connect", "Curl attempt should have succeeded") + tr2.Processes.Default.Streams.All += Testers.ContainsExpression("CN=foo.com", "Cert should contain foo.com") + tr2.Processes.Default.Streams.All += Testers.ExcludesExpression("CN=bar.com", "Cert should not contain bar.com") + tr.Processes.Default.Streams.All += Testers.ContainsExpression("404", "Should make an exchange") + urtest.execute() diff --git a/tests/uranium_tests/tls/test_tls_check_cert_selection_reload.py b/tests/uranium_tests/tls/test_tls_check_cert_selection_reload.py new file mode 100644 index 00000000000..4e329548e67 --- /dev/null +++ b/tests/uranium_tests/tls/test_tls_check_cert_selection_reload.py @@ -0,0 +1,149 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_tls_check_cert_selection_reload(urtest: UraniumTest) -> None: + ''' + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + urtest.Summary = ''' + Test ATS offering different certificates based on SNI + ''' + + # Define default ATS + ts = urtest.MakeATSProcess("ts", enable_tls=True) + server = urtest.MakeOriginServer("server", ssl=True) + server3 = urtest.MakeOriginServer("server3", ssl=True) + + request_header = {"headers": "GET / HTTP/1.1\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + server.addResponse("sessionlog.json", request_header, response_header) + + # add ssl materials like key, certificates for the server + ts.addSSLfile("ssl/signed-bar.pem") + ts.addSSLfile("ssl/signed2-bar.pem") + ts.addSSLfile("ssl/signed-bar.key") + ts.addSSLfile("ssl/signer.pem") + ts.addSSLfile("ssl/signer.key") + ts.addSSLfile("ssl/combo.pem") + + ts.Disk.remap_config.AddLine('map /stuff https://foo.com:{1}'.format(ts.Variables.ssl_port, server.Variables.SSL_Port)) + + ts.Disk.ssl_multicert_yaml.AddLines( + """ + ssl_multicert: + - ssl_cert_name: signed-bar.pem + ssl_key_name: signed-bar.key + - dest_ip: "*" + ssl_cert_name: combo.pem + """.split("\n")) + + # Case 1, global config policy=permissive properties=signature + # override for foo.com policy=enforced properties=all + ts.Disk.records_config.update( + { + 'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir), + 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts.Variables.SSLDir), + 'proxy.config.url_remap.pristine_host_hdr': 1, + 'proxy.config.exec_thread.autoconfig.scale': 1.0, + 'proxy.config.ssl.client.verify.server.policy': 'PERMISSIVE', + 'proxy.config.diags.debug.tags': 'ssl|http|lm', + 'proxy.config.diags.debug.enabled': 1 + }) + + # Should receive a bar.com cert issued by first signer + tr = urtest.AddTestRun("bar.com cert signer1") + tr.Setup.Copy("ssl/signer.pem") + tr.Setup.Copy("ssl/signer2.pem") + tr.MakeCurlCommand( + "-v --cacert ./signer.pem --resolve 'bar.com:{0}:127.0.0.1' https://bar.com:{0}/random".format(ts.Variables.ssl_port), + ts=ts) + tr.ReturnCode = 0 + tr.Processes.Default.StartBefore(server) + tr.Processes.Default.StartBefore(urtest.Processes.ts) + tr.StillRunningAfter = server + tr.StillRunningAfter = ts + tr.Processes.Default.Streams.All = Testers.ExcludesExpression("Could Not Connect", "Curl attempt should have succeeded") + tr.Processes.Default.Streams.All += Testers.ContainsExpression("CN=bar.com", "Cert should contain bar.com") + tr.Processes.Default.Streams.All += Testers.ExcludesExpression("CN=foo.com", "Cert should not contain foo.com") + tr.Processes.Default.Streams.All += Testers.ContainsExpression("404", "Should make an exchange") + + tr = urtest.AddTestRun("bar.com cert signer2") + tr.MakeCurlCommand( + "-v --cacert ./signer2.pem --resolve 'bar.com:{0}:127.0.0.1' https://bar.com:{0}/random".format(ts.Variables.ssl_port), + ts=ts) + tr.ReturnCode = 60 + tr.StillRunningAfter = server + tr.StillRunningAfter = ts + tr.Processes.Default.Streams.All = Testers.ContainsExpression( + "unable to get local issuer certificate", "Server certificate not issued by expected signer") + + # Pause a little to ensure mtime will be updated + tr = urtest.AddTestRun("Pause a little to ensure mtime will be different") + tr.Processes.Default.Command = "sleep 2" + tr.Processes.Default.ReturnCode = 0 + + tr = urtest.AddTestRun("Update the bar cert to the signed 2 version") + tr.Setup.CopyAs("ssl/signed2-bar.pem", ".", "{0}/signed-bar.pem".format(ts.Variables.SSLDir)) + # For some reason the Setup.CopyAs does not change the modification time, so we touch + tr.Processes.Default.Env = ts.Env + tr.Processes.Default.Command = 'touch {0}/signed-bar.pem'.format(ts.Variables.SSLDir) + # Need to copy over the environment so traffic_ctl knows where to find the unix domain socket + tr.Processes.Default.ReturnCode = 0 + + tr = urtest.AddConfigReload(ts, expect_tasks=["ssl_multicert.yaml"], description="Reload config") + tr.StillRunningAfter = server + + tr = urtest.AddTestRun("Try with signer 1 again") + tr.Processes.Default.StartBefore(server3) + tr.StillRunningAfter = ts + tr.StillRunningAfter = server + tr.MakeCurlCommand( + "-v --cacert ./signer.pem --resolve 'bar.com:{0}:127.0.0.1' https://bar.com:{0}/random".format(ts.Variables.ssl_port), + ts=ts) + tr.ReturnCode = 60 + tr.Processes.Default.Streams.All = Testers.ContainsExpression( + "unable to get local issuer certificate", "Server certificate not issued by expected signer") + + tr = urtest.AddTestRun("Try with signer 2 again") + tr.MakeCurlCommand( + "-v --cacert ./signer2.pem --resolve 'bar.com:{0}:127.0.0.1' https://bar.com:{0}/random".format(ts.Variables.ssl_port), + ts=ts) + tr.ReturnCode = 0 + tr.StillRunningAfter = server + tr.StillRunningAfter = ts + tr.Processes.Default.Streams.All = Testers.ExcludesExpression("Could Not Connect", "Curl attempt should have succeeded") + tr.Processes.Default.Streams.All += Testers.ContainsExpression("CN=bar.com", "Cert should contain bar.com") + tr.Processes.Default.Streams.All += Testers.ExcludesExpression("CN=foo.com", "Cert should not contain foo.com") + tr.Processes.Default.Streams.All += Testers.ContainsExpression("404", "Should make an exchange") + urtest.execute() diff --git a/tests/uranium_tests/tls/test_tls_check_dual_cert_selection.py b/tests/uranium_tests/tls/test_tls_check_dual_cert_selection.py new file mode 100644 index 00000000000..48cead7a38c --- /dev/null +++ b/tests/uranium_tests/tls/test_tls_check_dual_cert_selection.py @@ -0,0 +1,183 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_tls_check_dual_cert_selection(urtest: UraniumTest) -> None: + ''' + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import os + import re + + urtest.Summary = ''' + Test ATS offering both RSA and EC certificates + ''' + + # Define default ATS + ts = urtest.MakeATSProcess("ts", enable_tls=True) + server = urtest.MakeOriginServer("server", ssl=True) + dns = urtest.MakeDNServer("dns") + + request_header = {"headers": "GET / HTTP/1.1\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + server.addResponse("sessionlog.json", request_header, response_header) + + # add ssl materials like key, certificates for the server + ts.addSSLfile("ssl/signed-foo.pem") + ts.addSSLfile("ssl/signed-foo.key") + ts.addSSLfile("ssl/signed-foo-ec.pem") + ts.addSSLfile("ssl/signed-foo-ec.key") + ts.addSSLfile("ssl/signed-san.pem") + ts.addSSLfile("ssl/signed-san.key") + ts.addSSLfile("ssl/signed-san-ec.pem") + ts.addSSLfile("ssl/signed-san-ec.key") + ts.addSSLfile("ssl/signer.pem") + ts.addSSLfile("ssl/signer.key") + ts.addSSLfile("ssl/server.pem") + ts.addSSLfile("ssl/server.key") + + ts.Disk.remap_config.AddLine('map / https://foo.com:{1}'.format(ts.Variables.ssl_port, server.Variables.SSL_Port)) + + ts.Disk.ssl_multicert_yaml.AddLines( + """ + ssl_multicert: + - ssl_cert_name: signed-foo-ec.pem,signed-foo.pem + ssl_key_name: signed-foo-ec.key,signed-foo.key + - ssl_cert_name: signed-san-ec.pem,signed-san.pem + ssl_key_name: signed-san-ec.key,signed-san.key + - dest_ip: "*" + ssl_cert_name: server.pem + ssl_key_name: server.key + """.split("\n")) + + # Case 1, global config policy=permissive properties=signature + # override for foo.com policy=enforced properties=all + ts.Disk.records_config.update( + { + 'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir), + 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts.Variables.SSLDir), + 'proxy.config.ssl.server.cipher_suite': 'ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256', + 'proxy.config.url_remap.pristine_host_hdr': 1, + 'proxy.config.dns.nameservers': '127.0.0.1:{0}'.format(dns.Variables.Port), + 'proxy.config.exec_thread.autoconfig.scale': 1.0, + 'proxy.config.dns.resolv_conf': 'NULL', + 'proxy.config.diags.debug.tags': 'ssl', + 'proxy.config.diags.debug.enabled': 1 + }) + + dns.addRecords(records={"foo.com.": ["127.0.0.1"]}) + dns.addRecords(records={"bar.com.": ["127.0.0.1"]}) + + foo_ec_string = "" + foo_rsa_string = "" + san_ec_string = "" + san_rsa_string = "" + with open(os.path.join(urtest.TestDirectory, 'ssl', 'signed-foo-ec.pem'), 'r') as myfile: + file_string = myfile.read() + cert_end = file_string.find("END CERTIFICATE-----") + foo_ec_string = re.escape(file_string[0:cert_end]) + with open(os.path.join(urtest.TestDirectory, 'ssl', 'signed-foo.pem'), 'r') as myfile: + file_string = myfile.read() + cert_end = file_string.find("END CERTIFICATE-----") + foo_rsa_string = re.escape(file_string[0:cert_end]) + with open(os.path.join(urtest.TestDirectory, 'ssl', 'signed-san-ec.pem'), 'r') as myfile: + file_string = myfile.read() + cert_end = file_string.find("END CERTIFICATE-----") + san_ec_string = re.escape(file_string[0:cert_end]) + with open(os.path.join(urtest.TestDirectory, 'ssl', 'signed-san.pem'), 'r') as myfile: + file_string = myfile.read() + cert_end = file_string.find("END CERTIFICATE-----") + san_rsa_string = re.escape(file_string[0:cert_end]) + + # Should receive a EC cert since ATS cipher list prefers EC + tr = urtest.AddTestRun("Default for foo should return EC cert") + tr.Setup.Copy("ssl/signer.pem") + tr.Processes.Default.Command = "echo foo | openssl s_client -tls1_2 -servername foo.com -connect 127.0.0.1:{0}".format( + ts.Variables.ssl_port, foo_ec_string) + tr.ReturnCode = 0 + tr.Processes.Default.StartBefore(server) + tr.Processes.Default.StartBefore(dns) + tr.Processes.Default.StartBefore(urtest.Processes.ts) + tr.StillRunningAfter = server + tr.StillRunningAfter = ts + tr.Processes.Default.Streams.All += Testers.ContainsExpression(foo_ec_string, "Should select EC cert", reflags=re.S | re.M) + + # Should receive a RSA cert + tr = urtest.AddTestRun("Only offer RSA ciphers, should receive RSA cert") + tr.Processes.Default.Command = "echo foo | openssl s_client -tls1_2 -servername foo.com -cipher 'ECDHE-RSA-AES128-GCM-SHA256' -connect 127.0.0.1:{0}".format( + ts.Variables.ssl_port) + tr.ReturnCode = 0 + tr.StillRunningAfter = server + tr.StillRunningAfter = ts + tr.Processes.Default.Streams.All += Testers.ContainsExpression(foo_rsa_string, "Should select RSA cert", reflags=re.S | re.M) + + # Should receive a EC cert + tr = urtest.AddTestRun("Default for two.com should return EC cert") + tr.Processes.Default.Command = "echo foo | openssl s_client -tls1_2 -servername two.com -connect 127.0.0.1:{0}".format( + ts.Variables.ssl_port) + tr.ReturnCode = 0 + tr.StillRunningAfter = server + tr.StillRunningAfter = ts + tr.Processes.Default.Streams.All += Testers.ContainsExpression(san_ec_string, "Should select EC cert", reflags=re.S | re.M) + tr.Processes.Default.Streams.All += Testers.ContainsExpression("CN ?= ?group.com", "Should select a group SAN") + + # Should receive a RSA cert + tr = urtest.AddTestRun("Only offer RSA ciphers, should receive RSA cert") + tr.Processes.Default.Command = "echo foo | openssl s_client -tls1_2 -servername two.com -cipher 'ECDHE-RSA-AES128-GCM-SHA256' -connect 127.0.0.1:{0}".format( + ts.Variables.ssl_port) + tr.ReturnCode = 0 + tr.StillRunningAfter = server + tr.StillRunningAfter = ts + tr.Processes.Default.Streams.All += Testers.ContainsExpression(san_rsa_string, "Should select RSA cert", reflags=re.S | re.M) + tr.Processes.Default.Streams.All += Testers.ContainsExpression("CN ?= ?group.com", "Should select a group SAN") + + # Should receive a RSA cert + tr = urtest.AddTestRun("rsa.com only in rsa cert") + tr.Processes.Default.Command = "echo foo | openssl s_client -tls1_2 -servername rsa.com -connect 127.0.0.1:{0}".format( + ts.Variables.ssl_port) + tr.ReturnCode = 0 + tr.StillRunningAfter = server + tr.StillRunningAfter = ts + tr.Processes.Default.Streams.All += Testers.ContainsExpression(san_rsa_string, "Should select RSA cert", reflags=re.S | re.M) + tr.Processes.Default.Streams.All += Testers.ContainsExpression("CN ?= ?group.com", "Should select a group SAN") + + # Should receive a EC cert + tr = urtest.AddTestRun("ec.com only in ec cert") + tr.Processes.Default.Command = "echo foo | openssl s_client -tls1_2 -servername ec.com -connect 127.0.0.1:{0}".format( + ts.Variables.ssl_port) + tr.ReturnCode = 0 + tr.StillRunningAfter = server + tr.StillRunningAfter = ts + tr.Processes.Default.Streams.All += Testers.ContainsExpression(san_ec_string, "Should select EC cert", reflags=re.S | re.M) + tr.Processes.Default.Streams.All += Testers.ContainsExpression("CN ?= ?group.com", "Should select a group SAN") + urtest.execute() diff --git a/tests/uranium_tests/tls/test_tls_check_dual_cert_selection2.py b/tests/uranium_tests/tls/test_tls_check_dual_cert_selection2.py new file mode 100644 index 00000000000..02b811a2b71 --- /dev/null +++ b/tests/uranium_tests/tls/test_tls_check_dual_cert_selection2.py @@ -0,0 +1,211 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_tls_check_dual_cert_selection2(urtest: UraniumTest) -> None: + ''' + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import os + import re + + urtest.Summary = ''' + Test ATS offering both RSA and EC certificates + Combined key and cert files. Faulty key path + ''' + + # Define default ATS + ts = urtest.MakeATSProcess("ts", enable_tls=True) + server = urtest.MakeOriginServer("server", ssl=True) + dns = urtest.MakeDNServer("dns") + + request_header = {"headers": "GET / HTTP/1.1\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + server.addResponse("sessionlog.json", request_header, response_header) + + # add ssl materials like key, certificates for the server + ts.addSSLfile("ssl/signed-foo.pem") + ts.addSSLfile("ssl/signed-foo.key") + ts.addSSLfile("ssl/signed-foo-ec.pem") + ts.addSSLfile("ssl/signed-foo-ec.key") + ts.addSSLfile("ssl/signed-san.pem") + ts.addSSLfile("ssl/signed-san.key") + ts.addSSLfile("ssl/signed-san-ec.pem") + ts.addSSLfile("ssl/signed-san-ec.key") + ts.addSSLfile("ssl/combined-ec.pem") + ts.addSSLfile("ssl/combined.pem") + ts.addSSLfile("ssl/signer.pem") + ts.addSSLfile("ssl/signer.key") + + ts.Disk.remap_config.AddLine('map / https://foo.com:{1}'.format(ts.Variables.ssl_port, server.Variables.SSL_Port)) + + ts.Disk.ssl_multicert_yaml.AddLines( + """ + ssl_multicert: + - ssl_cert_name: combined-ec.pem,combined.pem + - ssl_cert_name: signed-foo-ec.pem,signed-foo.pem + - dest_ip: "*" + ssl_cert_name: signed-san-ec.pem,signed-san.pem + """.split("\n")) + + # Case 1, global config policy=permissive properties=signature + # override for foo.com policy=enforced properties=all + ts.Disk.records_config.update( + { + 'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir), + 'proxy.config.ssl.server.private_key.path': '/tmp', # Faulty key path should not matter, since there are no key files + 'proxy.config.ssl.server.cipher_suite': 'ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256', + 'proxy.config.url_remap.pristine_host_hdr': 1, + 'proxy.config.dns.nameservers': '127.0.0.1:{0}'.format(dns.Variables.Port), + 'proxy.config.exec_thread.autoconfig.scale': 1.0, + 'proxy.config.dns.resolv_conf': 'NULL', + 'proxy.config.diags.debug.tags': 'ssl', + 'proxy.config.diags.debug.enabled': 0 + }) + + dns.addRecords(records={"foo.com.": ["127.0.0.1"]}) + dns.addRecords(records={"bar.com.": ["127.0.0.1"]}) + + foo_ec_string = "" + foo_rsa_string = "" + san_ec_string = "" + san_rsa_string = "" + combo_ec_string = "" + combo_rsa_string = "" + with open(os.path.join(urtest.TestDirectory, 'ssl', 'signed-foo-ec.pem'), 'r') as myfile: + file_string = myfile.read() + cert_end = file_string.find("END CERTIFICATE-----") + foo_ec_string = re.escape(file_string[0:cert_end]) + with open(os.path.join(urtest.TestDirectory, 'ssl', 'signed-foo.pem'), 'r') as myfile: + file_string = myfile.read() + cert_end = file_string.find("END CERTIFICATE-----") + foo_rsa_string = re.escape(file_string[0:cert_end]) + with open(os.path.join(urtest.TestDirectory, 'ssl', 'signed-san-ec.pem'), 'r') as myfile: + file_string = myfile.read() + cert_end = file_string.find("END CERTIFICATE-----") + san_ec_string = re.escape(file_string[0:cert_end]) + with open(os.path.join(urtest.TestDirectory, 'ssl', 'signed-san.pem'), 'r') as myfile: + file_string = myfile.read() + cert_end = file_string.find("END CERTIFICATE-----") + san_rsa_string = re.escape(file_string[0:cert_end]) + with open(os.path.join(urtest.TestDirectory, 'ssl', 'combined-ec.pem'), 'r') as myfile: + file_string = myfile.read() + cert_end = file_string.find("END CERTIFICATE-----") + combo_ec_string = re.escape(file_string[0:cert_end]) + with open(os.path.join(urtest.TestDirectory, 'ssl', 'combined.pem'), 'r') as myfile: + file_string = myfile.read() + cert_end = file_string.find("END CERTIFICATE-----") + combo_rsa_string = re.escape(file_string[0:cert_end]) + + # Should receive a EC cert since ATS cipher list prefers EC + tr = urtest.AddTestRun("Default for foo should return EC cert") + tr.Setup.Copy("ssl/signer.pem") + tr.Processes.Default.Command = "echo foo | openssl s_client -tls1_2 -servername foo.com -connect 127.0.0.1:{0}".format( + ts.Variables.ssl_port, foo_ec_string) + tr.ReturnCode = 0 + tr.Processes.Default.StartBefore(server) + tr.Processes.Default.StartBefore(dns) + tr.Processes.Default.StartBefore(urtest.Processes.ts, ready=When.PortOpen(ts.Variables.ssl_port)) + tr.StillRunningAfter = server + tr.StillRunningAfter = ts + tr.Processes.Default.Streams.All += Testers.ContainsExpression(foo_ec_string, "Should select EC cert", reflags=re.S | re.M) + + # Should receive a RSA cert + tr = urtest.AddTestRun("Only offer RSA ciphers, should receive RSA cert") + tr.Processes.Default.Command = "echo foo | openssl s_client -tls1_2 -servername foo.com -cipher 'ECDHE-RSA-AES128-GCM-SHA256' -connect 127.0.0.1:{0}".format( + ts.Variables.ssl_port) + tr.ReturnCode = 0 + tr.StillRunningAfter = server + tr.StillRunningAfter = ts + tr.Processes.Default.Streams.All += Testers.ContainsExpression(foo_rsa_string, "Should select RSA cert", reflags=re.S | re.M) + + # Should receive a EC cert + tr = urtest.AddTestRun("Default for two.com should return EC cert") + tr.Processes.Default.Command = "echo foo | openssl s_client -tls1_2 -servername two.com -connect 127.0.0.1:{0}".format( + ts.Variables.ssl_port) + tr.ReturnCode = 0 + tr.StillRunningAfter = server + tr.StillRunningAfter = ts + tr.Processes.Default.Streams.All += Testers.ContainsExpression(san_ec_string, "Should select EC cert", reflags=re.S | re.M) + tr.Processes.Default.Streams.All += Testers.ContainsExpression("CN ?= ?group.com", "Should select a group SAN") + + # Should receive a RSA cert + tr = urtest.AddTestRun("Only offer RSA ciphers, should receive RSA cert") + tr.Processes.Default.Command = "echo foo | openssl s_client -tls1_2 -servername two.com -cipher 'ECDHE-RSA-AES128-GCM-SHA256' -connect 127.0.0.1:{0}".format( + ts.Variables.ssl_port) + tr.ReturnCode = 0 + tr.StillRunningAfter = server + tr.StillRunningAfter = ts + tr.Processes.Default.Streams.All += Testers.ContainsExpression(san_rsa_string, "Should select RSA cert", reflags=re.S | re.M) + tr.Processes.Default.Streams.All += Testers.ContainsExpression("CN ?= ?group.com", "Should select a group SAN") + + # Should receive a RSA cert + tr = urtest.AddTestRun("rsa.com only in rsa cert") + tr.Processes.Default.Command = "echo foo | openssl s_client -tls1_2 -servername rsa.com -connect 127.0.0.1:{0}".format( + ts.Variables.ssl_port) + tr.ReturnCode = 0 + tr.StillRunningAfter = server + tr.StillRunningAfter = ts + tr.Processes.Default.Streams.All += Testers.ContainsExpression(san_rsa_string, "Should select RSA cert", reflags=re.S | re.M) + tr.Processes.Default.Streams.All += Testers.ContainsExpression("CN ?= ?group.com", "Should select a group SAN") + + # Should receive a EC cert + tr = urtest.AddTestRun("ec.com only in ec cert") + tr.Processes.Default.Command = "echo foo | openssl s_client -tls1_2 -servername ec.com -connect 127.0.0.1:{0}".format( + ts.Variables.ssl_port) + tr.ReturnCode = 0 + tr.StillRunningAfter = server + tr.StillRunningAfter = ts + tr.Processes.Default.Streams.All += Testers.ContainsExpression(san_ec_string, "Should select EC cert", reflags=re.S | re.M) + tr.Processes.Default.Streams.All += Testers.ContainsExpression("CN ?= ?group.com", "Should select a group SAN") + + # Should receive a EC cert + tr = urtest.AddTestRun("Default for combined.com should return EC cert") + tr.Processes.Default.Command = "echo foo | openssl s_client -tls1_2 -servername combined.com -connect 127.0.0.1:{0}".format( + ts.Variables.ssl_port) + tr.ReturnCode = 0 + tr.StillRunningAfter = server + tr.StillRunningAfter = ts + tr.Processes.Default.Streams.All += Testers.ContainsExpression(combo_ec_string, "Should select EC cert", reflags=re.S | re.M) + tr.Processes.Default.Streams.All += Testers.ContainsExpression("CN ?= ?combined.com", "Should select combined pem") + + # Should receive a RSA cert + tr = urtest.AddTestRun("Only offer RSA ciphers, should receive RSA cert") + tr.Processes.Default.Command = "echo foo | openssl s_client -tls1_2 -servername combined.com -cipher 'ECDHE-RSA-AES128-GCM-SHA256' -connect 127.0.0.1:{0}".format( + ts.Variables.ssl_port) + tr.ReturnCode = 0 + tr.StillRunningAfter = server + tr.StillRunningAfter = ts + tr.Processes.Default.Streams.All += Testers.ContainsExpression(combo_rsa_string, "Should select RSA cert", reflags=re.S | re.M) + tr.Processes.Default.Streams.All += Testers.ContainsExpression("CN ?= ?combined.com", "Should select combined pem") + urtest.execute() diff --git a/tests/uranium_tests/tls/test_tls_check_dual_cert_selection_plugin.py b/tests/uranium_tests/tls/test_tls_check_dual_cert_selection_plugin.py new file mode 100644 index 00000000000..65d11b6c864 --- /dev/null +++ b/tests/uranium_tests/tls/test_tls_check_dual_cert_selection_plugin.py @@ -0,0 +1,226 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_tls_check_dual_cert_selection_plugin(urtest: UraniumTest) -> None: + ''' + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import os + + urtest.Summary = ''' + Test ATS offering both RSA and EC certificates loaded via plugin + ''' + + urtest.SkipUnless(Condition.HasOpenSSLVersion('1.1.1')) + + # Define default ATS + ts = urtest.MakeATSProcess("ts", enable_tls=True) + server = urtest.MakeOriginServer("server", ssl=True) + dns = urtest.MakeDNServer("dns") + + request_header = {"headers": "GET / HTTP/1.1\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + server.addResponse("sessionlog.json", request_header, response_header) + + # add ssl materials like key, certificates for the server + ts.addSSLfile("ssl/signed-foo.pem") + ts.addSSLfile("ssl/signed2-foo.pem") + ts.addSSLfile("ssl/signed-foo.key") + ts.addSSLfile("ssl/signed-foo-ec.pem") + ts.addSSLfile("ssl/signed-foo-ec.key") + ts.addSSLfile("ssl/signed-san.pem") + ts.addSSLfile("ssl/signed-san.key") + ts.addSSLfile("ssl/signed-san-ec.pem") + ts.addSSLfile("ssl/signed-san-ec.key") + ts.addSSLfile("ssl/signer.pem") + ts.addSSLfile("ssl/signer.key") + ts.addSSLfile("ssl/server.pem") + ts.addSSLfile("ssl/server.key") + + ts.Disk.remap_config.AddLine('map / https://foo.com:{1}'.format(ts.Variables.ssl_port, server.Variables.SSL_Port)) + + ts.Disk.ssl_multicert_yaml.AddLines( + """ + ssl_multicert: + - ssl_cert_name: signed-foo-ec.pem,signed-foo.pem + ssl_key_name: signed-foo-ec.key,signed-foo.key + - ssl_cert_name: signed-san-ec.pem,signed-san.pem + ssl_key_name: signed-san-ec.key,signed-san.key + - dest_ip: "*" + ssl_cert_name: server.pem + ssl_key_name: server.key + """.split("\n")) + + urtest.PrepareTestPlugin(os.path.join(urtest.Variables.AtsTestPluginsDir, 'ssl_secret_load_test.so'), ts) + + # Case 1, global config policy=permissive properties=signature + # override for foo.com policy=enforced properties=all + ts.Disk.records_config.update( + { + 'proxy.config.ssl.server.cert.path': '{0}/../'.format(ts.Variables.SSLDir), + 'proxy.config.ssl.server.private_key.path': '{0}/../'.format(ts.Variables.SSLDir), + 'proxy.config.ssl.server.cipher_suite': + 'ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-SHA384:AES128-GCM-SHA256:AES256-GCM-SHA384:ECDHE-RSA-RC4-SHA:ECDHE-RSA-AES128-SHA:ECDHE-RSA-AES256-SHA:RC4-SHA:RC4-MD5:AES128-SHA:AES256-SHA:DES-CBC3-SHA!SRP:!DSS:!PSK:!aNULL:!eNULL:!SSLv2', + 'proxy.config.url_remap.pristine_host_hdr': 1, + 'proxy.config.dns.nameservers': '127.0.0.1:{0}'.format(dns.Variables.Port), + 'proxy.config.exec_thread.autoconfig.scale': 1.0, + 'proxy.config.dns.resolv_conf': 'NULL', + 'proxy.config.diags.debug.tags': 'ssl_secret_load_test', + 'proxy.config.diags.debug.enabled': 1, + }) + + dns.addRecords(records={"foo.com.": ["127.0.0.1"]}) + dns.addRecords(records={"bar.com.": ["127.0.0.1"]}) + + # Should receive a EC cert + tr = urtest.AddTestRun("Default for foo should return EC cert") + tr.Setup.Copy("ssl/signer.pem") + tr.Setup.Copy("ssl/signer2.pem") + tr.Processes.Default.Command = "echo foo | openssl s_client -CAfile signer.pem -servername foo.com -connect 127.0.0.1:{0}".format( + ts.Variables.ssl_port) + tr.ReturnCode = 0 + tr.Processes.Default.StartBefore(server) + tr.Processes.Default.StartBefore(dns) + tr.Processes.Default.StartBefore(urtest.Processes.ts, ready=When.PortOpen(ts.Variables.ssl_port)) + tr.StillRunningAfter = server + tr.StillRunningAfter = ts + tr.Processes.Default.Streams.All += Testers.ContainsExpression("Peer signature type: (ECDSA|ecdsa_)", "Should select EC cert") + tr.Processes.Default.Streams.All += Testers.ExcludesExpression("unable to verify the first certificate", "Correct signer") + + # Should receive a RSA cert + tr = urtest.AddTestRun("Only offer RSA ciphers, should receive RSA cert") + tr.Processes.Default.Command = "echo foo | openssl s_client -CAfile signer.pem -servername foo.com -sigalgs 'RSA-PSS+SHA256' -connect 127.0.0.1:{0}".format( + ts.Variables.ssl_port) + tr.ReturnCode = 0 + tr.StillRunningAfter = server + tr.StillRunningAfter = ts + tr.Processes.Default.Streams.All += Testers.ContainsExpression( + "Peer signature type: (RSA-PSS|rsa_pss_)", "Should select RSA cert") + tr.Processes.Default.Streams.All += Testers.ExcludesExpression("unable to verify the first certificate", "Correct signer") + + # Should receive a EC cert + tr = urtest.AddTestRun("Default for one.com should return EC cert") + tr.Processes.Default.Command = "echo foo | openssl s_client -CAfile signer.pem -servername one.com -connect 127.0.0.1:{0}".format( + ts.Variables.ssl_port) + tr.ReturnCode = 0 + tr.StillRunningAfter = server + tr.StillRunningAfter = ts + tr.Processes.Default.Streams.All += Testers.ContainsExpression("Peer signature type: (ECDSA|ecdsa_)", "Should select EC cert") + tr.Processes.Default.Streams.All += Testers.ContainsExpression("CN ?= ?group.com", "Should select a group SAN") + tr.Processes.Default.Streams.All += Testers.ExcludesExpression("unable to verify the first certificate", "Correct signer") + + # Should receive a RSA cert + tr = urtest.AddTestRun("Only offer RSA ciphers, should receive RSA cert") + tr.Processes.Default.Command = "echo foo | openssl s_client -CAfile signer.pem -servername one.com -sigalgs 'RSA-PSS+SHA256' -connect 127.0.0.1:{0}".format( + ts.Variables.ssl_port) + tr.ReturnCode = 0 + tr.StillRunningAfter = server + tr.StillRunningAfter = ts + tr.Processes.Default.Streams.All += Testers.ContainsExpression( + "Peer signature type: (RSA-PSS|rsa_pss_)", "Should select RSA cert") + tr.Processes.Default.Streams.All += Testers.ContainsExpression("CN ?= ?group.com", "Should select a group SAN") + tr.Processes.Default.Streams.All += Testers.ExcludesExpression("unable to verify the first certificate", "Correct signer") + + # Should receive a RSA cert + tr = urtest.AddTestRun("rsa.com only in rsa cert") + tr.Processes.Default.Command = "echo foo | openssl s_client -CAfile signer.pem -servername rsa.com -connect 127.0.0.1:{0}".format( + ts.Variables.ssl_port) + tr.ReturnCode = 0 + tr.StillRunningAfter = server + tr.StillRunningAfter = ts + tr.Processes.Default.Streams.All += Testers.ContainsExpression( + "Peer signature type: (RSA-PSS|rsa_pss_)", "Should select RSA cert") + tr.Processes.Default.Streams.All += Testers.ContainsExpression("CN ?= ?group.com", "Should select a group SAN") + tr.Processes.Default.Streams.All += Testers.ExcludesExpression("unable to verify the first certificate", "Correct signer") + + # Should receive a EC cert + tr = urtest.AddTestRun("ec.com only in ec cert") + tr.Processes.Default.Command = "echo foo | openssl s_client -CAfile signer.pem -servername ec.com -connect 127.0.0.1:{0}".format( + ts.Variables.ssl_port) + tr.ReturnCode = 0 + tr.StillRunningAfter = server + tr.StillRunningAfter = ts + tr.Processes.Default.Streams.All += Testers.ContainsExpression("Peer signature type: (ECDSA|ecdsa_)", "Should select EC cert") + tr.Processes.Default.Streams.All += Testers.ContainsExpression("CN ?= ?group.com", "Should select a group SAN") + tr.Processes.Default.Streams.All += Testers.ExcludesExpression("unable to verify the first certificate", "Correct signer") + + # Copy in a new version of the foo.com cert. Replace it with the version + # signed by signer 2. Wait at least a second to sure the file update time + # differs + trupdate = urtest.AddTestRun("Update server bar cert file in place") + trupdate.StillRunningAfter = ts + trupdate.StillRunningAfter = server + trupdate.Setup.CopyAs("ssl/signed2-foo.pem", ".", "{0}/signed-foo.pem".format(ts.Variables.SSLDir)) + # For some reason the Setup.CopyAs does not change the modification time, so we touch + trupdate.Processes.Default.Command = 'touch {0}/signed-foo.pem'.format(ts.Variables.SSLDir) + # Need to copy over the environment so traffic_ctl knows where to find the unix domain socket + trupdate.Processes.Default.Env = ts.Env + trupdate.Processes.Default.ReturnCode = 0 + + # The plugin will pull every 3 seconds. So wait 4 seconds and test again. Request with CA=signer2.pem should work. Request with CA=signer.pem should fail + # Should receive a RSA cert + tr = urtest.AddTestRun("Only offer RSA ciphers, should receive RSA cert") + tr.Processes.Default.Command = "echo foo | openssl s_client -CAfile signer.pem -servername foo.com -sigalgs 'RSA-PSS+SHA256' -connect 127.0.0.1:{0}".format( + ts.Variables.ssl_port) + tr.DelayStart = 4 + tr.ReturnCode = 0 + tr.StillRunningAfter = server + tr.StillRunningAfter = ts + tr.Processes.Default.Streams.All += Testers.ContainsExpression( + "Peer signature type: (RSA-PSS|rsa_pss_)", "Should select RSA cert") + tr.Processes.Default.Streams.All += Testers.ContainsExpression("CN ?= ?foo.com", "Should select foo.com") + tr.Processes.Default.Streams.All += Testers.ContainsExpression("unable to verify the first certificate", "Different signer") + + tr = urtest.AddTestRun("Only offer RSA ciphers, should receive RSA cert with correct CA") + tr.Processes.Default.Command = "echo foo | openssl s_client -CAfile signer2.pem -servername foo.com -sigalgs 'RSA-PSS+SHA256' -connect 127.0.0.1:{0}".format( + ts.Variables.ssl_port) + tr.ReturnCode = 0 + tr.StillRunningAfter = server + tr.StillRunningAfter = ts + tr.Processes.Default.Streams.All += Testers.ContainsExpression( + "Peer signature type: (RSA-PSS|rsa_pss_)", "Should select RSA cert") + tr.Processes.Default.Streams.All += Testers.ContainsExpression("CN ?= ?foo.com", "Should select foo.com") + tr.Processes.Default.Streams.All += Testers.ExcludesExpression("unable to verify the first certificate", "Correct signer") + + # The EC case should be unchanged + tr = urtest.AddTestRun("Offer any cipher") + tr.Processes.Default.Command = "echo foo | openssl s_client -CAfile signer.pem -servername foo.com -connect 127.0.0.1:{0}".format( + ts.Variables.ssl_port) + tr.ReturnCode = 0 + tr.StillRunningAfter = server + tr.StillRunningAfter = ts + tr.Processes.Default.Streams.All += Testers.ContainsExpression("Peer signature type: (ECDSA|ecdsa_)", "Should select EC cert") + tr.Processes.Default.Streams.All += Testers.ContainsExpression("CN ?= ?foo.com", "Should select foo.com") + tr.Processes.Default.Streams.All += Testers.ExcludesExpression("unable to verify the first certificate", "Correct signer") + urtest.execute() diff --git a/tests/uranium_tests/tls/test_tls_client_alpn_configuration.py b/tests/uranium_tests/tls/test_tls_client_alpn_configuration.py new file mode 100644 index 00000000000..df0817a40d2 --- /dev/null +++ b/tests/uranium_tests/tls/test_tls_client_alpn_configuration.py @@ -0,0 +1,183 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_tls_client_alpn_configuration(urtest: UraniumTest) -> None: + """Verify ALPN to origin functionality.""" + + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + from typing import Optional + + urtest.Summary = __doc__ + + class TestAlpnFunctionality: + """Define an object to test a set of ALPN functionality.""" + + _replay_file: str = 'tls_client_alpn_configuration.replay.yaml' + _server_counter: int = 0 + _ts_counter: int = 0 + _client_counter: int = 0 + + def __init__( + self, + records_config_alpn: Optional[str] = None, + conf_remap_alpn: Optional[str] = None, + alpn_is_malformed: bool = False): + """Declare the various test Processes. + + :param records_config_alpn: The string with which to configure the ATS + ALPN via proxy.config.ssl.client.alpn_protocols in the records.yaml. + If the paramenter is None, then no ALPN configuration will be + explicitly set and ATS will use the default value. + + :param conf_remap_alpn: The string with which to configure the Traffic + Server ALPN proxy.config.http.alpn_protocols configuration via + conf_remap. If the parameter is None, then no conf_remap configuration + will be set. + + :param alpn_is_malformed: If True, then the configured ALPN string in + the records.yaml will be malformed. The TestRun will be configured to + expect a warning and the server will be configured to receive no ALPN. + """ + self._alpn = records_config_alpn + self._alpn_conf_remap_alpn = conf_remap_alpn + self._alpn_is_malformed = alpn_is_malformed + + configured_alpn = records_config_alpn if conf_remap_alpn is None else conf_remap_alpn + if alpn_is_malformed: + configured_alpn = None + self._server = self._configure_server(configured_alpn) + + self._ts = self._configure_trafficserver(records_config_alpn, conf_remap_alpn, alpn_is_malformed) + + def _configure_server(self, expected_alpn: Optional[str] = None): + """Configure the test server. + + :param expected_alpn: The ALPN expected from the client. If this is + None, then the server will not expect an ALPN value. + """ + server = urtest.MakeVerifierServerProcess(f'server-{TestAlpnFunctionality._server_counter}', self._replay_file) + TestAlpnFunctionality._server_counter += 1 + + if expected_alpn is None: + server.Streams.stdout = Testers.ContainsExpression('Negotiated ALPN: none', 'Verify that ATS sent no ALPN string.') + else: + protocols = expected_alpn.split(',') + for protocol in protocols: + server.Streams.stdout = Testers.ContainsExpression( + f'ALPN.*:.*{protocol}', 'Verify that the server parsed the configured ALPN string from ATS.') + return server + + def _configure_trafficserver( + self, + records_config_alpn: Optional[str] = None, + conf_remap_alpn: Optional[str] = None, + alpn_is_malformed: bool = False): + """Configure a Traffic Server process. + + :param records_config_alpn: See the description of this parameter in + TestAlpnFunctionality._init__. + """ + ts = urtest.MakeATSProcess(f'ts-{TestAlpnFunctionality._ts_counter}', enable_tls=True, enable_cache=False) + TestAlpnFunctionality._ts_counter += 1 + + ts.addDefaultSSLFiles() + ts.Disk.records_config.update( + { + "proxy.config.ssl.server.cert.path": f'{ts.Variables.SSLDir}', + "proxy.config.ssl.server.private_key.path": f'{ts.Variables.SSLDir}', + "proxy.config.ssl.client.verify.server.policy": 'PERMISSIVE', + 'proxy.config.diags.debug.enabled': 3, + 'proxy.config.diags.debug.tags': 'ssl|http', + }) + + if records_config_alpn is not None: + ts.Disk.records_config.update({ + 'proxy.config.ssl.client.alpn_protocols': records_config_alpn, + }) + + ts.Disk.ssl_multicert_yaml.AddLines( + """ + ssl_multicert: + - dest_ip: "*" + ssl_cert_name: server.pem + ssl_key_name: server.key + """.split("\n")) + + conf_remap_specification = '' + if conf_remap_alpn is not None: + conf_remap_specification = ( + '@plugin=conf_remap.so ' + f'@pparam=proxy.config.ssl.client.alpn_protocols={conf_remap_alpn}') + + ts.Disk.remap_config.AddLine(f'map / https://127.0.0.1:{self._server.Variables.https_port} {conf_remap_specification}') + + if alpn_is_malformed: + ts.Disk.diags_log.Content = Testers.ContainsExpression("ERROR.*ALPN", "There should be no ALPN parse warnings.") + else: + ts.Disk.diags_log.Content += Testers.ExcludesExpression("ERROR.*ALPN", "There should be no ALPN parse warnings.") + + return ts + + def run(self): + """Configure the TestRun.""" + description = "default" if self._alpn is None else self._alpn + tr = urtest.AddTestRun(f'ATS ALPN configuration: {description}') + tr.Processes.Default.StartBefore(self._server) + tr.Processes.Default.StartBefore(self._ts) + + tr.AddVerifierClientProcess( + f'client-{TestAlpnFunctionality._client_counter}', self._replay_file, https_ports=[self._ts.Variables.ssl_port]) + TestAlpnFunctionality._client_counter += 1 + + # + # Test default configuration. + # + TestAlpnFunctionality().run() + + # + # Test various valid ALPN configurations. + # + TestAlpnFunctionality(records_config_alpn='http/1.1').run() + TestAlpnFunctionality(records_config_alpn='http/1.1,http/1.0').run() + TestAlpnFunctionality(records_config_alpn='http/1.1', conf_remap_alpn='http/1.1,http/1.0').run() + TestAlpnFunctionality(records_config_alpn='h2,http/1.1').run() + TestAlpnFunctionality(records_config_alpn='h2').run() + + # + # Test malformed ALPN configurations. + # + TestAlpnFunctionality(records_config_alpn='not_a_protocol', alpn_is_malformed=True).run() + # Note that HTTP/3 to origin is not currently supported. + TestAlpnFunctionality(records_config_alpn='h3', alpn_is_malformed=True).run() + urtest.execute() diff --git a/tests/uranium_tests/tls/test_tls_client_cert.py b/tests/uranium_tests/tls/test_tls_client_cert.py new file mode 100644 index 00000000000..08c5b83dec1 --- /dev/null +++ b/tests/uranium_tests/tls/test_tls_client_cert.py @@ -0,0 +1,315 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_tls_client_cert(urtest: UraniumTest) -> None: + ''' + Test offering client cert to origin + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + urtest.Summary = ''' + Test different combinations of TLS handshake hooks to ensure they are applied consistently. + ''' + + ts = urtest.MakeATSProcess("ts") + cafile = "{0}/signer.pem".format(urtest.RunDirectory) + cafile2 = "{0}/signer2.pem".format(urtest.RunDirectory) + # --clientverify: "" empty string because microserver does store_true for argparse, but options is a dictionary + server = urtest.MakeOriginServer( + "server", + ssl=True, + options={ + "--clientCA": cafile, + "--clientverify": "" + }, + clientcert="{0}/signed-foo.pem".format(urtest.RunDirectory), + clientkey="{0}/signed-foo.key".format(urtest.RunDirectory)) + server2 = urtest.MakeOriginServer( + "server2", + ssl=True, + options={ + "--clientCA": cafile2, + "--clientverify": "" + }, + clientcert="{0}/signed2-bar.pem".format(urtest.RunDirectory), + clientkey="{0}/signed-bar.key".format(urtest.RunDirectory)) + server3 = urtest.MakeOriginServer("server3") + server4 = urtest.MakeOriginServer("server4") + server.Setup.Copy("ssl/signer.pem") + server.Setup.Copy("ssl/signer2.pem") + server.Setup.Copy("ssl/signed-foo.pem") + server.Setup.Copy("ssl/signed-foo.key") + server.Setup.Copy("ssl/signed2-foo.pem") + server.Setup.Copy("ssl/signed2-bar.pem") + server.Setup.Copy("ssl/signed-bar.key") + server2.Setup.Copy("ssl/signer.pem") + server2.Setup.Copy("ssl/signer2.pem") + server2.Setup.Copy("ssl/signed-foo.pem") + server2.Setup.Copy("ssl/signed-foo.key") + server2.Setup.Copy("ssl/signed2-foo.pem") + server2.Setup.Copy("ssl/signed2-bar.pem") + server2.Setup.Copy("ssl/signed-bar.key") + + request_header = {"headers": "GET / HTTP/1.1\r\nHost: example.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + server.addResponse("sessionlog.json", request_header, response_header) + request_header = {"headers": "GET / HTTP/1.1\r\nHost: bar.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + server.addResponse("sessionlog.json", request_header, response_header) + + ts.addSSLfile("ssl/server.pem") + ts.addSSLfile("ssl/server.key") + ts.addSSLfile("ssl/signed-foo.pem") + ts.addSSLfile("ssl/signed-foo.key") + ts.addSSLfile("ssl/signed2-foo.pem") + ts.addSSLfile("ssl/signed-bar.pem") + ts.addSSLfile("ssl/signed2-bar.pem") + ts.addSSLfile("ssl/signed-bar.key") + + ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'ssl_verify_test', + 'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir), + 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts.Variables.SSLDir), + 'proxy.config.ssl.client.cert.path': '{0}'.format(ts.Variables.SSLDir), + 'proxy.config.ssl.client.cert.filename': 'signed-foo.pem', + 'proxy.config.ssl.client.private_key.path': '{0}'.format(ts.Variables.SSLDir), + 'proxy.config.ssl.client.private_key.filename': 'signed-foo.key', + 'proxy.config.exec_thread.autoconfig.scale': 1.0, + 'proxy.config.url_remap.pristine_host_hdr': 1, + 'proxy.config.ssl.client.verify.server.policy': 'PERMISSIVE', + }) + + ts.Disk.ssl_multicert_yaml.AddLines( + """ + ssl_multicert: + - dest_ip: "*" + ssl_cert_name: server.pem + ssl_key_name: server.key + """.split("\n")) + + ts.Disk.remap_config.AddLine('map /case1 https://127.0.0.1:{0}/'.format(server.Variables.SSL_Port)) + ts.Disk.remap_config.AddLine('map /case2 https://127.0.0.1:{0}/'.format(server2.Variables.SSL_Port)) + + ts.Disk.sni_yaml.AddLine('sni:') + ts.Disk.sni_yaml.AddLine('- fqdn: bar.com') + ts.Disk.sni_yaml.AddLine(' client_cert: {0}/signed2-bar.pem'.format(ts.Variables.SSLDir)) + ts.Disk.sni_yaml.AddLine(' client_key: {0}/signed-bar.key'.format(ts.Variables.SSLDir)) + + ts.Disk.logging_yaml.AddLines( + ''' + logging: + formats: + - name: testformat + format: '% % % %' + logs: + - mode: ascii + format: testformat + filename: squid + '''.split("\n")) + + # Should succeed + tr = urtest.AddTestRun("Connect with first client cert to first server") + tr.Processes.Default.StartBefore(urtest.Processes.ts) + tr.Processes.Default.StartBefore(server) + tr.Processes.Default.StartBefore(server2) + tr.StillRunningAfter = ts + tr.StillRunningAfter = server + tr.StillRunningAfter = server2 + tr.MakeCurlCommand("-H host:example.com http://127.0.0.1:{0}/case1".format(ts.Variables.port), ts=ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.stdout = Testers.ExcludesExpression("Could Not Connect", "Check response") + + # Should fail + trfail = urtest.AddTestRun("Connect with first client cert to second server") + trfail.StillRunningAfter = ts + trfail.StillRunningAfter = server + trfail.StillRunningAfter = server2 + trfail.MakeCurlCommand('-H host:example.com http://127.0.0.1:{0}/case2'.format(ts.Variables.port), ts=ts) + trfail.Processes.Default.ReturnCode = 0 + trfail.Processes.Default.Streams.stdout = Testers.ContainsExpression("Could Not Connect", "Check response") + + # Should succeed + trbar = urtest.AddTestRun("Connect with signed2 bar to second server") + trbar.StillRunningAfter = ts + trbar.StillRunningAfter = server + trbar.StillRunningAfter = server2 + trbar.MakeCurlCommand("-H host:bar.com http://127.0.0.1:{0}/case2".format(ts.Variables.port), ts=ts) + trbar.Processes.Default.ReturnCode = 0 + trbar.Processes.Default.Streams.stdout = Testers.ExcludesExpression("Could Not Connect", "Check response") + + # Should fail + trbarfail = urtest.AddTestRun("Connect with signed2 bar cert to first server") + trbarfail.StillRunningAfter = ts + trbarfail.StillRunningAfter = server + trbarfail.StillRunningAfter = server2 + trbarfail.MakeCurlCommand('-H host:bar.com http://127.0.0.1:{0}/case1'.format(ts.Variables.port), ts=ts) + trbarfail.Processes.Default.ReturnCode = 0 + trbarfail.Processes.Default.Streams.stdout = Testers.ContainsExpression("Could Not Connect", "Check response") + + tr2 = urtest.AddTestRun("Update config files") + # Update the SNI config + snipath = ts.Disk.sni_yaml.AbsPath + recordspath = ts.Disk.records_config.AbsPath + tr2.Disk.File(snipath, id="sni_yaml", typename="ats:config"), + tr2.Disk.sni_yaml.AddLine('sni:') + tr2.Disk.sni_yaml.AddLine('- fqdn: bar.com') + tr2.Disk.sni_yaml.AddLine(' client_cert: {0}/signed-bar.pem'.format(ts.Variables.SSLDir)) + tr2.Disk.sni_yaml.AddLine(' client_key: {0}/signed-bar.key'.format(ts.Variables.SSLDir)) + # recreate the records.yaml with the cert filename changed + tr2.Disk.File(recordspath, id="records_config", typename="ats:config:records"), + tr2.Disk.records_config.update( + { + 'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir), + 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts.Variables.SSLDir), + 'proxy.config.ssl.client.cert.path': '{0}'.format(ts.Variables.SSLDir), + 'proxy.config.ssl.client.cert.filename': 'signed2-foo.pem', + 'proxy.config.ssl.client.private_key.path': '{0}'.format(ts.Variables.SSLDir), + 'proxy.config.ssl.client.private_key.filename': 'signed-foo.key', + 'proxy.config.url_remap.pristine_host_hdr': 1, + 'proxy.config.ssl.client.verify.server.policy': 'PERMISSIVE', + }) + tr2.StillRunningAfter = ts + tr2.StillRunningAfter = server + tr2.StillRunningAfter = server2 + tr2.Processes.Default.Command = 'echo Updated configs' + # Need to copy over the environment so traffic_ctl knows where to find the unix domain socket + tr2.Processes.Default.Env = ts.Env + tr2.Processes.Default.ReturnCode = 0 + + tr2reload = urtest.AddConfigReload(ts, expect_tasks=["sni.yaml"], description="Reload config") + tr2reload.StillRunningAfter = server + tr2reload.StillRunningAfter = server2 + + # Should succeed + tr3bar = urtest.AddTestRun("Make request with other bar cert to first server") + tr3bar.Processes.Default.StartBefore(server3) + tr3bar.StillRunningAfter = ts + tr3bar.StillRunningAfter = server + tr3bar.StillRunningAfter = server2 + tr3bar.MakeCurlCommand(' -H host:bar.com http://127.0.0.1:{0}/case1'.format(ts.Variables.port), ts=ts) + tr3bar.Processes.Default.ReturnCode = 0 + tr3bar.Processes.Default.Streams.stdout = Testers.ExcludesExpression("Could Not Connect", "Check response") + + # Should fail + tr3barfail = urtest.AddTestRun("Make request with other bar cert to second server") + tr3barfail.StillRunningAfter = ts + tr3barfail.StillRunningAfter = server + tr3barfail.StillRunningAfter = server2 + tr3barfail.MakeCurlCommand(' -H host:bar.com http://127.0.0.1:{0}/case2'.format(ts.Variables.port), ts=ts) + tr3barfail.Processes.Default.ReturnCode = 0 + tr3barfail.Processes.Default.Streams.stdout = Testers.ContainsExpression("Could Not Connect", "Check response") + + # Should succeed + tr3 = urtest.AddTestRun("Make request with other cert to second server") + # Wait for the reload to complete + tr3.StillRunningAfter = ts + tr3.StillRunningAfter = server + tr3.StillRunningAfter = server2 + tr3.MakeCurlCommand(' -H host:example.com http://127.0.0.1:{0}/case2'.format(ts.Variables.port), ts=ts) + tr3.Processes.Default.ReturnCode = 0 + tr3.Processes.Default.Streams.stdout = Testers.ExcludesExpression("Could Not Connect", "Check response") + + # Should fail + tr3fail = urtest.AddTestRun("Make request with other cert to first server") + tr3fail.StillRunningAfter = ts + tr3fail.StillRunningAfter = server + tr3fail.StillRunningAfter = server2 + tr3fail.MakeCurlCommand(' -H host:example.com http://127.0.0.1:{0}/case1'.format(ts.Variables.port), ts=ts) + tr3fail.Processes.Default.ReturnCode = 0 + tr3fail.Processes.Default.Streams.stdout = Testers.ContainsExpression("Could Not Connect", "Check response") + + # Test the case of updating certificate contents without changing file name. + trupdate = urtest.AddTestRun("Update client cert file in place") + trupdate.StillRunningAfter = ts + trupdate.StillRunningAfter = server + trupdate.StillRunningAfter = server2 + # Make a meaningless config change on the path so the records.yaml reload logic will trigger + trupdate.Setup.CopyAs("ssl/signed2-bar.pem", ".", "{0}/signed-bar.pem".format(ts.Variables.SSLDir)) + # in the config/ssl directory for records.yaml + trupdate.Setup.CopyAs("ssl/signed-foo.pem", ".", "{0}/signed2-foo.pem".format(ts.Variables.SSLDir)) + trupdate.Processes.Default.Command = 'traffic_ctl config set proxy.config.ssl.client.cert.path {0}/; touch {1}'.format( + ts.Variables.SSLDir, snipath) + # Need to copy over the environment so traffic_ctl knows where to find the unix domain socket + trupdate.Processes.Default.Env = ts.Env + trupdate.Processes.Default.ReturnCode = 0 + + trreload = urtest.AddConfigReload(ts, expect_tasks=["sni.yaml"], description="Reload config after renaming certs") + trreload.StillRunningAfter = server + trreload.StillRunningAfter = server2 + + # Should succeed + tr4bar = urtest.AddTestRun("Make request with renamed bar cert to second server") + tr4bar.StillRunningAfter = ts + tr4bar.StillRunningAfter = server + tr4bar.StillRunningAfter = server2 + tr4bar.MakeCurlCommand(' -H host:bar.com http://127.0.0.1:{0}/case2'.format(ts.Variables.port), ts=ts) + tr4bar.Processes.Default.ReturnCode = 0 + tr4bar.Processes.Default.Streams.stdout = Testers.ExcludesExpression("Could Not Connect", "Check response") + + # Should fail + tr4barfail = urtest.AddTestRun("Make request with renamed bar cert to first server") + tr4barfail.StillRunningAfter = ts + tr4barfail.StillRunningAfter = server + tr4barfail.StillRunningAfter = server2 + tr4barfail.MakeCurlCommand(' -H host:bar.com http://127.0.0.1:{0}/case1'.format(ts.Variables.port), ts=ts) + tr4barfail.Processes.Default.ReturnCode = 0 + tr4barfail.Processes.Default.Streams.stdout = Testers.ContainsExpression("Could Not Connect", "Check response") + + # Should succeed + tr4 = urtest.AddTestRun("Make request with renamed foo cert to first server") + tr4.StillRunningAfter = ts + tr4.StillRunningAfter = server + tr4.StillRunningAfter = server2 + tr4.MakeCurlCommand(' -H host:example.com http://127.0.0.1:{0}/case1'.format(ts.Variables.port), ts=ts) + tr4.Processes.Default.ReturnCode = 0 + tr4.Processes.Default.Streams.stdout = Testers.ExcludesExpression("Could Not Connect", "Check response") + + # Should fail + tr4fail = urtest.AddTestRun("Make request with renamed foo cert to second server") + tr4fail.StillRunningAfter = ts + tr4fail.StillRunningAfter = server + tr4fail.StillRunningAfter = server2 + tr4fail.MakeCurlCommand(' -H host:example.com http://127.0.0.1:{0}/case2'.format(ts.Variables.port), ts=ts) + tr4fail.Processes.Default.ReturnCode = 0 + tr4fail.Processes.Default.Streams.stdout = Testers.ContainsExpression("Could Not Connect", "Check response") + + tr = urtest.AddTestRun("Wait for the access log to write out") + tr.Processes.Default.StartBefore(server4, ready=When.FileContains(ts.Disk.squid_log.Name, 'https', 12)) + tr.StillRunningAfter = ts + tr.Processes.Default.Command = 'echo "log file exists"' + tr.Processes.Default.ReturnCode = 0 + + ts.Disk.squid_log.Content = "gold/proxycert-accesslog.gold" + urtest.execute() diff --git a/tests/uranium_tests/tls/test_tls_client_cert2.py b/tests/uranium_tests/tls/test_tls_client_cert2.py new file mode 100644 index 00000000000..560fe27aeea --- /dev/null +++ b/tests/uranium_tests/tls/test_tls_client_cert2.py @@ -0,0 +1,230 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_tls_client_cert2(urtest: UraniumTest) -> None: + ''' + Test offering client cert to origin + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + urtest.Summary = ''' + Test client certs to origin selected via wildcard names in sni + ''' + + ts = urtest.MakeATSProcess("ts") + cafile = "{0}/signer.pem".format(urtest.RunDirectory) + cafile2 = "{0}/signer2.pem".format(urtest.RunDirectory) + server = urtest.MakeOriginServer( + "server", + ssl=True, + options={ + "--clientCA": cafile, + "--clientverify": "" + }, + clientcert="{0}/signed-foo.pem".format(urtest.RunDirectory), + clientkey="{0}/signed-foo.key".format(urtest.RunDirectory)) + server2 = urtest.MakeOriginServer( + "server2", + ssl=True, + options={ + "--clientCA": cafile2, + "--clientverify": "" + }, + clientcert="{0}/signed2-bar.pem".format(urtest.RunDirectory), + clientkey="{0}/signed-bar.key".format(urtest.RunDirectory)) + server4 = urtest.MakeOriginServer("server4") + server.Setup.Copy("ssl/signer.pem") + server.Setup.Copy("ssl/signer2.pem") + server.Setup.Copy("ssl/signed-foo.pem") + server.Setup.Copy("ssl/signed-foo.key") + server.Setup.Copy("ssl/signed2-foo.pem") + server.Setup.Copy("ssl/signed2-bar.pem") + server.Setup.Copy("ssl/signed-bar.key") + server2.Setup.Copy("ssl/signer.pem") + server2.Setup.Copy("ssl/signer2.pem") + server2.Setup.Copy("ssl/signed-foo.pem") + server2.Setup.Copy("ssl/signed-foo.key") + server2.Setup.Copy("ssl/signed2-foo.pem") + server2.Setup.Copy("ssl/signed2-bar.pem") + server2.Setup.Copy("ssl/signed-bar.key") + + request_header = {"headers": "GET / HTTP/1.1\r\nHost: example.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + server.addResponse("sessionlog.json", request_header, response_header) + request_header = {"headers": "GET / HTTP/1.1\r\nHost: bar.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + server.addResponse("sessionlog.json", request_header, response_header) + + ts.addSSLfile("ssl/server.pem") + ts.addSSLfile("ssl/server.key") + ts.addSSLfile("ssl/combo-signed-foo.pem") + ts.addSSLfile("ssl/signed-foo.pem") + ts.addSSLfile("ssl/signed-foo.key") + ts.addSSLfile("ssl/signed2-foo.pem") + ts.addSSLfile("ssl/signed-bar.pem") + ts.addSSLfile("ssl/signed2-bar.pem") + ts.addSSLfile("ssl/signed-bar.key") + + ts.Disk.records_config.update( + { + 'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir), + 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts.Variables.SSLDir), + 'proxy.config.ssl.client.cert.path': '{0}'.format(ts.Variables.SSLDir), + 'proxy.config.ssl.client.private_key.path': '{0}'.format(ts.Variables.SSLDir), + 'proxy.config.exec_thread.autoconfig.scale': 1.0, + 'proxy.config.url_remap.pristine_host_hdr': 1, + 'proxy.config.ssl.client.verify.server.policy': 'PERMISSIVE', + }) + + ts.Disk.ssl_multicert_yaml.AddLines( + """ + ssl_multicert: + - dest_ip: "*" + ssl_cert_name: server.pem + ssl_key_name: server.key + """.split("\n")) + + ts.Disk.remap_config.AddLine('map /case1 https://127.0.0.1:{0}/'.format(server.Variables.SSL_Port)) + ts.Disk.remap_config.AddLine('map /case2 https://127.0.0.1:{0}/'.format(server2.Variables.SSL_Port)) + + ts.Disk.sni_yaml.AddLines( + [ + 'sni:', + '- fqdn: bob.bar.com', + ' client_cert: signed-bar.pem', + ' client_key: signed-bar.key', + '- fqdn: "bob.foo.com"', + ' client_cert: {0}/combo-signed-foo.pem'.format(ts.Variables.SSLDir), + '- fqdn: "*.bar.com"', + ' client_cert: {0}/signed2-bar.pem'.format(ts.Variables.SSLDir), + ' client_key: {0}/signed-bar.key'.format(ts.Variables.SSLDir), + '- fqdn: "foo.com"', + ' client_cert: {0}/signed2-foo.pem'.format(ts.Variables.SSLDir), + ' client_key: {0}/signed-foo.key'.format(ts.Variables.SSLDir), + ]) + + ts.Disk.logging_yaml.AddLines( + ''' + logging: + formats: + - name: testformat + format: '% % % %' + logs: + - mode: ascii + format: testformat + filename: squid + '''.split("\n")) + + # Should succeed + tr = urtest.AddTestRun("bob.bar.com to server 1") + tr.Processes.Default.StartBefore(urtest.Processes.ts) + tr.Processes.Default.StartBefore(server) + tr.Processes.Default.StartBefore(server2) + tr.StillRunningAfter = ts + tr.StillRunningAfter = server + tr.StillRunningAfter = server2 + tr.MakeCurlCommand("-H host:bob.bar.com http://127.0.0.1:{0}/case1".format(ts.Variables.port), ts=ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.stdout = Testers.ExcludesExpression("Could Not Connect", "Check response") + + # Should fail + trfail = urtest.AddTestRun("bob.bar.com to server 2") + trfail.StillRunningAfter = ts + trfail.StillRunningAfter = server + trfail.StillRunningAfter = server2 + trfail.MakeCurlCommand('-H host:bob.bar.com http://127.0.0.1:{0}/case2'.format(ts.Variables.port), ts=ts) + trfail.Processes.Default.ReturnCode = 0 + trfail.Processes.Default.Streams.stdout = Testers.ContainsExpression("Could Not Connect", "Check response") + + # Should succeed + tr = urtest.AddTestRun("bob.foo.com to server 1") + tr.StillRunningAfter = ts + tr.StillRunningAfter = server + tr.StillRunningAfter = server2 + tr.MakeCurlCommand("-H host:bob.foo.com http://127.0.0.1:{0}/case1".format(ts.Variables.port), ts=ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.stdout = Testers.ExcludesExpression("Could Not Connect", "Check response") + + # Should fail + trfail = urtest.AddTestRun("bob.foo.com to server 2") + trfail.StillRunningAfter = ts + trfail.StillRunningAfter = server + trfail.StillRunningAfter = server2 + trfail.MakeCurlCommand('-H host:bob.foo.com http://127.0.0.1:{0}/case2'.format(ts.Variables.port), ts=ts) + trfail.Processes.Default.ReturnCode = 0 + trfail.Processes.Default.Streams.stdout = Testers.ContainsExpression("Could Not Connect", "Check response") + + # Should succeed + tr = urtest.AddTestRun("random.bar.com to server 2") + tr.StillRunningAfter = ts + tr.StillRunningAfter = server + tr.StillRunningAfter = server2 + tr.MakeCurlCommand("-H host:random.bar.com http://127.0.0.1:{0}/case2".format(ts.Variables.port), ts=ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.stdout = Testers.ExcludesExpression("Could Not Connect", "Check response") + + # Should fail + trfail = urtest.AddTestRun("random.bar.com to server 1") + trfail.StillRunningAfter = ts + trfail.StillRunningAfter = server + trfail.StillRunningAfter = server2 + trfail.MakeCurlCommand('-H host:random.bar.com http://127.0.0.1:{0}/case1'.format(ts.Variables.port), ts=ts) + trfail.Processes.Default.ReturnCode = 0 + trfail.Processes.Default.Streams.stdout = Testers.ContainsExpression("Could Not Connect", "Check response") + + # Should fail + tr = urtest.AddTestRun("random.foo.com to server 2") + tr.StillRunningAfter = ts + tr.StillRunningAfter = server + tr.StillRunningAfter = server2 + tr.MakeCurlCommand("-H host:random.foo.com http://127.0.0.1:{0}/case2".format(ts.Variables.port), ts=ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.stdout = Testers.ContainsExpression("Could Not Connect", "Check response") + + # Should fail + trfail = urtest.AddTestRun("random.foo.com to server 1") + trfail.StillRunningAfter = ts + trfail.StillRunningAfter = server + trfail.StillRunningAfter = server2 + trfail.MakeCurlCommand('-H host:random.foo.com http://127.0.0.1:{0}/case1'.format(ts.Variables.port), ts=ts) + trfail.Processes.Default.ReturnCode = 0 + trfail.Processes.Default.Streams.stdout = Testers.ContainsExpression("Could Not Connect", "Check response") + + tr = urtest.AddTestRun("Wait for the access log to write out") + tr.Processes.Default.StartBefore(server4, ready=When.FileExists(ts.Disk.squid_log)) + tr.StillRunningAfter = ts + tr.Processes.Default.Command = 'echo "Log file exists"' + tr.Processes.Default.ReturnCode = 0 + + ts.Disk.squid_log.Content = "gold/proxycert2-accesslog.gold" + urtest.execute() diff --git a/tests/uranium_tests/tls/test_tls_client_cert2_plugin.py b/tests/uranium_tests/tls/test_tls_client_cert2_plugin.py new file mode 100644 index 00000000000..42c00711efc --- /dev/null +++ b/tests/uranium_tests/tls/test_tls_client_cert2_plugin.py @@ -0,0 +1,219 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_tls_client_cert2_plugin(urtest: UraniumTest) -> None: + ''' + Test offering client cert to origin, but using plugin for cert loading + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import subprocess + import os + + urtest.Summary = ''' + Test offering client cert to origin, but using plugin for cert loading + ''' + + ts = urtest.MakeATSProcess("ts") + cafile = "{0}/signer.pem".format(urtest.RunDirectory) + cafile2 = "{0}/signer2.pem".format(urtest.RunDirectory) + server = urtest.MakeOriginServer( + "server", + ssl=True, + options={ + "--clientCA": cafile, + "--clientverify": "" + }, + clientcert="{0}/signed-foo.pem".format(urtest.RunDirectory), + clientkey="{0}/signed-foo.key".format(urtest.RunDirectory)) + server2 = urtest.MakeOriginServer( + "server2", + ssl=True, + options={ + "--clientCA": cafile2, + "--clientverify": "" + }, + clientcert="{0}/signed2-bar.pem".format(urtest.RunDirectory), + clientkey="{0}/signed-bar.key".format(urtest.RunDirectory)) + server.Setup.Copy("ssl/signer.pem") + server.Setup.Copy("ssl/signer2.pem") + server.Setup.Copy("ssl/signed-foo.pem") + server.Setup.Copy("ssl/signed-foo.key") + server.Setup.Copy("ssl/signed2-foo.pem") + server.Setup.Copy("ssl/signed2-bar.pem") + server.Setup.Copy("ssl/signed-bar.key") + server2.Setup.Copy("ssl/signer.pem") + server2.Setup.Copy("ssl/signer2.pem") + server2.Setup.Copy("ssl/signed-foo.pem") + server2.Setup.Copy("ssl/signed-foo.key") + server2.Setup.Copy("ssl/signed2-foo.pem") + server2.Setup.Copy("ssl/signed2-bar.pem") + server2.Setup.Copy("ssl/signed-bar.key") + + request_header = {"headers": "GET / HTTP/1.1\r\nHost: example.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + server.addResponse("sessionlog.json", request_header, response_header) + request_header = {"headers": "GET / HTTP/1.1\r\nHost: bar.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + server.addResponse("sessionlog.json", request_header, response_header) + + # + # Certs and keys loaded into the ts/ssl directory, but the paths in the + # configs omit the ssl subdirectory. The ssl_secret_load_test plugin adds this back in + ts.addSSLfile("ssl/server.pem") + ts.addSSLfile("ssl/server.key") + ts.addSSLfile("ssl/combo-signed-foo.pem") + ts.addSSLfile("ssl/signed-foo.pem") + ts.addSSLfile("ssl/signed-foo.key") + ts.addSSLfile("ssl/signed2-foo.pem") + ts.addSSLfile("ssl/signed-bar.pem") + ts.addSSLfile("ssl/signed2-bar.pem") + ts.addSSLfile("ssl/signed-bar.key") + + urtest.PrepareTestPlugin(os.path.join(urtest.Variables.AtsTestPluginsDir, 'ssl_secret_load_test.so'), ts) + + ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'ssl_secret_load_test', + 'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir), + 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts.Variables.SSLDir), + 'proxy.config.ssl.client.cert.path': '{0}/../'.format(ts.Variables.SSLDir), + 'proxy.config.ssl.client.private_key.path': '{0}'.format(ts.Variables.SSLDir), + 'proxy.config.ssl.client.verify.server.policy': 'PERMISSIVE', + 'proxy.config.exec_thread.autoconfig.scale': 1.0, + 'proxy.config.url_remap.pristine_host_hdr': 1, + }) + + ts.Disk.ssl_multicert_yaml.AddLines( + """ + ssl_multicert: + - dest_ip: "*" + ssl_cert_name: server.pem + ssl_key_name: server.key + """.split("\n")) + + ts.Disk.remap_config.AddLine('map /case1 https://127.0.0.1:{0}/'.format(server.Variables.SSL_Port)) + ts.Disk.remap_config.AddLine('map /case2 https://127.0.0.1:{0}/'.format(server2.Variables.SSL_Port)) + + ts.Disk.sni_yaml.AddLines( + [ + 'sni:', + '- fqdn: bob.bar.com', + ' client_cert: {0}/../signed-bar.pem'.format(ts.Variables.SSLDir), + ' client_key: {0}/../signed-bar.key'.format(ts.Variables.SSLDir), + '- fqdn: "bob.foo.com"', + ' client_cert: {0}/../combo-signed-foo.pem'.format(ts.Variables.SSLDir), + '- fqdn: "*.bar.com"', + ' client_cert: {0}/../signed2-bar.pem'.format(ts.Variables.SSLDir), + ' client_key: {0}/../signed-bar.key'.format(ts.Variables.SSLDir), + '- fqdn: "foo.com"', + ' client_cert: {0}/../signed2-foo.pem'.format(ts.Variables.SSLDir), + ' client_key: {0}/../signed-foo.key'.format(ts.Variables.SSLDir), + ]) + + # Should succeed + tr = urtest.AddTestRun("bob.bar.com to server 1") + tr.Processes.Default.StartBefore(urtest.Processes.ts, ready=When.PortOpen(ts.Variables.port)) + tr.Processes.Default.StartBefore(server) + tr.Processes.Default.StartBefore(server2) + tr.StillRunningAfter = ts + tr.StillRunningAfter = server + tr.StillRunningAfter = server2 + tr.MakeCurlCommand("-H host:bob.bar.com http://127.0.0.1:{0}/case1".format(ts.Variables.port), ts=ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.stdout = Testers.ExcludesExpression("Could Not Connect", "Check response") + + # Should fail + trfail = urtest.AddTestRun("bob.bar.com to server 2") + trfail.StillRunningAfter = ts + trfail.StillRunningAfter = server + trfail.StillRunningAfter = server2 + trfail.MakeCurlCommand('-H host:bob.bar.com http://127.0.0.1:{0}/case2'.format(ts.Variables.port), ts=ts) + trfail.Processes.Default.ReturnCode = 0 + trfail.Processes.Default.Streams.stdout = Testers.ContainsExpression("Could Not Connect", "Check response") + + # Should succeed + tr = urtest.AddTestRun("bob.foo.com to server 1") + tr.StillRunningAfter = ts + tr.StillRunningAfter = server + tr.StillRunningAfter = server2 + tr.MakeCurlCommand("-H host:bob.foo.com http://127.0.0.1:{0}/case1".format(ts.Variables.port), ts=ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.stdout = Testers.ExcludesExpression("Could Not Connect", "Check response") + + # Should fail + trfail = urtest.AddTestRun("bob.foo.com to server 2") + trfail.StillRunningAfter = ts + trfail.StillRunningAfter = server + trfail.StillRunningAfter = server2 + trfail.MakeCurlCommand('-H host:bob.foo.com http://127.0.0.1:{0}/case2'.format(ts.Variables.port), ts=ts) + trfail.Processes.Default.ReturnCode = 0 + trfail.Processes.Default.Streams.stdout = Testers.ContainsExpression("Could Not Connect", "Check response") + + # Should succeed + tr = urtest.AddTestRun("random.bar.com to server 2") + tr.StillRunningAfter = ts + tr.StillRunningAfter = server + tr.StillRunningAfter = server2 + tr.MakeCurlCommand("-H host:random.bar.com http://127.0.0.1:{0}/case2".format(ts.Variables.port), ts=ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.stdout = Testers.ExcludesExpression("Could Not Connect", "Check response") + + # Should fail + trfail = urtest.AddTestRun("random.bar.com to server 1") + trfail.StillRunningAfter = ts + trfail.StillRunningAfter = server + trfail.StillRunningAfter = server2 + trfail.MakeCurlCommand('-H host:random.bar.com http://127.0.0.1:{0}/case1'.format(ts.Variables.port), ts=ts) + trfail.Processes.Default.ReturnCode = 0 + trfail.Processes.Default.Streams.stdout = Testers.ContainsExpression("Could Not Connect", "Check response") + + # Should fail + tr = urtest.AddTestRun("random.foo.com to server 2") + tr.StillRunningAfter = ts + tr.StillRunningAfter = server + tr.StillRunningAfter = server2 + tr.MakeCurlCommand("-H host:random.foo.com http://127.0.0.1:{0}/case2".format(ts.Variables.port), ts=ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.stdout = Testers.ContainsExpression("Could Not Connect", "Check response") + + # Should fail + trfail = urtest.AddTestRun("random.foo.com to server 1") + trfail.StillRunningAfter = ts + trfail.StillRunningAfter = server + trfail.StillRunningAfter = server2 + trfail.MakeCurlCommand('-H host:random.foo.com http://127.0.0.1:{0}/case1'.format(ts.Variables.port), ts=ts) + trfail.Processes.Default.ReturnCode = 0 + trfail.Processes.Default.Streams.stdout = Testers.ContainsExpression("Could Not Connect", "Check response") + urtest.execute() diff --git a/tests/uranium_tests/tls/test_tls_client_cert_override.py b/tests/uranium_tests/tls/test_tls_client_cert_override.py new file mode 100644 index 00000000000..b4ea6efebcc --- /dev/null +++ b/tests/uranium_tests/tls/test_tls_client_cert_override.py @@ -0,0 +1,170 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_tls_client_cert_override(urtest: UraniumTest) -> None: + ''' + Test offering client cert to origin + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + urtest.Summary = ''' + Test conf_remp to specify different client certificates to offer to the origin + ''' + + ts = urtest.MakeATSProcess("ts") + cafile = "{0}/signer.pem".format(urtest.RunDirectory) + cafile2 = "{0}/signer2.pem".format(urtest.RunDirectory) + server = urtest.MakeOriginServer( + "server", + ssl=True, + options={ + "--clientCA": cafile, + "--clientverify": "" + }, + clientcert="{0}/signed-foo.pem".format(urtest.RunDirectory), + clientkey="{0}/signed-foo.key".format(urtest.RunDirectory)) + server2 = urtest.MakeOriginServer( + "server2", + ssl=True, + options={ + "--clientCA": cafile2, + "--clientverify": "" + }, + clientcert="{0}/signed2-bar.pem".format(urtest.RunDirectory), + clientkey="{0}/signed-bar.key".format(urtest.RunDirectory)) + server.Setup.Copy("ssl/signer.pem") + server.Setup.Copy("ssl/signer2.pem") + server.Setup.Copy("ssl/signed-foo.pem") + server.Setup.Copy("ssl/signed-foo.key") + server.Setup.Copy("ssl/signed2-foo.pem") + server.Setup.Copy("ssl/signed2-bar.pem") + server.Setup.Copy("ssl/signed-bar.key") + server2.Setup.Copy("ssl/signer.pem") + server2.Setup.Copy("ssl/signer2.pem") + server2.Setup.Copy("ssl/signed-foo.pem") + server2.Setup.Copy("ssl/signed-foo.key") + server2.Setup.Copy("ssl/signed2-foo.pem") + server2.Setup.Copy("ssl/signed2-bar.pem") + server2.Setup.Copy("ssl/signed-bar.key") + + request_header = {"headers": "GET / HTTP/1.1\r\nHost: example.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + server.addResponse("sessionlog.json", request_header, response_header) + request_header = {"headers": "GET / HTTP/1.1\r\nHost: bar.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + server.addResponse("sessionlog.json", request_header, response_header) + + ts.addSSLfile("ssl/server.pem") + ts.addSSLfile("ssl/server.key") + ts.addSSLfile("ssl/signed-foo.pem") + ts.addSSLfile("ssl/signed-foo.key") + ts.addSSLfile("ssl/signed2-foo.pem") + ts.addSSLfile("ssl/signed-bar.pem") + ts.addSSLfile("ssl/signed2-bar.pem") + ts.addSSLfile("ssl/signed-bar.key") + + ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'ssl', + 'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir), + 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts.Variables.SSLDir), + 'proxy.config.ssl.client.cert.path': '{0}'.format(ts.Variables.SSLDir), + 'proxy.config.ssl.client.cert.filename': 'signed-foo.pem', + 'proxy.config.ssl.client.private_key.path': '{0}'.format(ts.Variables.SSLDir), + 'proxy.config.ssl.client.private_key.filename': 'signed-foo.key', + 'proxy.config.exec_thread.autoconfig.scale': 1.0, + 'proxy.config.url_remap.pristine_host_hdr': 1, + 'proxy.config.ssl.client.verify.server.policy': 'PERMISSIVE', + }) + + ts.Disk.ssl_multicert_yaml.AddLines( + """ + ssl_multicert: + - dest_ip: "*" + ssl_cert_name: server.pem + ssl_key_name: server.key + """.split("\n")) + + ts.Disk.remap_config.AddLine( + 'map /case1 https://127.0.0.1:{0}/ @plugin=conf_remap.so @pparam=proxy.config.ssl.client.cert.filename={1} plugin=conf_remap.so @pparam=proxy.config.ssl.client.private_key.filename={2}' + .format(server.Variables.SSL_Port, "signed-foo.pem", "signed-foo.key")) + ts.Disk.remap_config.AddLine( + 'map /badcase1 https://127.0.0.1:{0}/ @plugin=conf_remap.so @pparam=proxy.config.ssl.client.cert.filename={1} plugin=conf_remap.so @pparam=proxy.config.ssl.client.private_key.filename={2}' + .format(server.Variables.SSL_Port, "signed2-foo.pem", "signed-foo.key")) + ts.Disk.remap_config.AddLine( + 'map /case2 https://127.0.0.1:{0}/ @plugin=conf_remap.so @pparam=proxy.config.ssl.client.cert.filename={1} plugin=conf_remap.so @pparam=proxy.config.ssl.client.private_key.filename={2}' + .format(server2.Variables.SSL_Port, "signed2-foo.pem", "signed-foo.key")) + ts.Disk.remap_config.AddLine( + 'map /badcase2 https://127.0.0.1:{0}/ @plugin=conf_remap.so @pparam=proxy.config.ssl.client.cert.filename={1} plugin=conf_remap.so @pparam=proxy.config.ssl.client.private_key.filename={2}' + .format(server2.Variables.SSL_Port, "signed-foo.pem", "signed-foo.key")) + + # Should succeed + tr = urtest.AddTestRun("Connect with correct client cert to first server") + tr.Processes.Default.StartBefore(urtest.Processes.ts) + tr.Processes.Default.StartBefore(server) + tr.Processes.Default.StartBefore(server2) + tr.StillRunningAfter = ts + tr.StillRunningAfter = server + tr.StillRunningAfter = server2 + tr.MakeCurlCommand("-H host:example.com http://127.0.0.1:{0}/case1".format(ts.Variables.port), ts=ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.stdout = Testers.ExcludesExpression("Could Not Connect", "Check response") + + # Should fail + trfail = urtest.AddTestRun("Connect with bad client cert to first server") + trfail.StillRunningAfter = ts + trfail.StillRunningAfter = server + trfail.StillRunningAfter = server2 + trfail.MakeCurlCommand('-H host:example.com http://127.0.0.1:{0}/badcase1'.format(ts.Variables.port), ts=ts) + trfail.Processes.Default.ReturnCode = 0 + trfail.Processes.Default.Streams.stdout = Testers.ContainsExpression("Could Not Connect", "Check response") + + # Should succeed + trbar = urtest.AddTestRun("Connect with correct client cert to second server") + trbar.StillRunningAfter = ts + trbar.StillRunningAfter = server + trbar.StillRunningAfter = server2 + trbar.MakeCurlCommand("-H host:bar.com http://127.0.0.1:{0}/case2".format(ts.Variables.port), ts=ts) + trbar.Processes.Default.ReturnCode = 0 + trbar.Processes.Default.Streams.stdout = Testers.ExcludesExpression("Could Not Connect", "Check response") + + # Should fail + trbarfail = urtest.AddTestRun("Connect with bad client cert to second server") + trbarfail.StillRunningAfter = ts + trbarfail.StillRunningAfter = server + trbarfail.StillRunningAfter = server2 + trbarfail.MakeCurlCommand('-H host:bar.com http://127.0.0.1:{0}/badcase2'.format(ts.Variables.port), ts=ts) + trbarfail.Processes.Default.ReturnCode = 0 + trbarfail.Processes.Default.Streams.stdout = Testers.ContainsExpression("Could Not Connect", "Check response") + urtest.execute() diff --git a/tests/uranium_tests/tls/test_tls_client_cert_override_plugin.py b/tests/uranium_tests/tls/test_tls_client_cert_override_plugin.py new file mode 100644 index 00000000000..e81cafd5bb5 --- /dev/null +++ b/tests/uranium_tests/tls/test_tls_client_cert_override_plugin.py @@ -0,0 +1,248 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_tls_client_cert_override_plugin(urtest: UraniumTest) -> None: + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import subprocess + import os + + urtest.Summary = ''' + Test conf_remp to specify different client certificates to offer to the origin. Loading certs/keys via plugin. + ''' + + ts = urtest.MakeATSProcess("ts") + + cafile = "{0}/signer.pem".format(urtest.RunDirectory) + cafile2 = "{0}/signer2.pem".format(urtest.RunDirectory) + server = urtest.MakeOriginServer( + "server", + ssl=True, + options={ + "--clientCA": cafile, + "--clientverify": "" + }, + clientcert="{0}/signed-foo.pem".format(urtest.RunDirectory), + clientkey="{0}/signed-foo.key".format(urtest.RunDirectory)) + server2 = urtest.MakeOriginServer( + "server2", + ssl=True, + options={ + "--clientCA": cafile2, + "--clientverify": "" + }, + clientcert="{0}/signed2-bar.pem".format(urtest.RunDirectory), + clientkey="{0}/signed-bar.key".format(urtest.RunDirectory)) + server3 = urtest.MakeOriginServer("server3") + server.Setup.Copy("ssl/signer.pem") + server.Setup.Copy("ssl/signer2.pem") + server.Setup.Copy("ssl/signed-foo.pem") + server.Setup.Copy("ssl/signed-foo.key") + server.Setup.Copy("ssl/signed2-foo.pem") + server.Setup.Copy("ssl/signed2-bar.pem") + server.Setup.Copy("ssl/signed-bar.key") + server2.Setup.Copy("ssl/signer.pem") + server2.Setup.Copy("ssl/signer2.pem") + server2.Setup.Copy("ssl/signed-foo.pem") + server2.Setup.Copy("ssl/signed-foo.key") + server2.Setup.Copy("ssl/signed2-foo.pem") + server2.Setup.Copy("ssl/signed2-bar.pem") + server2.Setup.Copy("ssl/signed-bar.key") + + request_header = {"headers": "GET / HTTP/1.1\r\nHost: example.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + response_header = { + "headers": "HTTP/1.1 200 OK\r\nConnection: close\r\nCache-Control: no-cache\r\n\r\n", + "timestamp": "1469733493.993", + "body": "" + } + server.addResponse("sessionlog.json", request_header, response_header) + request_header = {"headers": "GET / HTTP/1.1\r\nHost: bar.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + response_header = { + "headers": "HTTP/1.1 200 OK\r\nCache-Control: no-cache\r\nConnection: close\r\n\r\n", + "timestamp": "1469733493.993", + "body": "" + } + server.addResponse("sessionlog.json", request_header, response_header) + + ts.addSSLfile("ssl/server.pem") + ts.addSSLfile("ssl/server.key") + ts.addSSLfile("ssl/signed-foo.pem") + ts.addSSLfile("ssl/signed-foo.key") + ts.addSSLfile("ssl/signed2-foo.pem") + ts.addSSLfile("ssl/signed-bar.pem") + ts.addSSLfile("ssl/signed2-bar.pem") + ts.addSSLfile("ssl/signed-bar.key") + + ts.Disk.sni_yaml.AddLine('sni:') + ts.Disk.sni_yaml.AddLine('- fqdn: random') + ts.Disk.sni_yaml.AddLine(' verify_server_properties: NONE') + snipath = ts.Disk.sni_yaml.AbsPath + + urtest.PrepareTestPlugin(os.path.join(urtest.Variables.AtsTestPluginsDir, 'ssl_secret_load_test.so'), ts) + + shortdir = ts.Variables.SSLDir[0:ts.Variables.SSLDir.rfind("/")] + + ts.Disk.records_config.update( + { + 'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir), + 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts.Variables.SSLDir), + 'proxy.config.ssl.client.verify.server.policy': 'PERMISSIVE', + 'proxy.config.ssl.client.cert.path': '{0}'.format(shortdir), + 'proxy.config.ssl.client.cert.filename': 'signed-foo.pem', + 'proxy.config.ssl.client.private_key.path': '{0}'.format(shortdir), + 'proxy.config.ssl.client.private_key.filename': 'signed-foo.key', + 'proxy.config.exec_thread.autoconfig.scale': 1.0, + 'proxy.config.url_remap.pristine_host_hdr': 1, + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'ssl_secret_load|http|ssl', + }) + + ts.Disk.ssl_multicert_yaml.AddLines( + """ + ssl_multicert: + - dest_ip: "*" + ssl_cert_name: server.pem + ssl_key_name: server.key + """.split("\n")) + + ts.Disk.remap_config.AddLine( + 'map /case1 https://127.0.0.1:{0}/ @plugin=conf_remap.so @pparam=proxy.config.ssl.client.cert.filename={1} plugin=conf_remap.so @pparam=proxy.config.ssl.client.private_key.filename={2}' + .format(server.Variables.SSL_Port, "signed-foo.pem", "signed-foo.key")) + ts.Disk.remap_config.AddLine( + 'map /badcase1 https://127.0.0.1:{0}/ @plugin=conf_remap.so @pparam=proxy.config.ssl.client.cert.filename={1} plugin=conf_remap.so @pparam=proxy.config.ssl.client.private_key.filename={2}' + .format(server.Variables.SSL_Port, "signed2-foo.pem", "signed-foo.key")) + ts.Disk.remap_config.AddLine( + 'map /case2 https://127.0.0.1:{0}/ @plugin=conf_remap.so @pparam=proxy.config.ssl.client.cert.filename={1} plugin=conf_remap.so @pparam=proxy.config.ssl.client.private_key.filename={2}' + .format(server2.Variables.SSL_Port, "signed2-foo.pem", "signed-foo.key")) + ts.Disk.remap_config.AddLine( + 'map /badcase2 https://127.0.0.1:{0}/ @plugin=conf_remap.so @pparam=proxy.config.ssl.client.cert.filename={1} plugin=conf_remap.so @pparam=proxy.config.ssl.client.private_key.filename={2}' + .format(server2.Variables.SSL_Port, "signed-foo.pem", "signed-foo.key")) + + # Should succeed + tr = urtest.AddTestRun("Connect with correct client cert to first server") + tr.Processes.Default.StartBefore(urtest.Processes.ts) + tr.Processes.Default.StartBefore(server) + tr.Processes.Default.StartBefore(server2) + tr.StillRunningAfter = ts + tr.StillRunningAfter = server + tr.StillRunningAfter = server2 + tr.MakeCurlCommand("-H host:example.com http://127.0.0.1:{0}/case1".format(ts.Variables.port), ts=ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.stdout = Testers.ExcludesExpression("Could Not Connect", "Check response") + + # Should fail + trfail = urtest.AddTestRun("Connect with bad client cert to first server") + trfail.StillRunningAfter = ts + trfail.StillRunningAfter = server + trfail.StillRunningAfter = server2 + trfail.MakeCurlCommand('-H host:example.com http://127.0.0.1:{0}/badcase1'.format(ts.Variables.port), ts=ts) + trfail.Processes.Default.ReturnCode = 0 + trfail.Processes.Default.Streams.stdout = Testers.ContainsExpression("Could Not Connect", "Check response") + + # Should succeed + trbar = urtest.AddTestRun("Connect with correct client cert to second server") + trbar.StillRunningAfter = ts + trbar.StillRunningAfter = server + trbar.StillRunningAfter = server2 + trbar.MakeCurlCommand("-H host:bar.com http://127.0.0.1:{0}/case2".format(ts.Variables.port), ts=ts) + trbar.Processes.Default.ReturnCode = 0 + trbar.Processes.Default.Streams.stdout = Testers.ExcludesExpression("Could Not Connect", "Check response") + + # Should fail + trbarfail = urtest.AddTestRun("Connect with bad client cert to second server") + trbarfail.StillRunningAfter = ts + trbarfail.StillRunningAfter = server + trbarfail.StillRunningAfter = server2 + trbarfail.MakeCurlCommand('-H host:bar.com http://127.0.0.1:{0}/badcase2'.format(ts.Variables.port), ts=ts) + trbarfail.Processes.Default.ReturnCode = 0 + trbarfail.Processes.Default.Streams.stdout = Testers.ContainsExpression("Could Not Connect", "Check response") + + # Test the case of updating certificate contents without changing file name. + trupdate = urtest.AddTestRun("Update client cert file in place") + trupdate.StillRunningAfter = ts + trupdate.StillRunningAfter = server + trupdate.StillRunningAfter = server2 + # in the config/ssl directory for records.yaml + trupdate.Setup.CopyAs("ssl/signed-foo.pem", ".", "{0}/signed2-foo.pem".format(ts.Variables.SSLDir)) + trupdate.Processes.Default.Command = 'traffic_ctl config set proxy.config.ssl.client.cert.path {0}/; touch {1}'.format( + shortdir, snipath) + # Need to copy over the environment so traffic_ctl knows where to find the unix domain socket + trupdate.Processes.Default.Env = ts.Env + trupdate.Processes.Default.ReturnCode = 0 + + tr2reload = urtest.AddConfigReload(ts, expect_tasks=["sni.yaml"], description="Reload config") + tr2reload.StillRunningAfter = server + tr2reload.StillRunningAfter = server2 + + tr3bar = urtest.AddTestRun("Make request with other foo. badcase1 should now work") + tr3bar.Processes.Default.StartBefore(server3) + tr3bar.StillRunningAfter = ts + tr3bar.StillRunningAfter = server + tr3bar.StillRunningAfter = server2 + tr3bar.MakeCurlCommand(' -H host:foo.com http://127.0.0.1:{0}/badcase1'.format(ts.Variables.port), ts=ts) + tr3bar.Processes.Default.ReturnCode = 0 + tr3bar.Processes.Default.Streams.stdout = Testers.ExcludesExpression("Could Not Connect", "Check response") + + # Test the hot-reload feature. Update cert file and wait about. Should update without reload + trupdate = urtest.AddTestRun("Update signed-foo cert file in place") + trupdate.StillRunningAfter = ts + trupdate.StillRunningAfter = server + trupdate.Setup.CopyAs("ssl/signed2-foo.pem", ".", "{0}/signed-foo.pem".format(ts.Variables.SSLDir)) + # For some reason the Setup.CopyAs does not change the modification time, so we touch + trupdate.Processes.Default.Command = 'touch {0}/signed-foo.pem {0}/signed-foo.key'.format(ts.Variables.SSLDir) + # Need to copy over the environment so traffic_ctl knows where to find the unix domain socket + trupdate.Processes.Default.Env = ts.Env + trupdate.Processes.Default.ReturnCode = 0 + + # The plugin will pull every 3 seconds. So wait 4 seconds and test again. + # case1 should fail + # badcase1 should succeed + tr = urtest.AddTestRun("Retest case1") + tr.DelayStart = 4 + tr.ReturnCode = 0 + tr.StillRunningAfter = server + tr.StillRunningAfter = server2 + tr.StillRunningAfter = ts + tr.MakeCurlCommand("-H host:example.com http://127.0.0.1:{0}/case1".format(ts.Variables.port), ts=ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.stdout = Testers.ContainsExpression("Could Not Connect", "Check response") + + tr = urtest.AddTestRun("Retest badcase1") + tr.ReturnCode = 0 + tr.StillRunningAfter = server + tr.StillRunningAfter = server2 + tr.StillRunningAfter = ts + tr.MakeCurlCommand("-H host:example.com http://127.0.0.1:{0}/badcase1".format(ts.Variables.port), ts=ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.stdout = Testers.ExcludesExpression("Could Not Connect", "Check response") + urtest.execute() diff --git a/tests/uranium_tests/tls/test_tls_client_cert_plugin.py b/tests/uranium_tests/tls/test_tls_client_cert_plugin.py new file mode 100644 index 00000000000..80f5a985e7a --- /dev/null +++ b/tests/uranium_tests/tls/test_tls_client_cert_plugin.py @@ -0,0 +1,304 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_tls_client_cert_plugin(urtest: UraniumTest) -> None: + ''' + Test offering client cert to origin, but using plugin for cert loading + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import subprocess + import os + + urtest.Summary = ''' + Test offering client cert to origin, but using plugin for cert loading + ''' + + ts = urtest.MakeATSProcess("ts") + cafile = "{0}/signer.pem".format(urtest.RunDirectory) + cafile2 = "{0}/signer2.pem".format(urtest.RunDirectory) + # --clientverify: "" empty string because microserver does store_true for argparse, but options is a dictionary + server = urtest.MakeOriginServer( + "server", + ssl=True, + options={ + "--clientCA": cafile, + "--clientverify": "" + }, + clientcert="{0}/signed-foo.pem".format(urtest.RunDirectory), + clientkey="{0}/signed-foo.key".format(urtest.RunDirectory)) + server2 = urtest.MakeOriginServer( + "server2", + ssl=True, + options={ + "--clientCA": cafile2, + "--clientverify": "" + }, + clientcert="{0}/signed2-bar.pem".format(urtest.RunDirectory), + clientkey="{0}/signed-bar.key".format(urtest.RunDirectory)) + server3 = urtest.MakeOriginServer("server3") + server.Setup.Copy("ssl/signer.pem") + server.Setup.Copy("ssl/signer2.pem") + server.Setup.Copy("ssl/signed-foo.pem") + server.Setup.Copy("ssl/signed-foo.key") + server.Setup.Copy("ssl/signed2-foo.pem") + server.Setup.Copy("ssl/signed2-bar.pem") + server.Setup.Copy("ssl/signed-bar.key") + server2.Setup.Copy("ssl/signer.pem") + server2.Setup.Copy("ssl/signer2.pem") + server2.Setup.Copy("ssl/signed-foo.pem") + server2.Setup.Copy("ssl/signed-foo.key") + server2.Setup.Copy("ssl/signed2-foo.pem") + server2.Setup.Copy("ssl/signed2-bar.pem") + server2.Setup.Copy("ssl/signed-bar.key") + + request_header = {"headers": "GET / HTTP/1.1\r\nHost: example.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + server.addResponse("sessionlog.json", request_header, response_header) + request_header = {"headers": "GET / HTTP/1.1\r\nHost: bar.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + server.addResponse("sessionlog.json", request_header, response_header) + + # + # Certs and keys loaded into the ts/ssl directory, but the paths in the + # configs omit the ssl subdirectory. The ssl_secret_load_test plugin adds this back in + ts.addSSLfile("ssl/server.pem") + ts.addSSLfile("ssl/server.key") + ts.addSSLfile("ssl/signed-foo.pem") + ts.addSSLfile("ssl/signed-foo.key") + ts.addSSLfile("ssl/signed2-foo.pem") + ts.addSSLfile("ssl/signed-bar.pem") + ts.addSSLfile("ssl/signed2-bar.pem") + ts.addSSLfile("ssl/signed-bar.key") + + urtest.PrepareTestPlugin(os.path.join(urtest.Variables.AtsTestPluginsDir, 'ssl_secret_load_test.so'), ts) + + ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'ssl_secret_load_test|ssl', + 'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir), + 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts.Variables.SSLDir), + 'proxy.config.ssl.client.verify.server.policy': 'PERMISSIVE', + 'proxy.config.ssl.client.cert.path': '{0}/../'.format(ts.Variables.SSLDir), + 'proxy.config.ssl.client.cert.filename': 'signed-foo.pem', + 'proxy.config.ssl.client.private_key.path': '{0}/../'.format(ts.Variables.SSLDir), + 'proxy.config.ssl.client.private_key.filename': 'signed-foo.key', + 'proxy.config.exec_thread.autoconfig.scale': 1.0, + 'proxy.config.url_remap.pristine_host_hdr': 1, + }) + + ts.Disk.ssl_multicert_yaml.AddLines( + """ + ssl_multicert: + - dest_ip: "*" + ssl_cert_name: server.pem + ssl_key_name: server.key + """.split("\n")) + + ts.Disk.remap_config.AddLine('map /case1 https://127.0.0.1:{0}/'.format(server.Variables.SSL_Port)) + ts.Disk.remap_config.AddLine('map /case2 https://127.0.0.1:{0}/'.format(server2.Variables.SSL_Port)) + + ts.Disk.sni_yaml.AddLine('sni:') + ts.Disk.sni_yaml.AddLine('- fqdn: bar.com') + ts.Disk.sni_yaml.AddLine(' client_cert: {0}/../signed2-bar.pem'.format(ts.Variables.SSLDir)) + ts.Disk.sni_yaml.AddLine(' client_key: {0}/../signed-bar.key'.format(ts.Variables.SSLDir)) + + # Should succeed + tr = urtest.AddTestRun("Connect with first client cert to first server") + tr.Processes.Default.StartBefore(urtest.Processes.ts) + tr.Processes.Default.StartBefore(server) + tr.Processes.Default.StartBefore(server2) + tr.StillRunningAfter = ts + tr.StillRunningAfter = server + tr.StillRunningAfter = server2 + tr.MakeCurlCommand("-H host:example.com http://127.0.0.1:{0}/case1".format(ts.Variables.port), ts=ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.stdout = Testers.ExcludesExpression("Could Not Connect", "Check response") + + # Should fail + trfail = urtest.AddTestRun("Connect with first client cert to second server") + trfail.StillRunningAfter = ts + trfail.StillRunningAfter = server + trfail.StillRunningAfter = server2 + trfail.MakeCurlCommand('-H host:example.com http://127.0.0.1:{0}/case2'.format(ts.Variables.port), ts=ts) + trfail.Processes.Default.ReturnCode = 0 + trfail.Processes.Default.Streams.stdout = Testers.ContainsExpression("Could Not Connect", "Check response") + + # Should succeed + trbar = urtest.AddTestRun("Connect with signed2 bar to second server") + trbar.StillRunningAfter = ts + trbar.StillRunningAfter = server + trbar.StillRunningAfter = server2 + trbar.MakeCurlCommand("-H host:bar.com http://127.0.0.1:{0}/case2".format(ts.Variables.port), ts=ts) + trbar.Processes.Default.ReturnCode = 0 + trbar.Processes.Default.Streams.stdout = Testers.ExcludesExpression("Could Not Connect", "Check response") + + # Should fail + trbarfail = urtest.AddTestRun("Connect with signed2 bar cert to first server") + trbarfail.StillRunningAfter = ts + trbarfail.StillRunningAfter = server + trbarfail.StillRunningAfter = server2 + trbarfail.MakeCurlCommand('-H host:bar.com http://127.0.0.1:{0}/case1'.format(ts.Variables.port), ts=ts) + trbarfail.Processes.Default.ReturnCode = 0 + trbarfail.Processes.Default.Streams.stdout = Testers.ContainsExpression("Could Not Connect", "Check response") + + tr2 = urtest.AddTestRun("Update config files") + # Update the SNI config + snipath = ts.Disk.sni_yaml.AbsPath + recordspath = ts.Disk.records_config.AbsPath + tr2.Disk.File(snipath, id="sni_yaml", typename="ats:config"), + tr2.Disk.sni_yaml.AddLine('sni:') + tr2.Disk.sni_yaml.AddLine('- fqdn: bar.com') + tr2.Disk.sni_yaml.AddLine(' client_cert: {0}/../signed-bar.pem'.format(ts.Variables.SSLDir)) + tr2.Disk.sni_yaml.AddLine(' client_key: {0}/../signed-bar.key'.format(ts.Variables.SSLDir)) + # recreate the records.yaml with the cert filename changed + tr2.Disk.File(recordspath, id="records_config", typename="ats:config:records"), + tr2.Disk.records_config.update( + { + 'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir), + 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts.Variables.SSLDir), + 'proxy.config.ssl.client.verify.server.policy': 'PERMISSIVE', + 'proxy.config.ssl.client.cert.path': '{0}/../'.format(ts.Variables.SSLDir), + 'proxy.config.ssl.client.cert.filename': 'signed2-foo.pem', + 'proxy.config.ssl.client.private_key.path': '{0}/../'.format(ts.Variables.SSLDir), + 'proxy.config.ssl.client.private_key.filename': 'signed-foo.key', + 'proxy.config.url_remap.pristine_host_hdr': 1, + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'ssl_secret_load_test|ssl', + }) + tr2.StillRunningAfter = ts + tr2.StillRunningAfter = server + tr2.StillRunningAfter = server2 + tr2.Processes.Default.Command = 'echo Updated configs' + # Need to copy over the environment so traffic_ctl knows where to find the unix domain socket + tr2.Processes.Default.Env = ts.Env + tr2.Processes.Default.ReturnCode = 0 + + tr2reload = urtest.AddConfigReload(ts, expect_tasks=["sni.yaml"], description="Reload config") + tr2reload.StillRunningAfter = server + tr2reload.StillRunningAfter = server2 + + # Should succeed + tr3bar = urtest.AddTestRun("Make request with other bar cert to first server") + tr3bar.Processes.Default.StartBefore(server3) + tr3bar.StillRunningAfter = ts + tr3bar.StillRunningAfter = server + tr3bar.StillRunningAfter = server2 + tr3bar.MakeCurlCommand(' -H host:bar.com http://127.0.0.1:{0}/case1'.format(ts.Variables.port), ts=ts) + tr3bar.Processes.Default.ReturnCode = 0 + tr3bar.Processes.Default.Streams.stdout = Testers.ExcludesExpression("Could Not Connect", "Check response") + + # Should fail + tr3barfail = urtest.AddTestRun("Make request with other bar cert to second server") + tr3barfail.StillRunningAfter = ts + tr3barfail.StillRunningAfter = server + tr3barfail.StillRunningAfter = server2 + tr3barfail.MakeCurlCommand(' -H host:bar.com http://127.0.0.1:{0}/case2'.format(ts.Variables.port), ts=ts) + tr3barfail.Processes.Default.ReturnCode = 0 + tr3barfail.Processes.Default.Streams.stdout = Testers.ContainsExpression("Could Not Connect", "Check response") + + # Should succeed + tr3 = urtest.AddTestRun("Make request with other cert to second server") + # Wait for the reload to complete + tr3.StillRunningAfter = ts + tr3.StillRunningAfter = server + tr3.StillRunningAfter = server2 + tr3.MakeCurlCommand(' -H host:example.com http://127.0.0.1:{0}/case2'.format(ts.Variables.port), ts=ts) + tr3.Processes.Default.ReturnCode = 0 + tr3.Processes.Default.Streams.stdout = Testers.ExcludesExpression("Could Not Connect", "Check response") + + # Should fail + tr3fail = urtest.AddTestRun("Make request with other cert to first server") + tr3fail.StillRunningAfter = ts + tr3fail.StillRunningAfter = server + tr3fail.StillRunningAfter = server2 + tr3fail.MakeCurlCommand(' -H host:example.com http://127.0.0.1:{0}/case1'.format(ts.Variables.port), ts=ts) + tr3fail.Processes.Default.ReturnCode = 0 + tr3fail.Processes.Default.Streams.stdout = Testers.ContainsExpression("Could Not Connect", "Check response") + + # Test the case of updating certificate contents without changing file name. + trupdate = urtest.AddTestRun("Update client cert file in place") + trupdate.StillRunningAfter = ts + trupdate.StillRunningAfter = server + trupdate.StillRunningAfter = server2 + # Make a meaningless config change on the path so the records.yaml reload logic will trigger + trupdate.Setup.CopyAs("ssl/signed2-bar.pem", ".", "{0}/signed-bar.pem".format(ts.Variables.SSLDir)) + # in the config/ssl directory for records.yaml + trupdate.Setup.CopyAs("ssl/signed-foo.pem", ".", "{0}/signed2-foo.pem".format(ts.Variables.SSLDir)) + trupdate.Processes.Default.Command = 'traffic_ctl config set proxy.config.ssl.client.cert.path {0}/../; touch {1}; touch {2}'.format( + ts.Variables.SSLDir, snipath, recordspath) + # Need to copy over the environment so traffic_ctl knows where to find the unix domain socket + trupdate.Processes.Default.Env = ts.Env + trupdate.Processes.Default.ReturnCode = 0 + + trreload = urtest.AddConfigReload(ts, expect_tasks=["sni.yaml"], description="Reload config after renaming certs") + trreload.StillRunningAfter = server + trreload.StillRunningAfter = server2 + + # Should succeed + tr4bar = urtest.AddTestRun("Make request with renamed bar cert to second server") + tr4bar.StillRunningAfter = ts + tr4bar.StillRunningAfter = server + tr4bar.StillRunningAfter = server2 + tr4bar.MakeCurlCommand(' -H host:bar.com http://127.0.0.1:{0}/case2'.format(ts.Variables.port), ts=ts) + tr4bar.Processes.Default.ReturnCode = 0 + tr4bar.Processes.Default.Streams.stdout = Testers.ExcludesExpression("Could Not Connect", "Check response") + + # Should fail + tr4barfail = urtest.AddTestRun("Make request with renamed bar cert to first server") + tr4barfail.StillRunningAfter = ts + tr4barfail.StillRunningAfter = server + tr4barfail.StillRunningAfter = server2 + tr4barfail.MakeCurlCommand(' -H host:bar.com http://127.0.0.1:{0}/case1'.format(ts.Variables.port), ts=ts) + tr4barfail.Processes.Default.ReturnCode = 0 + tr4barfail.Processes.Default.Streams.stdout = Testers.ContainsExpression("Could Not Connect", "Check response") + + # Should succeed + tr4 = urtest.AddTestRun("Make request with renamed foo cert to first server") + tr4.StillRunningAfter = ts + tr4.StillRunningAfter = server + tr4.StillRunningAfter = server2 + tr4.MakeCurlCommand(' -H host:example.com http://127.0.0.1:{0}/case1'.format(ts.Variables.port), ts=ts) + tr4.Processes.Default.ReturnCode = 0 + tr4.Processes.Default.Streams.stdout = Testers.ExcludesExpression("Could Not Connect", "Check response") + + # Should fail + tr4fail = urtest.AddTestRun("Make request with renamed foo cert to second server") + tr4fail.StillRunningAfter = ts + tr4fail.StillRunningAfter = server + tr4fail.StillRunningAfter = server2 + tr4fail.MakeCurlCommand(' -H host:example.com http://127.0.0.1:{0}/case2'.format(ts.Variables.port), ts=ts) + tr4fail.Processes.Default.ReturnCode = 0 + tr4fail.Processes.Default.Streams.stdout = Testers.ContainsExpression("Could Not Connect", "Check response") + urtest.execute() diff --git a/tests/uranium_tests/tls/test_tls_client_verify.py b/tests/uranium_tests/tls/test_tls_client_verify.py new file mode 100644 index 00000000000..3c522a221db --- /dev/null +++ b/tests/uranium_tests/tls/test_tls_client_verify.py @@ -0,0 +1,262 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_tls_client_verify(urtest: UraniumTest) -> None: + ''' + Test requiring certificate from user agent + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + urtest.Summary = ''' + Test various options for requiring certificate from client for mutual authentication TLS + ''' + + ts = urtest.MakeATSProcess("ts", enable_tls=True) + cafile = "{0}/signer.pem".format(urtest.RunDirectory) + cafile2 = "{0}/signer2.pem".format(urtest.RunDirectory) + server = urtest.MakeOriginServer("server") + server2 = urtest.MakeOriginServer("server2") + + request_header = {"headers": "GET / HTTP/1.1\r\nHost: example.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + server.addResponse("sessionlog.json", request_header, response_header) + request_header = {"headers": "GET / HTTP/1.1\r\nHost: bar.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + server.addResponse("sessionlog.json", request_header, response_header) + + ts.addSSLfile("ssl/server.pem") + ts.addSSLfile("ssl/server.key") + ts.addSSLfile("ssl/signer.pem") + + ts.Disk.records_config.update( + { + 'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir), + 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts.Variables.SSLDir), + 'proxy.config.url_remap.pristine_host_hdr': 1, + 'proxy.config.ssl.client.certification_level': 2, + 'proxy.config.ssl.CA.cert.filename': '{0}/signer.pem'.format(ts.Variables.SSLDir), + 'proxy.config.exec_thread.autoconfig.scale': 1.0, + 'proxy.config.ssl.TLSv1_3.enabled': 0 + }) + + ts.Disk.ssl_multicert_yaml.AddLines( + """ + ssl_multicert: + - dest_ip: "*" + ssl_cert_name: server.pem + ssl_key_name: server.key + """.split("\n")) + + # Just map everything through to origin. This test is concentrating on the user-agent side + ts.Disk.remap_config.AddLine('map / http://127.0.0.1:{0}/'.format(server.Variables.Port)) + + # Scenario 1: Default no client cert required. cert required for bar.com + ts.Disk.sni_yaml.AddLines( + [ + 'sni:', + '- fqdn: bob.bar.com', + ' verify_client: NONE', + '- fqdn: "bob.com"', + ' verify_client: STRICT', + '- fqdn: "*.foo.com"', + ' verify_client: NONE', + '- fqdn: "*.bar.com"', + ' verify_client: STRICT', + ]) + + ts.Disk.logging_yaml.AddLines( + ''' + logging: + formats: + - name: testformat + format: '% % % %' + logs: + - mode: ascii + format: testformat + filename: squid + '''.split("\n")) + + # to foo.com w/o client cert. Should fail + tr = urtest.AddTestRun("Connect to foo.com without cert") + tr.Processes.Default.StartBefore(urtest.Processes.ts) + tr.Processes.Default.StartBefore(server) + tr.StillRunningAfter = ts + tr.StillRunningAfter = server + tr.MakeCurlCommand( + "--tls-max 1.2 -k --resolve 'foo.com:{0}:127.0.0.1' https://foo.com:{0}/case1".format(ts.Variables.ssl_port), ts=ts) + tr.Processes.Default.ReturnCode = 35 + + tr = urtest.AddTestRun("Connect to foo.com with bad cert") + tr.Setup.Copy("ssl/server.pem") + tr.Setup.Copy("ssl/server.key") + tr.StillRunningAfter = ts + tr.StillRunningAfter = server + tr.MakeCurlCommand( + "--tls-max 1.2 -k --cert ./server.pem --key ./server.key --resolve 'foo.com:{0}:127.0.0.1' https://foo.com:{0}/case2" + .format(ts.Variables.ssl_port), + ts=ts) + # Should fail with badly signed certs + tr.Processes.Default.ReturnCode = 35 + + tr = urtest.AddTestRun("Connect to foo.com with cert") + tr.Setup.Copy("ssl/signed-foo.pem") + tr.Setup.Copy("ssl/signed-foo.key") + tr.StillRunningAfter = ts + tr.StillRunningAfter = server + tr.MakeCurlCommand( + "--tls-max 1.2 -k --cert ./signed-foo.pem --key ./signed-foo.key --resolve 'foo.com:{0}:127.0.0.1' https://foo.com:{0}/case3" + .format(ts.Variables.ssl_port), + ts=ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.All = Testers.ExcludesExpression("error", "Check response") + + tr = urtest.AddTestRun("Connect to bob.bar.com without cert") + tr.StillRunningAfter = ts + tr.StillRunningAfter = server + tr.MakeCurlCommand( + "--tls-max 1.2 -k --resolve 'bob.bar.com:{0}:127.0.0.1' https://bob.bar.com:{0}/case4".format(ts.Variables.ssl_port), ts=ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.All = Testers.ExcludesExpression("alert", "TLS handshake should succeed") + + tr = urtest.AddTestRun("Connect to bob.bar.com with cert") + tr.Setup.Copy("ssl/signed-bob-bar.pem") + tr.Setup.Copy("ssl/signed-bar.key") + tr.StillRunningAfter = ts + tr.StillRunningAfter = server + tr.MakeCurlCommand( + "--tls-max 1.2 -k --cert ./signed-bob-bar.pem --key ./signed-bar.key --resolve 'bob.bar.com:{0}:127.0.0.1' https://bob.bar.com:{0}/case5" + .format(ts.Variables.ssl_port), + ts=ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.All = Testers.ExcludesExpression("error", "Check response") + + tr = urtest.AddTestRun("Connect to bob.bar.com with bad cert") + tr.Setup.Copy("ssl/server.pem") + tr.Setup.Copy("ssl/server.key") + tr.StillRunningAfter = ts + tr.StillRunningAfter = server + tr.MakeCurlCommand( + "--tls-max 1.2 -k --cert ./server.pem --key ./server.key --resolve 'bob.bar.com:{0}:127.0.0.1' https://bob.bar.com:{0}/case6" + .format(ts.Variables.ssl_port), + ts=ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.All = Testers.ExcludesExpression("error", "Check response") + + tr = urtest.AddTestRun("Connect to bob.foo.com without cert") + tr.StillRunningAfter = ts + tr.StillRunningAfter = server + tr.MakeCurlCommand( + "--tls-max 1.2 -k --resolve 'bob.foo.com:{0}:127.0.0.1' https://bob.foo.com:{0}/case7".format(ts.Variables.ssl_port), ts=ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.All = Testers.ExcludesExpression("alert", "TLS handshake should succeed") + + tr = urtest.AddTestRun("Connect to bob.foo.com with cert") + tr.Setup.Copy("ssl/signed-bob-foo.pem") + tr.Setup.Copy("ssl/signed-foo.key") + tr.StillRunningAfter = ts + tr.StillRunningAfter = server + tr.MakeCurlCommand( + "--tls-max 1.2 -k --cert ./signed-bob-foo.pem --key ./signed-foo.key --resolve 'bob.foo.com:{0}:127.0.0.1' https://bob.foo.com:{0}/case8" + .format(ts.Variables.ssl_port), + ts=ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.All = Testers.ExcludesExpression("error", "Check response") + + tr = urtest.AddTestRun("Connect to bob.foo.com with bad cert") + tr.Setup.Copy("ssl/server.pem") + tr.Setup.Copy("ssl/server.key") + tr.StillRunningAfter = ts + tr.StillRunningAfter = server + tr.MakeCurlCommand( + "--tls-max 1.2 -k --cert ./server.pem --key ./server.key --resolve 'bob.foo.com:{0}:127.0.0.1' https://bob.foo.com:{0}/case9" + .format(ts.Variables.ssl_port), + ts=ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.All = Testers.ExcludesExpression("error", "Check response") + + tr = urtest.AddTestRun("Connect to bar.com without cert") + tr.StillRunningAfter = ts + tr.StillRunningAfter = server + tr.MakeCurlCommand( + "--tls-max 1.2 -k --resolve 'bar.com:{0}:127.0.0.1' https://bar.com:{0}/case10".format(ts.Variables.ssl_port), ts=ts) + tr.Processes.Default.ReturnCode = 35 + + tr = urtest.AddTestRun("Connect to bar.com with cert") + tr.Setup.Copy("ssl/signed-bar.pem") + tr.Setup.Copy("ssl/signed-bar.key") + tr.StillRunningAfter = ts + tr.StillRunningAfter = server + tr.MakeCurlCommand( + "--tls-max 1.2 -k --cert ./signed-bar.pem --key ./signed-bar.key --resolve 'bar.com:{0}:127.0.0.1' https://bar.com:{0}/case11" + .format(ts.Variables.ssl_port), + ts=ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.All = Testers.ExcludesExpression("error", "TLS handshake should succeed") + + tr = urtest.AddTestRun("Connect to bar.com with bad cert") + tr.Setup.Copy("ssl/server.pem") + tr.Setup.Copy("ssl/server.key") + tr.StillRunningAfter = ts + tr.StillRunningAfter = server + tr.MakeCurlCommand( + "--tls-max 1.2 -k --cert ./server.pem --key ./server.key --resolve 'bar.com:{0}:127.0.0.1' https://bar.com:{0}/case12" + .format(ts.Variables.ssl_port), + ts=ts) + tr.Processes.Default.ReturnCode = 35 + + # Test that the fqdn's match completely. bob.com should require client certificate. bob.com.com should not + tr = urtest.AddTestRun("Connect to bob.com without cert, should fail") + tr.StillRunningAfter = ts + tr.StillRunningAfter = server + tr.MakeCurlCommand( + "--tls-max 1.2 -k --resolve 'bob.com:{0}:127.0.0.1' https://bob.com:{0}/case13".format(ts.Variables.ssl_port), ts=ts) + tr.Processes.Default.ReturnCode = 35 + + tr = urtest.AddTestRun("Connect to bob.foo.com without cert, should succeed") + tr.StillRunningAfter = ts + tr.StillRunningAfter = server + tr.MakeCurlCommand( + "--tls-max 1.2 -k --resolve 'bob.foo.com:{0}:127.0.0.1' https://bob.foo.com:{0}/case14".format(ts.Variables.ssl_port), + ts=ts) + tr.Processes.Default.ReturnCode = 0 + + tr = urtest.AddTestRun("Wait for the access log to write out") + tr.Processes.Default.StartBefore(server2, ready=When.FileExists(ts.Disk.squid_log)) + tr.StillRunningAfter = ts + tr.StillRunningAfter = server + tr.StillRunningAfter = server2 + tr.Processes.Default.Command = 'echo "Log file exists"' + tr.Processes.Default.ReturnCode = 0 + + ts.Disk.squid_log.Content = "gold/clientcert-accesslog.gold" + urtest.execute() diff --git a/tests/uranium_tests/tls/test_tls_client_verify2.py b/tests/uranium_tests/tls/test_tls_client_verify2.py new file mode 100644 index 00000000000..7323fd9aa70 --- /dev/null +++ b/tests/uranium_tests/tls/test_tls_client_verify2.py @@ -0,0 +1,208 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_tls_client_verify2(urtest: UraniumTest) -> None: + ''' + Test requiring certificate from user agent + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + urtest.Summary = ''' + Test various options for requiring certificate from client for mutual authentication TLS + ''' + + ts = urtest.MakeATSProcess("ts", enable_tls=True) + cafile = "{0}/signer.pem".format(urtest.RunDirectory) + cafile2 = "{0}/signer2.pem".format(urtest.RunDirectory) + server = urtest.MakeOriginServer("server") + + request_header = {"headers": "GET / HTTP/1.1\r\nHost: example.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + server.addResponse("sessionlog.json", request_header, response_header) + request_header = {"headers": "GET / HTTP/1.1\r\nHost: bar.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + server.addResponse("sessionlog.json", request_header, response_header) + + ts.addSSLfile("ssl/server.pem") + ts.addSSLfile("ssl/server.key") + ts.addSSLfile("ssl/signer.pem") + + ts.Disk.records_config.update( + { + 'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir), + 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts.Variables.SSLDir), + 'proxy.config.url_remap.pristine_host_hdr': 1, + 'proxy.config.ssl.client.certification_level': 0, + 'proxy.config.ssl.CA.cert.path': '', + 'proxy.config.exec_thread.autoconfig.scale': 1.0, + 'proxy.config.ssl.CA.cert.filename': '{0}/signer.pem'.format(ts.Variables.SSLDir) + }) + + ts.Disk.ssl_multicert_yaml.AddLines( + """ + ssl_multicert: + - dest_ip: "*" + ssl_cert_name: server.pem + ssl_key_name: server.key + """.split("\n")) + + # Just map everything through to origin. This test is concentrating on the user-agent side + ts.Disk.remap_config.AddLine('map / http://127.0.0.1:{0}/'.format(server.Variables.Port)) + + # Scenario 1: Default no client cert required. cert required for bar.com + ts.Disk.sni_yaml.AddLines( + [ + 'sni:', + '- fqdn: bob.bar.com', + ' verify_client: STRICT', + '- fqdn: "*.foo.com"', + ' verify_client: STRICT', + '- fqdn: "*.bar.com"', + ' verify_client: NONE', + ]) + + # to foo.com w/o client cert. Should succeed + tr = urtest.AddTestRun("Connect to foo.com without cert") + tr.Processes.Default.StartBefore(urtest.Processes.ts) + tr.Processes.Default.StartBefore(server) + tr.StillRunningAfter = ts + tr.StillRunningAfter = server + tr.MakeCurlCommand( + "--tls-max 1.2 -k --resolve 'foo.com:{0}:127.0.0.1' https://foo.com:{0}/case1".format(ts.Variables.ssl_port), ts=ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.All = Testers.ExcludesExpression("error", "Check response") + + tr = urtest.AddTestRun("Connect to foo.com with cert") + tr.Setup.Copy("ssl/signed-foo.pem") + tr.Setup.Copy("ssl/signed-foo.key") + tr.StillRunningAfter = ts + tr.StillRunningAfter = server + tr.MakeCurlCommand( + "--tls-max 1.2 -k --cert ./signed-foo.pem --key ./signed-foo.key --resolve 'foo.com:{0}:127.0.0.1' https://foo.com:{0}/case1" + .format(ts.Variables.ssl_port), + ts=ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.All = Testers.ExcludesExpression("error", "Check response") + + tr = urtest.AddTestRun("Connect to bob.bar.com without cert") + tr.StillRunningAfter = ts + tr.StillRunningAfter = server + tr.MakeCurlCommand( + "--tls-max 1.2 -k --resolve 'bob.bar.com:{0}:127.0.0.1' https://bob.bar.com:{0}/case1".format(ts.Variables.ssl_port), ts=ts) + tr.Processes.Default.ReturnCode = 35 + + tr = urtest.AddTestRun("Connect to bob.bar.com with cert") + tr.Setup.Copy("ssl/signed-bob-bar.pem") + tr.Setup.Copy("ssl/signed-bar.key") + tr.StillRunningAfter = ts + tr.StillRunningAfter = server + tr.MakeCurlCommand( + "--tls-max 1.2 -k --cert ./signed-bob-bar.pem --key ./signed-bar.key --resolve 'bob.bar.com:{0}:127.0.0.1' https://bob.bar.com:{0}/case1" + .format(ts.Variables.ssl_port), + ts=ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.All = Testers.ExcludesExpression("error", "TLS handshake should succeed") + + tr = urtest.AddTestRun("Connect to bob.bar.com with bad cert") + tr.Setup.Copy("ssl/server.pem") + tr.Setup.Copy("ssl/server.key") + tr.StillRunningAfter = ts + tr.StillRunningAfter = server + tr.MakeCurlCommand( + "--tls-max 1.2 -k --cert ./server.pem --key ./server.key --resolve 'bob.bar.com:{0}:127.0.0.1' https://bob.bar.com:{0}/case1" + .format(ts.Variables.ssl_port), + ts=ts) + tr.Processes.Default.ReturnCode = 35 + + tr = urtest.AddTestRun("Connect to bob.foo.com without cert") + tr.StillRunningAfter = ts + tr.StillRunningAfter = server + tr.MakeCurlCommand( + "--tls-max 1.2 -k --resolve 'bob.foo.com:{0}:127.0.0.1' https://bob.foo.com:{0}/case1".format(ts.Variables.ssl_port), ts=ts) + tr.Processes.Default.ReturnCode = 35 + + tr = urtest.AddTestRun("Connect to bob.foo.com with cert") + tr.Setup.Copy("ssl/signed-bob-foo.pem") + tr.Setup.Copy("ssl/signed-foo.key") + tr.StillRunningAfter = ts + tr.StillRunningAfter = server + tr.MakeCurlCommand( + "--tls-max 1.2 -k --cert ./signed-bob-foo.pem --key ./signed-foo.key --resolve 'bob.foo.com:{0}:127.0.0.1' https://bob.foo.com:{0}/case1" + .format(ts.Variables.ssl_port), + ts=ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.All = Testers.ExcludesExpression("error", "TLS handshake should succeed") + + tr = urtest.AddTestRun("Connect to bob.foo.com with bad cert") + tr.Setup.Copy("ssl/server.pem") + tr.Setup.Copy("ssl/server.key") + tr.StillRunningAfter = ts + tr.StillRunningAfter = server + tr.MakeCurlCommand( + "--tls-max 1.2 -k --cert ./server.pem --key ./server.key --resolve 'bob.foo.com:{0}:127.0.0.1' https://bob.foo.com:{0}/case1" + .format(ts.Variables.ssl_port), + ts=ts) + tr.Processes.Default.ReturnCode = 35 + + tr = urtest.AddTestRun("Connect to bar.com without cert") + tr.StillRunningAfter = ts + tr.StillRunningAfter = server + tr.MakeCurlCommand( + "--tls-max 1.2 -k --resolve 'bar.com:{0}:127.0.0.1' https://bar.com:{0}/case1".format(ts.Variables.ssl_port), ts=ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.All = Testers.ExcludesExpression("alert", "TLS handshake should succeed") + + tr = urtest.AddTestRun("Connect to bar.com with cert") + tr.Setup.Copy("ssl/signed-bar.pem") + tr.Setup.Copy("ssl/signed-bar.key") + tr.StillRunningAfter = ts + tr.StillRunningAfter = server + tr.MakeCurlCommand( + "--tls-max 1.2 -k --cert ./signed-bar.pem --key ./signed-bar.key --resolve 'bar.com:{0}:127.0.0.1' https://bar.com:{0}/case1" + .format(ts.Variables.ssl_port), + ts=ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.All = Testers.ExcludesExpression("error", "Check response") + + tr = urtest.AddTestRun("Connect to bar.com with bad cert") + tr.Setup.Copy("ssl/server.pem") + tr.Setup.Copy("ssl/server.key") + tr.StillRunningAfter = ts + tr.StillRunningAfter = server + tr.MakeCurlCommand( + "--tls-max 1.2 -k --cert ./server.pem --key ./server.key --resolve 'bar.com:{0}:127.0.0.1' https://bar.com:{0}/case1" + .format(ts.Variables.ssl_port), + ts=ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.All = Testers.ExcludesExpression("alert unknown ca", "TLS handshake should succeed") + urtest.execute() diff --git a/tests/uranium_tests/tls/test_tls_client_verify3.py b/tests/uranium_tests/tls/test_tls_client_verify3.py new file mode 100644 index 00000000000..688da59caf5 --- /dev/null +++ b/tests/uranium_tests/tls/test_tls_client_verify3.py @@ -0,0 +1,171 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_tls_client_verify3(urtest: UraniumTest) -> None: + ''' + Test per SNI server name selection of CA certs for validating cert sent by client. + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + urtest.Summary = ''' + Test per SNI server name selection of CA certs for validating cert sent by client. + ''' + + ts = urtest.MakeATSProcess("ts", enable_tls=True) + + server = urtest.MakeOriginServer("server") + + request_header = {"headers": "GET /xyz HTTP/1.1\r\nHost: example.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + response_header = { + "headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", + "timestamp": "1469733493.993", + "body": "yadayadayada" + } + server.addResponse("sessionlog.json", request_header, response_header) + + ts.addSSLfile("ssl/server.pem") + ts.addSSLfile("ssl/server.key") + + ts.Setup.Copy("ssl/bbb-ca.pem", ts.Variables.CONFIGDIR) + ts.Setup.Copy("ssl/bbb-signed.key", ts.Variables.SSLDir) + ts.Setup.Copy("ssl/bbb-signed.pem", ts.Variables.SSLDir) + ts.Setup.Copy("ssl/aaa-ca.pem", ts.Variables.SSLDir) + ts.Setup.Copy("ssl/ccc-ca.pem", ts.Variables.SSLDir) + + ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'ssl', + 'proxy.config.ssl.server.cert.path': ts.Variables.SSLDir, + 'proxy.config.ssl.server.private_key.path': ts.Variables.SSLDir, + 'proxy.config.url_remap.pristine_host_hdr': 1, + 'proxy.config.ssl.client.certification_level': 2, + 'proxy.config.ssl.CA.cert.filename': f'{ts.Variables.SSLDir}/aaa-ca.pem', + 'proxy.config.ssl.TLSv1_3.enabled': 0 + }) + + ts.Disk.ssl_multicert_yaml.AddLines( + """ + ssl_multicert: + - ssl_cert_name: bbb-signed.pem + ssl_key_name: bbb-signed.key + """.split("\n")) + ts.Disk.ssl_multicert_yaml.AddLines( + """ + ssl_multicert: + - dest_ip: "*" + ssl_cert_name: server.pem + ssl_key_name: server.key + """.split("\n")) + + # Just map everything through to origin. This test is concentrating on the user-agent side. + ts.Disk.remap_config.AddLine(f'map / http://127.0.0.1:{server.Variables.Port}/') + + ts.Disk.sni_yaml.AddLines( + [ + 'sni:', + '- fqdn: bbb.com', + ' verify_client: STRICT', + ' verify_client_ca_certs: bbb-ca.pem', + '- fqdn: bbb-signed', + ' verify_client: STRICT', + ' verify_client_ca_certs: bbb-ca.pem', + '- fqdn: ccc.com', + ' verify_client: STRICT', + ' verify_client_ca_certs:', + f' file: {ts.Variables.SSLDir}/ccc-ca.pem', + ]) + + # Success test runs. + + tr = urtest.AddTestRun() + tr.Processes.Default.StartBefore(urtest.Processes.ts) + tr.Processes.Default.StartBefore(server) + tr.StillRunningAfter = ts + tr.StillRunningAfter = server + tr.MakeCurlCommand( + ("-v -k --tls-max 1.2 --cert {1}.pem --key {1}.key --resolve 'aaa.com:{0}:127.0.0.1'" + " https://aaa.com:{0}/xyz").format( + ts.Variables.ssl_port, urtest.TestDirectory + "/ssl/aaa-signed"), + ts=ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.All = Testers.ExcludesExpression("error", "Check response") + + tr = urtest.AddTestRun() + tr.StillRunningAfter = ts + tr.StillRunningAfter = server + tr.MakeCurlCommand( + ("-v -k --tls-max 1.2 --cert {1}.pem --key {1}.key --resolve 'bbb-signed:{0}:127.0.0.1'" + + " https://bbb-signed:{0}/xyz").format(ts.Variables.ssl_port, urtest.TestDirectory + "/ssl/bbb-signed"), + ts=ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.All = Testers.ExcludesExpression("error", "Check response") + + tr = urtest.AddTestRun() + tr.StillRunningAfter = ts + tr.StillRunningAfter = server + tr.MakeCurlCommand( + ("-v -k --tls-max 1.2 --cert {1}.pem --key {1}.key --resolve 'ccc.com:{0}:127.0.0.1'" + " https://ccc.com:{0}/xyz").format( + ts.Variables.ssl_port, urtest.TestDirectory + "/ssl/ccc-signed"), + ts=ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.All = Testers.ExcludesExpression("error", "Check response") + + # Failure test runs. + + tr = urtest.AddTestRun() + tr.StillRunningAfter = ts + tr.StillRunningAfter = server + tr.MakeCurlCommand( + ("-v -k --tls-max 1.2 --cert {1}.pem --key {1}.key --resolve 'aaa.com:{0}:127.0.0.1'" + " https://aaa.com:{0}/xyz").format( + ts.Variables.ssl_port, urtest.TestDirectory + "/ssl/bbb-signed"), + ts=ts) + tr.Processes.Default.ReturnCode = 35 + + tr = urtest.AddTestRun() + tr.StillRunningAfter = ts + tr.StillRunningAfter = server + tr.MakeCurlCommand( + ("-v -k --tls-max 1.2 --cert {1}.pem --key {1}.key --resolve 'bbb.com:{0}:127.0.0.1'" + " https://bbb.com:{0}/xyz").format( + ts.Variables.ssl_port, urtest.TestDirectory + "/ssl/ccc-signed"), + ts=ts) + tr.Processes.Default.ReturnCode = 35 + + tr = urtest.AddTestRun() + tr.StillRunningAfter = ts + tr.StillRunningAfter = server + tr.MakeCurlCommand( + (" -v -k --tls-max 1.2 --cert {1}.pem --key {1}.key --resolve 'ccc.com:{0}:127.0.0.1'" + + " https://ccc.com:{0}/xyz").format(ts.Variables.ssl_port, urtest.TestDirectory + "/ssl/aaa-signed"), + ts=ts) + tr.Processes.Default.ReturnCode = 35 + urtest.execute() diff --git a/tests/uranium_tests/tls/test_tls_client_versions.py b/tests/uranium_tests/tls/test_tls_client_versions.py new file mode 100644 index 00000000000..e69ddc11632 --- /dev/null +++ b/tests/uranium_tests/tls/test_tls_client_versions.py @@ -0,0 +1,146 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_tls_client_versions(urtest: UraniumTest) -> None: + ''' + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + urtest.Summary = ''' + Test TLS protocol offering based on SNI + ''' + + # By default only offer TLSv1_2 + # for special domain foo.com only offer TLSv1 and TLSv1_1 + + urtest.SkipUnless(Condition.HasOpenSSLVersion("1.1.1")) + urtest.SkipUnless(Condition.HasLegacyTLSSupport()) + has_curl_tlsv1 = Condition.HasCurlTLSVersionSupport("1.0") + + # Define default ATS + ts = urtest.MakeATSProcess("ts", enable_tls=True) + server = urtest.MakeOriginServer("server", ssl=True) + + request_foo_header = {"headers": "GET / HTTP/1.1\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + response_foo_header = { + "headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", + "timestamp": "1469733493.993", + "body": "foo ok" + } + server.addResponse("sessionlog.json", request_foo_header, response_foo_header) + + # add ssl materials like key, certificates for the server + ts.addSSLfile("ssl/server.pem") + ts.addSSLfile("ssl/server.key") + + # Need no remap rules. Everything should be processed by sni + + # Make sure the TS server certs are different from the origin certs + ts.Disk.ssl_multicert_yaml.AddLines( + """ + ssl_multicert: + - dest_ip: "*" + ssl_cert_name: server.pem + ssl_key_name: server.key + """.split("\n")) + + cipher_suite = 'ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-SHA384:AES128-GCM-SHA256:AES256-GCM-SHA384:ECDHE-RSA-RC4-SHA:ECDHE-RSA-AES128-SHA:ECDHE-RSA-AES256-SHA:RC4-SHA:RC4-MD5:AES128-SHA:AES256-SHA:DES-CBC3-SHA!SRP:!DSS:!PSK:!aNULL:!eNULL:!SSLv2' + if Condition.HasOpenSSLVersion("3.0.0"): + cipher_suite += ":@SECLEVEL=0" + + ts.Disk.records_config.update( + { + 'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir), + 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts.Variables.SSLDir), + 'proxy.config.ssl.server.cipher_suite': cipher_suite, + 'proxy.config.ssl.client.CA.cert.path': '{0}'.format(ts.Variables.SSLDir), + 'proxy.config.url_remap.pristine_host_hdr': 1, + 'proxy.config.ssl.TLSv1': 0, + 'proxy.config.ssl.TLSv1_1': 0, + 'proxy.config.exec_thread.autoconfig.scale': 1.0, + 'proxy.config.ssl.TLSv1_2': 1 + }) + + # foo.com should only offer the older TLS protocols + # bar.com should terminate. + # empty SNI should tunnel to server_bar + ts.Disk.sni_yaml.AddLines([ + 'sni:', + '- fqdn: foo.com', + ' valid_tls_versions_in: [ TLSv1, TLSv1_1 ]', + ]) + + # Target foo.com for TLSv1_2. Should fail + tr = urtest.AddTestRun("foo.com TLSv1_2") + tr.Processes.Default.StartBefore(server) + tr.Processes.Default.StartBefore(urtest.Processes.ts) + # Newer versions of OpenSSL further restrict the ciphers they accept. Setting + # the security level to 0 "retains compatibility with previous versions of + # OpenSSL." See: + # https://www.openssl.org/docs/manmaster/man3/SSL_CTX_set_security_level.html + tr.MakeCurlCommand( + "-v --ciphers DEFAULT@SECLEVEL=0 --tls-max 1.2 --tlsv1.2 --resolve 'foo.com:{0}:127.0.0.1' -k https://foo.com:{0}".format( + ts.Variables.ssl_port), + ts=ts) + tr.ReturnCode = 35 + tr.StillRunningAfter = ts + + # Target foo.com for TLSv1. Should succeed + if has_curl_tlsv1: + tr = urtest.AddTestRun("foo.com TLSv1") + tr.MakeCurlCommand( + "-v --ciphers DEFAULT@SECLEVEL=0 --tls-max 1.0 --tlsv1 --resolve 'foo.com:{0}:127.0.0.1' -k https://foo.com:{0}" + .format(ts.Variables.ssl_port), + ts=ts) + tr.ReturnCode = 0 + tr.StillRunningAfter = ts + + # Target bar.com for TLSv1. Should fail + if has_curl_tlsv1: + tr = urtest.AddTestRun("bar.com TLSv1") + tr.MakeCurlCommand( + "-v --ciphers DEFAULT@SECLEVEL=0 --tls-max 1.0 --tlsv1 --resolve 'bar.com:{0}:127.0.0.1' -k https://bar.com:{0}" + .format(ts.Variables.ssl_port), + ts=ts) + tr.ReturnCode = 35 + tr.StillRunningAfter = ts + + # Target bar.com for TLSv1_2. Should succeed + tr = urtest.AddTestRun("bar.com TLSv1_2") + tr.MakeCurlCommand( + "-v --ciphers DEFAULT@SECLEVEL=0 --tls-max 1.2 --tlsv1.2 --resolve 'bar.com:{0}:127.0.0.1' -k https://bar.com:{0}".format( + ts.Variables.ssl_port), + ts=ts) + tr.ReturnCode = 0 + tr.StillRunningAfter = ts + urtest.execute() diff --git a/tests/uranium_tests/tls/test_tls_client_versions_minmax.py b/tests/uranium_tests/tls/test_tls_client_versions_minmax.py new file mode 100644 index 00000000000..f970443216c --- /dev/null +++ b/tests/uranium_tests/tls/test_tls_client_versions_minmax.py @@ -0,0 +1,162 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_tls_client_versions_minmax(urtest: UraniumTest) -> None: + ''' + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + urtest.Summary = ''' + Test TLS protocol offering based on SNI + ''' + + # By default only offer TLSv1_2 + # for special domain foo.com only offer TLSv1 and TLSv1_1 + + urtest.SkipUnless(Condition.HasOpenSSLVersion("1.1.1")) + urtest.SkipUnless(Condition.HasLegacyTLSSupport()) + has_curl_tlsv1 = Condition.HasCurlTLSVersionSupport("1.0") + has_curl_tlsv1_1 = Condition.HasCurlTLSVersionSupport("1.1") + + # Define default ATS + ts = urtest.MakeATSProcess("ts", enable_tls=True) + server = urtest.MakeOriginServer("server", ssl=True) + + request_foo_header = {"headers": "GET / HTTP/1.1\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + response_foo_header = { + "headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", + "timestamp": "1469733493.993", + "body": "foo ok" + } + server.addResponse("sessionlog.json", request_foo_header, response_foo_header) + + # add ssl materials like key, certificates for the server + ts.addSSLfile("ssl/server.pem") + ts.addSSLfile("ssl/server.key") + + # Need no remap rules. Everything should be processed by sni + + # Make sure the TS server certs are different from the origin certs + ts.Disk.ssl_multicert_yaml.AddLines( + """ + ssl_multicert: + - dest_ip: "*" + ssl_cert_name: server.pem + ssl_key_name: server.key + """.split("\n")) + + ts.Disk.records_config.update( + { + 'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir), + 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts.Variables.SSLDir), + 'proxy.config.ssl.client.CA.cert.path': '{0}'.format(ts.Variables.SSLDir), + 'proxy.config.url_remap.pristine_host_hdr': 1, + 'proxy.config.ssl.server.version.min': 2, + 'proxy.config.ssl.server.version.max': 2, + 'proxy.config.ssl.TLSv1_2': 0, # This setting should be ignored in favor of a version range setting + 'proxy.config.exec_thread.autoconfig.scale': 1.0, + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'ssl', + }) + + cipher_suite = 'ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-SHA384:AES128-GCM-SHA256:AES256-GCM-SHA384:ECDHE-RSA-RC4-SHA:ECDHE-RSA-AES128-SHA:ECDHE-RSA-AES256-SHA:RC4-SHA:RC4-MD5:AES128-SHA:AES256-SHA:DES-CBC3-SHA!SRP:!DSS:!PSK:!aNULL:!eNULL:!SSLv2' + if Condition.HasOpenSSLVersion("3.0.0"): + cipher_suite += ":@SECLEVEL=0" + + # foo.com should only offer the older TLS protocols + # bar.com should terminate. + # empty SNI should tunnel to server_bar + ts.Disk.sni_yaml.AddLines( + [ + 'sni:', + '- fqdn: foo.com', + ' valid_tls_versions_in: [ TLSv1_2 ]', # This setting should be ignored in favor of a version range setting + ' valid_tls_version_min_in: TLSv1', + ' valid_tls_version_max_in: TLSv1_1', + f' server_cipher_suite: {cipher_suite}', + ]) + + # Target foo.com for TLSv1_2. Should fail + tr = urtest.AddTestRun("foo.com TLSv1_2") + tr.Processes.Default.StartBefore(server) + tr.Processes.Default.StartBefore(urtest.Processes.ts) + # Newer versions of OpenSSL further restrict the ciphers they accept. Setting + # the security level to 0 "retains compatibility with previous versions of + # OpenSSL." See: + # https://www.openssl.org/docs/manmaster/man3/SSL_CTX_set_security_level.html + tr.MakeCurlCommand( + "-v --ciphers DEFAULT@SECLEVEL=0 --tls-max 1.2 --tlsv1.2 --resolve 'foo.com:{0}:127.0.0.1' -k https://foo.com:{0}".format( + ts.Variables.ssl_port), + ts=ts) + tr.ReturnCode = 35 + tr.StillRunningAfter = ts + + # Target foo.com for TLSv1. Should succeed + if has_curl_tlsv1: + tr = urtest.AddTestRun("foo.com TLSv1") + tr.MakeCurlCommand( + "-v --ciphers DEFAULT@SECLEVEL=0 --tls-max 1.0 --tlsv1 --resolve 'foo.com:{0}:127.0.0.1' -k https://foo.com:{0}" + .format(ts.Variables.ssl_port), + ts=ts) + tr.ReturnCode = 0 + tr.StillRunningAfter = ts + + # Target foo.com for TLSv1_1. Should succeed + if has_curl_tlsv1_1: + tr = urtest.AddTestRun("foo.com TLSv1_1") + tr.MakeCurlCommand( + "-v --ciphers DEFAULT@SECLEVEL=0 --tls-max 1.1 --tlsv1.1 --resolve 'foo.com:{0}:127.0.0.1' -k https://foo.com:{0}" + .format(ts.Variables.ssl_port), + ts=ts) + tr.ReturnCode = 0 + tr.StillRunningAfter = ts + + # Target bar.com for TLSv1. Should fail + if has_curl_tlsv1: + tr = urtest.AddTestRun("bar.com TLSv1") + tr.MakeCurlCommand( + "-v --ciphers DEFAULT@SECLEVEL=0 --tls-max 1.0 --tlsv1 --resolve 'bar.com:{0}:127.0.0.1' -k https://bar.com:{0}" + .format(ts.Variables.ssl_port), + ts=ts) + tr.ReturnCode = 35 + tr.StillRunningAfter = ts + + # Target bar.com for TLSv1_2. Should succeed + tr = urtest.AddTestRun("bar.com TLSv1_2") + tr.MakeCurlCommand( + "-v --ciphers DEFAULT@SECLEVEL=0 --tls-max 1.2 --tlsv1.2 --resolve 'bar.com:{0}:127.0.0.1' -k https://bar.com:{0}".format( + ts.Variables.ssl_port), + ts=ts) + tr.ReturnCode = 0 + tr.StillRunningAfter = ts + urtest.execute() diff --git a/tests/uranium_tests/tls/test_tls_engine_abort.py b/tests/uranium_tests/tls/test_tls_engine_abort.py new file mode 100644 index 00000000000..398d376a8d4 --- /dev/null +++ b/tests/uranium_tests/tls/test_tls_engine_abort.py @@ -0,0 +1,141 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_tls_engine_abort(urtest: UraniumTest) -> None: + ''' + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import os + import sys + + urtest.Summary = ''' + Abort TLS handshakes while an OpenSSL async job is mid-pause, to exercise the + SSLNetVConnection teardown path. When a handshake returns SSL_ERROR_WANT_ASYNC + an eventfd is registered on the poller with the connection as its target, and a + connection torn down before the async job finishes must deregister that eventfd + so the poller is not left pointing at a freed connection. Built under ASan the + server must survive the abort barrage with no sanitizer error and still serve a + normal request. + ''' + + async_handshake = os.path.join(urtest.Variables.AtsTestPluginsDir, 'async_handshake.so') + + urtest.SkipUnless( + Condition.HasOpenSSLVersion('1.1.1'), + Condition.IsOpenSSL(), + Condition(lambda: os.path.isfile(async_handshake), async_handshake + " not found."), + ) + + ts = urtest.MakeATSProcess("ts", enable_tls=True) + server = urtest.MakeOriginServer("server") + + # A wide pause window (well beyond the abort client's 0.4s handshake attempt) + # so the abort reliably lands while the async job is still in flight. + if os.path.isfile(async_handshake): + urtest.PrepareTestPlugin(async_handshake, ts, '-delay-ms=2000') + + server.addResponse( + "sessionlog.json", { + "headers": "GET / HTTP/1.1\r\nuuid: basic\r\n\r\n", + "timestamp": "1469733493.993", + "body": "" + }, { + "headers": + "HTTP/1.1 200 OK\r\nServer: microserver\r\nConnection: close\r\nCache-Control: max-age=3600\r\nContent-Length: 2\r\n\r\n", + "timestamp": "1469733493.993", + "body": "ok" + }) + + ts.addSSLfile("ssl/server.pem") + ts.addSSLfile("ssl/server.key") + + ts.Disk.remap_config.AddLine('map / http://127.0.0.1:{0}'.format(server.Variables.Port)) + + ts.Disk.ssl_multicert_yaml.AddLines( + """ + ssl_multicert: + - dest_ip: "*" + ssl_cert_name: server.pem + ssl_key_name: server.key + """.split("\n")) + + ts.Disk.records_config.update( + { + 'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir), + 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts.Variables.SSLDir), + 'proxy.config.exec_thread.autoconfig.scale': 1.0, + 'proxy.config.ssl.async.handshake.enabled': 1, + 'proxy.config.diags.debug.enabled': 0, + 'proxy.config.diags.debug.tags': 'ssl' + }) + + # Fire a barrage of handshakes that abort while the async job is mid-pause. Correct + # teardown is validated by ATS surviving this with no crash and, under ASan, no + # sanitizer error. Without deregistration the connection is freed while its + # eventfd still has a live poller registration. + abort_client = os.path.join(urtest.TestDirectory, 'tls_engine_abort.py') + + tr = urtest.AddTestRun("abort-during-async-handshake") + tr.Processes.Default.Command = "{0} {1} {2} 30".format(sys.executable, abort_client, ts.Variables.ssl_port) + tr.ReturnCode = 0 + tr.Processes.Default.StartBefore(server) + tr.Processes.Default.StartBefore(urtest.Processes.ts, ready=When.PortOpen(ts.Variables.ssl_port)) + tr.Processes.Default.Streams.All = Testers.ContainsExpression("sent 30 aborted handshakes", "Abort client ran") + tr.StillRunningAfter = ts + tr.StillRunningAfter = server + + # After the abort barrage, a normal request must still succeed: the server is + # healthy, not crashed or wedged. + tr2 = urtest.AddTestRun("normal-request-after-aborts") + tr2.MakeCurlCommand("-k -v -H uuid:basic -H host:example.com https://127.0.0.1:{0}/".format(ts.Variables.ssl_port), ts=ts) + tr2.ReturnCode = 0 + tr2.Processes.Default.Streams.All = Testers.ContainsExpression(r"HTTP/(2|1\.1) 200", "Request succeeds after the abort barrage") + tr2.StillRunningAfter = ts + tr2.StillRunningAfter = server + + # The abort barrage must actually drive handshakes into the async pause, + # otherwise the eventfd is never registered and the test proves nothing. The + # async_handshake plugin's wake thread prints this to stderr (-> traffic.out) + # when it signals the eventfd at the end of its pause; that only happens if a + # handshake entered the WANT_ASYNC path and armed the eventfd, so its presence + # confirms the teardown path was exercised (and that the async job completed + # after the abort -- exactly the use-after-free window this fix closes). + ts.Disk.traffic_out.Content += Testers.ContainsExpression( + "sent async wake signal to", "Async job engaged on at least one handshake") + + # The server process must not have reported an AddressSanitizer error. ASan + # writes to stderr, which the harness binds to traffic.out -- not diags.log -- + # so the exclusion has to be checked against traffic.out to catch the UAF. + ts.Disk.traffic_out.Content += Testers.ExcludesExpression("AddressSanitizer", "No ASan error in the server") + urtest.execute() diff --git a/tests/uranium_tests/tls/test_tls_flow_control.py b/tests/uranium_tests/tls/test_tls_flow_control.py new file mode 100644 index 00000000000..0c404c8c3d6 --- /dev/null +++ b/tests/uranium_tests/tls/test_tls_flow_control.py @@ -0,0 +1,135 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_tls_flow_control(urtest: UraniumTest) -> None: + ''' + Exercise HTTP tunnel flow control (proxy.config.http.flow_control) on a TLS + client connection: a slow reader makes ATS throttle the origin and unthrottle as + the buffered data drains. The full response body must still be delivered. + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import os + import sys + + urtest.Summary = __doc__ + + class TestTlsFlowControl: + '''Verify a slow TLS reader under flow control still receives the whole body.''' + + # Comfortably larger than the water marks so the tunnel throttles and has to + # unthrottle many times over the transfer. + _body_len: int = 8 * 1024 * 1024 + _high_water: int = 64 * 1024 + _low_water: int = 32 * 1024 + + _server_counter: int = 0 + _ts_counter: int = 0 + + def __init__(self) -> None: + '''Declare the test Processes.''' + self._server = self._configure_server() + self._ts = self._configure_trafficserver() + + def _configure_server(self) -> 'Process': + '''Configure the origin server with a large response body. + + :return: The origin server Process. + ''' + server = urtest.MakeOriginServer(f'server-{TestTlsFlowControl._server_counter}') + TestTlsFlowControl._server_counter += 1 + + request_header = {"headers": "GET /obj HTTP/1.1\r\nHost: ex.test\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + response_header = { + "headers": + "HTTP/1.1 200 OK\r\nServer: microserver\r\nConnection: close\r\n" + f"Content-Length: {TestTlsFlowControl._body_len}\r\n\r\n", + "timestamp": "1469733493.993", + "body": "x" * TestTlsFlowControl._body_len + } + server.addResponse("sessionlog.json", request_header, response_header) + return server + + def _configure_trafficserver(self) -> 'Process': + '''Configure Traffic Server with HTTP flow control enabled over TLS. + + :return: The Traffic Server Process. + ''' + ts = urtest.MakeATSProcess(f'ts-{TestTlsFlowControl._ts_counter}', enable_tls=True, enable_cache=False) + TestTlsFlowControl._ts_counter += 1 + + ts.addDefaultSSLFiles() + ts.Disk.ssl_multicert_yaml.AddLines( + """ + ssl_multicert: + - dest_ip: "*" + ssl_cert_name: server.pem + ssl_key_name: server.key + """.split("\n")) + ts.Disk.remap_config.AddLine(f'map / http://127.0.0.1:{self._server.Variables.Port}') + ts.Disk.records_config.update( + { + 'proxy.config.ssl.server.cert.path': f'{ts.Variables.SSLDir}', + 'proxy.config.ssl.server.private_key.path': f'{ts.Variables.SSLDir}', + # Small thresholds on a network sink: the tunnel reenable fires before + # the socket write, so the unthrottle depends on the buffer draining. + 'proxy.config.http.flow_control.enabled': 1, + 'proxy.config.http.flow_control.high_water': TestTlsFlowControl._high_water, + 'proxy.config.http.flow_control.low_water': TestTlsFlowControl._low_water, + }) + + ts.Disk.traffic_out.Content = Testers.ExcludesExpression( + "received signal|failed assertion", "ATS must not crash under TLS flow control") + ts.Disk.traffic_out.Content += Testers.ExcludesExpression( + "AddressSanitizer|use-after-free|runtime error:", "no memory-safety error under TLS flow control") + return ts + + def run(self) -> None: + '''Configure and run the TestRun.''' + tr = urtest.AddTestRun("slow TLS reader under flow control must receive the whole body") + tr.Processes.Default.StartBefore(self._server) + tr.Processes.Default.StartBefore(self._ts) + tr.Processes.Default.Command = ( + f'{sys.executable} {os.path.join(urtest.TestDirectory, "tls_flow_control_client.py")} ' + f'-p {self._ts.Variables.ssl_port} --host ex.test --path /obj ' + f'--expect-bytes {TestTlsFlowControl._body_len}') + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.All = Testers.ContainsExpression( + "RESULT=PASS", "the full body must be delivered under flow control") + tr.Processes.Default.Streams.All += Testers.ContainsExpression( + f"BODY_BYTES={TestTlsFlowControl._body_len}", "every body byte must arrive (no flow-control stall)") + tr.StillRunningAfter = self._ts + tr.StillRunningAfter = self._server + + TestTlsFlowControl().run() + urtest.execute() diff --git a/tests/uranium_tests/tls/test_tls_forward_nonhttp.py b/tests/uranium_tests/tls/test_tls_forward_nonhttp.py new file mode 100644 index 00000000000..b39e26d847e --- /dev/null +++ b/tests/uranium_tests/tls/test_tls_forward_nonhttp.py @@ -0,0 +1,104 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_tls_forward_nonhttp(urtest: UraniumTest) -> None: + ''' + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import os + import ports + + urtest.Summary = ''' + Forwarding a non-HTTP protocol out of TLS + ''' + + # Define default ATS + ts = urtest.MakeATSProcess("ts", enable_tls=True) + + # add ssl materials like key, certificates for the server + ts.addSSLfile("ssl/server.pem") + ts.addSSLfile("ssl/server.key") + + # reserve a port of the s_client that will be released with 'ts' + ports.get_port(ts, 's_client_port') + + nameserver = urtest.MakeDNServer("dns", default='127.0.0.1') + + # Need no remap rules. Everything should be processed by sni + + # Make sure the TS server certs are different from the origin certs + ts.Disk.ssl_multicert_yaml.AddLines( + """ + ssl_multicert: + - dest_ip: "*" + ssl_cert_name: server.pem + ssl_key_name: server.key + """.split("\n")) + + # Case 1, global config policy=permissive properties=signature + # override for foo.com policy=enforced properties=all + ts.Disk.records_config.update( + { + 'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir), + 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts.Variables.SSLDir), + 'proxy.config.http.connect_ports': '{0} {1}'.format(ts.Variables.ssl_port, ts.Variables.s_client_port), + 'proxy.config.exec_thread.autoconfig.scale': 1.0, + 'proxy.config.url_remap.pristine_host_hdr': 1, + 'proxy.config.dns.nameservers': f"127.0.0.1:{nameserver.Variables.Port}", + 'proxy.config.dns.resolv_conf': 'NULL' + }) + ts.addPrivateConnectAllowYaml() + + # foo.com should not terminate. Just tunnel to server_foo + # bar.com should terminate. Forward its tcp stream to server_bar + ts.Disk.sni_yaml.AddLines([ + "sni:", + "- fqdn: bar.com", + " forward_route: localhost:{0}".format(ts.Variables.s_client_port), + ]) + + tr = urtest.AddTestRun("forward-non-http") + tr.Setup.Copy("test-nc-s_client.sh") + cmd_args = ["sh", "test-nc-s_client.sh", str(ts.Variables.s_client_port), str(ts.Variables.ssl_port)] + if Condition.HasOpenSSLVersion("3.0.0"): + cmd_args += ["-ignore_unexpected_eof"] + tr.Processes.Default.Command = " ".join(cmd_args) + tr.ReturnCode = 0 + tr.Processes.Default.StartBefore(nameserver) + tr.Processes.Default.StartBefore(urtest.Processes.ts) + tr.StillRunningAfter = ts + testout_path = os.path.join(urtest.RunDirectory, "test.out") + tr.Disk.File(testout_path, id="testout") + tr.Processes.Default.Streams.All += Testers.IncludesExpression("This is a reply", "s_client should get response") + urtest.execute() diff --git a/tests/uranium_tests/tls/test_tls_h2_cipher_suite.py b/tests/uranium_tests/tls/test_tls_h2_cipher_suite.py new file mode 100644 index 00000000000..f0080f11e58 --- /dev/null +++ b/tests/uranium_tests/tls/test_tls_h2_cipher_suite.py @@ -0,0 +1,115 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_tls_h2_cipher_suite(urtest: UraniumTest) -> None: + """Verify that HTTP/2 is not negotiated with prohibited TLS cipher suites.""" + + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + urtest.Summary = __doc__ + + urtest.SkipUnless(Condition.HasOpenSSLVersion("1.1.1")) + + class TestH2CipherSuite: + """Verify HTTP/2 cipher suite restrictions.""" + + def __init__(self) -> None: + """Configure Traffic Server and the cipher suite test runs.""" + self._ts = self._configure_traffic_server() + self._configure_allowed_cipher_test() + + for description, cipher in ( + ("non-ephemeral AEAD", "AES128-GCM-SHA256"), + ("ephemeral CBC", "ECDHE-RSA-AES128-SHA256"), + ): + self._configure_prohibited_cipher_test(description, cipher) + + def _configure_traffic_server(self) -> 'Process': + """Configure Traffic Server with allowed and prohibited cipher suites.""" + ts = urtest.MakeATSProcess("ts", enable_tls=True) + ts.addSSLfile("ssl/server.pem") + ts.addSSLfile("ssl/server.key") + + ts.Disk.ssl_multicert_yaml.AddLines( + """ + ssl_multicert: + - dest_ip: "*" + ssl_cert_name: server.pem + ssl_key_name: server.key + """.split("\n")) + + ts.Disk.records_config.update( + { + "proxy.config.ssl.server.cert.path": ts.Variables.SSLDir, + "proxy.config.ssl.server.private_key.path": ts.Variables.SSLDir, + "proxy.config.ssl.server.version.min": 2, + "proxy.config.ssl.server.version.max": 2, + "proxy.config.ssl.server.cipher_suite": + "ECDHE-RSA-AES128-GCM-SHA256:" + "AES128-GCM-SHA256:" + "ECDHE-RSA-AES128-SHA256:" + "@SECLEVEL=0", + }) + return ts + + def _configure_allowed_cipher_test(self) -> None: + """Verify that an ephemeral AEAD cipher can negotiate HTTP/2.""" + tr = urtest.AddTestRun("Allow HTTP/2 with an ephemeral AEAD cipher") + tr.Processes.Default.Command = ( + "openssl s_client -tls1_2 -cipher ECDHE-RSA-AES128-GCM-SHA256 " + f"-alpn h2,http/1.1 -connect 127.0.0.1:{self._ts.Variables.ssl_port} None: + """Verify that a prohibited cipher falls back to HTTP/1.1. + + :param description: A human-readable description of the cipher. + :param cipher: The OpenSSL cipher suite name. + """ + tr = urtest.AddTestRun(f"Fall back to HTTP/1.1 with a prohibited {description} cipher") + tr.Processes.Default.Command = ( + "printf 'GET / HTTP/1.1\\r\\nHost: example.com\\r\\nConnection: close\\r\\n\\r\\n' | " + f"openssl s_client -ign_eof -tls1_2 -cipher {cipher} " + f"-alpn h2,http/1.1 -connect 127.0.0.1:{self._ts.Variables.ssl_port}") + tr.Processes.Default.Streams.All += Testers.IncludesExpression( + "ALPN protocol: http/1.1", "HTTP/1.1 should be negotiated") + tr.Processes.Default.Streams.All += Testers.IncludesExpression( + "HTTP/1.1 404", "The HTTP/1.1 request should be processed") + tr.ReturnCode = 0 + tr.StillRunningAfter = self._ts + + TestH2CipherSuite() + urtest.execute() diff --git a/tests/uranium_tests/tls/test_tls_hooks_client_verify.py b/tests/uranium_tests/tls/test_tls_hooks_client_verify.py new file mode 100644 index 00000000000..905981560e9 --- /dev/null +++ b/tests/uranium_tests/tls/test_tls_hooks_client_verify.py @@ -0,0 +1,144 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_tls_hooks_client_verify(urtest: UraniumTest) -> None: + ''' + Test SERVER_VERIFY_HOOK + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import os + + urtest.Summary = ''' + Test different combinations of TLS handshake hooks to ensure they are applied consistently. + ''' + + urtest.SkipUnless(Condition.HasOpenSSLVersion("1.1.1")) + + ts = urtest.MakeATSProcess("ts", enable_tls=True) + server = urtest.MakeOriginServer("server", ssl=True) + request_header = {"headers": "GET / HTTP/1.1\r\nHost: www.example.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + # desired response form the origin server + response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + server.addResponse("sessionlog.json", request_header, response_header) + + ts.addSSLfile("ssl/server.pem") + ts.addSSLfile("ssl/server.key") + ts.addSSLfile("ssl/signer.pem") + + ts.Disk.records_config.update( + { + # Test looks for debug output from the plugin + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'ssl_client_verify_test', + 'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir), + 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts.Variables.SSLDir), + 'proxy.config.exec_thread.autoconfig.scale': 1.0, + 'proxy.config.ssl.CA.cert.filename': '{0}/signer.pem'.format(ts.Variables.SSLDir), + 'proxy.config.url_remap.pristine_host_hdr': 1 + }) + + ts.Disk.ssl_multicert_yaml.AddLines( + """ + ssl_multicert: + - dest_ip: "*" + ssl_cert_name: server.pem + ssl_key_name: server.key + """.split("\n")) + + ts.Disk.remap_config.AddLine( + 'map https://foo.com:{1}/ https://127.0.0.1:{0}'.format(server.Variables.SSL_Port, ts.Variables.ssl_port)) + ts.Disk.remap_config.AddLine( + 'map https://bar.com:{1}/ https://127.0.0.1:{0}'.format(server.Variables.SSL_Port, ts.Variables.ssl_port)) + ts.Disk.remap_config.AddLine( + 'map https://random.com:{1}/ https://127.0.0.1:{0}'.format(server.Variables.SSL_Port, ts.Variables.ssl_port)) + + ts.Disk.sni_yaml.AddLines( + [ + 'sni:', + '- fqdn: bar.com', + ' verify_client: STRICT', + '- fqdn: foo.com', + ' verify_client: STRICT', + ]) + + urtest.PrepareTestPlugin( + os.path.join(urtest.Variables.AtsTestPluginsDir, 'ssl_client_verify_test.so'), ts, '-count=2 -good=foo.com') + + tr = urtest.AddTestRun("request good name") + tr.Setup.Copy("ssl/signed-foo.pem") + tr.Setup.Copy("ssl/signed-foo.key") + tr.Setup.Copy("ssl/signed-bar.pem") + tr.Setup.Copy("ssl/signed-bar.key") + tr.Processes.Default.StartBefore(server) + tr.Processes.Default.StartBefore(urtest.Processes.ts) + tr.StillRunningAfter = ts + tr.StillRunningAfter = server + tr.MakeCurlCommand( + "--tls-max 1.2 -k --cert ./signed-foo.pem --key ./signed-foo.key --resolve 'foo.com:{0}:127.0.0.1' https://foo.com:{0}/case1" + .format(ts.Variables.ssl_port), + ts=ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.all = Testers.ExcludesExpression("Could Not Connect", "Curl attempt should have succeeded") + + tr2 = urtest.AddTestRun("request bad name") + tr2.StillRunningAfter = ts + tr2.StillRunningAfter = server + tr2.MakeCurlCommand( + "--tls-max 1.2 -k --cert ./signed-bar.pem --key ./signed-bar.key --resolve 'foo.com:{0}:127.0.0.1' https://foo.com:{0}/case1" + .format(ts.Variables.ssl_port), + ts=ts) + tr2.Processes.Default.ReturnCode = 35 + tr2.Processes.Default.Streams.all = Testers.ContainsExpression("error", "Curl attempt should have failed") + + tr3 = urtest.AddTestRun("request badly signed cert") + tr3.Setup.Copy("ssl/server.pem") + tr3.Setup.Copy("ssl/server.key") + tr3.StillRunningAfter = ts + tr3.StillRunningAfter = server + tr3.MakeCurlCommand( + "--tls-max 1.2 -k --cert ./server.pem --key ./server.key --resolve 'foo.com:{0}:127.0.0.1' https://foo.com:{0}/case1" + .format(ts.Variables.ssl_port), + ts=ts) + tr3.Processes.Default.ReturnCode = 35 + tr3.Processes.Default.Streams.all = Testers.ContainsExpression("error", "Curl attempt should have failed") + + ts.Disk.traffic_out.Content += Testers.ContainsExpression( + r"Client verify callback 0 [\da-fx]+? - event is good good HS", "verify callback happens 2 times") + ts.Disk.traffic_out.Content += Testers.ContainsExpression( + r"Client verify callback 1 [\da-fx]+? - event is good good HS", "verify callback happens 2 times") + ts.Disk.traffic_out.Content += Testers.ContainsExpression( + r"Client verify callback 0 [\da-fx]+? - event is good error HS", "verify callback happens 2 times") + ts.Disk.traffic_out.Content += Testers.ContainsExpression( + r"Client verify callback 1 [\da-fx]+? - event is good error HS", "verify callback happens 2 times") + urtest.execute() diff --git a/tests/uranium_tests/tls/test_tls_hooks_verify.py b/tests/uranium_tests/tls/test_tls_hooks_verify.py new file mode 100644 index 00000000000..ada177781d4 --- /dev/null +++ b/tests/uranium_tests/tls/test_tls_hooks_verify.py @@ -0,0 +1,135 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_tls_hooks_verify(urtest: UraniumTest) -> None: + ''' + Test SERVER_VERIFY_HOOK + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import os + + urtest.Summary = ''' + Test different combinations of TLS handshake hooks to ensure they are applied consistently. + ''' + + ts = urtest.MakeATSProcess("ts", enable_tls=True) + server = urtest.MakeOriginServer("server", ssl=True) + request_header = {"headers": "GET / HTTP/1.1\r\nHost: www.example.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + # desired response form the origin server + response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + server.addResponse("sessionlog.json", request_header, response_header) + + ts.addSSLfile("ssl/server.pem") + ts.addSSLfile("ssl/server.key") + + ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'ssl_verify_test', + 'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir), + 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts.Variables.SSLDir), + 'proxy.config.ssl.client.verify.server.policy': 'ENFORCED', + 'proxy.config.ssl.client.verify.server.properties': 'NONE', + 'proxy.config.url_remap.pristine_host_hdr': 1 + }) + + ts.Disk.ssl_multicert_yaml.AddLines( + """ + ssl_multicert: + - dest_ip: "*" + ssl_cert_name: server.pem + ssl_key_name: server.key + """.split("\n")) + + ts.Disk.remap_config.AddLine( + 'map https://foo.com:{1}/ https://127.0.0.1:{0}'.format(server.Variables.SSL_Port, ts.Variables.ssl_port)) + ts.Disk.remap_config.AddLine( + 'map https://bar.com:{1}/ https://127.0.0.1:{0}'.format(server.Variables.SSL_Port, ts.Variables.ssl_port)) + ts.Disk.remap_config.AddLine( + 'map https://random.com:{1}/ https://127.0.0.1:{0}'.format(server.Variables.SSL_Port, ts.Variables.ssl_port)) + + ts.Disk.sni_yaml.AddLine('sni:') + ts.Disk.sni_yaml.AddLine('- fqdn: bar.com') + ts.Disk.sni_yaml.AddLine(' verify_server_policy: PERMISSIVE') + + urtest.PrepareTestPlugin( + os.path.join(urtest.Variables.AtsTestPluginsDir, 'ssl_verify_test.so'), ts, '-count=2 -bad=random.com -bad=bar.com') + + tr = urtest.AddTestRun("request good name") + tr.Processes.Default.StartBefore(server) + tr.Processes.Default.StartBefore(urtest.Processes.ts) + tr.StillRunningAfter = ts + tr.StillRunningAfter = server + tr.MakeCurlCommand("--resolve \"foo.com:{0}:127.0.0.1\" -k https://foo.com:{0}".format(ts.Variables.ssl_port), ts=ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.stdout = Testers.ExcludesExpression("Could Not Connect", "Curl attempt should have failed") + + tr2 = urtest.AddTestRun("request bad name") + tr2.StillRunningAfter = ts + tr2.StillRunningAfter = server + tr2.MakeCurlCommand("--resolve \"random.com:{0}:127.0.0.1\" -k https://random.com:{0}".format(ts.Variables.ssl_port), ts=ts) + tr2.Processes.Default.ReturnCode = 0 + tr2.Processes.Default.Streams.stdout = Testers.ContainsExpression("Could Not Connect", "Curl attempt should have failed") + + tr3 = urtest.AddTestRun("request bad name permissive") + tr3.StillRunningAfter = ts + tr3.StillRunningAfter = server + tr3.MakeCurlCommand("--resolve \"bar.com:{0}:127.0.0.1\" -k https://bar.com:{0}".format(ts.Variables.ssl_port), ts=ts) + tr3.Processes.Default.ReturnCode = 0 + tr3.Processes.Default.Streams.stdout = Testers.ExcludesExpression("Could Not Connect", "Curl attempt should have failed") + + # Overriding the built in ERROR check since we expect tr2 to fail + ts.Disk.diags_log.Content = Testers.ContainsExpression( + "WARNING: TS_EVENT_SSL_VERIFY_SERVER plugin failed the origin certificate check for 127.0.0.1. Action=Terminate SNI=random.com", + "random.com should fail") + ts.Disk.diags_log.Content += Testers.ContainsExpression( + "WARNING: TS_EVENT_SSL_VERIFY_SERVER plugin failed the origin certificate check for 127.0.0.1. Action=Continue SNI=bar.com", + "bar.com should fail but continue") + ts.Disk.diags_log.Content += Testers.ExcludesExpression("SNI=foo.com", "foo.com should not fail in any way") + + ts.Disk.traffic_out.Content += Testers.ContainsExpression( + r"Server verify callback 0 [\da-fx]+? - event is good SNI=foo.com good HS", "verify callback happens 2 times") + ts.Disk.traffic_out.Content += Testers.ContainsExpression( + r"Server verify callback 1 [\da-fx]+? - event is good SNI=foo.com good HS", "verify callback happens 2 times") + ts.Disk.traffic_out.Content += Testers.ContainsExpression( + r"Server verify callback 0 [\da-fx]+? - event is good SNI=random.com error HS", "verify callback happens 2 times") + ts.Disk.traffic_out.Content += Testers.ContainsExpression( + r"Server verify callback 1 [\da-fx]+? - event is good SNI=random.com error HS", "verify callback happens 2 times") + ts.Disk.traffic_out.Content += Testers.ContainsExpression( + r"Server verify callback 0 [\da-fx]+? - event is good SNI=bar.com error HS", "verify callback happens 2 times") + ts.Disk.traffic_out.Content += Testers.ContainsExpression( + r"Server verify callback 1 [\da-fx]+? - event is good SNI=bar.com error HS", "verify callback happens 2 times") + ts.Disk.traffic_out.Content += Testers.ContainsExpression( + "Server verify callback SNI APIs match=true", "verify SNI names match") + urtest.execute() diff --git a/tests/uranium_tests/tls/test_tls_keepalive.py b/tests/uranium_tests/tls/test_tls_keepalive.py new file mode 100644 index 00000000000..197573327f0 --- /dev/null +++ b/tests/uranium_tests/tls/test_tls_keepalive.py @@ -0,0 +1,138 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_tls_keepalive(urtest: UraniumTest) -> None: + ''' + Use pre-accept hook to verify that both requests are made over the same TLS session + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import os + + urtest.Summary = ''' + Verify that the client-side keep alive is honored for TLS and different versions of HTTP + ''' + + urtest.SkipUnless(Condition.HasCurlFeature('http2')) + + ts = urtest.MakeATSProcess("ts", enable_tls=True) + server = urtest.MakeOriginServer("server") + request_header = {"headers": "GET / HTTP/1.1\r\nHost: www.example.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + # desired response form the origin server + response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + server.addResponse("sessionlog.json", request_header, response_header) + + ts.addSSLfile("ssl/server.pem") + ts.addSSLfile("ssl/server.key") + + ts.Disk.records_config.update( + { + 'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir), + 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts.Variables.SSLDir), + 'proxy.config.ssl.TLSv1_3.enabled': 0, + 'proxy.config.exec_thread.autoconfig.scale': 1.0, + 'proxy.config.log.max_secs_per_buffer': 1 + }) + + ts.Disk.ssl_multicert_yaml.AddLines( + """ + ssl_multicert: + - dest_ip: "*" + ssl_cert_name: server.pem + ssl_key_name: server.key + """.split("\n")) + + ts.Disk.remap_config.AddLine( + 'map https://example.com:{0} http://127.0.0.1:{1}'.format(ts.Variables.ssl_port, server.Variables.Port)) + + ts.Disk.logging_yaml.AddLines( + ''' + logging: + formats: + - name: testformat + format: '% %' + logs: + - mode: ascii + format: testformat + filename: squid + '''.split("\n")) + + urtest.PrepareTestPlugin(os.path.join(urtest.Variables.AtsTestPluginsDir, 'ssl_secret_load_test.so'), ts) + + tr = urtest.AddTestRun("Test two HTTP/1.1 requests over one TLS connection") + tr.Processes.Default.StartBefore(server) + tr.Processes.Default.StartBefore(urtest.Processes.ts) + tr.StillRunningAfter = ts + tr.StillRunningAfter = server + tr.MakeCurlCommand( + '-k -v --http1.1 -H \'host:example.com:{0}\' https://127.0.0.1:{0} https://127.0.0.1:{0}'.format(ts.Variables.ssl_port), + ts=ts) + tr.Processes.Default.ReturnCode = 0 + + tr = urtest.AddTestRun("Test two HTTP/1.1 requests over two TLS connections") + tr.StillRunningAfter = ts + tr.StillRunningAfter = server + tr.MakeCurlCommandMulti( + '{{curl}} -k -v --http1.1 -H \'host:example.com:{0}\' https://127.0.0.1:{0}; {{curl}} -k -v --http1.1 -H \'host:example.com:{0}\' https://127.0.0.1:{0}' + .format(ts.Variables.ssl_port), + ts=ts) + tr.Processes.Default.ReturnCode = 0 + + tr = urtest.AddTestRun("Test two HTTP/2 requests over one TLS connection") + tr.StillRunningAfter = ts + tr.StillRunningAfter = server + tr.MakeCurlCommandMulti( + '{{curl}} -k -v --http2 -H \'host:example.com:{0}\' https://127.0.0.1:{0} https://127.0.0.1:{0}'.format( + ts.Variables.ssl_port), + ts=ts) + tr.Processes.Default.ReturnCode = 0 + + tr = urtest.AddTestRun("Test two HTTP/2 requests over two TLS connections") + tr.StillRunningAfter = ts + tr.StillRunningAfter = server + tr.MakeCurlCommandMulti( + '{{curl}} -k -v --http2 -H \'host:example.com:{0}\' https://127.0.0.1:{0}; {{curl}} -k -v --http1.1 -H \'host:example.com:{0}\' https://127.0.0.1:{0}' + .format(ts.Variables.ssl_port), + ts=ts) + tr.Processes.Default.ReturnCode = 0 + + # Just a check to flush out the traffic log until we have a clean shutdown for traffic_server + tr = urtest.AddTestRun("Wait for the access log to write out") + tr.DelayStart = 5 + tr.StillRunningAfter = ts + tr.StillRunningAfter = server + tr.Processes.Default.Command = 'ls' + tr.Processes.Default.ReturnCode = 0 + + ts.Disk.squid_log.Content = "gold/accesslog.gold" + urtest.execute() diff --git a/tests/uranium_tests/tls/test_tls_ocsp.py b/tests/uranium_tests/tls/test_tls_ocsp.py new file mode 100644 index 00000000000..b3b1d5ab92a --- /dev/null +++ b/tests/uranium_tests/tls/test_tls_ocsp.py @@ -0,0 +1,105 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_tls_ocsp(urtest: UraniumTest) -> None: + ''' + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import os + + urtest.Summary = ''' + Test tls server prefetched OCSP responses + ''' + + urtest.SkipUnless( + Condition.HasCurlVersion("7.41.0"), # curl --cert-status option has been introduced in version 7.41.0 + ) + + # Define default ATS + ts = urtest.MakeATSProcess("ts", enable_tls=True) + server = urtest.MakeOriginServer("server") + request_header = {"headers": "GET / HTTP/1.1\r\nHost: example.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + # desired response form the origin server + response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + server.addResponse("sessionlog.json", request_header, response_header) + + ts.addSSLfile("ssl/ca.ocsp.pem") + ts.addSSLfile("ssl/server.ocsp.pem") + ts.addSSLfile("ssl/server.ocsp.key") + ts.addSSLfile("ssl/ocsp_response.der") + + ts.Disk.remap_config.AddLine( + 'map https://example.com:{0} http://127.0.0.1:{1}'.format(ts.Variables.ssl_port, server.Variables.Port)) + + ts.Disk.ssl_multicert_yaml.AddLines( + """ + + + ssl_multicert: + + + - dest_ip: "*" + ssl_cert_name: server.ocsp.pem + ssl_key_name: server.ocsp.key + ssl_ocsp_name: ocsp_response.der + + + """.split("\n")) + + # Case 1, global config policy=permissive properties=signature + # override for foo.com policy=enforced properties=all + ts.Disk.records_config.update( + { + 'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir), + 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts.Variables.SSLDir), + 'proxy.config.ssl.server.cert_chain.filename': 'ca.ocsp.pem', + # enable prefetched OCSP responses + 'proxy.config.ssl.ocsp.response.path': '{0}'.format(ts.Variables.SSLDir), + 'proxy.config.ssl.ocsp.enabled': 1, + 'proxy.config.exec_thread.autoconfig.scale': 1.0, + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'ssl_ocsp' + }) + + tr = urtest.AddTestRun("Check OCSP response using curl") + tr.Processes.Default.StartBefore(server) + tr.Processes.Default.StartBefore(urtest.Processes.ts) + tr.StillRunningAfter = server + tr.StillRunningAfter = ts + tr.MakeCurlCommand( + "-v --cacert {0} --cert-status -H \")host:example.com\" https://127.0.0.1:{1}".format( + os.path.join(ts.Variables.SSLDir, "ca.ocsp.pem"), ts.Variables.ssl_port), + ts=ts) + tr.ReturnCode = 0 + urtest.execute() diff --git a/tests/uranium_tests/tls/test_tls_origin_open_failed.py b/tests/uranium_tests/tls/test_tls_origin_open_failed.py new file mode 100644 index 00000000000..b45efa2de6f --- /dev/null +++ b/tests/uranium_tests/tls/test_tls_origin_open_failed.py @@ -0,0 +1,98 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_tls_origin_open_failed(urtest: UraniumTest) -> None: + ''' + Verify that a failed outbound TLS origin connection is surfaced to the client as + an error (5xx) without crashing ATS. + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import ports + + urtest.Summary = __doc__ + + class TestOriginOpenFailed: + '''Verify a failed outbound TLS connect is surfaced as a 5xx, not a crash.''' + + _ts_counter: int = 0 + + def __init__(self) -> None: + '''Declare the test Processes.''' + self._ts = self._configure_trafficserver() + + def _configure_trafficserver(self) -> 'Process': + '''Configure Traffic Server with an https origin whose connect fails. + + :return: The Traffic Server Process. + ''' + ts = urtest.MakeATSProcess(f'ts-{TestOriginOpenFailed._ts_counter}') + TestOriginOpenFailed._ts_counter += 1 + + # Reserve a port for a TLS origin; its liveness is irrelevant since the + # source bind below fails before any connect is attempted. + ports.get_port(ts, 'origin_port') + + ts.Disk.remap_config.AddLine(f'map http://dead.test/ https://127.0.0.1:{ts.Variables.origin_port}/') + ts.Disk.records_config.update( + { + # Bind every outbound connection to a non-local source address + # (RFC 5737 documentation range) so bind() fails synchronously with + # EADDRNOTAVAIL, i.e. the connect fails before any TCP/TLS exchange + # rather than via a later, routable connect that would be refused + # asynchronously. + 'proxy.config.outgoing_ip_to_bind': '192.0.2.1', + 'proxy.config.http.connect_attempts_max_retries': 1, + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'http|ssl', + }) + + # Tearing down the failed outbound TLS connect must not crash ATS. + ts.Disk.traffic_out.Content = Testers.ExcludesExpression( + "received signal|failed assertion", "ATS must not crash on a failed outbound TLS connect") + return ts + + def run(self) -> None: + '''Configure and run the TestRun.''' + tr = urtest.AddTestRun("a failed outbound TLS connect is surfaced cleanly") + tr.Processes.Default.StartBefore(self._ts) + tr.MakeCurlCommand( + f'-s -o /dev/null -w "%{{http_code}}" -H "Host: dead.test" http://127.0.0.1:{self._ts.Variables.port}/', + ts=self._ts) + tr.Processes.Default.ReturnCode = 0 + # A failed origin connection is surfaced as a 5xx (502 Bad Gateway). + tr.Processes.Default.Streams.stdout = Testers.ContainsExpression("50[02]", "a failed origin connect yields a 5xx") + tr.StillRunningAfter = self._ts + + TestOriginOpenFailed().run() + urtest.execute() diff --git a/tests/uranium_tests/tls/test_tls_origin_post_abort.py b/tests/uranium_tests/tls/test_tls_origin_post_abort.py new file mode 100644 index 00000000000..f369b582e1b --- /dev/null +++ b/tests/uranium_tests/tls/test_tls_origin_post_abort.py @@ -0,0 +1,133 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_tls_origin_post_abort(urtest: UraniumTest) -> None: + ''' + When a TLS origin resets the connection while ATS is still sending it a POST + request body, ATS must fail the transaction promptly -- the transport error must + reach the request-body tunnel right away as VC_EVENT_ERROR, not sit until the + outbound inactivity timeout rescues it. + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import os + import sys + + urtest.Summary = __doc__ + + class TestOriginPostAbort: + '''Verify a TLS origin RST mid-POST-body fails the transaction promptly.''' + + _ts_counter: int = 0 + _origin_counter: int = 0 + + def __init__(self) -> None: + '''Declare the test Processes.''' + urtest.GetTcpPort("origin_port") + self._ts = self._configure_trafficserver() + self._origin = self._configure_origin() + + def _configure_trafficserver(self) -> 'Process': + '''Configure Traffic Server with a TLS origin and a generous rescue timeout. + + :return: The Traffic Server Process. + ''' + ts = urtest.MakeATSProcess(f'ts-{TestOriginPostAbort._ts_counter}', enable_cache=False) + TestOriginPostAbort._ts_counter += 1 + + ts.Disk.records_config.update( + { + 'proxy.config.url_remap.remap_required': 1, + 'proxy.config.http.connect_attempts_max_retries': 0, + 'proxy.config.http.connect_attempts_timeout': 15, + # The rescue timeout: a prompt failure must beat this by a wide margin. + 'proxy.config.http.transaction_no_activity_timeout_out': 15, + 'proxy.config.http.transaction_no_activity_timeout_in': 30, + # Keep the kernel send buffer small so the request body cannot be + # absorbed by the kernel: ATS must still be mid-send at the RST. + 'proxy.config.net.sock_send_buffer_size_out': 65536, + 'proxy.config.ssl.client.verify.server.policy': 'DISABLED', + 'proxy.config.diags.debug.enabled': 0, + 'proxy.config.diags.debug.tags': 'http|ssl|ssl_io', + }) + ts.Disk.remap_config.AddLine(f'map /post https://127.0.0.1:{urtest.Variables.origin_port}') + + ts.Disk.traffic_out.Content = Testers.ExcludesExpression( + 'received signal|failed assertion', 'ATS must not crash handling the mid-body origin abort') + return ts + + def _configure_origin(self) -> 'Process': + '''Configure a raw-socket TLS origin that resets mid-request-body. + + The scenario needs a TLS origin that accepts the request body and then RSTs + the connection mid-body (SO_LINGER 0). Proxy Verifier can only close cleanly + (close_notify), so it cannot express a mid-body reset; hence the small + raw-socket origin helper rather than an ATSReplayTest. + + :return: The origin server Process. + ''' + origin = urtest.Processes.Process( + f'origin-{TestOriginPostAbort._origin_counter}', + f'{sys.executable} {os.path.join(urtest.TestDirectory, "tls_post_abort_origin.py")} ' + f'-p {urtest.Variables.origin_port} ' + f'-c {os.path.join(urtest.Variables.AtsTestToolsDir, "ssl", "server.pem")} -d 1.0') + TestOriginPostAbort._origin_counter += 1 + + # These markers prove the reset path was actually exercised: the origin + # completed the TLS handshake, received the forwarded request headers, and + # then reset the connection mid-body. Without them the client could pass on + # any prompt response -- e.g. a broken remap or a never-started origin that + # yields a fast 5xx without the body ever reaching a resetting origin. + origin.Streams.stdout = Testers.ContainsExpression( + 'request headers received', 'the origin must receive the forwarded request headers') + origin.Streams.stdout += Testers.ContainsExpression( + 'connection reset sent', 'the origin must reset the connection mid-body') + return origin + + def run(self) -> None: + '''Configure and run the TestRun.''' + tr = urtest.AddTestRun("POST to a TLS origin that resets mid-body must fail promptly") + tr.Processes.Default.StartBefore(self._ts) + tr.Processes.Default.StartBefore(self._origin, ready=When.PortOpen(urtest.Variables.origin_port)) + tr.Processes.Default.Command = ( + f'{sys.executable} {os.path.join(urtest.TestDirectory, "tls_post_abort_client.py")} ' + f'-p {self._ts.Variables.port} -t 8') + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.All += Testers.ContainsExpression( + 'PASS: transaction failed promptly', 'the POST must fail promptly on the origin RST') + tr.Processes.Default.Streams.All += Testers.ContainsExpression( + 'status-code: 5', 'ATS must surface the mid-body origin reset as a 5xx') + tr.StillRunningAfter = self._ts + + TestOriginPostAbort().run() + urtest.execute() diff --git a/tests/uranium_tests/tls/test_tls_origin_session_reuse.py b/tests/uranium_tests/tls/test_tls_origin_session_reuse.py new file mode 100644 index 00000000000..dd601514ced --- /dev/null +++ b/tests/uranium_tests/tls/test_tls_origin_session_reuse.py @@ -0,0 +1,195 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_tls_origin_session_reuse(urtest: UraniumTest) -> None: + ''' + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import re + + urtest.Summary = ''' + Test tls origin session reuse + ''' + + # Define default ATS + ts1 = urtest.MakeATSProcess("ts1", enable_tls=True) + ts2 = urtest.MakeATSProcess("ts2", enable_tls=True) + ts3 = urtest.MakeATSProcess("ts3", enable_tls=True) + ts4 = urtest.MakeATSProcess("ts4", enable_tls=True) + server = urtest.MakeOriginServer("server") + + # Add info the origin server responses + request_header = {'headers': 'GET / HTTP/1.1\r\nHost: www.example.com\r\n\r\n', 'timestamp': '1469733493.993', 'body': ''} + response_header = { + 'headers': 'HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n', + 'timestamp': '1469733493.993', + 'body': 'curl test' + } + server.addResponse("sessionlog.json", request_header, response_header) + + # add ssl materials like key, certificates for the server + ts1.addSSLfile("ssl/server.pem") + ts1.addSSLfile("ssl/server.key") + ts2.addSSLfile("ssl/server.pem") + ts2.addSSLfile("ssl/server.key") + ts3.addSSLfile("ssl/server.pem") + ts3.addSSLfile("ssl/server.key") + ts4.addSSLfile("ssl/server.pem") + ts4.addSSLfile("ssl/server.key") + + ts1.Disk.remap_config.AddLine('map / http://127.0.0.1:{0}'.format(server.Variables.Port)) + ts2.Disk.remap_config.AddLines( + [ + 'map /reuse_session https://127.0.0.1:{0}'.format(ts1.Variables.ssl_port), + 'map /remove_oldest https://127.0.1.1:{0}'.format(ts1.Variables.ssl_port), + ]) + ts3.Disk.remap_config.AddLine('map / http://127.0.0.1:{0}'.format(server.Variables.Port)) + ts4.Disk.remap_config.AddLine('map / https://127.0.0.1:{0}'.format(ts3.Variables.ssl_port)) + + ts1.Disk.ssl_multicert_yaml.AddLines( + """ + ssl_multicert: + - dest_ip: "*" + ssl_cert_name: server.pem + ssl_key_name: server.key + """.split("\n")) + ts2.Disk.ssl_multicert_yaml.AddLines( + """ + ssl_multicert: + - dest_ip: "*" + ssl_cert_name: server.pem + ssl_key_name: server.key + """.split("\n")) + ts3.Disk.ssl_multicert_yaml.AddLines( + """ + ssl_multicert: + - dest_ip: "*" + ssl_cert_name: server.pem + ssl_key_name: server.key + """.split("\n")) + ts4.Disk.ssl_multicert_yaml.AddLines( + """ + ssl_multicert: + - dest_ip: "*" + ssl_cert_name: server.pem + ssl_key_name: server.key + """.split("\n")) + + ts1.Disk.records_config.update( + { + 'proxy.config.http.cache.http': 0, + 'proxy.config.ssl.server.cert.path': '{0}'.format(ts1.Variables.SSLDir), + 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts1.Variables.SSLDir), + 'proxy.config.exec_thread.autoconfig.scale': 1.0, + 'proxy.config.ssl.server.session_ticket.enable': 1, + 'proxy.config.ssl.origin_session_cache.enabled': 1, + 'proxy.config.ssl.origin_session_cache.size': 1, + 'proxy.config.ssl.client.verify.server.policy': 'PERMISSIVE', + }) + ts2.Disk.records_config.update( + { + 'proxy.config.http.cache.http': 0, + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'ssl.origin_session_cache', + 'proxy.config.ssl.server.cert.path': '{0}'.format(ts2.Variables.SSLDir), + 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts2.Variables.SSLDir), + 'proxy.config.exec_thread.autoconfig.scale': 1.0, + 'proxy.config.ssl.server.session_ticket.enable': 1, + 'proxy.config.ssl.origin_session_cache.enabled': 1, + 'proxy.config.ssl.origin_session_cache.size': 1, + 'proxy.config.ssl.client.verify.server.policy': 'PERMISSIVE', + }) + ts3.Disk.records_config.update( + { + 'proxy.config.http.cache.http': 0, + 'proxy.config.ssl.server.cert.path': '{0}'.format(ts3.Variables.SSLDir), + 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts3.Variables.SSLDir), + 'proxy.config.exec_thread.autoconfig.scale': 1.0, + 'proxy.config.ssl.server.session_ticket.enable': 1, + 'proxy.config.ssl.origin_session_cache.enabled': 1, + 'proxy.config.ssl.origin_session_cache.size': 1, + 'proxy.config.ssl.client.verify.server.policy': 'PERMISSIVE', + }) + ts4.Disk.records_config.update( + { + 'proxy.config.http.cache.http': 0, + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'ssl.origin_session_cache', + 'proxy.config.ssl.server.cert.path': '{0}'.format(ts4.Variables.SSLDir), + 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts4.Variables.SSLDir), + 'proxy.config.exec_thread.autoconfig.scale': 1.0, + 'proxy.config.ssl.server.session_ticket.enable': 1, + 'proxy.config.ssl.origin_session_cache.enabled': 0, + 'proxy.config.ssl.origin_session_cache.size': 1, + 'proxy.config.ssl.client.verify.server.policy': 'PERMISSIVE', + }) + + tr = urtest.AddTestRun('new session then reuse') + tr.MakeCurlCommandMulti( + '{{curl}} https://127.0.0.1:{0}/reuse_session -k && {{curl}} https://127.0.0.1:{0}/reuse_session -k'.format( + ts2.Variables.ssl_port), + ts=ts2) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.StartBefore(server) + tr.Processes.Default.StartBefore(ts1) + tr.Processes.Default.StartBefore(ts2) + tr.Processes.Default.Streams.All = Testers.ContainsExpression('curl test', 'Making sure the basics still work') + ts2.Disk.traffic_out.Content = Testers.ContainsExpression('new session to origin', '') + ts2.Disk.traffic_out.Content += Testers.ContainsExpression('reused session to origin', '') + tr.StillRunningAfter = server + tr.StillRunningAfter += ts1 + tr.StillRunningAfter += ts2 + + tr = urtest.AddTestRun('remove oldest session, new session then reuse') + tr.MakeCurlCommandMulti( + '{{curl}} https://127.0.0.1:{0}/remove_oldest -k && {{curl}} https://127.0.0.1:{0}/remove_oldest -k'.format( + ts2.Variables.ssl_port), + ts=ts2) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.All = Testers.ContainsExpression('curl test', 'Making sure the basics still work') + ts2.Disk.traffic_out.Content = Testers.ContainsExpression('remove oldest session', '') + ts2.Disk.traffic_out.Content += Testers.ContainsExpression('new session to origin', '') + ts2.Disk.traffic_out.Content += Testers.ContainsExpression('reused session to origin', '') + tr.StillRunningAfter = server + + tr = urtest.AddTestRun('disable origin session reuse, reuse should fail') + tr.MakeCurlCommandMulti( + '{{curl}} https://127.0.0.1:{0} -k && {{curl}} https://127.0.0.1:{0} -k'.format(ts4.Variables.ssl_port), ts=ts4) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.StartBefore(ts3) + tr.Processes.Default.StartBefore(ts4) + tr.Processes.Default.Streams.All = Testers.ContainsExpression('curl test', 'Making sure the basics still work') + ts4.Disk.traffic_out.Content = Testers.ContainsExpression('new session to origin', '') + ts4.Disk.traffic_out.Content += Testers.ExcludesExpression('reused session to origin', '') + urtest.execute() diff --git a/tests/uranium_tests/tls/test_tls_partial_blind_tunnel.py b/tests/uranium_tests/tls/test_tls_partial_blind_tunnel.py new file mode 100644 index 00000000000..f4f1579ce41 --- /dev/null +++ b/tests/uranium_tests/tls/test_tls_partial_blind_tunnel.py @@ -0,0 +1,129 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_tls_partial_blind_tunnel(urtest: UraniumTest) -> None: + ''' + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + urtest.Summary = 'Testing partial_blind_tunnel' + + ts = urtest.MakeATSProcess("ts", enable_tls=True) + server_bar = urtest.MakeOriginServer("server_bar", ssl=True) + nameserver = urtest.MakeDNServer("dns", default='127.0.0.1') + + request_bar_header = {"headers": "GET / HTTP/1.1\r\nHost: bar.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + response_bar_header = { + "headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", + "timestamp": "1469733493.993", + "body": "ok bar" + } + server_bar.addResponse("sessionlog_bar.json", request_bar_header, response_bar_header) + + # add ssl materials like key, certificates for the server + ts.addSSLfile("ssl/signed-foo.pem") + ts.addSSLfile("ssl/signed-foo.key") + ts.addSSLfile("ssl/signed-bar.pem") + ts.addSSLfile("ssl/signed-bar.key") + ts.addSSLfile("ssl/signer.pem") + + # Need no remap rules. Everything should be processed by sni + + # Make sure the TS server certs are different from the origin certs + ts.Disk.ssl_multicert_yaml.AddLines( + """ + ssl_multicert: + - dest_ip: "*" + ssl_cert_name: signed-foo.pem + ssl_key_name: signed-foo.key + """.split("\n")) + + # Case 1, global config policy=permissive properties=signature + # override for foo.com policy=enforced properties=all + ts.Disk.records_config.update( + { + 'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir), + 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts.Variables.SSLDir), + 'proxy.config.http.connect_ports': '{0} {1}'.format(ts.Variables.ssl_port, server_bar.Variables.SSL_Port), + 'proxy.config.ssl.client.CA.cert.path': '{0}'.format(ts.Variables.SSLDir), + 'proxy.config.ssl.client.CA.cert.filename': 'signer.pem', + 'proxy.config.ssl.client.verify.server.policy': 'PERMISSIVE', + 'proxy.config.dns.nameservers': f"127.0.0.1:{nameserver.Variables.Port}", + 'proxy.config.dns.resolv_conf': 'NULL' + }) + ts.addPrivateConnectAllowYaml() + + # foo.com should terminate. and reconnect via TLS upstream to bar.com + ts.Disk.sni_yaml.AddLines( + [ + "sni:", + "- fqdn: 'foo.com'", + " partial_blind_route: 'localhost:{0}'".format(server_bar.Variables.SSL_Port), + ]) + + tr = urtest.AddTestRun("Partial Blind Route") + tr.MakeCurlCommand("--http1.1 -v --resolve 'foo.com:{0}:127.0.0.1' -k https://foo.com:{0}".format(ts.Variables.ssl_port), ts=ts) + tr.ReturnCode = 0 + tr.Processes.Default.StartBefore(server_bar) + tr.Processes.Default.StartBefore(nameserver) + tr.Processes.Default.StartBefore(urtest.Processes.ts) + tr.StillRunningAfter = ts + + tr.Processes.Default.Streams.All += Testers.ExcludesExpression("Could Not Connect", "Curl attempt should have succeeded") + tr.Processes.Default.Streams.All += Testers.ExcludesExpression( + "Not Found on Accelerato", "Should not try to remap on Traffic Server") + tr.Processes.Default.Streams.All += Testers.ContainsExpression("HTTP/1.1 200 OK", "Should get a successful response") + tr.Processes.Default.Streams.All += Testers.ContainsExpression("ok bar", "Body is expected") + + tr = urtest.AddTestRun("Test Metrics") + tr.Processes.Default.Command = ( + f"{urtest.Variables.AtsTestToolsDir}/stdout_wait" + " 'traffic_ctl metric get" + + " proxy.process.http.total_incoming_connections" + " proxy.process.http.total_client_connections" + + " proxy.process.http.total_client_connections_ipv4" + " proxy.process.http.total_client_connections_ipv6" + + " proxy.process.http.total_server_connections" + " proxy.process.http2.total_client_connections" + + " proxy.process.http.connect_requests" + " proxy.process.tunnel.total_client_connections_blind_tcp" + + " proxy.process.tunnel.current_client_connections_blind_tcp" + " proxy.process.tunnel.total_server_connections_blind_tcp" + + " proxy.process.tunnel.current_server_connections_blind_tcp" + " proxy.process.tunnel.total_client_connections_tls_tunnel" + + " proxy.process.tunnel.current_client_connections_tls_tunnel" + + " proxy.process.tunnel.total_client_connections_tls_forward" + + " proxy.process.tunnel.current_client_connections_tls_forward" + + " proxy.process.tunnel.total_client_connections_tls_partial_blind" + + " proxy.process.tunnel.current_client_connections_tls_partial_blind" + + " proxy.process.tunnel.total_client_connections_tls_http" + " proxy.process.tunnel.current_client_connections_tls_http" + + " proxy.process.tunnel.total_server_connections_tls" + " proxy.process.tunnel.current_server_connections_tls'" + + f" {urtest.TestDirectory}/gold/tls-partial-blind-tunnel-metrics.gold") + # Need to copy over the environment so traffic_ctl knows where to find the unix domain socket + tr.Processes.Default.Env = ts.Env + tr.Processes.Default.ReturnCode = 0 + tr.StillRunningAfter = ts + urtest.execute() diff --git a/tests/uranium_tests/tls/test_tls_record_size.py b/tests/uranium_tests/tls/test_tls_record_size.py new file mode 100644 index 00000000000..6afd3eadfdb --- /dev/null +++ b/tests/uranium_tests/tls/test_tls_record_size.py @@ -0,0 +1,142 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_tls_record_size(urtest: UraniumTest) -> None: + ''' + Exercise fixed and dynamic TLS record sizing. On a large TLS download the body + must arrive intact and application-data records on the wire must follow the + configured sizing strategy. + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import os + import sys + + urtest.Summary = __doc__ + + class TestRecordSize: + '''Verify fixed and dynamic TLS record sizing on large downloads.''' + + _server_counter: int = 0 + _ts_counter: int = 0 + + def __init__(self, max_record: int, body_len: int) -> None: + '''Declare the test Processes.''' + self._max_record = max_record + self._body_len = body_len + self._server = self._configure_server() + self._ts = self._configure_trafficserver() + + def _configure_server(self) -> 'Process': + '''Configure the origin server with a large response body. + + :return: The origin server Process. + ''' + server = urtest.MakeOriginServer(f'server-{TestRecordSize._server_counter}') + TestRecordSize._server_counter += 1 + + body = "x" * self._body_len + request_header = {"headers": "GET /obj HTTP/1.1\r\nHost: ex.test\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + response_header = { + "headers": + "HTTP/1.1 200 OK\r\nServer: microserver\r\nConnection: close\r\n" + f"Cache-Control: max-age=3600\r\nContent-Length: {self._body_len}\r\n\r\n", + "timestamp": "1469733493.993", + "body": body + } + server.addResponse("sessionlog.json", request_header, response_header) + return server + + def _configure_trafficserver(self) -> 'Process': + '''Configure Traffic Server with the requested record-size strategy. + + :return: The Traffic Server Process. + ''' + ts = urtest.MakeATSProcess(f'ts-{TestRecordSize._ts_counter}', enable_tls=True) + TestRecordSize._ts_counter += 1 + + ts.addDefaultSSLFiles() + ts.Disk.ssl_multicert_yaml.AddLines( + """ + ssl_multicert: + - dest_ip: "*" + ssl_cert_name: server.pem + ssl_key_name: server.key + """.split("\n")) + ts.Disk.remap_config.AddLine(f'map / http://127.0.0.1:{self._server.Variables.Port}') + ts.Disk.records_config.update( + { + 'proxy.config.ssl.server.cert.path': f'{ts.Variables.SSLDir}', + 'proxy.config.ssl.server.private_key.path': f'{ts.Variables.SSLDir}', + 'proxy.config.ssl.max_record_size': self._max_record, + }) + if self._max_record == -1: + ts.Disk.traffic_out.Content = Testers.ExcludesExpression( + r'proxy\.config\.ssl\.max_record_size.*Validity Check error', + 'The dynamic record-size sentinel should pass records validation') + return ts + + def run(self) -> None: + '''Configure and run the TestRun. + + The client downloads the object and measures the TLS records on the wire. + ''' + if self._max_record == -1: + description = 'max_record_size=-1 dynamically sizes records on a large TLS download' + client_option = '--dynamic' + expected_output = 'PASS: TLS records ramp from small to large after the dynamic threshold' + else: + description = 'max_record_size>0 clamps records on a large TLS download' + client_option = f'--max-record {self._max_record}' + expected_output = 'PASS: every application-data record is within the configured clamp' + + tr = urtest.AddTestRun(description) + tr.Processes.Default.StartBefore(self._server) + tr.Processes.Default.StartBefore(self._ts) + tr.Processes.Default.Command = ( + f'{sys.executable} {os.path.join(urtest.TestDirectory, "tls_record_size_client.py")} ' + f'-p {self._ts.Variables.ssl_port} --host ex.test --path /obj ' + f'{client_option} --expect-bytes {self._body_len}') + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.All += Testers.ContainsExpression( + expected_output, 'TLS records must follow the configured sizing strategy') + tr.StillRunningAfter = self._ts + tr.StillRunningAfter = self._server + + # The fixed-size test response is comfortably larger than its 4,096-byte clamp, + # ensuring that many records exercise the clamp. The dynamic-sizing test + # response must exceed its 1,000,000-byte threshold by enough data to demonstrate + # both phases. + TestRecordSize(4096, 1024 * 1024).run() + TestRecordSize(-1, 2 * 1024 * 1024).run() + urtest.execute() diff --git a/tests/uranium_tests/tls/test_tls_reload_under_load.py b/tests/uranium_tests/tls/test_tls_reload_under_load.py new file mode 100644 index 00000000000..1038dbd5e5e --- /dev/null +++ b/tests/uranium_tests/tls/test_tls_reload_under_load.py @@ -0,0 +1,119 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_tls_reload_under_load(urtest: UraniumTest) -> None: + ''' + Existing cert/SNI reload tests reload while the server is idle. This one drives + continuous concurrent TLS handshakes and reloads ssl_multicert.yaml on top of + them, stressing the SSL/BIO ownership boundary of the layered TLS VConnection. + The swapped-in certificate must take effect, every handshake must succeed, and + ATS must not crash. + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import os + import sys + + urtest.Summary = __doc__ + + class TestTlsReloadUnderLoad: + '''Reload TLS certs while concurrent handshakes are in flight; serving must not break.''' + + _ts_counter: int = 0 + + def __init__(self) -> None: + '''Declare the test Processes.''' + self._ts = self._configure_trafficserver() + + def _configure_trafficserver(self) -> 'Process': + '''Configure Traffic Server with a swappable live certificate. + + :return: The Traffic Server Process. + ''' + ts = urtest.MakeATSProcess(f'ts-{TestTlsReloadUnderLoad._ts_counter}', enable_tls=True, enable_cache=False) + TestTlsReloadUnderLoad._ts_counter += 1 + + # signed-bar is the live cert; signed2-bar shares its key and is copied over + # the live cert at reload time. Distinct fingerprints let the client prove the + # swap took effect under load. + ts.addSSLfile("ssl/signed-bar.pem") + ts.addSSLfile("ssl/signed-bar.key") + ts.addSSLfile("ssl/signed2-bar.pem") + + ts.Disk.ssl_multicert_yaml.AddLines( + """ + ssl_multicert: + - dest_ip: "*" + ssl_cert_name: signed-bar.pem + ssl_key_name: signed-bar.key + """.split("\n")) + ts.Disk.records_config.update( + { + 'proxy.config.ssl.server.cert.path': f'{ts.Variables.SSLDir}', + 'proxy.config.ssl.server.private_key.path': f'{ts.Variables.SSLDir}', + 'proxy.config.exec_thread.autoconfig.scale': 1.0, + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'ssl', + }) + + # The reload must actually have run (otherwise the test would be vacuous). + ts.Disk.diags_log.Content = Testers.ContainsExpression( + "ssl_multicert.yaml finished loading", "the cert configuration must reload while load is in flight") + # The reload-under-load must not crash or trip an assertion / sanitizer. + ts.Disk.traffic_out.Content = Testers.ExcludesExpression( + "received signal|failed assertion", "ATS must not crash reloading certs under load") + ts.Disk.traffic_out.Content += Testers.ExcludesExpression( + "AddressSanitizer|use-after-free|runtime error:", "no memory-safety error reloading certs under load") + return ts + + def run(self) -> None: + '''Configure and run the TestRun.''' + tr = urtest.AddTestRun("reload certs while TLS handshakes are in flight") + tr.Processes.Default.StartBefore(self._ts) + tr.Processes.Default.Command = ( + f'{sys.executable} {os.path.join(urtest.TestDirectory, "tls_reload_under_load_client.py")} ' + f'-p {self._ts.Variables.ssl_port} --sni bar.com --ssldir {self._ts.Variables.SSLDir} ' + f'--live-cert signed-bar.pem ' + f'--v2-cert {os.path.join(self._ts.Variables.SSLDir, "signed2-bar.pem")} ' + f'--reloads 3 --duration 8 --concurrency 4') + # traffic_ctl (invoked by the client to reload) needs the runroot environment. + tr.Processes.Default.Env = self._ts.Env + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.All = Testers.ContainsExpression("RESULT=PASS", "load + reload run must pass") + tr.Processes.Default.Streams.All += Testers.ContainsExpression("FAILURES=0", "no handshake may fail across the reload") + tr.Processes.Default.Streams.All += Testers.ContainsExpression( + "CERT_CHANGED=1", "the swapped-in certificate must be served after the reload") + tr.StillRunningAfter = self._ts + + TestTlsReloadUnderLoad().run() + urtest.execute() diff --git a/tests/uranium_tests/tls/test_tls_renegotiation.py b/tests/uranium_tests/tls/test_tls_renegotiation.py new file mode 100644 index 00000000000..130411668ce --- /dev/null +++ b/tests/uranium_tests/tls/test_tls_renegotiation.py @@ -0,0 +1,152 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_tls_renegotiation(urtest: UraniumTest) -> None: + ''' + With client renegotiation disallowed (the default), a TLSv1.2 client that asks to + renegotiate must have its connection refused without taking down the server. + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import os + import sys + + urtest.Summary = __doc__ + + # Renegotiation only exists in TLS 1.2 and earlier. + urtest.SkipUnless(Condition.HasOpenSSLVersion("1.1.1"), Condition.HasLegacyTLSSupport()) + + class TestRenegotiationRefused: + '''Verify a refused client-initiated renegotiation does not crash ATS.''' + + _server_counter: int = 0 + _ts_counter: int = 0 + + def __init__(self) -> None: + '''Declare the test Processes.''' + self._server = self._configure_server() + self._ts = self._configure_trafficserver() + + def _configure_server(self) -> 'Process': + '''Configure the origin server. + + :return: The origin server Process. + ''' + server = urtest.MakeOriginServer(f'server-{TestRenegotiationRefused._server_counter}') + TestRenegotiationRefused._server_counter += 1 + + request_header = {"headers": "GET / HTTP/1.1\r\nHost: example.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + response_header = { + "headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", + "timestamp": "1469733493.993", + "body": "ok" + } + server.addResponse("sessionlog.json", request_header, response_header) + return server + + def _configure_trafficserver(self) -> 'Process': + '''Configure Traffic Server to refuse client renegotiation (the default). + + :return: The Traffic Server Process. + ''' + ts = urtest.MakeATSProcess(f'ts-{TestRenegotiationRefused._ts_counter}', enable_tls=True) + TestRenegotiationRefused._ts_counter += 1 + + ts.addSSLfile("ssl/server.pem") + ts.addSSLfile("ssl/server.key") + ts.Disk.ssl_multicert_yaml.AddLines( + """ + ssl_multicert: + - dest_ip: "*" + ssl_cert_name: server.pem + ssl_key_name: server.key + """.split("\n")) + ts.Disk.records_config.update( + { + 'proxy.config.ssl.server.cert.path': f'{ts.Variables.SSLDir}', + 'proxy.config.ssl.server.private_key.path': f'{ts.Variables.SSLDir}', + # The secure default: never honor a client-initiated renegotiation. + 'proxy.config.ssl.allow_client_renegotiation': 0, + # Renegotiation requires a sub-TLS1.3 protocol. + 'proxy.config.ssl.TLSv1_3.enabled': 0, + 'proxy.config.ssl.TLSv1_2': 1, + # So the renegotiation-detection log line below is emitted. + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'ssl_load', + }) + ts.Disk.remap_config.AddLine(f'map / http://127.0.0.1:{self._server.Variables.Port}') + + # The refused renegotiation must not abort the process. + ts.Disk.traffic_out.Content = Testers.ExcludesExpression( + "received signal|failed assertion", "ATS must refuse the renegotiation without crashing") + # ...and, on OpenSSL, it must actually reach ATS's renegotiation-detection + # path (otherwise a no-crash pass could mean the client never managed to + # renegotiate at all). BoringSSL rejects a peer-initiated renegotiation inside + # the library before ATS's info callback runs -- SSL_get_state() there only + # ever returns SSL_ST_INIT or SSL_ST_OK, never SSL_ST_RENEGOTIATE -- so the + # detection line is never logged. The no-crash check above still covers it. + if Condition.IsOpenSSL(): + ts.Disk.traffic_out.Content += Testers.ContainsExpression( + "trying to renegotiate from the client", "ATS must detect the client-initiated renegotiation") + return ts + + def run(self) -> None: + '''Configure and run the TestRuns.''' + # Complete a TLSv1.2 handshake, then ask to renegotiate. ATS must detect + # and refuse the renegotiation without aborting the process. + tr = urtest.AddTestRun("client renegotiation is refused without crashing ATS") + tr.Processes.Default.StartBefore(self._server) + tr.Processes.Default.StartBefore(self._ts) + tr.Processes.Default.Command = ( + f'{sys.executable} {os.path.join(urtest.TestDirectory, "tls_renegotiation_client.py")} ' + f'-p {self._ts.Variables.ssl_port} -s example.com') + tr.Processes.Default.ReturnCode = 0 + tr.StillRunningAfter = self._ts + tr.StillRunningAfter = self._server + + # ATS survived, so a normal request still succeeds. + tr = urtest.AddTestRun("server still serves requests after the refused renegotiation") + tr.MakeCurlCommand( + ( + "-v --http1.1 --tls-max 1.2 --tlsv1.2 --ciphers DEFAULT@SECLEVEL=0 -k " + f"--resolve 'example.com:{self._ts.Variables.ssl_port}:127.0.0.1' " + f"https://example.com:{self._ts.Variables.ssl_port}/"), + ts=self._ts) + tr.Processes.Default.ReturnCode = 0 + # curl -v writes the response status line to stderr. + tr.Processes.Default.Streams.stderr = Testers.ContainsExpression( + "HTTP/1.1 200 OK", "request after renegotiation should succeed") + tr.StillRunningAfter = self._ts + + TestRenegotiationRefused().run() + urtest.execute() diff --git a/tests/uranium_tests/tls/test_tls_renegotiation_allowed.py b/tests/uranium_tests/tls/test_tls_renegotiation_allowed.py new file mode 100644 index 00000000000..6b241c92771 --- /dev/null +++ b/tests/uranium_tests/tls/test_tls_renegotiation_allowed.py @@ -0,0 +1,164 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_tls_renegotiation_allowed(urtest: UraniumTest) -> None: + ''' + With client renegotiation allowed, a TLSv1.2 client that asks to renegotiate must + get a prompt answer from ATS (the renegotiation either completes or is refused + with an alert) rather than hanging. ATS must flush the SSL protocol output the + renegotiation generates; otherwise the answer is never sent and the client + stalls. + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import os + import sys + + from ports import get_port + + urtest.Summary = __doc__ + + # Renegotiation only exists in TLS 1.2 and earlier. + urtest.SkipUnless(Condition.HasOpenSSLVersion("1.1.1"), Condition.HasLegacyTLSSupport()) + + class TestRenegotiationAllowed: + '''Verify an allowed client renegotiation is answered promptly, not stranded.''' + + _server_counter: int = 0 + _ts_counter: int = 0 + + def __init__(self) -> None: + '''Declare the test Processes.''' + self._server = self._configure_server() + self._ts = self._configure_trafficserver() + + def _configure_server(self) -> 'Process': + '''Configure a plain HTTP/1.0 origin that closes each connection. + + A python http.server origin answers each request on its own connection and + closes it, so ATS's client-facing TLS write face is fully drained between + requests. When the renegotiation ClientHello then arrives there is no + in-flight transport write to coincidentally carry the renegotiation answer + out -- exactly the condition under which a proxy that fails to flush its own + SSL protocol output would strand the answer and hang the client. (The + Proxy-Verifier microserver keeps the response connection in a state that + masks the stall, so it is unsuitable for this regression.) + + :return: The origin server Process. + ''' + server = urtest.Processes.Process(f'server-{TestRenegotiationAllowed._server_counter}') + TestRenegotiationAllowed._server_counter += 1 + + origin_dir = os.path.join(urtest.RunDirectory, "origin") + os.makedirs(origin_dir, exist_ok=True) + with open(os.path.join(origin_dir, "index.html"), "w") as f: + f.write("ok\n") + + origin_port = get_port(server, "http_port") + server.Command = f'{sys.executable} -m http.server {origin_port} --bind 127.0.0.1 --directory {origin_dir}' + server.Ready = When.PortOpenv4(origin_port) + # The server is a long-running process the harness terminates at the end of + # the test, so its exit status is whatever the signal leaves behind. + server.ReturnCode = Any(None, 0, -2, -15) + return server + + def _configure_trafficserver(self) -> 'Process': + '''Configure Traffic Server to honor client renegotiation. + + :return: The Traffic Server Process. + ''' + ts = urtest.MakeATSProcess(f'ts-{TestRenegotiationAllowed._ts_counter}', enable_tls=True) + TestRenegotiationAllowed._ts_counter += 1 + + ts.addSSLfile("ssl/server.pem") + ts.addSSLfile("ssl/server.key") + ts.Disk.ssl_multicert_yaml.AddLines( + """ + ssl_multicert: + - dest_ip: "*" + ssl_cert_name: server.pem + ssl_key_name: server.key + """.split("\n")) + ts.Disk.records_config.update( + { + 'proxy.config.ssl.server.cert.path': f'{ts.Variables.SSLDir}', + 'proxy.config.ssl.server.private_key.path': f'{ts.Variables.SSLDir}', + # The operator opts in: honor a client-initiated renegotiation. + 'proxy.config.ssl.allow_client_renegotiation': 1, + # Renegotiation requires a sub-TLS1.3 protocol. + 'proxy.config.ssl.TLSv1_3.enabled': 0, + 'proxy.config.ssl.TLSv1_2': 1, + }) + ts.Disk.remap_config.AddLine(f'map / http://127.0.0.1:{self._server.Variables.http_port}') + + # The renegotiation must not abort the process. + ts.Disk.traffic_out.Content = Testers.ExcludesExpression( + "received signal|failed assertion", "ATS must service the renegotiation without crashing") + return ts + + def run(self) -> None: + '''Configure and run the TestRuns.''' + # Complete a TLSv1.2 handshake and one request, let the response drain, ask + # to renegotiate, then drive a second request. ATS must answer the + # renegotiation promptly; the helper reports RENEGOTIATION-STALLED (and + # exits non-zero) if the client hangs waiting for the answer. + tr = urtest.AddTestRun("client renegotiation is answered promptly") + tr.Processes.Default.StartBefore(self._server) + tr.Processes.Default.StartBefore(self._ts) + tr.Processes.Default.Command = ( + f'{sys.executable} {os.path.join(urtest.TestDirectory, "tls_renegotiation_allowed_client.py")} ' + f'-p {self._ts.Variables.ssl_port} -s example.com') + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.stdout = Testers.ContainsExpression( + "RENEGOTIATION-COMPLETED|RENEGOTIATION-REFUSED-PROMPTLY", "the renegotiation must be answered, not stranded") + tr.Processes.Default.Streams.stdout += Testers.ExcludesExpression( + "RENEGOTIATION-STALLED", "the client must not hang waiting for the renegotiation answer") + tr.StillRunningAfter = self._ts + tr.StillRunningAfter = self._server + + # ATS survived and keeps serving, so a normal request still succeeds. + tr = urtest.AddTestRun("server still serves requests after the renegotiation") + tr.MakeCurlCommand( + ( + "-v --http1.1 --tls-max 1.2 --tlsv1.2 --ciphers DEFAULT@SECLEVEL=0 -k " + f"--resolve 'example.com:{self._ts.Variables.ssl_port}:127.0.0.1' " + f"https://example.com:{self._ts.Variables.ssl_port}/"), + ts=self._ts) + tr.Processes.Default.ReturnCode = 0 + # curl -v writes the response status line to stderr. + tr.Processes.Default.Streams.stderr = Testers.ContainsExpression( + "HTTP/1.1 200 OK", "request after renegotiation should succeed") + tr.StillRunningAfter = self._ts + + TestRenegotiationAllowed().run() + urtest.execute() diff --git a/tests/uranium_tests/tls/test_tls_secret_update_default.py b/tests/uranium_tests/tls/test_tls_secret_update_default.py new file mode 100644 index 00000000000..9c5092af50a --- /dev/null +++ b/tests/uranium_tests/tls/test_tls_secret_update_default.py @@ -0,0 +1,255 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_tls_secret_update_default(urtest: UraniumTest) -> None: + ''' + Test default server certificate updates through TSSslSecretSet/TSSslSecretUpdate. + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import os + import re + + urtest.Summary = ''' + Verify that secret updates refresh the default server SSL_CTX used without SNI. + ''' + + class TestDefaultSecretUpdate: + '''Verify default server certificate updates through the secret API.''' + + initial_cert: str = 'signed-bar.pem' + updated_cert: str = 'signed2-bar.pem' + key_file: str = 'signed-bar.key' + shadowed_cert: str = 'signed-foo.pem' + updated_shadowed_cert: str = 'signed2-foo.pem' + shadowed_key_file: str = 'signed-foo.key' + plugin: str = 'ssl_secret_load_test.so' + + def __init__(self) -> None: + '''Configure the ATS process, origin server, and test runs.''' + self._configure_server() + self._configure_traffic_server() + self._add_start_run() + self._add_config_touch_run() + self._add_config_reload_run() + self._add_config_reload_wait_run() + self._add_initial_certificate_run() + self._add_mtime_delay_run() + self._add_shadowed_certificate_update_run() + self._add_secret_update_wait_run(self.shadowed_cert) + self._add_unchanged_default_certificate_run() + self._add_default_certificate_update_run() + self._add_secret_update_wait_run(self.initial_cert) + self._add_updated_certificate_run() + + def _configure_server(self) -> 'Process': + '''Configure the origin server.''' + server = urtest.MakeOriginServer('server') + self._server = server + + request_header = {'headers': 'GET / HTTP/1.1\r\nHost: doesnotmatter\r\n\r\n', 'timestamp': '1469733493.993', 'body': ''} + response_header = {'headers': 'HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n', 'timestamp': '1469733493.993', 'body': ''} + server.addResponse('sessionlog.json', request_header, response_header) + return server + + def _configure_traffic_server(self) -> 'Process': + '''Configure the Traffic Server process.''' + ts = urtest.MakeATSProcess('ts', enable_tls=True) + self._ts = ts + + ts.addSSLfile(f'ssl/{self.initial_cert}') + ts.addSSLfile(f'ssl/{self.updated_cert}') + ts.addSSLfile(f'ssl/{self.key_file}') + ts.addSSLfile(f'ssl/{self.shadowed_cert}') + ts.addSSLfile(f'ssl/{self.updated_shadowed_cert}') + ts.addSSLfile(f'ssl/{self.shadowed_key_file}') + urtest.PrepareTestPlugin(os.path.join(urtest.Variables.AtsTestPluginsDir, self.plugin), ts) + + ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'ssl_secret_load_test', + 'proxy.config.ssl.server.cert.path': f'{ts.Variables.SSLDir}/../', + 'proxy.config.ssl.server.private_key.path': f'{ts.Variables.SSLDir}/../', + 'proxy.config.ssl.server.multicert.concurrency': 1, + 'proxy.config.exec_thread.autoconfig.scale': 1.0, + 'proxy.config.url_remap.pristine_host_hdr': 1, + }) + + ts.Disk.ssl_multicert_yaml.AddLines( + [ + 'ssl_multicert:', + ' - dest_ip: "*"', + f' ssl_cert_name: {self.initial_cert}', + f' ssl_key_name: {self.key_file}', + ' - dest_ip: "*"', + f' ssl_cert_name: {self.shadowed_cert}', + f' ssl_key_name: {self.shadowed_key_file}', + ]) + ts.Disk.remap_config.AddLine(f'map / http://127.0.0.1:{self._server.Variables.Port}') + return ts + + def _add_start_run(self) -> 'TestRun': + '''Start ATS before reloading the multicert configuration serially.''' + tr = urtest.AddTestRun('Start ATS') + tr.Processes.Default.Command = 'echo starting ATS' + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.StartBefore(self._server) + tr.Processes.Default.StartBefore(self._ts) + self._keep_processes_running(tr) + return tr + + def _add_config_reload_run(self) -> 'TestRun': + '''Reload the multicert configuration with deterministic ordering.''' + tr = urtest.AddTestRun('Reload the multicert configuration serially') + tr.Processes.Default.Command = 'traffic_ctl config reload' + tr.Processes.Default.Env = self._ts.Env + tr.Processes.Default.ReturnCode = 0 + self._keep_processes_running(tr) + return tr + + def _add_config_touch_run(self) -> 'TestRun': + '''Mark the multicert configuration for reload.''' + tr = urtest.AddTestRun('Mark the multicert configuration for reload') + tr.Processes.Default.Command = f'touch {self._ts.Disk.ssl_multicert_yaml.AbsPath}' + tr.Processes.Default.ReturnCode = 0 + self._keep_processes_running(tr) + return tr + + def _add_config_reload_wait_run(self) -> 'TestRun': + '''Wait for the serial multicert configuration reload to finish.''' + tr = urtest.AddTestRun('Wait for the multicert configuration reload') + tr.Processes.Default.Command = 'echo awaiting multicert reload' + tr.Processes.Default.Env = self._ts.Env + tr.Processes.Default.ReturnCode = 0 + await_reload = tr.Processes.Process('await_reload', 'sleep 30') + await_reload.Ready = When.FileContains(self._ts.Disk.diags_log.Name, 'ssl_multicert.yaml finished loading', 2) + tr.Processes.Default.StartBefore(await_reload) + self._keep_processes_running(tr) + return tr + + def _add_initial_certificate_run(self) -> 'TestRun': + '''Verify the original default certificate before the secret update.''' + tr = self._add_curl_run( + 'Initial default certificate', + expected_cn='signer.yahoo.com', + unexpected_cn='signer2.yahoo.com', + expected_description='Initial cert uses signer.', + unexpected_description='Initial cert is not updated yet.') + tr.Processes.Default.Streams.All += Testers.ContainsExpression(r'subject:.*CN=bar\.com', 'Default cert is bar.com.') + tr.Processes.Default.Streams.All += Testers.ExcludesExpression(r'subject:.*CN=foo\.com', 'Shadowed cert is not served.') + return tr + + def _add_mtime_delay_run(self) -> 'TestRun': + '''Make the replacement certificate mtime differ from the original.''' + tr = urtest.AddTestRun('Make the cert mtime differ') + tr.Processes.Default.Command = 'sleep 2' + tr.Processes.Default.ReturnCode = 0 + self._keep_processes_running(tr) + return tr + + def _add_shadowed_certificate_update_run(self) -> 'TestRun': + '''Replace the certificate for the shadowed wildcard policy.''' + tr = urtest.AddTestRun('Update the shadowed wildcard certificate') + tr.Setup.CopyAs(f'ssl/{self.updated_shadowed_cert}', '.', f'{self._ts.Variables.SSLDir}/{self.shadowed_cert}') + tr.Processes.Default.Command = f'touch {self._ts.Variables.SSLDir}/{self.shadowed_cert}' + tr.Processes.Default.ReturnCode = 0 + self._keep_processes_running(tr) + return tr + + def _add_unchanged_default_certificate_run(self) -> 'TestRun': + '''Verify that refreshing a shadowed wildcard does not replace the default.''' + tr = self._add_curl_run( + 'Default certificate unchanged after shadowed wildcard update', + expected_cn='signer.yahoo.com', + unexpected_cn='signer2.yahoo.com', + expected_description='Default cert still uses signer.', + unexpected_description='Shadowed wildcard cert did not become the default.') + tr.Processes.Default.Streams.All += Testers.ContainsExpression( + r'subject:.*CN=bar\.com', 'Default cert is still bar.com.') + tr.Processes.Default.Streams.All += Testers.ExcludesExpression( + r'subject:.*CN=foo\.com', 'Shadowed foo.com cert is not served.') + return tr + + def _add_default_certificate_update_run(self) -> 'TestRun': + '''Replace the plugin backing default certificate file.''' + tr = urtest.AddTestRun('Update the default certificate') + tr.Setup.CopyAs(f'ssl/{self.updated_cert}', '.', f'{self._ts.Variables.SSLDir}/{self.initial_cert}') + tr.Processes.Default.Command = f'touch {self._ts.Variables.SSLDir}/{self.initial_cert}' + tr.Processes.Default.ReturnCode = 0 + self._keep_processes_running(tr) + return tr + + def _add_secret_update_wait_run(self, secret_name: str) -> 'TestRun': + '''Wait for the test plugin to update the certificate secret.''' + tr = urtest.AddTestRun(f'Wait for the {secret_name} secret update') + tr.Processes.Default.Command = 'echo awaiting secret update' + tr.Processes.Default.ReturnCode = 0 + await_update = tr.Processes.Process('await_update', 'sleep 30') + await_update.Ready = When.FileContains( + self._ts.Disk.traffic_out.Name, f'updated cert for secret .*{re.escape(secret_name)}') + tr.Processes.Default.StartBefore(await_update) + self._keep_processes_running(tr) + return tr + + def _add_updated_certificate_run(self) -> 'TestRun': + '''Verify the updated default certificate after the secret update.''' + return self._add_curl_run( + 'Updated default certificate', + expected_cn='signer2.yahoo.com', + unexpected_cn='signer.yahoo.com', + expected_description='Updated cert uses signer2.', + unexpected_description='Updated cert no longer uses signer.') + + def _add_curl_run( + self, name: str, expected_cn: str, unexpected_cn: str, expected_description: str, + unexpected_description: str) -> 'TestRun': + '''Add a curl run against ATS without SNI.''' + tr = urtest.AddTestRun(name) + self._keep_processes_running(tr) + tr.MakeCurlCommand( + f"-k -v --http1.1 -H 'host: doesnotmatter' https://127.0.0.1:{self._ts.Variables.ssl_port}/", ts=self._ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.All = Testers.ContainsExpression( + f'issuer:.*CN={re.escape(expected_cn)}', expected_description) + tr.Processes.Default.Streams.All += Testers.ExcludesExpression( + f'issuer:.*CN={re.escape(unexpected_cn)}', unexpected_description) + return tr + + def _keep_processes_running(self, tr: 'TestRun') -> None: + '''Keep ATS and the origin server running after a TestRun.''' + tr.StillRunningAfter = self._ts + tr.StillRunningAfter = self._server + + TestDefaultSecretUpdate() + urtest.execute() diff --git a/tests/uranium_tests/tls/test_tls_session_key_logging.py b/tests/uranium_tests/tls/test_tls_session_key_logging.py new file mode 100644 index 00000000000..53470861948 --- /dev/null +++ b/tests/uranium_tests/tls/test_tls_session_key_logging.py @@ -0,0 +1,121 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_tls_session_key_logging(urtest: UraniumTest) -> None: + ''' + Test TLS secrets logging. + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import os + + urtest.Summary = ''' + Test TLS secrets logging. + ''' + + class TlsKeyloggingTest: + + replay_file = "tls_session_key_logging.replay.yaml" + + server_counter = 0 + ts_counter = 0 + client_counter = 0 + + def __init__(self, enable_secrets_logging): + self.setupOriginServer() + self.setupTS(enable_secrets_logging) + + def setupOriginServer(self): + server_name = f"server_{TlsKeyloggingTest.server_counter}" + TlsKeyloggingTest.server_counter += 1 + self.server = urtest.MakeVerifierServerProcess(server_name, TlsKeyloggingTest.replay_file) + + def setupTS(self, enable_secrets_logging): + ts_name = f"ts_{TlsKeyloggingTest.ts_counter}" + TlsKeyloggingTest.ts_counter += 1 + self.ts = urtest.MakeATSProcess(ts_name, enable_tls=True, enable_cache=False) + + self.ts.addDefaultSSLFiles() + self.ts.Disk.records_config.update( + { + "proxy.config.ssl.server.cert.path": f'{self.ts.Variables.SSLDir}', + "proxy.config.ssl.server.private_key.path": f'{self.ts.Variables.SSLDir}', + "proxy.config.ssl.client.verify.server.policy": 'PERMISSIVE', + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'ssl_keylog' + }) + self.ts.Disk.ssl_multicert_yaml.AddLines( + """ + ssl_multicert: + - dest_ip: "*" + ssl_cert_name: server.pem + ssl_key_name: server.key + """.split("\n")) + self.ts.Disk.remap_config.AddLine(f'map / https://127.0.0.1:{self.server.Variables.https_port}') + + keylog_file = os.path.join(self.ts.Variables.LOGDIR, "tls_secrets.txt") + + # Remove the keylog_file configuration automatically configured via the + # trafficserver AuTest extension. + self.ts.Disk.records_config.update(''' + ssl: + keylog_file: null + ''') + + if enable_secrets_logging: + self.ts.Disk.records_config.update({ + 'proxy.config.ssl.keylog_file': keylog_file, + }) + + self.ts.Disk.diags_log.Content += Testers.ContainsExpression( + f"Opened {keylog_file} for TLS key logging", "Verify the user was notified of TLS secrets logging.") + self.ts.Disk.File(keylog_file, id="keylog", exists=True) + # It would be nice to verify the content of certain lines in the + # keylog file, but the content is dependent upon the particular TLS + # protocol version. Thus I'm hesitant to add ContainsExpression + # checks here which will be fragile and eventually become outdated. + else: + self.ts.Disk.File(keylog_file, exists=False) + + def run(self): + tr = urtest.AddTestRun() + tr.Processes.Default.StartBefore(self.server) + tr.Processes.Default.StartBefore(self.ts) + + client_name = f"client_{TlsKeyloggingTest.client_counter}" + TlsKeyloggingTest.client_counter += 1 + tr.AddVerifierClientProcess(client_name, TlsKeyloggingTest.replay_file, https_ports=[self.ts.Variables.ssl_port]) + + TlsKeyloggingTest(enable_secrets_logging=False).run() + TlsKeyloggingTest(enable_secrets_logging=True).run() + urtest.execute() diff --git a/tests/uranium_tests/tls/test_tls_session_reuse.py b/tests/uranium_tests/tls/test_tls_session_reuse.py new file mode 100644 index 00000000000..de26485438e --- /dev/null +++ b/tests/uranium_tests/tls/test_tls_session_reuse.py @@ -0,0 +1,179 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_tls_session_reuse(urtest: UraniumTest) -> None: + ''' + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import re + import os + + urtest.Summary = ''' + Test tls session reuse + ''' + + # Define default ATS + ts1 = urtest.MakeATSProcess("ts1", enable_tls=True) + ts2 = urtest.MakeATSProcess("ts2", enable_tls=True) + server = urtest.MakeOriginServer("server") + + # Add info the origin server responses + request_header = {"headers": "GET / HTTP/1.1\r\nHost: www.example.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + server.addResponse("sessionlog.json", request_header, response_header) + + # add ssl materials like key, certificates for the server + ts1.addSSLfile("ssl/server.pem") + ts1.addSSLfile("ssl/server.key") + ts2.addSSLfile("ssl/server.pem") + ts2.addSSLfile("ssl/server.key") + + ts1.Disk.remap_config.AddLine('map / http://127.0.0.1:{0}'.format(server.Variables.Port)) + ts2.Disk.remap_config.AddLine('map / http://127.0.0.1:{0}'.format(server.Variables.Port)) + + ts1.Disk.ssl_multicert_yaml.AddLines( + """ + ssl_multicert: + - dest_ip: "*" + ssl_cert_name: server.pem + ssl_key_name: server.key + """.split("\n")) + ts2.Disk.ssl_multicert_yaml.AddLines( + """ + ssl_multicert: + - dest_ip: "*" + ssl_cert_name: server.pem + ssl_key_name: server.key + """.split("\n")) + + ts1.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'ssl', + 'proxy.config.ssl.server.cert.path': '{0}'.format(ts1.Variables.SSLDir), + 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts1.Variables.SSLDir), + 'proxy.config.ssl.server.cipher_suite': + 'ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-DSS-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-DSS-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA256:DHE-RSA-AES128-SHA256:DHE-DSS-AES128-SHA256:DHE-RSA-AES256-SHA:DHE-DSS-AES256-SHA:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!aECDH:!EDH-DSS-DES-CBC3-SHA:!EDH-RSA-DES-CBC3-SHA:!KRB5-DES-CBC3-SHA', + 'proxy.config.exec_thread.autoconfig.scale': 1.0, + 'proxy.config.ssl.server.session_ticket.enable': 1, + 'proxy.config.ssl.server.session_ticket.number': 2, + }) + ts2.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'ssl', + 'proxy.config.ssl.server.cert.path': '{0}'.format(ts2.Variables.SSLDir), + 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts2.Variables.SSLDir), + 'proxy.config.ssl.server.cipher_suite': + 'ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-DSS-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-DSS-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA256:DHE-RSA-AES128-SHA256:DHE-DSS-AES128-SHA256:DHE-RSA-AES256-SHA:DHE-DSS-AES256-SHA:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!aECDH:!EDH-DSS-DES-CBC3-SHA:!EDH-RSA-DES-CBC3-SHA:!KRB5-DES-CBC3-SHA', + 'proxy.config.exec_thread.autoconfig.scale': 1.0, + 'proxy.config.ssl.server.session_ticket.enable': 0, + 'proxy.config.ssl.server.session_ticket.number': 0, + }) + + def check_session(output_path, tls_ver, reuse_count): + retval = False + f = open(output_path, 'r') + if not f: + err = "Failed to open {0}".format(output_path) + return (retval, "Check session is reused", err) + + content = f.read() + match = re.findall(f'Reused, {tls_ver}', content) + if len(match) == reuse_count: + retval = True + err = "Reused successfully {0} times".format(len(match)) + else: + err = "Session is not being reused as expected" + f.close() + return (retval, "Check session is reused", err) + + tr1 = urtest.AddTestRun("TLSv1.2 Session Resumption Enabled") + tr1.Command = \ + 'printf "GET / HTTP/1.1\r\n" | openssl s_client -connect 127.0.0.1:{0} -sess_out {1} -tls1_2 && ' \ + 'printf "GET / HTTP/1.1\r\n" | openssl s_client -connect 127.0.0.1:{0} -sess_in {1} -tls1_2 && ' \ + 'printf "GET / HTTP/1.1\r\n" | openssl s_client -connect 127.0.0.1:{0} -sess_in {1} -tls1_2 && ' \ + 'printf "GET / HTTP/1.1\r\n" | openssl s_client -connect 127.0.0.1:{0} -sess_in {1} -tls1_2 && ' \ + 'printf "GET / HTTP/1.1\r\n" | openssl s_client -connect 127.0.0.1:{0} -sess_in {1} -tls1_2 && ' \ + 'printf "GET / HTTP/1.1\r\n" | openssl s_client -connect 127.0.0.1:{0} -sess_in {1} -tls1_2' \ + .format(ts1.Variables.ssl_port, os.path.join(urtest.RunDirectory, 'sess1.dat')) + tr1.ReturnCode = 0 + tr1.Processes.Default.StartBefore(server) + tr1.Processes.Default.StartBefore(ts1) + tr1.Processes.Default.Streams.All.Content = Testers.Lambda( + lambda info, tester: check_session(tr1.Processes.Default.Streams.All.AbsPath, 'TLSv1.2', 5)) + tr1.StillRunningAfter += server + tr1.StillRunningAfter += ts1 + + tr2 = urtest.AddTestRun("TLSv1.3 Session Resumption Enabled") + tr2.Command = \ + 'printf "GET / HTTP/1.1\r\n" | openssl s_client -connect 127.0.0.1:{0} -sess_out {1} -tls1_2 && ' \ + 'printf "GET / HTTP/1.1\r\n" | openssl s_client -connect 127.0.0.1:{0} -sess_in {1} -tls1_2 && ' \ + 'printf "GET / HTTP/1.1\r\n" | openssl s_client -connect 127.0.0.1:{0} -sess_in {1} -tls1_2 && ' \ + 'printf "GET / HTTP/1.1\r\n" | openssl s_client -connect 127.0.0.1:{0} -sess_in {1} -tls1_2 && ' \ + 'printf "GET / HTTP/1.1\r\n" | openssl s_client -connect 127.0.0.1:{0} -sess_in {1} -tls1_2 && ' \ + 'printf "GET / HTTP/1.1\r\n" | openssl s_client -connect 127.0.0.1:{0} -sess_in {1} -tls1_2' \ + .format(ts1.Variables.ssl_port, os.path.join(urtest.RunDirectory, 'sess2.dat')) + tr2.ReturnCode = 0 + tr2.Processes.Default.Streams.All.Content = Testers.Lambda( + lambda info, tester: check_session(tr2.Processes.Default.Streams.All.AbsPath, 'TLSv1.2', 5)) + tr2.StillRunningAfter += server + + tr3 = urtest.AddTestRun("TLSv1.2 Session Resumption Disabled") + tr3.Command = \ + 'printf "GET / HTTP/1.1\r\n" | openssl s_client -connect 127.0.0.1:{0} -sess_out {1} -tls1_2 && ' \ + 'printf "GET / HTTP/1.1\r\n" | openssl s_client -connect 127.0.0.1:{0} -sess_in {1} -tls1_2 && ' \ + 'printf "GET / HTTP/1.1\r\n" | openssl s_client -connect 127.0.0.1:{0} -sess_in {1} -tls1_2 && ' \ + 'printf "GET / HTTP/1.1\r\n" | openssl s_client -connect 127.0.0.1:{0} -sess_in {1} -tls1_2 && ' \ + 'printf "GET / HTTP/1.1\r\n" | openssl s_client -connect 127.0.0.1:{0} -sess_in {1} -tls1_2 && ' \ + 'printf "GET / HTTP/1.1\r\n" | openssl s_client -connect 127.0.0.1:{0} -sess_in {1} -tls1_2' \ + .format(ts2.Variables.ssl_port, os.path.join(urtest.RunDirectory, 'sess3.dat')) + tr3.Processes.Default.StartBefore(ts2) + tr3.Processes.Default.Streams.All = Testers.ExcludesExpression('Reused', '') + tr3.Processes.Default.Streams.All += Testers.ContainsExpression('TLSv1.2', '') + tr3.StillRunningAfter += server + tr3.StillRunningAfter += ts2 + + tr4 = urtest.AddTestRun("TLSv1.3 Session Resumption Disabled") + tr4.Command = \ + 'printf "GET / HTTP/1.1\r\n" | openssl s_client -connect 127.0.0.1:{0} -sess_out {1} -tls1_3 && ' \ + 'printf "GET / HTTP/1.1\r\n" | openssl s_client -connect 127.0.0.1:{0} -sess_in {1} -tls1_3 && ' \ + 'printf "GET / HTTP/1.1\r\n" | openssl s_client -connect 127.0.0.1:{0} -sess_in {1} -tls1_3 && ' \ + 'printf "GET / HTTP/1.1\r\n" | openssl s_client -connect 127.0.0.1:{0} -sess_in {1} -tls1_3 && ' \ + 'printf "GET / HTTP/1.1\r\n" | openssl s_client -connect 127.0.0.1:{0} -sess_in {1} -tls1_3 && ' \ + 'printf "GET / HTTP/1.1\r\n" | openssl s_client -connect 127.0.0.1:{0} -sess_in {1} -tls1_3' \ + .format(ts2.Variables.ssl_port, os.path.join(urtest.RunDirectory, 'sess4.dat')) + tr4.Processes.Default.Streams.All = Testers.ExcludesExpression('Reused', '') + tr4.Processes.Default.Streams.All += Testers.ContainsExpression('TLSv1.3', '') + urtest.execute() diff --git a/tests/uranium_tests/tls/test_tls_sni_groups.py b/tests/uranium_tests/tls/test_tls_sni_groups.py new file mode 100644 index 00000000000..ec99376a804 --- /dev/null +++ b/tests/uranium_tests/tls/test_tls_sni_groups.py @@ -0,0 +1,135 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_tls_sni_groups(urtest: UraniumTest) -> None: + ''' + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + urtest.Summary = ''' + Test SNI configuration server_groups_list + ''' + # The groups function was added in OpenSSL 1.1.1 + urtest.SkipUnless(Condition.HasOpenSSLVersion("1.1.1")) + + # Define default ATS + ts = urtest.MakeATSProcess("ts", enable_tls=True) + server = urtest.MakeOriginServer("server", ssl=True) + + request_header = {"headers": "GET / HTTP/1.1\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": "foo ok"} + server.addResponse("sessionlog.json", request_header, response_header) + + # add ssl materials like key, certificates for the server + ts.addSSLfile("ssl/server.pem") + ts.addSSLfile("ssl/server.key") + + # Need no remap rules. Everything should be processed by sni + + # Make sure the TS server certs are different from the origin certs + ts.Disk.ssl_multicert_yaml.AddLines( + """ + ssl_multicert: + - dest_ip: "*" + ssl_cert_name: server.pem + ssl_key_name: server.key + """.split("\n")) + + ts.Disk.records_config.update( + { + 'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir), + 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts.Variables.SSLDir), + 'proxy.config.ssl.client.CA.cert.path': '{0}'.format(ts.Variables.SSLDir), + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'ssl_sni', + }) + + ts.Disk.sni_yaml.AddLines( + [ + 'sni:', + '- fqdn: aaa.com', + ' server_groups_list: X25519MLKEM768', + ' valid_tls_versions_in: [ TLSv1_3 ]', + ' server_TLSv1_3_cipher_suites: TLS_AES_256_GCM_SHA384', + '- fqdn: bbb.com', + ' server_groups_list: x25519', + ' valid_tls_versions_in: [ TLSv1_2 ]', + ' server_cipher_suite: ECDHE-RSA-AES256-GCM-SHA384', + '- fqdn: ccc.com', + ' server_groups_list: ABC123', + ' valid_tls_versions_in: [ TLSv1_2 ]', + ' server_cipher_suite: ECDHE-RSA-AES256-GCM-SHA384', + ]) + + tr = urtest.AddTestRun("Test 0: x25519") + tr.Processes.Default.StartBefore(server) + tr.Processes.Default.StartBefore(urtest.Processes.ts) + tr.MakeCurlCommand( + "-v --ciphers ECDHE-RSA-AES256-GCM-SHA384 --resolve 'bbb.com:{0}:127.0.0.1' -k https://bbb.com:{0}".format( + ts.Variables.ssl_port), + ts=ts) + tr.ReturnCode = 0 + tr.StillRunningAfter = ts + ts.Disk.traffic_out.Content += Testers.ContainsExpression( + "Setting groups list from server_groups_list to x25519", "Should log setting the server groups") + tr.Processes.Default.Streams.all = Testers.IncludesExpression( + f"SSL connection using TLSv1.2 / ECDHE-RSA-AES256-GCM-SHA384 / x25519", + "Curl should log using x25519 in the SSL connection") + + tr = urtest.AddTestRun("Test 1: fail") + tr.MakeCurlCommand( + "-v --ciphers ECDHE-RSA-AES256-GCM-SHA384 --resolve 'ccc.com:{0}:127.0.0.1' -k https://ccc.com:{0}".format( + ts.Variables.ssl_port), + ts=ts) + # The error code is 35, which indicates there was a ssl connection error + tr.ReturnCode = 35 + tr.StillRunningAfter = ts + tr.StillRunningAfter = server + ts.Disk.diags_log.Content = Testers.ContainsExpression( + "ERROR: Invalid server_groups_list: ABC123", "Curl attempt should have failed") + + # Hybrid ECDH PQ key exchange TLS groups were added in OpenSSL 3.5 + if Condition.HasOpenSSLVersion("3.5.0"): + tr = urtest.AddTestRun("Test 2: X25519MLKEM768") + tr.MakeCurlCommand( + "-v --tls13-ciphers TLS_AES_256_GCM_SHA384 --resolve 'aaa.com:{0}:127.0.0.1' -k https://aaa.com:{0}".format( + ts.Variables.ssl_port), + ts=ts) + tr.ReturnCode = 0 + tr.StillRunningAfter = ts + ts.Disk.traffic_out.Content += Testers.ContainsExpression( + "Setting groups list from server_groups_list to X25519MLKEM768", "Should log setting the server groups") + tr.Processes.Default.Streams.all = Testers.IncludesExpression( + f"SSL connection using TLSv1.3 / TLS_AES_256_GCM_SHA384 / X25519MLKEM768", + f"Curl should log using X25519MLKEM768 in the SSL connection") + urtest.execute() diff --git a/tests/uranium_tests/tls/test_tls_sni_host_policy.py b/tests/uranium_tests/tls/test_tls_sni_host_policy.py new file mode 100644 index 00000000000..ceca2b9ad75 --- /dev/null +++ b/tests/uranium_tests/tls/test_tls_sni_host_policy.py @@ -0,0 +1,331 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_tls_sni_host_policy(urtest: UraniumTest) -> None: + ''' + Test exercising host and SNI mismatch controls + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import os + + urtest.Summary = ''' + Test exercising host and SNI mismatch controls + ''' + + ts = urtest.MakeATSProcess("ts", enable_tls=True) + cafile = "{0}/signer.pem".format(urtest.RunDirectory) + cafile2 = "{0}/signer2.pem".format(urtest.RunDirectory) + server = urtest.MakeOriginServer("server") + + request_header = {"headers": "GET /case1 HTTP/1.1\r\nHost: example.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + server.addResponse("sessionlog.json", request_header, response_header) + request_header = {"headers": "GET / HTTP/1.1\r\nHost: bar.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + server.addResponse("sessionlog.json", request_header, response_header) + + ts.addSSLfile("ssl/server.pem") + ts.addSSLfile("ssl/server.key") + ts.addSSLfile("ssl/signer.pem") + + ts.Disk.records_config.update( + { + 'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir), + 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts.Variables.SSLDir), + 'proxy.config.url_remap.pristine_host_hdr': 1, + 'proxy.config.ssl.CA.cert.filename': '{0}/signer.pem'.format(ts.Variables.SSLDir), + 'proxy.config.exec_thread.autoconfig.scale': 1.0, + 'proxy.config.http.host_sni_policy': 2, + 'proxy.config.ssl.TLSv1_3.enabled': 0, + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'ssl', + }) + + ts.Disk.ssl_multicert_yaml.AddLines( + """ + ssl_multicert: + - dest_ip: "*" + ssl_cert_name: server.pem + ssl_key_name: server.key + """.split("\n")) + + # Just map everything through to origin. This test is concentrating on the user-agent side + ts.Disk.remap_config.AddLine('map / http://127.0.0.1:{0}/'.format(server.Variables.Port)) + + # Scenario 1: Default no client cert required. cert required for bar.com. + # Make boblite and bob mixed case to verify that we can match hostnames case + # insensitively. + ts.Disk.sni_yaml.AddLines( + [ + 'sni:', + '- fqdn: boBliTe', + ' verify_client: STRICT', + ' host_sni_policy: PERMISSIVE', + '- fqdn: bOb', + ' verify_client: STRICT', + '- fqdn: bob.bar.com', + ' verify_client: STRICT', + '- fqdn: dave.bob', + ' verify_client: STRICT', + '- fqdn: noipallow.example.com', + ' http2: off', + '- fqdn: ipallow_nomatch.example.com', + ' ip_allow: 192.168.1.1', + ]) + + # case 1 + # sni=Bob and host=dave. Do not provide client cert. This should match fqdn bOb which has + # verify_client: STRICT and thus should fail. + tr = urtest.AddTestRun("Connect to bob without cert") + tr.Processes.Default.StartBefore(urtest.Processes.ts) + tr.Processes.Default.StartBefore(server) + tr.StillRunningAfter = ts + tr.StillRunningAfter = server + tr.MakeCurlCommand( + "-v --tls-max 1.2 -k -H 'host:dave' --resolve 'Bob:{0}:127.0.0.1' https://Bob:{0}/case1".format(ts.Variables.ssl_port), + ts=ts) + tr.Processes.Default.ReturnCode = 35 + + # case 2 + # sni=Bob and host=dave. Do provide client cert. This should match fqdn bOb which has + # verify_client: STRICT, but since the cert is good it should succeed. + tr = urtest.AddTestRun("Connect to bob with good cert") + tr.Setup.Copy("ssl/signed-foo.pem") + tr.Setup.Copy("ssl/signed-foo.key") + tr.StillRunningAfter = ts + tr.StillRunningAfter = server + tr.MakeCurlCommand( + "-v --tls-max 1.2 -k --cert ./signed-foo.pem --key ./signed-foo.key -H 'host:dave' --resolve 'Bob:{0}:127.0.0.1' https://Bob:{0}/case1" + .format(ts.Variables.ssl_port), + ts=ts) + tr.Processes.Default.ReturnCode = 0 + + # case 3 + # sni=dave and host=bob. Do not provide client cert. This should not need a cert, but should still + # fail due to sni-host mismatch. + tr = urtest.AddTestRun("Connect to dave without cert") + tr.StillRunningAfter = ts + tr.StillRunningAfter = server + tr.MakeCurlCommand( + "-v --tls-max 1.2 -k -H 'host:Bob' --resolve 'dave:{0}:127.0.0.1' https://dave:{0}/case1".format(ts.Variables.ssl_port), + ts=ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.All = Testers.ContainsExpression("Access Denied", "Check response") + + # case 4 + # sni=dave and host=bob. Do provide client cert. Again, this should not need a cert, but provide + # one anyway and verify it fails due to sni-host mismatch. + tr = urtest.AddTestRun("Connect to dave with cert") + tr.StillRunningAfter = ts + tr.StillRunningAfter = server + tr.MakeCurlCommand( + "-v --tls-max 1.2 -k --cert ./signed-foo.pem --key ./signed-foo.key -H 'host:bob' --resolve 'dave:{0}:127.0.0.1' https://dave:{0}/case1" + .format(ts.Variables.ssl_port), + ts=ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.All = Testers.ContainsExpression("Access Denied", "Check response") + + # case 5 + # sni=Bob and host=boB. Do provide client cert. Should succeed because the hosts match and the cert is provided. + tr = urtest.AddTestRun("Connect to bob with cert") + tr.StillRunningAfter = ts + tr.StillRunningAfter = server + tr.MakeCurlCommand( + "--tls-max 1.2 -k --cert ./signed-foo.pem --key ./signed-foo.key -H 'host:boB' --resolve 'Bob:{0}:127.0.0.1' https://bob:{0}/case1" + .format(ts.Variables.ssl_port), + ts=ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.All = Testers.ExcludesExpression("Access Denied", "Check response") + + # case 6 + # sni=ellen and host=Boblite. Do not provide client cert. Should warn due to sni-host mismatch, + # but should note get an Access Denied because boblite has host_sni_policy: PERMISSIVE configured. + tr = urtest.AddTestRun("Connect to ellen without cert") + tr.StillRunningAfter = ts + tr.StillRunningAfter = server + tr.MakeCurlCommand( + "-v --tls-max 1.2 -k -H 'host:Boblite' --resolve 'ellen:{0}:127.0.0.1' https://ellen:{0}/warnonly".format( + ts.Variables.ssl_port), + ts=ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.All = Testers.ExcludesExpression("Access Denied", "Check response") + + # case 7 + # sni=ellen and host=Boblite. Do provide client cert. This should behave the same as above because + # providing a client cert does not mean SNI and hostname will not be compared. + tr = urtest.AddTestRun("Connect to ellen with cert") + tr.StillRunningAfter = ts + tr.StillRunningAfter = server + tr.MakeCurlCommand( + "-v --tls-max 1.2 -k --cert ./signed-foo.pem --key ./signed-foo.key -H 'host:Boblite' --resolve 'ellen:{0}:127.0.0.1' https://ellen:{0}/warnonly" + .format(ts.Variables.ssl_port), + ts=ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.All = Testers.ExcludesExpression("Access Denied", "Check response") + + # case 8 + # sni=ellen and host=fran. Do not provide client cert. No warning since neither name is mentioned in sni.yaml + tr = urtest.AddTestRun("Connect to ellen without cert") + tr.StillRunningAfter = ts + tr.StillRunningAfter = server + tr.MakeCurlCommand( + "-v --tls-max 1.2 -k -H 'host:fran' --resolve 'ellen:{0}:127.0.0.1' https://ellen:{0}/warnonly".format( + ts.Variables.ssl_port), + ts=ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.All = Testers.ExcludesExpression("Access Denied", "Check response") + + # case 9 + # sni=ellen and host=fran. Do provide client cert. No warning since neither name is mentioned in sni.yaml + tr = urtest.AddTestRun("Connect to ellen with cert") + tr.StillRunningAfter = ts + tr.StillRunningAfter = server + tr.MakeCurlCommand( + "-v --tls-max 1.2 -k --cert ./signed-foo.pem --key ./signed-foo.key -H 'host:fran' --resolve 'ellen:{0}:127.0.0.1' https://ellen:{0}/warnonly" + .format(ts.Variables.ssl_port), + ts=ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.All = Testers.ExcludesExpression("Access Denied", "Check response") + + # case 10 + # sni=bob.bar.com and host=bob. Do provide client cert. SNI is longer than host but shares the + # same prefix. Should fail due to sni-host mismatch. + tr = urtest.AddTestRun("Connect with SNI longer than host sharing prefix") + tr.StillRunningAfter = ts + tr.StillRunningAfter = server + tr.MakeCurlCommand( + "-v --tls-max 1.2 -k --cert ./signed-foo.pem --key ./signed-foo.key -H 'host:bob' --resolve 'bob.bar.com:{0}:127.0.0.1' https://bob.bar.com:{0}/case1" + .format(ts.Variables.ssl_port), + ts=ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.All = Testers.ContainsExpression("Access Denied", "Check response") + + # case 11 + # sni=bob and host=bob.bar.com. Do provide client cert. Host is longer than SNI but shares the + # same prefix. Should fail due to sni-host mismatch. + tr = urtest.AddTestRun("Connect with host longer than SNI sharing prefix") + tr.StillRunningAfter = ts + tr.StillRunningAfter = server + tr.MakeCurlCommand( + "-v --tls-max 1.2 -k --cert ./signed-foo.pem --key ./signed-foo.key -H 'host:bob.bar.com' --resolve 'bob:{0}:127.0.0.1' https://bob:{0}/case1" + .format(ts.Variables.ssl_port), + ts=ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.All = Testers.ContainsExpression("Access Denied", "Check response") + + # case 12 + # sni=bob and host=dave.bob. Do provide client cert. Host ends with the SNI value but is a + # different hostname. Should fail due to sni-host mismatch. + tr = urtest.AddTestRun("Connect with host ending with SNI value") + tr.StillRunningAfter = ts + tr.StillRunningAfter = server + tr.MakeCurlCommand( + "-v --tls-max 1.2 -k --cert ./signed-foo.pem --key ./signed-foo.key -H 'host:dave.bob' --resolve 'bob:{0}:127.0.0.1' https://bob:{0}/case1" + .format(ts.Variables.ssl_port), + ts=ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.All = Testers.ContainsExpression("Access Denied", "Check response") + + # case 13 + # sni=dave.bob and host=bob. Do provide client cert. SNI ends with the host value but is a + # different hostname. Should fail due to sni-host mismatch. + tr = urtest.AddTestRun("Connect with SNI ending with host value") + tr.StillRunningAfter = ts + tr.StillRunningAfter = server + tr.MakeCurlCommand( + "-v --tls-max 1.2 -k --cert ./signed-foo.pem --key ./signed-foo.key -H 'host:bob' --resolve 'dave.bob:{0}:127.0.0.1' https://dave.bob:{0}/case1" + .format(ts.Variables.ssl_port), + ts=ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.All = Testers.ContainsExpression("Access Denied", "Check response") + + # case 14 + # sni=other.example.com and host=ipallow_nomatch.example.com. Host header matches SNI entry but + # client IP is NOT in ip_allow list. TestClientSNIAction should still return true (because ip_addrs is + # non-empty), which means host_sni_policy IS enforced and the mismatch triggers "Access Denied". + tr = urtest.AddTestRun("Connect with ip_allow SNI entry not matching client IP should still enforce host_sni_policy") + tr.StillRunningAfter = ts + tr.StillRunningAfter = server + tr.MakeCurlCommand( + "-v --tls-max 1.2 -k -H 'host:ipallow_nomatch.example.com' --resolve 'other.example.com:{0}:127.0.0.1' https://other.example.com:{0}/case1" + .format(ts.Variables.ssl_port), + ts=ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.All = Testers.ContainsExpression( + "Access Denied", "Should get 403 due to host_sni_policy enforcement") + + # case 15 + # sni=other.example.com and host=noipallow.example.com. Host header matches SNI + # entry that only has http2: off configured (no verify_client, no ip_allow). This should + # NOT trigger host_sni_policy enforcement because the only action is a no-op. + tr = urtest.AddTestRun("Connect with SNI entry having no ip_allow should not enforce host_sni_policy") + tr.StillRunningAfter = ts + tr.StillRunningAfter = server + tr.MakeCurlCommand( + "-v --tls-max 1.2 -k -H 'host:noipallow.example.com' --resolve 'other.example.com:{0}:127.0.0.1' https://other.example.com:{0}/case1" + .format(ts.Variables.ssl_port), + ts=ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.All = Testers.ExcludesExpression("Access Denied", "No 403 for non-ip_allow SNI entry") + + # Wait for the error.log entry to be written. + test_run = urtest.AddAwaitFileContainsTestRun( + 'Await SNI mismatch error log entry.', + os.path.join(ts.Variables.LOGDIR, 'error.log'), + "SNI/hostname mismatch: connecting to .* for host='bob' sni='dave', returning a 403", + ) + + ts.Disk.diags_log.Content += Testers.ContainsExpression( + "WARNING: SNI/hostname mismatch sni=dave host=bob action=terminate", "Should have warning on mismatch") + ts.Disk.diags_log.Content += Testers.ContainsExpression( + "WARNING: SNI/hostname mismatch sni=ellen host=Boblite action=continue", "Should have warning on mismatch") + ts.Disk.diags_log.Content += Testers.ContainsExpression( + "WARNING: SNI/hostname mismatch sni=bob.bar.com host=bob action=terminate", "Should have warning on prefix mismatch") + ts.Disk.diags_log.Content += Testers.ContainsExpression( + "WARNING: SNI/hostname mismatch sni=bob host=bob.bar.com action=terminate", "Should have warning on prefix mismatch") + ts.Disk.diags_log.Content += Testers.ContainsExpression( + "WARNING: SNI/hostname mismatch sni=bob host=dave.bob action=terminate", "Should have warning on suffix mismatch") + ts.Disk.diags_log.Content += Testers.ContainsExpression( + "WARNING: SNI/hostname mismatch sni=dave.bob host=bob action=terminate", "Should have warning on suffix mismatch") + ts.Disk.diags_log.Content += Testers.ExcludesExpression( + "WARNING: SNI/hostname mismatch sni=ellen host=fran", "Should not have warning on mismatch with non-policy host") + ts.Disk.diags_log.Content += Testers.ExcludesExpression( + "WARNING: SNI/hostname mismatch sni=other.example.com host=noipallow.example.com", + "Should not have warning for SNI entry with no ip_allow") + + test_run.Processes.Default.ReturnCode = 0 + ts.Disk.error_log.Content += Testers.ContainsExpression( + "SNI/hostname mismatch: connecting to .* for host='bob' sni='dave', returning a 403", + "error.log should contain information about the 403 response.") + urtest.execute() diff --git a/tests/uranium_tests/tls/test_tls_sni_ip_allow.py b/tests/uranium_tests/tls/test_tls_sni_ip_allow.py new file mode 100644 index 00000000000..64252486378 --- /dev/null +++ b/tests/uranium_tests/tls/test_tls_sni_ip_allow.py @@ -0,0 +1,204 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_tls_sni_ip_allow(urtest: UraniumTest) -> None: + ''' + Test exercising ip_allow configuration of sni.yaml + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import os + + urtest.Summary = '''Test sni.yaml ip_allow.''' + + class ConnectionType: + GET = 0 + TUNNEL = 1 + PROXY = 2 + + class TestSniIpAllow: + '''Verify ip_allow of sni.yaml.''' + _dns_counter: int = 0 + _server_counter: int = 0 + _ts_counter: int = 0 + _client_counter: int = 0 + + def __init__(self, connect_type: int) -> None: + """Configure a test run. + :param connect_type: The type of connection to use. + """ + tr = urtest.AddTestRun(f'Verify ip_allow of sni.yaml ({connect_type})') + + if connect_type == ConnectionType.GET: + self._replay_file: str = "replay/ip_allow.replay.yaml" + elif connect_type == ConnectionType.TUNNEL: + self._replay_file: str = "replay/ip_allow_tunnel.replay.yaml" + elif connect_type == ConnectionType.PROXY: + self._replay_file: str = "replay/ip_allow_proxy.replay.yaml" + else: + raise ValueError(f'Invalid connect_type: {connect_type}') + + self._dns = self._configure_dns(tr) + self._server = self._configure_server(tr) + self._ts = self._configure_trafficserver(tr, connect_type, self._dns, self._server) + self._configure_client(tr, self._dns, self._server, self._ts, connect_type) + + def _configure_dns(self, tr: 'TestRun') -> 'Process': + """Configure a DNS for the TestRun. + :param tr: The TestRun to configure with the DNS. + :return: The DNS Process. + """ + name = f'dns{TestSniIpAllow._dns_counter}' + dns = tr.MakeDNServer(name, default='127.0.0.1') + TestSniIpAllow._dns_counter += 1 + return dns + + def _configure_server(self, tr: 'TestRun') -> 'Process': + """Configure an Origin Server for the TestRun. + :param tr: The TestRun to configure with the Origin Server. + :return: The Origin Server Process. + """ + name = f'server{TestSniIpAllow._server_counter}' + server = tr.AddVerifierServerProcess(name, self._replay_file) + TestSniIpAllow._server_counter += 1 + server.Streams.All += Testers.ContainsExpression('allowed-request', 'The allowed request should be recieved.') + server.Streams.All += Testers.ExcludesExpression( + 'blocked-request', 'The blocked request should not have been recieved.') + server.Streams.All += Testers.ExcludesExpression( + 'block.me.com', 'Nothing about the block.me.com sni should have been recieved.') + + return server + + def _configure_trafficserver(self, tr: 'TestRun', connect_type: int, dns: 'Process', server: 'Process') -> 'Process': + """Configure Traffic Server for the TestRun. + :param tr: The TestRun to configure with Traffic Server. + :param connect_type: The type of connection to use. + :param dns: The DNS Process. + :param server: The Origin Server Process. + :return: The Traffic Server Process. + """ + name = f'ts{TestSniIpAllow._ts_counter}' + ts = tr.MakeATSProcess(name, enable_tls=True, enable_cache=False, enable_proxy_protocol=True) + TestSniIpAllow._ts_counter += 1 + ts.Disk.sni_yaml.AddLines( + [ + 'sni:', + '- fqdn: block.me.com', + ' ip_allow: 192.168.10.1', # Therefore 127.0.0.1 should be blocked. + ]) + if connect_type == ConnectionType.TUNNEL: + ts.Disk.sni_yaml.AddLines([ + f' tunnel_route: backend.server.com:{server.Variables.https_port}', + ]) + if connect_type == ConnectionType.PROXY: + ts.Disk.sni_yaml.AddLines( + [ + '- fqdn: pp.block.me.com', + ' ip_allow: 192.168.10.1', # Therefore 1.2.3.4 should be blocked. + ]) + ts.Disk.sni_yaml.AddLines([ + '- fqdn: allow.me.com', + ' ip_allow: 127.0.0.1', + ]) + if connect_type == ConnectionType.TUNNEL: + ts.Disk.sni_yaml.AddLines([ + f' tunnel_route: backend.server.com:{server.Variables.https_port}', + ]) + if connect_type == ConnectionType.PROXY: + ts.Disk.sni_yaml.AddLines([ + '- fqdn: pp.allow.me.com', + ' ip_allow: 1.2.3.4', + ]) + ts.addDefaultSSLFiles() + ts.Disk.ssl_multicert_yaml.AddLines( + """ + ssl_multicert: + - dest_ip: "*" + ssl_cert_name: server.pem + ssl_key_name: server.key + """.split("\n")) + ts.Disk.remap_config.AddLine(f'map / http://remapped.backend.server.com:{server.Variables.http_port}/') + ts.Disk.records_config.update( + { + 'proxy.config.ssl.server.cert.path': ts.Variables.SSLDir, + 'proxy.config.ssl.server.private_key.path': ts.Variables.SSLDir, + 'proxy.config.ssl.client.verify.server.policy': 'PERMISSIVE', + 'proxy.config.dns.nameservers': f"127.0.0.1:{dns.Variables.Port}", + 'proxy.config.dns.resolv_conf': 'NULL', + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'http|ssl|proxyprotocol', + 'proxy.config.acl.subjects': 'PROXY,PEER', + }) + if connect_type == ConnectionType.TUNNEL: + ts.Disk.records_config.update({ + 'proxy.config.http.connect_ports': f"{server.Variables.https_port}", + }) + ts.addPrivateConnectAllowYaml() + return ts + + def _configure_client( + self, tr: 'TestRun', dns: 'Process', server: 'Process', ts: 'Process', connect_type: ConnectionType.GET) -> None: + """Configure the client for the TestRun. + :param tr: The TestRun to configure with the client. + :param dns: The DNS Process. + :param server: The Origin Server Process. + :param ts: The Traffic Server Process. + """ + name = f'client{TestSniIpAllow._client_counter}' + if connect_type == ConnectionType.PROXY: + p = tr.AddVerifierClientProcess( + name, + self._replay_file, + http_ports=[ts.Variables.proxy_protocol_port], + https_ports=[ts.Variables.proxy_protocol_ssl_port]) + else: + p = tr.AddVerifierClientProcess( + name, self._replay_file, http_ports=[ts.Variables.port], https_ports=[ts.Variables.ssl_port]) + TestSniIpAllow._client_counter += 1 + ts.StartBefore(server) + ts.StartBefore(dns) + p.StartBefore(ts) + + # Because the first connection will be aborted, the client will have a + # non-zero return code. + p.ReturnCode = 1 + + p.Streams.All += Testers.ContainsExpression( + 'allowed-response', 'The response to the allowed request should be recieved.') + p.Streams.All += Testers.ExcludesExpression( + 'blocked-response', 'The response to the blocked request should not have been recieved.') + + TestSniIpAllow(ConnectionType.GET) + TestSniIpAllow(ConnectionType.TUNNEL) + TestSniIpAllow(ConnectionType.PROXY) + urtest.execute() diff --git a/tests/uranium_tests/tls/test_tls_sni_parent_failover.py b/tests/uranium_tests/tls/test_tls_sni_parent_failover.py new file mode 100644 index 00000000000..fb45eacab13 --- /dev/null +++ b/tests/uranium_tests/tls/test_tls_sni_parent_failover.py @@ -0,0 +1,203 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_tls_sni_parent_failover(urtest: UraniumTest) -> None: + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + urtest.Summary = ''' + Test parent failover with SNI name handling, validating that Traffic Server + correctly fails over between HTTPS parents (including strategy-based parents) + while enforcing TLS server name verification. + ''' + + # Define default ATS + ts = urtest.MakeATSProcess( + "ts", + enable_tls=True, + enable_cache=False, + ) + + server_foo = urtest.MakeOriginServer( + "server_foo", + ssl=True, + options={ + "--key": "{0}/server-foo.key".format(urtest.RunDirectory), + "--cert": "{0}/server-foo.pem".format(urtest.RunDirectory), + }, + ) + server_bar = urtest.MakeOriginServer( + "server_bar", + ssl=True, + options={ + "--key": "{0}/server-bar.key".format(urtest.RunDirectory), + "--cert": "{0}/server-bar.pem".format(urtest.RunDirectory), + }, + ) + + # default check request/response + request_foo_header = {"headers": "GET / HTTP/1.1\r\nHost: foo.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + response_foo_header = { + "headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", + "timestamp": "1469733493.993", + "body": "foo ok" + } + request_bar_header = {"headers": "GET / HTTP/1.1\r\nHost: bar.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + response_bar_header = { + "headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", + "timestamp": "1469733493.993", + "body": "bar ok" + } + + server_foo.addResponse("sessionlog.json", request_foo_header, response_foo_header) + server_bar.addResponse("sessionlog.json", request_bar_header, response_bar_header) + + # successful request to be served by bar.com + request_bar_header = {"headers": "GET /path HTTP/1.1\r\nHost: bar.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + response_bar_header = { + "headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", + "timestamp": "1469733493.993", + "body": "path bar ok" + } + + server_bar.addResponse("sessionlog.json", request_bar_header, response_bar_header) + + ts.addSSLfile("ssl/server-foo.pem") + ts.addSSLfile("ssl/server-foo.key") + ts.addSSLfile("ssl/server-bar.pem") + ts.addSSLfile("ssl/server-bar.key") + + dns = urtest.MakeDNServer("dns") + + ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'http|ssl|parent_select|next_hop', + 'proxy.config.ssl.client.verify.server.policy': 'ENFORCED', + 'proxy.config.ssl.client.verify.server.properties': 'NAME', + 'proxy.config.url_remap.pristine_host_hdr': 0, + 'proxy.config.dns.nameservers': '127.0.0.1:{0}'.format(dns.Variables.Port), + 'proxy.config.dns.resolv_conf': 'NULL', + 'proxy.config.exec_thread.autoconfig.scale': 1.0, + 'proxy.config.http.connect.down.policy': 1, # tls failures don't mark down + }) + + dns.addRecords(records={"foo.com.": ["127.0.0.1"]}) + dns.addRecords(records={"bar.com.": ["127.0.0.1"]}) + dns.addRecords(records={"parent.": ["127.0.0.1"]}) + dns.addRecords(records={"strategy.": ["127.0.0.1"]}) + + ts.Disk.remap_config.AddLines( + [ + "map http://parent https://parent", + "map http://strategy https://strategy @strategy=strat", + "map http://parent_prist https://parent @plugin=conf_remap.so @pparam=proxy.config.url_remap.pristine_host_hdr=1", + "map http://strategy_prist https://strategy @strategy=strat @plugin=conf_remap.so @pparam=proxy.config.url_remap.pristine_host_hdr=1", + ]) + + ts.Disk.parent_config.AddLine( + 'dest_domain=. port=443 parent="foo.com:{0}|1;bar.com:{1}|1" parent_retry=simple_retry parent_is_proxy=false go_direct=false simple_server_retry_responses="404" host_override=true' + .format(server_foo.Variables.SSL_Port, server_bar.Variables.SSL_Port)) + + # build strategies.yaml file + ts.Disk.File(ts.Variables.CONFIGDIR + "/strategies.yaml", id="strategies", typename="ats:config") + + s = ts.Disk.strategies + s.AddLine("groups:") + s.AddLines( + [ + f" - &gstrat", + f" - host: foo.com", + f" protocol:", + f" - scheme: https", + f" port: {server_foo.Variables.SSL_Port}", + f" weight: 1.0", + f" - host: bar.com", + f" protocol:", + f" - scheme: https", + f" port: {server_bar.Variables.SSL_Port}", + f" weight: 1.0", + ]) + + s.AddLine("strategies:") + + s.AddLines( + [ + f" - strategy: strat", + f" policy: first_live", + f" go_direct: false", + f" parent_is_proxy: false", + f" ignore_self_detect: true", + f" host_override: true", + f" groups:", + f" - *gstrat", + f" scheme: https", + f" failover:", + f" ring_mode: exhaust_ring", + f" response_codes:", + f" - 404", + ]) + + curl_args = f"-s -L -o /dev/stdout -D /dev/stderr -x localhost:{ts.Variables.port} " + + tr = urtest.AddTestRun("request with failover, parent.config") + tr.Setup.Copy("ssl/server-foo.key") + tr.Setup.Copy("ssl/server-foo.pem") + tr.Setup.Copy("ssl/server-bar.key") + tr.Setup.Copy("ssl/server-bar.pem") + tr.MakeCurlCommand(curl_args + "http://parent/path", ts=ts) + tr.StillRunningAfter = ts + ps = tr.Processes.Default + ps.StartBefore(server_foo) + ps.StartBefore(server_bar) + ps.StartBefore(dns) + ps.StartBefore(urtest.Processes.ts) + ps.Streams.stdout = Testers.ContainsExpression("path bar ok", "Expected 200 response from bar.com") + + tr = urtest.AddTestRun("request with failover, strategies.yaml") + tr.MakeCurlCommand(curl_args + "http://strategy/path", ts=ts) + tr.StillRunningAfter = ts + ps = tr.Processes.Default + ps.Streams.stdout = Testers.ContainsExpression("path bar ok", "Expected 200 response from bar.com") + + tr = urtest.AddTestRun("request with failover, parent.config pristine_host_hdr") + tr.MakeCurlCommand(curl_args + "http://parent_prist/path", ts=ts) + tr.StillRunningAfter = ts + ps = tr.Processes.Default + ps.Streams.stdout = Testers.ContainsExpression("path bar ok", "Expected 200 response from bar.com") + + tr = urtest.AddTestRun("request with failover, strategies.yaml") + tr.MakeCurlCommand(curl_args + "http://strategy_prist/path", ts=ts) + tr.StillRunningAfter = ts + ps = tr.Processes.Default + ps.Streams.stdout = Testers.ContainsExpression("path bar ok", "Expected 200 response from bar.com") + urtest.execute() diff --git a/tests/uranium_tests/tls/test_tls_sni_ticket.py b/tests/uranium_tests/tls/test_tls_sni_ticket.py new file mode 100644 index 00000000000..ba320b85d88 --- /dev/null +++ b/tests/uranium_tests/tls/test_tls_sni_ticket.py @@ -0,0 +1,299 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_tls_sni_ticket(urtest: UraniumTest) -> None: + ''' + Test sni.yaml session ticket overrides. + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import os + import re + from typing import Any + + urtest.Summary = ''' + Test sni.yaml session ticket overrides + ''' + + urtest.SkipUnless(Condition.HasOpenSSLVersion('1.1.1')) + urtest.Setup.Copy('file.ticket') + + class TlsSniTicketTest: + _server_is_started = False + _ts_on_started = False + _ts_off_started = False + + def __init__(self) -> None: + """ + Initialize shared test state and configure the ATS processes. + """ + self.ticket_file = os.path.join(urtest.RunDirectory, 'file.ticket') + self.setupOriginServer() + self.setupEnabledTS() + self.setupDisabledTS() + + def setupOriginServer(self) -> None: + """ + Configure the origin server with a simple response for all requests. + """ + request_header = { + 'headers': 'GET / HTTP/1.1\r\nHost: tickets.example.com\r\n\r\n', + 'timestamp': '1469733493.993', + 'body': '' + } + response_header = { + 'headers': 'HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n', + 'timestamp': '1469733493.993', + 'body': 'ticket test' + } + self.server = urtest.MakeOriginServer('server') + self.server.addResponse('sessionlog.json', request_header, response_header) + + def setupTS( + self, + name: str, + sni_name: str, + global_ticket_enabled: int, + global_ticket_number: int, + sni_ticket_enabled: int, + sni_ticket_number: int | None = None) -> Any: + """ + Configure an ATS process for one SNI ticket override scenario. + + :param name: ATS process name. + :param sni_name: SNI hostname matched in sni.yaml. + :param global_ticket_enabled: Process-wide session ticket enable setting. + :param global_ticket_number: Process-wide TLSv1.3 ticket count. + :param sni_ticket_enabled: Per-SNI session ticket enable override. + :param sni_ticket_number: Per-SNI TLSv1.3 ticket count override. + :return: Configured ATS process. + """ + ts = urtest.MakeATSProcess(name, enable_tls=True) + + ts.addSSLfile('ssl/server.pem') + ts.addSSLfile('ssl/server.key') + ts.Disk.remap_config.AddLine(f'map / http://127.0.0.1:{self.server.Variables.Port}') + ts.Disk.ssl_multicert_yaml.AddLines( + """ + ssl_multicert: + - dest_ip: "*" + ssl_cert_name: server.pem + ssl_key_name: server.key + """.split("\n")) + + ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'ssl|http', + 'proxy.config.ssl.server.cert.path': f'{ts.Variables.SSLDir}', + 'proxy.config.ssl.server.private_key.path': f'{ts.Variables.SSLDir}', + 'proxy.config.exec_thread.autoconfig.scale': 1.0, + 'proxy.config.ssl.server.session_ticket.enable': global_ticket_enabled, + 'proxy.config.ssl.server.session_ticket.number': global_ticket_number, + 'proxy.config.ssl.server.ticket_key.filename': self.ticket_file, + }) + + sni_lines = [ + 'sni:', + f'- fqdn: {sni_name}', + f' ssl_ticket_enabled: {sni_ticket_enabled}', + ] + if sni_ticket_number is not None: + sni_lines.append(f' ssl_ticket_number: {sni_ticket_number}') + ts.Disk.sni_yaml.AddLines(sni_lines) + + return ts + + def setupEnabledTS(self) -> None: + """ + Create the ATS process whose SNI rule enables tickets. + """ + self.ts_on = self.setupTS('ts_on', 'tickets-on.com', 0, 0, 1, 3) + + def setupDisabledTS(self) -> None: + """ + Create the ATS process whose SNI rule disables tickets. + """ + self.ts_off = self.setupTS('ts_off', 'tickets-off.com', 1, 2, 0) + + def start_processes_if_needed( + self, tr: Any, start_server: bool = False, start_ts_on: bool = False, start_ts_off: bool = False) -> None: + """ + Register one-time StartBefore hooks for the processes needed by a test run. + + :param tr: The AuTest run definition being configured. + :param start_server: Whether the origin server should be started for this run. + :param start_ts_on: Whether the tickets-enabled ATS process should be started for this run. + :param start_ts_off: Whether the tickets-disabled ATS process should be started for this run. + """ + if start_server and not TlsSniTicketTest._server_is_started: + tr.Processes.Default.StartBefore(self.server) + TlsSniTicketTest._server_is_started = True + + if start_ts_on and not TlsSniTicketTest._ts_on_started: + tr.Processes.Default.StartBefore(self.ts_on) + TlsSniTicketTest._ts_on_started = True + + if start_ts_off and not TlsSniTicketTest._ts_off_started: + tr.Processes.Default.StartBefore(self.ts_off) + TlsSniTicketTest._ts_off_started = True + + @staticmethod + def check_regex_count(output_path: str, pattern: str, expected_count: int, description: str) -> tuple[bool, str, str]: + """ + Count regex matches in a process output file. + + :param output_path: Path to the output file to inspect. + :param pattern: Regex pattern to count. + :param expected_count: Expected number of matches. + :param description: Description reported by the tester. + :return: AuTest lambda result tuple. + """ + with open(output_path, 'r') as f: + content = f.read() + + matches = re.findall(pattern, content) + if len(matches) == expected_count: + return (True, description, f'Found {len(matches)} matches for {pattern}') + return (False, description, f'Expected {expected_count} matches for {pattern}, found {len(matches)}') + + @staticmethod + def session_reuse_command(port: int, servername: str) -> str: + """ + Build a TLSv1.2 resumption command for a specific SNI name. + + :param port: ATS TLS listening port. + :param servername: SNI hostname to send with the connection. + :return: Shell command for repeated TLSv1.2 session reuse attempts. + """ + return ( + f'session_path=`mktemp` && ' + f'printf "GET / HTTP/1.1\\r\\nHost: {servername}\\r\\n\\r\\n" | ' + f'openssl s_client -connect 127.0.0.1:{port} -servername {servername} -sess_out "$$session_path" -tls1_2 && ' + f'printf "GET / HTTP/1.1\\r\\nHost: {servername}\\r\\n\\r\\n" | ' + f'openssl s_client -connect 127.0.0.1:{port} -servername {servername} -sess_in "$$session_path" -tls1_2 && ' + f'printf "GET / HTTP/1.1\\r\\nHost: {servername}\\r\\n\\r\\n" | ' + f'openssl s_client -connect 127.0.0.1:{port} -servername {servername} -sess_in "$$session_path" -tls1_2 && ' + f'printf "GET / HTTP/1.1\\r\\nHost: {servername}\\r\\n\\r\\n" | ' + f'openssl s_client -connect 127.0.0.1:{port} -servername {servername} -sess_in "$$session_path" -tls1_2 && ' + f'printf "GET / HTTP/1.1\\r\\nHost: {servername}\\r\\n\\r\\n" | ' + f'openssl s_client -connect 127.0.0.1:{port} -servername {servername} -sess_in "$$session_path" -tls1_2 && ' + f'printf "GET / HTTP/1.1\\r\\nHost: {servername}\\r\\n\\r\\n" | ' + f'openssl s_client -connect 127.0.0.1:{port} -servername {servername} -sess_in "$$session_path" -tls1_2') + + def add_tls12_enabled_run(self) -> None: + """ + Register the TLSv1.2 resumption test for the enabled SNI case. + """ + tr = urtest.AddTestRun('sni.yaml enables TLSv1.2 ticket resumption') + tr.Command = TlsSniTicketTest.session_reuse_command(self.ts_on.Variables.ssl_port, 'tickets-on.com') + tr.ReturnCode = 0 + self.start_processes_if_needed(tr, start_server=True, start_ts_on=True) + tr.Processes.Default.Streams.All.Content = Testers.Lambda( + lambda info, tester: TlsSniTicketTest.check_regex_count( + tr.Processes.Default.Streams.All.AbsPath, r'Reused, TLSv1\.2', 5, + 'Check that tickets-on.com reuses TLSv1.2 sessions')) + tr.StillRunningAfter += self.server + tr.StillRunningAfter += self.ts_on + + def add_tls13_enabled_run(self) -> None: + """ + Register the TLSv1.3 ticket count test for the enabled SNI case. + """ + tr = urtest.AddTestRun('sni.yaml sets TLSv1.3 ticket count') + tr.Command = ( + f'printf "GET / HTTP/1.1\\r\\nHost: tickets-on.com\\r\\nConnection: close\\r\\n\\r\\n" | ' + f'openssl s_client -connect 127.0.0.1:{self.ts_on.Variables.ssl_port} -servername tickets-on.com -tls1_3 -msg -ign_eof' + ) + tr.ReturnCode = 0 + self.start_processes_if_needed(tr, start_server=True, start_ts_on=True) + + # BoringSSL only exposes SSL_CTX_set_num_tickets() (context-level), + # not SSL_set_num_tickets() (per-connection), so the per-SNI + # ssl_ticket_number override cannot change the ticket count. The + # CTX-level value for ts_on is 0, so expect 0 tickets. + if Condition.IsBoringSSL(): + expected_count = 0 + description = 'Check that tickets-on.com receives no TLSv1.3 tickets (BoringSSL ignores per-SNI ticket count)' + else: + expected_count = 3 + description = 'Check that tickets-on.com receives three TLSv1.3 tickets' + + tr.Processes.Default.Streams.All.Content = Testers.Lambda( + lambda info, tester: TlsSniTicketTest.check_regex_count( + tr.Processes.Default.Streams.All.AbsPath, r'NewSessionTicket', expected_count, description)) + tr.StillRunningAfter += self.server + tr.StillRunningAfter += self.ts_on + + def add_tls12_disabled_run(self) -> None: + """ + Register the TLSv1.2 non-resumption test for the disabled SNI case. + """ + tr = urtest.AddTestRun('sni.yaml disables TLSv1.2 ticket resumption') + tr.Command = TlsSniTicketTest.session_reuse_command(self.ts_off.Variables.ssl_port, 'tickets-off.com') + self.start_processes_if_needed(tr, start_server=True, start_ts_off=True) + tr.Processes.Default.Streams.All = Testers.ExcludesExpression( + 'Reused', 'tickets-off.com should not reuse TLSv1.2 sessions') + tr.Processes.Default.Streams.All += Testers.ContainsExpression( + 'TLSv1.2', 'tickets-off.com should still negotiate TLSv1.2') + tr.StillRunningAfter += self.server + tr.StillRunningAfter += self.ts_off + + def add_tls13_disabled_run(self) -> None: + """ + Register the TLSv1.3 no-ticket test for the disabled SNI case. + """ + tr = urtest.AddTestRun('sni.yaml disables TLSv1.3 ticket issuance') + tr.Command = ( + f'printf "GET / HTTP/1.1\\r\\nHost: tickets-off.com\\r\\nConnection: close\\r\\n\\r\\n" | ' + f'openssl s_client -connect 127.0.0.1:{self.ts_off.Variables.ssl_port} -servername tickets-off.com -tls1_3 -msg -ign_eof' + ) + self.start_processes_if_needed(tr, start_server=True, start_ts_off=True) + tr.Processes.Default.Streams.All.Content = Testers.Lambda( + lambda info, tester: TlsSniTicketTest.check_regex_count( + tr.Processes.Default.Streams.All.AbsPath, r'NewSessionTicket', 0, + 'Check that tickets-off.com receives no TLSv1.3 tickets')) + tr.StillRunningAfter += self.server + tr.StillRunningAfter += self.ts_off + + def run(self) -> None: + """ + Register all AuTest runs for the SNI ticket override coverage. + """ + self.add_tls12_enabled_run() + self.add_tls13_enabled_run() + self.add_tls12_disabled_run() + self.add_tls13_disabled_run() + + TlsSniTicketTest().run() + urtest.execute() diff --git a/tests/uranium_tests/tls/test_tls_sni_with_port.py b/tests/uranium_tests/tls/test_tls_sni_with_port.py new file mode 100644 index 00000000000..20a1d1654a6 --- /dev/null +++ b/tests/uranium_tests/tls/test_tls_sni_with_port.py @@ -0,0 +1,229 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_tls_sni_with_port(urtest: UraniumTest) -> None: + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import functools + from typing import Any, Callable, Dict, Optional + + from ports import get_port + + urtest.Summary = 'Tests SNI port-based routing' + + TestParams = Dict[str, Any] + + class TestSNIWithPort: + """Configure a test for SNI port-based routing .""" + + replay_filepath: str = "tls_sni_with_port.replay.yaml" + client_counter: int = 0 + server_counter: int = 0 + ts_counter: int = 0 + + def __init__(self, name: str, /, autorun) -> None: + """Initialize the test. + + :param name: The name of the test. + """ + self.name = name + self.autorun = autorun + + def _init_run(self) -> "TestRun": + """Initialize processes for the test run.""" + + tr = urtest.AddTestRun(self.name) + server_one = TestSNIWithPort.configure_server(tr, "yay.com") + server_two = TestSNIWithPort.configure_server(tr, "oof.com") + server_three = TestSNIWithPort.configure_server(tr, "wow.com") + self._configure_traffic_server(tr, server_one, server_two, server_three) + + tr.Processes.Default.StartBefore(server_one) + tr.Processes.Default.StartBefore(server_two) + tr.Processes.Default.StartBefore(self._ts) + + return { + "tr": tr, + "ts": self._ts, + "server_one": server_one, + "server_two": server_two, + "server_three": server_three, + "port_one": self._port_one, + "port_two": self._port_two, + "port_three": self._port_three, + "port_unmapped": self._port_unmapped + } + + @classmethod + def runner(cls, name: str, autorun: bool = True) -> Optional[Callable]: + """Create a runner for a test case. + + :param autorun: Run the test case once it's set up. Default is True. + """ + test = cls(name, autorun=autorun)._prepare_test_case + return test + + def _prepare_test_case(self, func: Callable) -> Callable: + """Set up a test case and possibly run it. + + :param func: The test case to set up. + """ + functools.wraps(func) + test_params = self._init_run() + + def wrapper(*args, **kwargs) -> Any: + return func(test_params, *args, **kwargs) + + if self.autorun: + wrapper() + return wrapper + + @staticmethod + def configure_server(tr: "TestRun", domain: str): + server = tr.AddVerifierServerProcess( + f"server{TestSNIWithPort.server_counter}.{domain}", TestSNIWithPort.replay_filepath) + TestSNIWithPort.server_counter += 1 + + return server + + def _configure_traffic_server(self, tr: "TestRun", server_one: "Process", server_two: "Process", server_three: "Process"): + """Configure Traffic Server. + + :param tr: The TestRun object to associate the ts process with. + """ + ts = tr.MakeATSProcess(f"ts-{TestSNIWithPort.ts_counter}", select_ports=False, enable_tls=True) + TestSNIWithPort.ts_counter += 1 + + ts.addDefaultSSLFiles() + self._port_one = get_port(ts, "PortOne") + self._port_two = get_port(ts, "PortTwo") + self._port_three = get_port(ts, "PortThree") + self._port_unmapped = get_port(ts, "UnspecifiedPort") + ts.Disk.records_config.update( + { + 'proxy.config.ssl.server.cert.path': f"{ts.Variables.SSLDir}", + 'proxy.config.ssl.server.private_key.path': f"{ts.Variables.SSLDir}", + 'proxy.config.http.server_ports': + f"{self._port_one}:ssl {self._port_two}:ssl {self._port_three}:ssl {self._port_unmapped}:ssl", + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'dns|http|ssl|sni', + }) + + ts.Disk.remap_config.AddLine(f"map / http://127.0.0.1:{server_three.Variables.http_port}") + ts.addPrivateConnectAllowYaml(methods='[ CONNECT, GET ]') + + ts.Disk.sni_yaml.AddLines( + [ + "sni:", + "- fqdn: yay.example.com", + f" inbound_port_ranges: {self._port_one}-{self._port_one}", + f" tunnel_route: localhost:{server_one.Variables.https_port}", + "- fqdn: yay.example.com", + " inbound_port_ranges:", + f" - {self._port_two}", + f" - {self._port_three}", + f" tunnel_route: localhost:{server_two.Variables.https_port}", + ]) + + ts.Disk.ssl_multicert_yaml.AddLines( + """ + ssl_multicert: + - dest_ip: "*" + ssl_cert_name: server.pem + ssl_key_name: server.key + """.split("\n")) + + self._ts = ts + + # Tests start. + + @TestSNIWithPort.runner("Test that a request to a port not in the SNI does not get through.") + def test0(params: TestParams) -> None: + client = params["tr"].AddVerifierClientProcess( + f"client0", TestSNIWithPort.replay_filepath, https_ports=[params["port_unmapped"]], keys="conn_remapped") + + params["tr"].Processes.Default.ReturnCode = 0 + params["ts"].Disk.diags_log.Content += Testers.ExcludesExpression( + "unsupported key 'inbound_port_range'", "we should not warn about the key") + params["ts"].Disk.traffic_out.Content += Testers.IncludesExpression( + "not available in the map", "the request should not match an SNI") + params["server_one"].Streams.All.Content += Testers.ExcludesExpression( + r"Received (\(with headers\) )?an HTTP/1 (Content-Length )?body of 16 bytes for key conn_remapped", + "the request should not go to server one") + params["server_two"].Streams.All.Content += Testers.ExcludesExpression( + r"Received (\(with headers\) )?an HTTP/1 (Content-Length )?body of 16 bytes for key conn_remapped", + "the request should not go to server two") + params["server_three"].Streams.All.Content += Testers.IncludesExpression( + r"Received (\(with headers\) )?an HTTP/1 (Content-Length )?body of 16 bytes for key conn_remapped", + "request was remaped to server three") + + @TestSNIWithPort.runner("Test that a request to a port one goes to server one.") + def test1(params: TestParams) -> None: + client = params["tr"].AddVerifierClientProcess( + f"client1", TestSNIWithPort.replay_filepath, https_ports=[params["port_one"]], keys="conn_accepted") + + params["tr"].Processes.Default.ReturnCode = 0 + params["server_one"].Streams.All.Content += Testers.IncludesExpression( + r"Received (\(with headers\) )?an HTTP/1 (Content-Length )?body of 16 bytes for key conn_accepted", + "the request should go to server one") + params["server_two"].Streams.All.Content += Testers.ExcludesExpression( + r"Received (\(with headers\) )?an HTTP/1 (Content-Length )?body of 16 bytes for key conn_accepted", + "the request should not go to server two") + + @TestSNIWithPort.runner("Test that a request to port two goes to server two.") + def test2(params: TestParams) -> None: + client = params["tr"].AddVerifierClientProcess( + f"client2", TestSNIWithPort.replay_filepath, https_ports=[params["port_two"]], keys="conn_accepted") + + params["tr"].Processes.Default.ReturnCode = 0 + params["server_two"].Streams.All.Content += Testers.IncludesExpression( + r"Received (\(with headers\) )?an HTTP/1 (Content-Length )?body of 16 bytes for key conn_accepted", + "the request should go to server two") + params["server_one"].Streams.All.Content += Testers.ExcludesExpression( + r"Received (\(with headers\) )?an HTTP/1 (Content-Length )?body of 16 bytes for key conn_accepted", + "the request should not go to server one") + + @TestSNIWithPort.runner("Test that a request to port three goes to server two.") + def test3(params: TestParams) -> None: + client = params["tr"].AddVerifierClientProcess( + f"client3", TestSNIWithPort.replay_filepath, https_ports=[params["port_three"]], keys="conn_accepted") + + params["tr"].Processes.Default.ReturnCode = 0 + params["server_two"].Streams.All.Content += Testers.IncludesExpression( + r"Received (\(with headers\) )?an HTTP/1 (Content-Length )?body of 16 bytes for key conn_accepted", + "the request should go to server two") + params["server_one"].Streams.All.Content += Testers.ExcludesExpression( + r"Received (\(with headers\) )?an HTTP/1 (Content-Length )?body of 16 bytes for key conn_accepted", + "the request should not go to server one") + + urtest.execute() diff --git a/tests/uranium_tests/tls/test_tls_sni_yaml_reload.py b/tests/uranium_tests/tls/test_tls_sni_yaml_reload.py new file mode 100644 index 00000000000..7ab938a11ed --- /dev/null +++ b/tests/uranium_tests/tls/test_tls_sni_yaml_reload.py @@ -0,0 +1,144 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_tls_sni_yaml_reload(urtest: UraniumTest) -> None: + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + urtest.Summary = ''' + Test reloading sni.yaml behaves as expected + ''' + + sni_domain = 'example.com' + + ts = urtest.MakeATSProcess("ts", enable_tls=True, disable_log_checks=True) + server = urtest.MakeOriginServer("server") + server2 = urtest.MakeOriginServer("server3") + request_header = {"headers": f"GET / HTTP/1.1\r\nHost: {sni_domain}\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + + response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + server.addResponse("sessionlog.json", request_header, response_header) + + ts.Disk.records_config.update( + { + 'proxy.config.ssl.server.cert.path': ts.Variables.SSLDir, + 'proxy.config.ssl.server.private_key.path': ts.Variables.SSLDir, + 'proxy.config.ssl.CA.cert.filename': f'{ts.Variables.SSLDir}/signer.pem', + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'ssl|http', + 'proxy.config.diags.output.debug': 'L', + }) + + ts.addDefaultSSLFiles() + ts.addSSLfile("ssl/signed-foo.pem") + ts.addSSLfile("ssl/signed-foo.key") + ts.addSSLfile("ssl/signer.pem") + + ts.Disk.remap_config.AddLine(f'map / http://127.0.0.1:{server.Variables.Port}') + + ts.Disk.ssl_multicert_yaml.AddLines( + """ + ssl_multicert: + - dest_ip: "*" + ssl_cert_name: server.pem + ssl_key_name: server.key + """.split("\n")) + + ts.Disk.ssl_multicert_yaml.AddLines( + """ + ssl_multicert: + - dest_ip: "*" + ssl_cert_name: server.pem + ssl_key_name: server.key + """.split("\n")) + + ts.Disk.sni_yaml.AddLines( + f""" + sni: + - fqdn: {sni_domain} + http2: off + client_cert: {ts.Variables.SSLDir}/signed-foo.pem + client_key: {ts.Variables.SSLDir}/signed-foo.key + verify_client: STRICT + """.split('\n')) + + tr = urtest.AddTestRun(f'ensure we can connect for SNI {sni_domain}') + tr.Setup.Copy("ssl/signed-foo.pem") + tr.Setup.Copy("ssl/signed-foo.key") + tr.Processes.Default.StartBefore(urtest.Processes.ts) + tr.Processes.Default.StartBefore(server) + tr.StillRunningAfter = ts + tr.StillRunningAfter = server + tr.MakeCurlCommand( + f"-q --tls-max 1.2 -s -v -k --cert ./signed-foo.pem --key ./signed-foo.key --resolve '{sni_domain}:{ts.Variables.ssl_port}:127.0.0.1' https://{sni_domain}:{ts.Variables.ssl_port}", + ts=ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.stdout = Testers.ExcludesExpression("Could Not Connect", "Verify curl could successfully connect") + tr.Processes.Default.Streams.stderr = Testers.IncludesExpression(f"CN={sni_domain}", f"Verify curl used the {sni_domain} SNI") + + # This config reload should fail because it references non-existent TLS key files + trupd = urtest.AddTestRun("Update config file") + # Update the configs - this will overwrite the sni.yaml file + sniyamlpath = ts.Disk.sni_yaml.AbsPath + trupd.Disk.File(sniyamlpath, id="sni_yaml", typename="ats:config") + trupd.Disk.sni_yaml.AddLines( + f""" + sni: + - fqdn: {sni_domain} + http2: on + client_cert: {ts.Variables.SSLDir}/signed-notexist.pem + client_key: {ts.Variables.SSLDir}/signed-notexist.key + verify_client: STRICT + """.split('\n')) + + trupd.StillRunningAfter = ts + trupd.StillRunningAfter = server + trupd.Processes.Default.Command = 'echo Updated configs' + trupd.Processes.Default.Env = ts.Env + trupd.Processes.Default.ReturnCode = 0 + + tr2reload = urtest.AddConfigReload(ts, expect="fail", expect_tasks=["sni.yaml"], description="Reload config") + tr2reload.StillRunningAfter = server + + tr3 = urtest.AddTestRun(f"Make request again for {sni_domain} that should still work") + tr3.Setup.Copy("ssl/signed-bar.pem") + tr3.Setup.Copy("ssl/signed-bar.key") + tr3.Processes.Default.StartBefore(server2) + tr3.StillRunningAfter = ts + tr3.StillRunningAfter = server + tr3.MakeCurlCommand( + f"-q --tls-max 1.2 -s -v -k --cert ./signed-bar.pem --key ./signed-bar.key --resolve '{sni_domain}:{ts.Variables.ssl_port}:127.0.0.1' https://{sni_domain}:{ts.Variables.ssl_port}", + ts=ts) + tr3.Processes.Default.ReturnCode = 0 + # since the 2nd config with http2 turned on should have failed and used the prior config, verify http2 was not used + tr3.Processes.Default.Streams.stderr = Testers.ExcludesExpression("GET / HTTP/2", "Confirm that HTTP2 is still not used") + urtest.execute() diff --git a/tests/uranium_tests/tls/test_tls_ticket.py b/tests/uranium_tests/tls/test_tls_ticket.py new file mode 100644 index 00000000000..c5c77379382 --- /dev/null +++ b/tests/uranium_tests/tls/test_tls_ticket.py @@ -0,0 +1,140 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_tls_ticket(urtest: UraniumTest) -> None: + ''' + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import re + + urtest.Summary = ''' + Test tls tickets + ''' + + # Define default ATS + ts = urtest.MakeATSProcess("ts", enable_tls=True) + ts2 = urtest.MakeATSProcess("ts2", enable_tls=True) + server = urtest.MakeOriginServer("server") + + # Add info the origin server responses + request_header = {"headers": "GET / HTTP/1.1\r\nHost: www.example.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + server.addResponse("sessionlog.json", request_header, response_header) + + # add ssl materials like key, certificates for the server + ts.addSSLfile("ssl/server.pem") + ts.addSSLfile("ssl/server.key") + ts2.addSSLfile("ssl/server.pem") + ts2.addSSLfile("ssl/server.key") + + ts.Disk.remap_config.AddLine('map / http://127.0.0.1:{0}'.format(server.Variables.Port)) + ts2.Disk.remap_config.AddLine('map / http://127.0.0.1:{0}'.format(server.Variables.Port)) + + ts.Disk.ssl_multicert_yaml.AddLines( + """ + ssl_multicert: + - dest_ip: "*" + ssl_cert_name: server.pem + ssl_key_name: server.key + """.split("\n")) + ts2.Disk.ssl_multicert_yaml.AddLines( + """ + ssl_multicert: + - dest_ip: "*" + ssl_cert_name: server.pem + ssl_key_name: server.key + """.split("\n")) + + ts.Disk.records_config.update( + { + 'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir), + 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts.Variables.SSLDir), + 'proxy.config.exec_thread.autoconfig.scale': 1.0, + 'proxy.config.ssl.server.session_ticket.enable': '1', + 'proxy.config.ssl.server.ticket_key.filename': '../../file.ticket' + }) + ts2.Disk.records_config.update( + { + 'proxy.config.ssl.server.cert.path': '{0}'.format(ts2.Variables.SSLDir), + 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts2.Variables.SSLDir), + 'proxy.config.ssl.server.session_ticket.enable': '1', + 'proxy.config.exec_thread.autoconfig.scale': 1.0, + 'proxy.config.ssl.server.ticket_key.filename': '../../file.ticket' + }) + + tr = urtest.AddTestRun("Create ticket") + tr.Setup.Copy('file.ticket') + tr.Command = 'printf "GET / HTTP/1.0\r\n" | openssl s_client -tls1_2 -connect 127.0.0.1:{0} -sess_out ticket.out'.format( + ts.Variables.ssl_port) + tr.ReturnCode = 0 + tr.Processes.Default.StartBefore(server) + tr.Processes.Default.StartBefore(urtest.Processes.ts) + path1 = tr.Processes.Default.Streams.stdout.AbsPath + tr.StillRunningAfter = server + + # Pull out session created in tr to test for session id in tr2 + + def checkSession(ev): + retval = False + f1 = open(path1, 'r') + f2 = open(path2, 'r') + err = "Session ids match" + if not f1 or not f2: + err = "Failed to open {0} or {1}".format(path1, path2) + return (retval, "Check that session ids match", err) + + f1Content = f1.read() + f2Content = f2.read() + match1 = re.findall('Session-ID: ([0-9A-F]+)', f1Content) + match2 = re.findall('Session-ID: ([0-9A-F]+)', f2Content) + + if match1 and match2: + if match1[0] == match2[0]: + err = "{0} and {1} do match".format(match1[0], match2[0]) + retval = True + else: + err = "{0} and {1} do not match".format(match1[0], match2[0]) + else: + err = "Didn't find session id" + return (retval, "Check that session ids match", err) + + tr2 = urtest.AddTestRun("Test ticket") + tr2.Setup.Copy('file.ticket') + tr2.Command = 'printf "GET / HTTP/1.0\r\n" | openssl s_client -tls1_2 -connect 127.0.0.1:{0} -sess_in ticket.out'.format( + ts2.Variables.ssl_port) + tr2.Processes.Default.StartBefore(urtest.Processes.ts2) + tr2.ReturnCode = 0 + path2 = tr2.Processes.Default.Streams.stdout.AbsPath + tr2.Processes.Default.Streams.All.Content = Testers.Lambda(checkSession) + urtest.execute() diff --git a/tests/uranium_tests/tls/test_tls_tunnel.py b/tests/uranium_tests/tls/test_tls_tunnel.py new file mode 100644 index 00000000000..64386b402d6 --- /dev/null +++ b/tests/uranium_tests/tls/test_tls_tunnel.py @@ -0,0 +1,435 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_tls_tunnel(urtest: UraniumTest) -> None: + ''' + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + from ports import get_port + import sys + + urtest.Summary = ''' + Test tunneling based on SNI + ''' + + # Define default ATS + ts = urtest.MakeATSProcess("ts", enable_tls=True, enable_proxy_protocol=True) + server_foo = urtest.MakeOriginServer("server_foo", ssl=True) + server_bar = urtest.MakeOriginServer("server_bar", ssl=True) + server2 = urtest.MakeOriginServer("server2") + # The following server will listen on a port that is not in the connect_ports + # list. + server_forbidden = urtest.MakeOriginServer("forbidden", ssl=True) + dns = urtest.MakeDNServer("dns") + + request_foo_header = {"headers": "GET / HTTP/1.1\r\nHost: foo.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + request_bar_header = {"headers": "GET / HTTP/1.1\r\nHost: bar.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + request_pp_header = { + "headers": "GET /proxy_protocol HTTP/1.1\r\nHost: proxy.protocol.port.com\r\n\r\n", + "timestamp": "1469733493.993", + "body": "" + } + response_foo_header = { + "headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", + "timestamp": "1469733493.993", + "body": "foo ok" + } + response_bar_header = { + "headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", + "timestamp": "1469733493.993", + "body": "bar ok" + } + response_pp_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": "pp ok"} + server_foo.addResponse("sessionlog.json", request_foo_header, response_foo_header) + server_foo.addResponse("sessionlog.json", request_pp_header, response_pp_header) + server_bar.addResponse("sessionlog.json", request_bar_header, response_bar_header) + server_forbidden.addResponse("sessionlog.json", request_pp_header, response_pp_header) + + # Server to interact with a split CLIENT_HELLO in a blind tunnel. + split_client_hello_server = urtest.Processes.Process('receive_split_client_hello_server') + server_port = get_port(split_client_hello_server, 'tcp_port') + server_command = f'{sys.executable} receive_split_client_hello.py 127.0.0.1 {server_port}' + split_client_hello_server.Command = server_command + + # add ssl materials like key, certificates for the server + ts.addSSLfile("ssl/signed-foo.pem") + ts.addSSLfile("ssl/signed-foo.key") + ts.addSSLfile("ssl/signed-bar.pem") + ts.addSSLfile("ssl/signed-bar.key") + ts.addSSLfile("ssl/server.pem") + ts.addSSLfile("ssl/server.key") + ts.addSSLfile("ssl/signer.pem") + ts.addSSLfile("ssl/signer.key") + + dns.addRecords(records={"localhost": ["127.0.0.1"]}) + dns.addRecords(records={"one.testmatch": ["127.0.0.1"]}) + dns.addRecords(records={"two.example.one": ["127.0.0.1"]}) + dns.addRecords(records={"backend.incoming.port.com": ["127.0.0.1"]}) + dns.addRecords(records={"backend.proxy.protocol.port.com": ["127.0.0.1"]}) + dns.addRecords(records={"backend.wildcard.with.incoming.port.com": ["127.0.0.1"]}) + dns.addRecords(records={"backend.wildcard.with.proxy.protocol.port.com": ["127.0.0.1"]}) + # Need no remap rules. Everything should be processed by sni + + # Make sure the TS server certs are different from the origin certs + ts.Disk.ssl_multicert_yaml.AddLines( + """ + ssl_multicert: + - dest_ip: "*" + ssl_cert_name: signed-foo.pem + ssl_key_name: signed-foo.key + """.split("\n")) + + # Case 1, global config policy=permissive properties=signature + # override for foo.com policy=enforced properties=all + ts.Disk.records_config.update( + { + 'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir), + 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts.Variables.SSLDir), + 'proxy.config.http.connect_ports': + '{0} {1} {2}'.format(ts.Variables.ssl_port, server_foo.Variables.SSL_Port, server_bar.Variables.SSL_Port), + 'proxy.config.ssl.client.CA.cert.path': '{0}'.format(ts.Variables.SSLDir), + 'proxy.config.ssl.client.CA.cert.filename': 'signer.pem', + 'proxy.config.exec_thread.autoconfig.scale': 1.0, + 'proxy.config.url_remap.pristine_host_hdr': 1, + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'http|ssl|proxyprotocol', + 'proxy.config.dns.nameservers': f'127.0.0.1:{dns.Variables.Port}', + 'proxy.config.dns.resolv_conf': 'NULL' + }) + ts.addPrivateConnectAllowYaml() + + # foo.com should not terminate. Just tunnel to server_foo + # bar.com should terminate. Forward its tcp stream to server_bar + # empty SNI should tunnel to server_bar + ts.Disk.sni_yaml.AddLines( + [ + 'sni:', + '- fqdn: foo.com', + f" tunnel_route: localhost:{server_foo.Variables.SSL_Port}", + '- fqdn: slashdot.org', + f" tunnel_route: 127.0.0.1:{split_client_hello_server.Variables.tcp_port}", + "- fqdn: '*.bar.com'", + f" tunnel_route: localhost:{server_foo.Variables.SSL_Port}", + "- fqdn: '*.match.com'", + f" tunnel_route: $1.testmatch:{server_foo.Variables.SSL_Port}", + "- fqdn: '*.ok.two.com'", + f" tunnel_route: two.example.$1:{server_foo.Variables.SSL_Port}", + "- fqdn: ''", # No SNI sent + f" tunnel_route: localhost:{server_bar.Variables.SSL_Port}", + "- fqdn: 'incoming.port.com'", + " tunnel_route: backend.incoming.port.com:{inbound_local_port}", + "- fqdn: 'proxy.protocol.port.com'", + " tunnel_route: backend.proxy.protocol.port.com:{proxy_protocol_port}", + "- fqdn: '*.backend.incoming.port.com'", + " tunnel_route: backend.$1.incoming.port.com:{inbound_local_port}", + "- fqdn: '*.with.incoming.port.com'", + " tunnel_route: backend.$1.with.incoming.port.com:{inbound_local_port}", + "- fqdn: '*.with.proxy.protocol.port.com'", + " tunnel_route: backend.$1.with.proxy.protocol.port.com:{proxy_protocol_port}", + ]) + + tr = urtest.AddTestRun("foo.com Tunnel-test") + tr.TimeOut = 5 + tr.MakeCurlCommand("-v --resolve 'foo.com:{0}:127.0.0.1' -k https://foo.com:{0}".format(ts.Variables.ssl_port), ts=ts) + tr.ReturnCode = 0 + tr.Processes.Default.StartBefore(server_foo) + tr.Processes.Default.StartBefore(server_bar) + tr.Processes.Default.StartBefore(dns) + tr.Processes.Default.StartBefore(urtest.Processes.ts) + tr.StillRunningAfter = ts + tr.StillRunningAfter = server_foo + tr.StillRunningAfter = server_bar + tr.StillRunningAfter = dns + tr.Processes.Default.Streams.All += Testers.ExcludesExpression("Could Not Connect", "Curl attempt should have succeeded") + tr.Processes.Default.Streams.All += Testers.ExcludesExpression( + "Not Found on Accelerato", "Should not try to remap on Traffic Server") + tr.Processes.Default.Streams.All += Testers.ExcludesExpression("CN=foo.com", "Should not TLS terminate on Traffic Server") + tr.Processes.Default.Streams.All += Testers.ContainsExpression("HTTP/1.1 200 OK", "Should get a successful response") + tr.Processes.Default.Streams.All += Testers.ExcludesExpression("ATS", "Do not terminate on Traffic Server") + tr.Processes.Default.Streams.All += Testers.ContainsExpression("foo ok", "Should get a response from bar") + + tr = urtest.AddTestRun("bob.bar.com Tunnel-test") + tr.TimeOut = 5 + tr.MakeCurlCommand("-v --resolve 'bob.bar.com:{0}:127.0.0.1' -k https://bob.bar.com:{0}".format(ts.Variables.ssl_port), ts=ts) + tr.ReturnCode = 0 + tr.StillRunningAfter = ts + tr.Processes.Default.Streams.All += Testers.ExcludesExpression("Could Not Connect", "Curl attempt should have succeeded") + tr.Processes.Default.Streams.All += Testers.ExcludesExpression( + "Not Found on Accelerato", "Should not try to remap on Traffic Server") + tr.Processes.Default.Streams.All += Testers.ExcludesExpression("CN=foo.com", "Should not TLS terminate on Traffic Server") + tr.Processes.Default.Streams.All += Testers.ContainsExpression("HTTP/1.1 200 OK", "Should get a successful response") + tr.Processes.Default.Streams.All += Testers.ExcludesExpression("ATS", "Do not terminate on Traffic Server") + tr.Processes.Default.Streams.All += Testers.ContainsExpression("foo ok", "Should get a response from bar") + + tr = urtest.AddTestRun("bar.com no Tunnel-test") + tr.TimeOut = 5 + tr.MakeCurlCommand("-v --resolve 'bar.com:{0}:127.0.0.1' -k https://bar.com:{0}".format(ts.Variables.ssl_port), ts=ts) + tr.ReturnCode = 0 + tr.StillRunningAfter = ts + tr.Processes.Default.Streams.All += Testers.ExcludesExpression("Could Not Connect", "Curl attempt should have succeeded") + tr.Processes.Default.Streams.All += Testers.ContainsExpression("Not Found on Accelerato", "Terminates on on Traffic Server") + tr.Processes.Default.Streams.All += Testers.ContainsExpression("ATS", "Terminate on Traffic Server") + + tr = urtest.AddTestRun("no SNI Tunnel-test") + tr.TimeOut = 5 + tr.MakeCurlCommand("-v -k https://127.0.0.1:{0}".format(ts.Variables.ssl_port), ts=ts) + tr.ReturnCode = 0 + tr.StillRunningAfter = ts + tr.Processes.Default.Streams.All += Testers.ExcludesExpression("Could Not Connect", "Curl attempt should have succeeded") + tr.Processes.Default.Streams.All += Testers.ExcludesExpression( + "Not Found on Accelerato", "Should not try to remap on Traffic Server") + tr.Processes.Default.Streams.All += Testers.ContainsExpression("HTTP/1.1 200 OK", "Should get a successful response") + tr.Processes.Default.Streams.All += Testers.ExcludesExpression("ATS", "Do not terminate on Traffic Server") + tr.Processes.Default.Streams.All += Testers.ContainsExpression("bar ok", "Should get a response from bar") + + tr = urtest.AddTestRun("one.match.com Tunnel-test") + tr.TimeOut = 5 + tr.MakeCurlCommand( + "-vvv --resolve 'one.match.com:{0}:127.0.0.1' -k https://one.match.com:{0}".format(ts.Variables.ssl_port), ts=ts) + tr.ReturnCode = 0 + tr.StillRunningAfter = ts + tr.Processes.Default.Streams.All += Testers.ExcludesExpression("Could Not Connect", "Curl attempt should have succeeded") + tr.Processes.Default.Streams.All += Testers.ExcludesExpression( + "Not Found on Accelerato", "Should not try to remap on Traffic Server") + tr.Processes.Default.Streams.All += Testers.ExcludesExpression("CN=foo.com", "Should not TLS terminate on Traffic Server") + tr.Processes.Default.Streams.All += Testers.ContainsExpression("HTTP/1.1 200 OK", "Should get a successful response") + tr.Processes.Default.Streams.All += Testers.ExcludesExpression("ATS", "Do not terminate on Traffic Server") + tr.Processes.Default.Streams.All += Testers.ContainsExpression("foo ok", "Should get a response from tm") + + tr = urtest.AddTestRun("one.ok.two.com Tunnel-test") + tr.TimeOut = 5 + tr.MakeCurlCommand( + "-vvv --resolve 'one.ok.two.com:{0}:127.0.0.1' -k https://one.ok.two.com:{0}".format(ts.Variables.ssl_port), ts=ts) + tr.ReturnCode = 0 + tr.StillRunningAfter = ts + tr.Processes.Default.Streams.All += Testers.ExcludesExpression("Could Not Connect", "Curl attempt should have succeeded") + tr.Processes.Default.Streams.All += Testers.ExcludesExpression( + "Not Found on Accelerato", "Should not try to remap on Traffic Server") + tr.Processes.Default.Streams.All += Testers.ExcludesExpression("CN=foo.com", "Should not TLS terminate on Traffic Server") + tr.Processes.Default.Streams.All += Testers.ContainsExpression("HTTP/1.1 200 OK", "Should get a successful response") + tr.Processes.Default.Streams.All += Testers.ExcludesExpression("ATS", "Do not terminate on Traffic Server") + tr.Processes.Default.Streams.All += Testers.ContainsExpression("foo ok", "Should get a response from tm") + + tr = urtest.AddTestRun("test {inbound_local_port}") + tr.TimeOut = 5 + tr.MakeCurlCommand( + "-vvv --resolve 'incoming.port.com:{0}:127.0.0.1' -k https://incoming.port.com:{0}".format(ts.Variables.ssl_port), ts=ts) + # The tunnel connecting to the outgoing port which is the same as the incoming + # port (per the `inbound_local_port` configuration) will result in ATS + # connecting back to itself. This will result in a connection close and a + # non-zero return code from curl. In production, the server will listen on the + # same port as ATS but have a different IP. + tr.ReturnCode = 35 + tr.StillRunningAfter = ts + ts.Disk.traffic_out.Content += Testers.ContainsExpression( + f"CONNECT tunnel://backend.incoming.port.com:{ts.Variables.ssl_port} HTTP/1.1", "Verify a CONNECT request is handled") + ts.Disk.traffic_out.Content += Testers.ContainsExpression("HTTP/1.1 400 Cycle Detected", "The loop should be detected") + + tr = urtest.AddTestRun("test {proxy_protocol_port}") + tr.TimeOut = 5 + tr.Setup.Copy('proxy_protocol_client.py') + tr.Processes.Default.Command = ( + f'{sys.executable} proxy_protocol_client.py ' + f'127.0.0.1 {ts.Variables.proxy_protocol_ssl_port} proxy.protocol.port.com ' + f'127.0.0.1 127.0.0.1 60123 {server_foo.Variables.SSL_Port} ' + f'2 --https') + tr.ReturnCode = 0 + tr.StillRunningAfter = ts + tr.Processes.Default.Streams.All += Testers.ContainsExpression("HTTP/1.1 200 OK", "Verify a successful response is received") + + # This test run causes an exception in proxy_protocol_client.py + # + tr = urtest.AddTestRun("test proxy_protocol_port - not in connect_ports") + tr.TimeOut = 5 + tr.Processes.Default.StartBefore(server_forbidden) + tr.Setup.Copy('proxy_protocol_client.py') + # Note that server_forbidden is listing on a port that is not in the + # connect_ports list. Therefore the tunnel should be rejected. + rejected_port = server_forbidden.Variables.SSL_Port + tr.Processes.Default.Command = ( + f'{sys.executable} proxy_protocol_client.py ' + f'127.0.0.1 {ts.Variables.proxy_protocol_ssl_port} proxy.protocol.port.com ' + f'127.0.0.1 127.0.0.1 60123 {rejected_port} ' + f'2 --https') + tr.ReturnCode = 1 + tr.StillRunningAfter = ts + tr.Processes.Default.Streams.All += Testers.ContainsExpression("ssl.SSL.*Error:.*EOF", "Verify a the handshake failed") + ts.Disk.traffic_out.Content += Testers.ContainsExpression( + f"Rejected a tunnel to port {rejected_port} not in connect_ports", "Verify the tunnel was rejected") + + tr = urtest.AddTestRun("test wildcard with inbound_local_port") + tr.TimeOut = 5 + tr.MakeCurlCommand( + "-vvv --resolve 'wildcard.with.incoming.port.com:{0}:127.0.0.1' -k https://wildcard.with.incoming.port.com:{0}".format( + ts.Variables.ssl_port), + ts=ts) + + # See the inbound_local_port test above for the explanation of the return code. + tr.ReturnCode = 35 + tr.StillRunningAfter = ts + ts.Disk.traffic_out.Content += Testers.ContainsExpression( + rf"Destination now is \[backend.wildcard.with.incoming.port.com:{ts.Variables.ssl_port}\]", + "Verify the tunnel destination is expanded correctly.") + ts.Disk.traffic_out.Content += Testers.ContainsExpression( + f"CONNECT tunnel://backend.wildcard.with.incoming.port.com:{ts.Variables.ssl_port} HTTP/1.1", + "Verify a CONNECT request is handled") + ts.Disk.traffic_out.Content += Testers.ContainsExpression("HTTP/1.1 400 Cycle Detected", "The loop should be detected") + + tr = urtest.AddTestRun("test wildcard with proxy_protocol_port") + tr.TimeOut = 5 + tr.Setup.Copy('proxy_protocol_client.py') + tr.Processes.Default.Command = ( + f'{sys.executable} proxy_protocol_client.py ' + f'127.0.0.1 {ts.Variables.proxy_protocol_ssl_port} wildcard.with.proxy.protocol.port.com ' + f'127.0.0.1 127.0.0.1 60123 {server_foo.Variables.SSL_Port} ' + f'2 --https') + tr.ReturnCode = 0 + tr.StillRunningAfter = ts + ts.Disk.traffic_out.Content += Testers.ContainsExpression( + rf"Destination now is \[backend.wildcard.with.proxy.protocol.port.com:{server_foo.Variables.SSL_Port}\]", + "Verify the tunnel destination is expanded correctly.") + tr.Processes.Default.Streams.All += Testers.ContainsExpression("HTTP/1.1 200 OK", "Verify a successful response is received") + + # Regression: a tunnel_route that combines $N match groups with a port variable + # must still enforce connect_ports. An earlier bug let MATCH_GROUPS clear the + # dynamic-port flag set by MAP_WITH_PROXY_PROTOCOL_PORT, bypassing the check. + tr = urtest.AddTestRun("test wildcard with proxy_protocol_port - not in connect_ports") + tr.TimeOut = 5 + tr.Setup.Copy('proxy_protocol_client.py') + wildcard_rejected_port = server_forbidden.Variables.SSL_Port + tr.Processes.Default.Command = ( + f'{sys.executable} proxy_protocol_client.py ' + f'127.0.0.1 {ts.Variables.proxy_protocol_ssl_port} wildcard.with.proxy.protocol.port.com ' + f'127.0.0.1 127.0.0.1 60123 {wildcard_rejected_port} ' + f'2 --https') + tr.ReturnCode = 1 + tr.StillRunningAfter = ts + tr.Processes.Default.Streams.All += Testers.ContainsExpression("ssl.SSL.*Error:.*EOF", "Verify the handshake failed") + ts.Disk.traffic_out.Content += Testers.ContainsExpression( + f"Rejected a tunnel to port {wildcard_rejected_port} not in connect_ports", + "Verify the tunnel was rejected even though the route uses a $N match group") + + # Update sni file and reload + tr = urtest.AddTestRun("Update config files") + # Update the SNI config + snipath = ts.Disk.sni_yaml.AbsPath + recordspath = ts.Disk.records_config.AbsPath + tr.Disk.File(snipath, id="sni_yaml", typename="ats:config"), + tr.Disk.sni_yaml.AddLines([ + 'sni:', + '- fqdn: bar.com', + ' tunnel_route: localhost:{0}'.format(server_bar.Variables.SSL_Port), + ]) + tr.StillRunningAfter = ts + tr.StillRunningAfter = server_foo + tr.StillRunningAfter = server_bar + tr.Processes.Default.Env = ts.Env + tr.Processes.Default.Command = 'echo Updated configs' + tr.Processes.Default.ReturnCode = 0 + + # Test a large CLIENT_HELLO. + tr = urtest.AddTestRun("Single large CLIENT_HELLO blind tunnel test") + tr.TimeOut = 5 + tr.Setup.Copy('split_client_hello.py') + tr.Setup.Copy('receive_split_client_hello.py') + p = tr.Processes.Default + p.StartBefore(split_client_hello_server, ready=When.PortOpen(server_port)) + p.Command = f'{sys.executable} split_client_hello.py 127.0.0.1 {ts.Variables.ssl_port} -s 0' + p.ReturnCode = 0 + tr.StillRunningAfter = ts + tr.StillRunningAfter = dns + p.Streams.All += Testers.ContainsExpression('dummy SERVER_HELLO', 'Verify a dummy SERVER_HELLO response is received') + p.Streams.All += Testers.ContainsExpression('data: 0', 'Verify that the first data packet was received.') + p.Streams.All += Testers.ContainsExpression('data: 1', 'Verify that the second data packet was received.') + + # Test a CLIENT_HELLO split over two TCP packets. + tr = urtest.AddTestRun("Split large CLIENT_HELLO blind tunnel test") + tr.TimeOut = 5 + tr.Setup.Copy('split_client_hello.py') + tr.Setup.Copy('receive_split_client_hello.py') + p = tr.Processes.Default + p.Command = f'{sys.executable} split_client_hello.py 127.0.0.1 {ts.Variables.ssl_port}' + p.ReturnCode = 0 + tr.StillRunningAfter = ts + tr.StillRunningAfter = dns + p.Streams.All += Testers.ContainsExpression('dummy SERVER_HELLO', 'Verify a dummy SERVER_HELLO response is received') + p.Streams.All += Testers.ContainsExpression('data: 0', 'Verify that the first data packet was received.') + p.Streams.All += Testers.ContainsExpression('data: 1', 'Verify that the second data packet was received.') + + trreload = urtest.AddConfigReload(ts, expect="success", expect_tasks=["sni.yaml"], description="Reload config") + trreload.StillRunningAfter = server_foo + trreload.StillRunningAfter = server_bar + + # Should terminate on traffic_server (not tunnel) + tr = urtest.AddTestRun("foo.com no Tunnel-test") + tr.TimeOut = 30 + tr.StillRunningAfter = ts + tr.Processes.Default.StartBefore(server2) + tr.MakeCurlCommand("-v --resolve 'foo.com:{0}:127.0.0.1' -k https://foo.com:{0}".format(ts.Variables.ssl_port), ts=ts) + tr.Processes.Default.Streams.All += Testers.ContainsExpression("Not Found on Accelerato", "Terminates on on Traffic Server") + tr.Processes.Default.Streams.All += Testers.ContainsExpression("ATS", "Terminate on Traffic Server") + tr.Processes.Default.Streams.All += Testers.ExcludesExpression("Could Not Connect", "Curl attempt should have succeeded") + + # Should tunnel to server_bar + tr = urtest.AddTestRun("bar.com Tunnel-test") + tr.MakeCurlCommand("-v --resolve 'bar.com:{0}:127.0.0.1' -k https://bar.com:{0}".format(ts.Variables.ssl_port), ts=ts) + tr.ReturnCode = 0 + tr.StillRunningAfter = ts + tr.Processes.Default.Streams.All += Testers.ExcludesExpression("Could Not Connect", "Curl attempt should have succeeded") + tr.Processes.Default.Streams.All += Testers.ExcludesExpression("Not Found on Accelerato", "Terminates on on Traffic Server") + tr.Processes.Default.Streams.All += Testers.ExcludesExpression("ATS", "Terminate on Traffic Server") + tr.Processes.Default.Streams.All += Testers.ContainsExpression("bar ok", "Should get a response from bar") + + tr = urtest.AddTestRun("Test Metrics") + tr.Processes.Default.Command = ( + f"{urtest.Variables.AtsTestToolsDir}/stdout_wait" + " 'traffic_ctl metric get" + + " proxy.process.http.total_incoming_connections" + " proxy.process.http.total_client_connections" + + " proxy.process.http.total_client_connections_ipv4" + " proxy.process.http.total_client_connections_ipv6" + + " proxy.process.http.total_server_connections" + " proxy.process.http2.total_client_connections" + + " proxy.process.http.connect_requests" + " proxy.process.tunnel.total_client_connections_blind_tcp" + + " proxy.process.tunnel.current_client_connections_blind_tcp" + " proxy.process.tunnel.total_server_connections_blind_tcp" + + " proxy.process.tunnel.current_server_connections_blind_tcp" + " proxy.process.tunnel.total_client_connections_tls_tunnel" + + " proxy.process.tunnel.current_client_connections_tls_tunnel" + + " proxy.process.tunnel.total_client_connections_tls_forward" + + " proxy.process.tunnel.current_client_connections_tls_forward" + + " proxy.process.tunnel.total_client_connections_tls_partial_blind" + + " proxy.process.tunnel.current_client_connections_tls_partial_blind" + + " proxy.process.tunnel.total_client_connections_tls_http" + " proxy.process.tunnel.current_client_connections_tls_http" + + " proxy.process.tunnel.total_server_connections_tls" + " proxy.process.tunnel.current_server_connections_tls'" + + f" {urtest.TestDirectory}/gold/tls-tunnel-metrics.gold") + # Need to copy over the environment so traffic_ctl knows where to find the unix domain socket + tr.Processes.Default.Env = ts.Env + tr.Processes.Default.ReturnCode = 0 + tr.StillRunningAfter = ts + urtest.execute() diff --git a/tests/uranium_tests/tls/test_tls_tunnel_forward.py b/tests/uranium_tests/tls/test_tls_tunnel_forward.py new file mode 100644 index 00000000000..9257c3a8f3a --- /dev/null +++ b/tests/uranium_tests/tls/test_tls_tunnel_forward.py @@ -0,0 +1,187 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_tls_tunnel_forward(urtest: UraniumTest) -> None: + ''' + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + urtest.Summary = ''' + Test tunneling and forwarding based on SNI + ''' + + # Define default ATS + ts = urtest.MakeATSProcess("ts", enable_tls=True) + server_foo = urtest.MakeOriginServer("server_foo", ssl=True) + server_bar = urtest.MakeOriginServer("server_bar", ssl=False) + server_random = urtest.MakeOriginServer("server_random", ssl=False) + nameserver = urtest.MakeDNServer("dns", default='127.0.0.1') + + request_foo_header = {"headers": "GET / HTTP/1.1\r\nHost: foo.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + request_bar_header = {"headers": "GET / HTTP/1.1\r\nHost: bar.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + request_random_header = {"headers": "GET / HTTP/1.1\r\nHost: random.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + response_foo_header = { + "headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", + "timestamp": "1469733493.993", + "body": "ok foo" + } + response_bar_header = { + "headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", + "timestamp": "1469733493.993", + "body": "ok bar" + } + response_random_header = { + "headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", + "timestamp": "1469733493.993", + "body": "ok random" + } + server_foo.addResponse("sessionlog_foo.json", request_foo_header, response_foo_header) + server_bar.addResponse("sessionlog_bar.json", request_bar_header, response_bar_header) + server_random.addResponse("sessionlog_random.json", request_random_header, response_random_header) + + # add ssl materials like key, certificates for the server + ts.addSSLfile("ssl/signed-foo.pem") + ts.addSSLfile("ssl/signed-foo.key") + ts.addSSLfile("ssl/signed-bar.pem") + ts.addSSLfile("ssl/signed-bar.key") + ts.addSSLfile("ssl/server.pem") + ts.addSSLfile("ssl/server.key") + ts.addSSLfile("ssl/signer.pem") + ts.addSSLfile("ssl/signer.key") + + # Need no remap rules. Everything should be processed by sni + + # Make sure the TS server certs are different from the origin certs + ts.Disk.ssl_multicert_yaml.AddLines( + """ + ssl_multicert: + - dest_ip: "*" + ssl_cert_name: signed-foo.pem + ssl_key_name: signed-foo.key + """.split("\n")) + + # Case 1, global config policy=permissive properties=signature + # override for foo.com policy=enforced properties=all + ts.Disk.records_config.update( + { + 'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir), + 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts.Variables.SSLDir), + 'proxy.config.http.connect_ports': + '{0} {1} {2} {3}'.format( + ts.Variables.ssl_port, server_foo.Variables.SSL_Port, server_bar.Variables.Port, server_random.Variables.Port), + 'proxy.config.ssl.client.CA.cert.path': '{0}'.format(ts.Variables.SSLDir), + 'proxy.config.ssl.client.CA.cert.filename': 'signer.pem', + 'proxy.config.exec_thread.autoconfig.scale': 1.0, + 'proxy.config.url_remap.pristine_host_hdr': 1, + 'proxy.config.dns.nameservers': f"127.0.0.1:{nameserver.Variables.Port}", + 'proxy.config.dns.resolv_conf': 'NULL' + }) + ts.addPrivateConnectAllowYaml() + + # foo.com should not terminate. Just tunnel to server_foo + # bar.com should terminate. Forward its tcp stream to server_bar + ts.Disk.sni_yaml.AddLines( + [ + "sni:", + "- fqdn: 'foo.com'", + " tunnel_route: 'localhost:{0}'".format(server_foo.Variables.SSL_Port), + "- fqdn: 'bar.com'", + " forward_route: 'localhost:{0}'".format(server_bar.Variables.Port), + "- fqdn: ''", # default case + " forward_route: 'localhost:{0}'".format(server_random.Variables.Port), + ]) + + tr = urtest.AddTestRun("Tunnel-test") + tr.MakeCurlCommand("-v --resolve 'foo.com:{0}:127.0.0.1' -k https://foo.com:{0}".format(ts.Variables.ssl_port), ts=ts) + tr.ReturnCode = 0 + tr.Processes.Default.StartBefore(server_foo) + tr.Processes.Default.StartBefore(server_bar) + tr.Processes.Default.StartBefore(server_random) + tr.Processes.Default.StartBefore(nameserver) + tr.Processes.Default.StartBefore(urtest.Processes.ts) + tr.StillRunningAfter = ts + tr.Processes.Default.Streams.All += Testers.ExcludesExpression("Could Not Connect", "Curl attempt should have succeeded") + tr.Processes.Default.Streams.All += Testers.ExcludesExpression( + "Not Found on Accelerato", "Should not try to remap on Traffic Server") + tr.Processes.Default.Streams.All += Testers.ExcludesExpression("CN=foo.com", "Should not TLS terminate on Traffic Server") + tr.Processes.Default.Streams.All += Testers.ContainsExpression("HTTP/1.1 200 OK", "Should get a successful response") + tr.Processes.Default.Streams.All += Testers.ContainsExpression("ok foo", "Body is expected") + + tr2 = urtest.AddTestRun("Forward-test") + tr2.MakeCurlCommand( + "-v --http1.1 -H 'host:bar.com' --resolve 'bar.com:{0}:127.0.0.1' -k https://bar.com:{0}".format(ts.Variables.ssl_port), + ts=ts) + tr2.ReturnCode = 0 + tr2.StillRunningAfter = server_bar + tr2.StillRunningAfter = ts + tr2.Processes.Default.Streams.All += Testers.ExcludesExpression("Could Not Connect", "Curl attempt should have succeeded") + tr2.Processes.Default.Streams.All += Testers.ExcludesExpression( + "Not Found on Accelerato", "Should not try to remap on Traffic Server") + tr2.Processes.Default.Streams.All += Testers.ContainsExpression("CN=foo.com", "Should TLS terminate on Traffic Server") + tr2.Processes.Default.Streams.All += Testers.ContainsExpression("HTTP/1.1 200 OK", "Should get a successful response") + tr2.Processes.Default.Streams.All += Testers.ContainsExpression("ok bar", "Body is expected") + + tr3 = urtest.AddTestRun("no-sni-forward-test") + tr3.MakeCurlCommand("--http1.1 -v -k -H 'host:random.com' https://127.0.0.1:{0}".format(ts.Variables.ssl_port), ts=ts) + tr3.ReturnCode = 0 + tr3.StillRunningAfter = server_random + tr3.StillRunningAfter = ts + tr3.Processes.Default.Streams.All += Testers.ExcludesExpression("Could Not Connect", "Curl attempt should have succeeded") + tr3.Processes.Default.Streams.All += Testers.ExcludesExpression( + "Not Found on Accelerato", "Should not try to remap on Traffic Server") + tr3.Processes.Default.Streams.All += Testers.ContainsExpression("CN=foo.com", "Should TLS terminate on Traffic Server") + tr3.Processes.Default.Streams.All += Testers.ContainsExpression("HTTP/1.1 200 OK", "Should get a successful response") + tr3.Processes.Default.Streams.All += Testers.ContainsExpression("ok random", "Body is expected") + + tr = urtest.AddTestRun("Test Metrics") + tr.Processes.Default.Command = ( + f"{urtest.Variables.AtsTestToolsDir}/stdout_wait" + " 'traffic_ctl metric get" + + " proxy.process.http.total_incoming_connections" + " proxy.process.http.total_client_connections" + + " proxy.process.http.total_client_connections_ipv4" + " proxy.process.http.total_client_connections_ipv6" + + " proxy.process.http.total_server_connections" + " proxy.process.http2.total_client_connections" + + " proxy.process.http.connect_requests" + " proxy.process.tunnel.total_client_connections_blind_tcp" + + " proxy.process.tunnel.current_client_connections_blind_tcp" + " proxy.process.tunnel.total_server_connections_blind_tcp" + + " proxy.process.tunnel.current_server_connections_blind_tcp" + " proxy.process.tunnel.total_client_connections_tls_tunnel" + + " proxy.process.tunnel.current_client_connections_tls_tunnel" + + " proxy.process.tunnel.total_client_connections_tls_forward" + + " proxy.process.tunnel.current_client_connections_tls_forward" + + " proxy.process.tunnel.total_client_connections_tls_partial_blind" + + " proxy.process.tunnel.current_client_connections_tls_partial_blind" + + " proxy.process.tunnel.total_client_connections_tls_http" + " proxy.process.tunnel.current_client_connections_tls_http" + + " proxy.process.tunnel.total_server_connections_tls" + " proxy.process.tunnel.current_server_connections_tls'" + + f" {urtest.TestDirectory}/gold/tls-tunnel-forward-metrics.gold") + # Need to copy over the environment so traffic_ctl knows where to find the unix domain socket + tr.Processes.Default.Env = ts.Env + tr.Processes.Default.ReturnCode = 0 + tr.StillRunningAfter = ts + urtest.execute() diff --git a/tests/uranium_tests/tls/test_tls_verify.py b/tests/uranium_tests/tls/test_tls_verify.py new file mode 100644 index 00000000000..5a0d71ed3c1 --- /dev/null +++ b/tests/uranium_tests/tls/test_tls_verify.py @@ -0,0 +1,186 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_tls_verify(urtest: UraniumTest) -> None: + ''' + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + urtest.Summary = ''' + Test tls server certificate verification options + ''' + + # Define default ATS + ts = urtest.MakeATSProcess("ts", enable_tls=True) + server_foo = urtest.MakeOriginServer( + "server_foo", + ssl=True, + options={ + "--key": "{0}/signed-foo.key".format(urtest.RunDirectory), + "--cert": "{0}/signed-foo.pem".format(urtest.RunDirectory) + }) + server_bar = urtest.MakeOriginServer( + "server_bar", + ssl=True, + options={ + "--key": "{0}/signed-bar.key".format(urtest.RunDirectory), + "--cert": "{0}/signed-bar.pem".format(urtest.RunDirectory) + }) + server_wild = urtest.MakeOriginServer( + "server_wild", + ssl=True, + options={ + "--key": "{0}/signed-wild.key".format(urtest.RunDirectory), + "--cert": "{0}/signed-wild.pem".format(urtest.RunDirectory) + }) + server = urtest.MakeOriginServer("server", ssl=True) + + request_foo_header = {"headers": "GET / HTTP/1.1\r\nHost: foo.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + request_bad_foo_header = {"headers": "GET / HTTP/1.1\r\nHost: badfoo.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + request_bar_header = {"headers": "GET / HTTP/1.1\r\nHost: bar.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + request_bad_bar_header = {"headers": "GET / HTTP/1.1\r\nHost: badbar.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + server_foo.addResponse("sessionlog.json", request_foo_header, response_header) + server_foo.addResponse("sessionlog.json", request_bad_foo_header, response_header) + server_bar.addResponse("sessionlog.json", request_bar_header, response_header) + server_bar.addResponse("sessionlog.json", request_bad_bar_header, response_header) + server_wild.addResponse("sessionlog.json", request_bar_header, response_header) + + # add ssl materials like key, certificates for the server + ts.addSSLfile("ssl/signed-foo.pem") + ts.addSSLfile("ssl/signed-foo.key") + ts.addSSLfile("ssl/signed-bar.pem") + ts.addSSLfile("ssl/signed-bar.key") + ts.addSSLfile("ssl/server.pem") + ts.addSSLfile("ssl/server.key") + ts.addSSLfile("ssl/signer.pem") + ts.addSSLfile("ssl/signer.key") + ts.addSSLfile("ssl/signed-wild.key") + ts.addSSLfile("ssl/signed-wild.pem") + + ts.Disk.remap_config.AddLine('map / https://127.0.0.1:{0}'.format(server.Variables.SSL_Port)) + ts.Disk.remap_config.AddLine('map https://foo.com/ https://127.0.0.1:{0}'.format(server_foo.Variables.SSL_Port)) + ts.Disk.remap_config.AddLine('map https://bad_foo.com/ https://127.0.0.1:{0}'.format(server_foo.Variables.SSL_Port)) + ts.Disk.remap_config.AddLine('map https://bar.com/ https://127.0.0.1:{0}'.format(server_bar.Variables.SSL_Port)) + ts.Disk.remap_config.AddLine('map https://bad_bar.com/ https://127.0.0.1:{0}'.format(server_bar.Variables.SSL_Port)) + ts.Disk.remap_config.AddLine('map https://foo.wild.com/ https://127.0.0.1:{0}'.format(server_wild.Variables.SSL_Port)) + ts.Disk.remap_config.AddLine('map https://foo_bar.wild.com/ https://127.0.0.1:{0}'.format(server_wild.Variables.SSL_Port)) + + ts.Disk.ssl_multicert_yaml.AddLines( + """ + ssl_multicert: + - dest_ip: "*" + ssl_cert_name: server.pem + ssl_key_name: server.key + """.split("\n")) + + # Case 1, global config policy=permissive properties=signature + # override for foo.com policy=enforced properties=all + ts.Disk.records_config.update( + { + 'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir), + 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts.Variables.SSLDir), + # set global policy + 'proxy.config.ssl.client.verify.server.policy': 'PERMISSIVE', + 'proxy.config.ssl.client.verify.server.properties': 'SIGNATURE', + 'proxy.config.ssl.client.CA.cert.path': '{0}'.format(ts.Variables.SSLDir), + 'proxy.config.ssl.client.CA.cert.filename': 'signer.pem', + 'proxy.config.exec_thread.autoconfig.scale': 1.0, + 'proxy.config.url_remap.pristine_host_hdr': 1 + }) + + ts.Disk.sni_yaml.AddLines( + [ + 'sni:', + '- fqdn: bar.com', + ' verify_server_policy: ENFORCED', + ' verify_server_properties: ALL', + '- fqdn: "*.wild.com"', + ' verify_server_policy: ENFORCED', + ' verify_server_properties: ALL', + '- fqdn: bad_bar.com', + ' verify_server_policy: ENFORCED', + ' verify_server_properties: ALL', + ]) + + tr = urtest.AddTestRun("Permissive-Test") + tr.Setup.Copy("ssl/signed-foo.key") + tr.Setup.Copy("ssl/signed-foo.pem") + tr.Setup.Copy("ssl/signed-bar.key") + tr.Setup.Copy("ssl/signed-bar.pem") + tr.Setup.Copy("ssl/signed-wild.pem") + tr.Setup.Copy("ssl/signed-wild.key") + tr.MakeCurlCommand("-v -k -H \"host: foo.com\" https://127.0.0.1:{0}".format(ts.Variables.ssl_port), ts=ts) + tr.ReturnCode = 0 + tr.Processes.Default.StartBefore(server_foo) + tr.Processes.Default.StartBefore(server_bar) + tr.Processes.Default.StartBefore(server) + tr.Processes.Default.StartBefore(server_wild) + tr.Processes.Default.StartBefore(urtest.Processes.ts) + tr.StillRunningAfter = server + tr.StillRunningAfter = ts + tr.Processes.Default.Streams.stdout = Testers.ExcludesExpression("Could Not Connect", "Curl attempt should have succeeded") + + tr2 = urtest.AddTestRun("Override-enforcing-Test") + tr2.MakeCurlCommand("-v -k -H \"host: bar.com\" https://127.0.0.1:{0}".format(ts.Variables.ssl_port), ts=ts) + tr2.ReturnCode = 0 + tr2.StillRunningAfter = server + tr2.StillRunningAfter = ts + tr2.Processes.Default.Streams.stdout = Testers.ExcludesExpression("Could Not Connect", "Curl attempt should have succeeded") + + tr3 = urtest.AddTestRun("Override-enforcing-Test-fail-name-check") + tr3.MakeCurlCommand("-v -k -H \"host: bad_bar.com\" https://127.0.0.1:{0}".format(ts.Variables.ssl_port), ts=ts) + tr3.Processes.Default.Streams.stdout = Testers.ContainsExpression("Could Not Connect", "Curl attempt should have failed") + tr3.ReturnCode = 0 + tr3.StillRunningAfter = server + tr3.StillRunningAfter = ts + + tr4 = urtest.AddTestRun("Exercise-wildcard-cert-name-check") + tr4.MakeCurlCommand("-v -k -H \"host: foo.wild.com\" https://127.0.0.1:{0}".format(ts.Variables.ssl_port), ts=ts) + tr4.Processes.Default.Streams.stdout = Testers.ExcludesExpression("Could Not Connect", "Curl attempt should have succeeded") + tr4.ReturnCode = 0 + tr4.StillRunningAfter = server + tr4.StillRunningAfter = ts + + tr5 = urtest.AddTestRun("Exercise-wildcard-cert-underscore-name-check") + tr5.MakeCurlCommand("-v -k -H \"host: foo_bar.wild.com\" https://127.0.0.1:{0}".format(ts.Variables.ssl_port), ts=ts) + tr5.Processes.Default.Streams.stdout = Testers.ExcludesExpression("Could Not Connect", "Curl attempt should have succeeded") + tr5.ReturnCode = 0 + tr5.StillRunningAfter = server + tr5.StillRunningAfter = ts + + # Over riding the built in ERROR check since we expect tr3 to fail + ts.Disk.diags_log.Content = Testers.ExcludesExpression("verification failed", "Make sure the signatures didn't fail") + ts.Disk.diags_log.Content += Testers.ContainsExpression( + r"WARNING: SNI \(bad_bar.com\) not in certificate", "Make sure bad_bar name checked failed.") + urtest.execute() diff --git a/tests/uranium_tests/tls/test_tls_verify2.py b/tests/uranium_tests/tls/test_tls_verify2.py new file mode 100644 index 00000000000..50989c6dbe5 --- /dev/null +++ b/tests/uranium_tests/tls/test_tls_verify2.py @@ -0,0 +1,183 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_tls_verify2(urtest: UraniumTest) -> None: + ''' + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + urtest.Summary = ''' + Test tls server certificate verification options + ''' + + # Define default ATS + ts = urtest.MakeATSProcess("ts", enable_tls=True) + server_foo = urtest.MakeOriginServer( + "server_foo", + ssl=True, + options={ + "--key": "{0}/signed-foo.key".format(urtest.RunDirectory), + "--cert": "{0}/signed-foo.pem".format(urtest.RunDirectory) + }) + server_bar = urtest.MakeOriginServer( + "server_bar", + ssl=True, + options={ + "--key": "{0}/signed-bar.key".format(urtest.RunDirectory), + "--cert": "{0}/signed-bar.pem".format(urtest.RunDirectory) + }) + server = urtest.MakeOriginServer("server", ssl=True) + + request_foo_header = {"headers": "GET / HTTP/1.1\r\nHost: foo.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + request_bad_foo_header = {"headers": "GET / HTTP/1.1\r\nHost: bad_foo.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + request_bar_header = {"headers": "GET / HTTP/1.1\r\nHost: bar.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + request_bad_bar_header = {"headers": "GET / HTTP/1.1\r\nHost: bad_bar.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + server_foo.addResponse("sessionlog.json", request_foo_header, response_header) + server_foo.addResponse("sessionlog.json", request_bad_foo_header, response_header) + server_bar.addResponse("sessionlog.json", request_bar_header, response_header) + server_bar.addResponse("sessionlog.json", request_bad_bar_header, response_header) + + # add ssl materials like key, certificates for the server + ts.addSSLfile("ssl/signed-foo.pem") + ts.addSSLfile("ssl/signed-foo.key") + ts.addSSLfile("ssl/signed-bar.pem") + ts.addSSLfile("ssl/signed-bar.key") + ts.addSSLfile("ssl/server.pem") + ts.addSSLfile("ssl/server.key") + ts.addSSLfile("ssl/signer.pem") + ts.addSSLfile("ssl/signer.key") + + ts.Disk.remap_config.AddLine('map https://foo.com/ https://127.0.0.1:{0}'.format(server_foo.Variables.SSL_Port)) + ts.Disk.remap_config.AddLine('map https://bad_foo.com/ https://127.0.0.1:{0}'.format(server_foo.Variables.SSL_Port)) + ts.Disk.remap_config.AddLine('map https://bar.com/ https://127.0.0.1:{0}'.format(server_bar.Variables.SSL_Port)) + ts.Disk.remap_config.AddLine('map https://bad_bar.com/ https://127.0.0.1:{0}'.format(server_bar.Variables.SSL_Port)) + ts.Disk.remap_config.AddLine('map / https://127.0.0.1:{0}'.format(server.Variables.SSL_Port)) + + ts.Disk.ssl_multicert_yaml.AddLines( + """ + ssl_multicert: + - dest_ip: "*" + ssl_cert_name: server.pem + ssl_key_name: server.key + """.split("\n")) + + # Case 1, global config policy=permissive properties=signature + # override for foo.com policy=enforced properties=all + ts.Disk.records_config.update( + { + 'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir), + 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts.Variables.SSLDir), + # set global policy + 'proxy.config.ssl.client.verify.server.policy': 'ENFORCED', + 'proxy.config.ssl.client.verify.server.properties': 'ALL', + 'proxy.config.ssl.client.CA.cert.path': '{0}'.format(ts.Variables.SSLDir), + 'proxy.config.ssl.client.CA.cert.filename': 'signer.pem', + 'proxy.config.exec_thread.autoconfig.scale': 1.0, + 'proxy.config.url_remap.pristine_host_hdr': 1 + }) + + ts.Disk.sni_yaml.AddLine('sni:') + ts.Disk.sni_yaml.AddLine('- fqdn: bar.com') + ts.Disk.sni_yaml.AddLine(' verify_server_policy: PERMISSIVE') + ts.Disk.sni_yaml.AddLine(' verify_server_properties: SIGNATURE') + ts.Disk.sni_yaml.AddLine('- fqdn: bad_bar.com') + ts.Disk.sni_yaml.AddLine(' verify_server_policy: PERMISSIVE') + ts.Disk.sni_yaml.AddLine(' verify_server_properties: SIGNATURE') + ts.Disk.sni_yaml.AddLine('- fqdn: random.com') + ts.Disk.sni_yaml.AddLine(' verify_server_policy: DISABLED') + + tr = urtest.AddTestRun("default-enforce") + tr.Setup.Copy("ssl/signed-foo.key") + tr.Setup.Copy("ssl/signed-foo.pem") + tr.Setup.Copy("ssl/signed-bar.key") + tr.Setup.Copy("ssl/signed-bar.pem") + tr.MakeCurlCommand("-k -H \"host: foo.com\" https://127.0.0.1:{0}".format(ts.Variables.ssl_port), ts=ts) + tr.ReturnCode = 0 + tr.Processes.Default.StartBefore(server_foo) + tr.Processes.Default.StartBefore(server_bar) + tr.Processes.Default.StartBefore(server) + tr.Processes.Default.StartBefore(urtest.Processes.ts) + tr.StillRunningAfter = server + tr.StillRunningAfter = ts + tr.Processes.Default.Streams.stdout = Testers.ExcludesExpression("Could Not Connect", "Curl attempt should have succeeded") + + tr2 = urtest.AddTestRun("override-disabled") + tr2.MakeCurlCommand("-k -H \"host: random.com\" https://127.0.0.1:{0}".format(ts.Variables.ssl_port), ts=ts) + tr2.ReturnCode = 0 + tr2.StillRunningAfter = server + tr2.StillRunningAfter = ts + tr2.Processes.Default.Streams.stdout = Testers.ExcludesExpression("Could Not Connect", "Curl attempt should have succeeded") + + tr3 = urtest.AddTestRun("override-permissive") + tr3.MakeCurlCommand("-k -H \"host: bar.com\" https://127.0.0.1:{0}".format(ts.Variables.ssl_port), ts=ts) + tr3.ReturnCode = 0 + tr3.StillRunningAfter = server + tr3.StillRunningAfter = ts + tr3.Processes.Default.Streams.stdout = Testers.ExcludesExpression("Could Not Connect", "Curl attempt should have succeeded") + + tr4 = urtest.AddTestRun("override-permissive-bad-name") + tr4.MakeCurlCommand("-k -H \"host: bad_bar.com\" https://127.0.0.1:{0}".format(ts.Variables.ssl_port), ts=ts) + tr4.ReturnCode = 0 + tr4.StillRunningAfter = server + tr4.StillRunningAfter = ts + tr4.Processes.Default.Streams.stdout = Testers.ExcludesExpression("Could Not Connect", "Curl attempt should have succeeded") + + tr5 = urtest.AddTestRun("default-enforce-bad-sig") + tr5.MakeCurlCommand("-k -H \"host: random2.com\" https://127.0.0.1:{0}".format(ts.Variables.ssl_port), ts=ts) + tr5.ReturnCode = 0 + tr5.Processes.Default.Streams.stdout = Testers.ContainsExpression("Could Not Connect", "Curl attempt should have failed") + tr5.StillRunningAfter = server + tr5.StillRunningAfter = ts + + tr6 = urtest.AddTestRun("default-enforce-fail") + tr6.MakeCurlCommand("-k -H \"host: bad_foo.com\" https://127.0.0.1:{0}".format(ts.Variables.ssl_port), ts=ts) + tr6.ReturnCode = 0 + tr6.Processes.Default.Streams.stdout = Testers.ContainsExpression("Could Not Connect", "Curl attempt should have failed") + tr6.StillRunningAfter = server + tr6.StillRunningAfter = ts + + # No name checking for the sig-only permissive override for bad_bar + ts.Disk.diags_log.Content += Testers.ExcludesExpression( + r"WARNING: SNI \(bad_bar.com\) not in certificate", "bad_bar name checked should be skipped.") + ts.Disk.diags_log.Content = Testers.ExcludesExpression( + r"WARNING: SNI \(foo.com\) not in certificate", "foo name checked should be skipped.") + # No checking for the self-signed on random.com. No messages + ts.Disk.diags_log.Content += Testers.ExcludesExpression( + r"WARNING: Core server certificate verification failed for \(random.com\)", + "signature check for random.com should be skipped") + ts.Disk.diags_log.Content += Testers.ContainsExpression( + r"WARNING: Core server certificate verification failed for \(random2.com\)", "signature check for random.com should fail'") + ts.Disk.diags_log.Content += Testers.ContainsExpression( + r"WARNING: SNI \(bad_foo.com\) not in certificate", "bad_foo name checked should be checked.") + urtest.execute() diff --git a/tests/uranium_tests/tls/test_tls_verify3.py b/tests/uranium_tests/tls/test_tls_verify3.py new file mode 100644 index 00000000000..ed3e199e133 --- /dev/null +++ b/tests/uranium_tests/tls/test_tls_verify3.py @@ -0,0 +1,178 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_tls_verify3(urtest: UraniumTest) -> None: + ''' + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + urtest.Summary = ''' + Test tls server certificate verification options + ''' + + # Define default ATS + ts = urtest.MakeATSProcess("ts", enable_tls=True) + server_foo = urtest.MakeOriginServer( + "server_foo", + ssl=True, + options={ + "--key": "{0}/signed-foo.key".format(urtest.RunDirectory), + "--cert": "{0}/signed-foo.pem".format(urtest.RunDirectory) + }) + server_bar = urtest.MakeOriginServer( + "server_bar", + ssl=True, + options={ + "--key": "{0}/signed-bar.key".format(urtest.RunDirectory), + "--cert": "{0}/signed-bar.pem".format(urtest.RunDirectory) + }) + server = urtest.MakeOriginServer("server", ssl=True) + + request_foo_header = {"headers": "GET / HTTP/1.1\r\nHost: foo.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + request_bad_foo_header = {"headers": "GET / HTTP/1.1\r\nHost: badfoo.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + request_bar_header = {"headers": "GET / HTTP/1.1\r\nHost: bar.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + request_bad_bar_header = {"headers": "GET / HTTP/1.1\r\nHost: badbar.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + server_foo.addResponse("sessionlog.json", request_foo_header, response_header) + server_foo.addResponse("sessionlog.json", request_bad_foo_header, response_header) + server_bar.addResponse("sessionlog.json", request_bar_header, response_header) + server_bar.addResponse("sessionlog.json", request_bad_bar_header, response_header) + + # add ssl materials like key, certificates for the server + ts.addSSLfile("ssl/signed-foo.pem") + ts.addSSLfile("ssl/signed-foo.key") + ts.addSSLfile("ssl/signed-bar.pem") + ts.addSSLfile("ssl/signed-bar.key") + ts.addSSLfile("ssl/server.pem") + ts.addSSLfile("ssl/server.key") + ts.addSSLfile("ssl/signer.pem") + ts.addSSLfile("ssl/signer.key") + + ts.Disk.remap_config.AddLine( + 'map https://foo.com:{1}/ https://127.0.0.1:{0}'.format(server_foo.Variables.SSL_Port, ts.Variables.ssl_port)) + ts.Disk.remap_config.AddLine( + 'map https://bob.foo.com:{1}/ https://127.0.0.1:{0}'.format(server_foo.Variables.SSL_Port, ts.Variables.ssl_port)) + ts.Disk.remap_config.AddLine( + 'map https://bar.com:{1}/ https://127.0.0.1:{0}'.format(server_bar.Variables.SSL_Port, ts.Variables.ssl_port)) + ts.Disk.remap_config.AddLine( + 'map https://bob.bar.com:{1}/ https://127.0.0.1:{0}'.format(server_bar.Variables.SSL_Port, ts.Variables.ssl_port)) + ts.Disk.remap_config.AddLine('map / https://127.0.0.1:{0}'.format(server.Variables.SSL_Port)) + + ts.Disk.ssl_multicert_yaml.AddLines( + """ + ssl_multicert: + - dest_ip: "*" + ssl_cert_name: server.pem + ssl_key_name: server.key + """.split("\n")) + + # Case 1, global config policy=permissive properties=signature + # override for foo.com policy=enforced properties=all + ts.Disk.records_config.update( + { + 'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir), + 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts.Variables.SSLDir), + # set global policy + 'proxy.config.ssl.client.verify.server.policy': 'PERMISSIVE', + 'proxy.config.ssl.client.verify.server.properties': 'ALL', + 'proxy.config.ssl.client.CA.cert.path': '{0}'.format(ts.Variables.SSLDir), + 'proxy.config.ssl.client.CA.cert.filename': 'signer.pem', + 'proxy.config.exec_thread.autoconfig.scale': 1.0, + 'proxy.config.url_remap.pristine_host_hdr': 1 + }) + + ts.Disk.sni_yaml.AddLines( + [ + 'sni:', + '- fqdn: bob.bar.com', + ' verify_server_policy: ENFORCED', + ' verify_server_properties: ALL', + '- fqdn: "*.foo.com"', + ' verify_server_policy: ENFORCED', + ' verify_server_properties: SIGNATURE', + "- fqdn: '*.bar.com'", + ' verify_server_policy: DISABLED', + ]) + + tr = urtest.AddTestRun("foo.com Permissive-Test") + tr.Setup.Copy("ssl/signed-foo.key") + tr.Setup.Copy("ssl/signed-foo.pem") + tr.Setup.Copy("ssl/signed-bar.key") + tr.Setup.Copy("ssl/signed-bar.pem") + tr.MakeCurlCommand("-v -k --resolve 'foo.com:{0}:127.0.0.1' https://foo.com:{0}".format(ts.Variables.ssl_port), ts=ts) + tr.ReturnCode = 0 + tr.Processes.Default.StartBefore(urtest.Processes.ts) + tr.Processes.Default.StartBefore(server_foo) + tr.Processes.Default.StartBefore(server_bar) + tr.Processes.Default.StartBefore(server) + tr.StillRunningAfter = server + tr.StillRunningAfter = ts + tr.Processes.Default.Streams.stdout = Testers.ExcludesExpression("Could Not Connect", "Curl attempt should have succeeded") + + tr = urtest.AddTestRun("my.random.com Permissive-Test log failure") + tr.MakeCurlCommand( + "-v -k --resolve 'my.random.com:{0}:127.0.0.1' https://my.random.com:{0}".format(ts.Variables.ssl_port), ts=ts) + tr.ReturnCode = 0 + tr.StillRunningAfter = server + tr.StillRunningAfter = ts + tr.Processes.Default.Streams.stdout = Testers.ExcludesExpression("Could Not Connect", "Curl attempt should have succeeded") + + tr2 = urtest.AddTestRun("bob.bar.com Override-enforcing-Test") + tr2.MakeCurlCommand("-v -k --resolve 'bob.bar.com:{0}:127.0.0.1' https://bob.bar.com:{0}/".format(ts.Variables.ssl_port), ts=ts) + tr2.ReturnCode = 0 + tr2.StillRunningAfter = server + tr2.StillRunningAfter = ts + tr2.Processes.Default.Streams.stdout = Testers.ContainsExpression("Could Not Connect", "Curl attempt should have succeeded") + + tr3 = urtest.AddTestRun("bob.foo.com override-enforcing-name-test") + tr3.MakeCurlCommand("-v -k --resolve 'bob.foo.com:{0}:127.0.0.1' https://bob.foo.com:{0}/".format(ts.Variables.ssl_port), ts=ts) + tr3.Processes.Default.Streams.stdout = Testers.ExcludesExpression("Could Not Connect", "Curl attempt should not fail") + tr3.ReturnCode = 0 + tr3.StillRunningAfter = server + tr3.StillRunningAfter = ts + + tr3 = urtest.AddTestRun("random.bar.com override-no-test") + tr3.MakeCurlCommand( + "-v -k --resolve 'random.bar.com:{0}:127.0.0.1' https://random.bar.com:{0}".format(ts.Variables.ssl_port), ts=ts) + tr3.Processes.Default.Streams.stdout = Testers.ExcludesExpression("Could Not Connect", "Curl attempt should not fail") + tr3.ReturnCode = 0 + tr3.StillRunningAfter = server + tr3.StillRunningAfter = ts + + # Over riding the built in ERROR check since we expect tr3 to fail + ts.Disk.diags_log.Content = Testers.ContainsExpression( + r"WARNING: SNI \(bob.bar.com\) not in certificate", "Make sure bob.bar name checked failed.") + ts.Disk.diags_log.Content += Testers.ContainsExpression( + r"WARNING: Core server certificate verification failed for \(my.random.com\). Action=Continue", + "Make sure default permissive action takes") + urtest.execute() diff --git a/tests/uranium_tests/tls/test_tls_verify4.py b/tests/uranium_tests/tls/test_tls_verify4.py new file mode 100644 index 00000000000..42642577bf3 --- /dev/null +++ b/tests/uranium_tests/tls/test_tls_verify4.py @@ -0,0 +1,209 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_tls_verify4(urtest: UraniumTest) -> None: + ''' + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + urtest.Summary = ''' + Test tls server certificate verification options + Specifically update via traffic_ctl (reloadable) + ''' + + # Define default ATS + ts = urtest.MakeATSProcess("ts", select_ports=True, enable_tls=True) + server_foo = urtest.MakeOriginServer( + "server_foo", + ssl=True, + options={ + "--key": "{0}/signed-foo.key".format(urtest.RunDirectory), + "--cert": "{0}/signed-foo.pem".format(urtest.RunDirectory) + }) + server_bar = urtest.MakeOriginServer( + "server_bar", + ssl=True, + options={ + "--key": "{0}/signed-bar.key".format(urtest.RunDirectory), + "--cert": "{0}/signed-bar.pem".format(urtest.RunDirectory) + }) + server = urtest.MakeOriginServer("server", ssl=True) + + request_foo_header = {"headers": "GET / HTTP/1.1\r\nHost: foo.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + request_bad_foo_header = {"headers": "GET / HTTP/1.1\r\nHost: bad_foo.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + request_bar_header = {"headers": "GET / HTTP/1.1\r\nHost: bar.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + request_bad_bar_header = {"headers": "GET / HTTP/1.1\r\nHost: bad_bar.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + server_foo.addResponse("sessionlog.json", request_foo_header, response_header) + server_foo.addResponse("sessionlog.json", request_bad_foo_header, response_header) + server_bar.addResponse("sessionlog.json", request_bar_header, response_header) + server_bar.addResponse("sessionlog.json", request_bad_bar_header, response_header) + + # add ssl materials like key, certificates for the server + ts.addSSLfile("ssl/signed-foo.pem") + ts.addSSLfile("ssl/signed-foo.key") + ts.addSSLfile("ssl/signed-bar.pem") + ts.addSSLfile("ssl/signed-bar.key") + ts.addSSLfile("ssl/server.pem") + ts.addSSLfile("ssl/server.key") + ts.addSSLfile("ssl/signer.pem") + ts.addSSLfile("ssl/signer.key") + + ts.Disk.remap_config.AddLine('map https://foo.com/ https://127.0.0.1:{0}'.format(server_foo.Variables.SSL_Port)) + ts.Disk.remap_config.AddLine('map https://bad_foo.com/ https://127.0.0.1:{0}'.format(server_foo.Variables.SSL_Port)) + ts.Disk.remap_config.AddLine('map https://bar.com/ https://127.0.0.1:{0}'.format(server_bar.Variables.SSL_Port)) + ts.Disk.remap_config.AddLine('map https://bad_bar.com/ https://127.0.0.1:{0}'.format(server_bar.Variables.SSL_Port)) + ts.Disk.remap_config.AddLine('map / https://127.0.0.1:{0}'.format(server.Variables.SSL_Port)) + + ts.Disk.ssl_multicert_yaml.AddLines( + """ + ssl_multicert: + - dest_ip: "*" + ssl_cert_name: server.pem + ssl_key_name: server.key + """.split("\n")) + + # Case 1, global config policy=permissive properties=signature + # override for foo.com policy=enforced properties=all + ts.Disk.records_config.update( + { + 'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir), + 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts.Variables.SSLDir), + 'proxy.config.ssl.server.cipher_suite': + 'ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-SHA384:AES128-GCM-SHA256:AES256-GCM-SHA384:ECDHE-RSA-RC4-SHA:ECDHE-RSA-AES128-SHA:ECDHE-RSA-AES256-SHA:RC4-SHA:RC4-MD5:AES128-SHA:AES256-SHA:DES-CBC3-SHA!SRP:!DSS:!PSK:!aNULL:!eNULL:!SSLv2', + # set global policy + 'proxy.config.ssl.client.verify.server.policy': 'ENFORCED', + 'proxy.config.ssl.client.verify.server.properties': 'ALL', + 'proxy.config.ssl.client.CA.cert.path': '{0}'.format(ts.Variables.SSLDir), + 'proxy.config.ssl.client.CA.cert.filename': 'signer.pem', + 'proxy.config.exec_thread.autoconfig.scale': 1.0, + 'proxy.config.url_remap.pristine_host_hdr': 1, + 'proxy.config.diags.debug.enabled': 0, + 'proxy.config.diags.debug.tags': 'ssl' + }) + + tr = urtest.AddTestRun("default-enforce-bad-sig") + tr.Setup.Copy("ssl/signed-foo.key") + tr.Setup.Copy("ssl/signed-foo.pem") + tr.Setup.Copy("ssl/signed-bar.key") + tr.Setup.Copy("ssl/signed-bar.pem") + tr.MakeCurlCommand("-k -H \"host: random2.com\" https://127.0.0.1:{0}".format(ts.Variables.ssl_port), ts=ts) + tr.ReturnCode = 0 + tr.Processes.Default.StartBefore(server_foo) + tr.Processes.Default.StartBefore(server_bar) + tr.Processes.Default.StartBefore(server) + tr.Processes.Default.StartBefore(urtest.Processes.ts) + tr.StillRunningAfter = server + tr.StillRunningAfter = ts + tr.Processes.Default.Streams.stdout = Testers.ContainsExpression("Could Not Connect", "Curl attempt should have failed") + + tr2 = urtest.AddTestRun("Update config files") + recordspath = ts.Disk.records_config.AbsPath + # recreate the records.config with the cert filename changed + tr2.Disk.File(recordspath, id="records_config", typename="ats:config:records"), + tr2.Disk.records_config.update( + { + 'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir), + 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts.Variables.SSLDir), + 'proxy.config.ssl.server.cipher_suite': + 'ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-SHA384:AES128-GCM-SHA256:AES256-GCM-SHA384:ECDHE-RSA-RC4-SHA:ECDHE-RSA-AES128-SHA:ECDHE-RSA-AES256-SHA:RC4-SHA:RC4-MD5:AES128-SHA:AES256-SHA:DES-CBC3-SHA!SRP:!DSS:!PSK:!aNULL:!eNULL:!SSLv2', + # set global policy + 'proxy.config.ssl.client.verify.server.policy': 'PERMISSIVE', + 'proxy.config.ssl.client.verify.server.properties': 'ALL', + 'proxy.config.ssl.client.CA.cert.path': '{0}'.format(ts.Variables.SSLDir), + 'proxy.config.ssl.client.CA.cert.filename': 'signer.pem', + 'proxy.config.exec_thread.autoconfig.scale': 1.0, + 'proxy.config.url_remap.pristine_host_hdr': 1, + 'proxy.config.diags.debug.enabled': 0, + 'proxy.config.diags.debug.tags': 'ssl' + }) + tr2.StillRunningAfter = ts + tr2.StillRunningAfter = server + tr2.Processes.Default.Command = 'echo Updated configs' + # Need to copy over the environment so traffic_ctl knows where to find the unix domain socket + tr2.Processes.Default.Env = ts.Env + tr2.Processes.Default.ReturnCode = 0 + + trreload = urtest.AddConfigReload(ts, description="Reload config") + trreload.StillRunningAfter = server + + tragain = urtest.AddTestRun("permissive-after-update") + tragain.MakeCurlCommand("-k -H \"host: random3.com\" https://127.0.0.1:{0}".format(ts.Variables.ssl_port), ts=ts) + tragain.ReturnCode = 0 + tragain.StillRunningAfter = server + tragain.StillRunningAfter = ts + tragain.Processes.Default.Streams.stdout = Testers.ExcludesExpression("Could Not Connect", "Curl attempt should have succeeded") + + tr2 = urtest.AddTestRun("Update config files to enforced") + recordspath = ts.Disk.records_config.AbsPath + # recreate the records.config with the cert filename changed + tr2.Disk.File(recordspath, id="records_config", typename="ats:config:records"), + tr2.Disk.records_config.update( + { + 'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir), + 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts.Variables.SSLDir), + 'proxy.config.ssl.server.cipher_suite': + 'ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-SHA384:AES128-GCM-SHA256:AES256-GCM-SHA384:ECDHE-RSA-RC4-SHA:ECDHE-RSA-AES128-SHA:ECDHE-RSA-AES256-SHA:RC4-SHA:RC4-MD5:AES128-SHA:AES256-SHA:DES-CBC3-SHA!SRP:!DSS:!PSK:!aNULL:!eNULL:!SSLv2', + # set global policy + 'proxy.config.ssl.client.verify.server.policy': 'ENFORCED', + 'proxy.config.ssl.client.verify.server.properties': 'ALL', + 'proxy.config.ssl.client.CA.cert.path': '{0}'.format(ts.Variables.SSLDir), + 'proxy.config.ssl.client.CA.cert.filename': 'signer.pem', + 'proxy.config.exec_thread.autoconfig.scale': 1.0, + 'proxy.config.url_remap.pristine_host_hdr': 1, + 'proxy.config.diags.debug.enabled': 0, + 'proxy.config.diags.debug.tags': 'ssl' + }) + tr2.StillRunningAfter = ts + tr2.StillRunningAfter = server + tr2.Processes.Default.Command = 'echo Updated configs to ENFORCED' + # Need to copy over the environment so traffic_ctl knows where to find the unix domain socket + tr2.Processes.Default.Env = ts.Env + tr2.Processes.Default.ReturnCode = 0 + + trreload = urtest.AddConfigReload(ts, description="Reload config to ENFORCED") + trreload.StillRunningAfter = server + + tragain = urtest.AddTestRun("enforced-after-update") + tragain.MakeCurlCommand("-k -H \"host: random4.com\" https://127.0.0.1:{0}".format(ts.Variables.ssl_port), ts=ts) + tragain.ReturnCode = 0 + tragain.StillRunningAfter = server + tragain.StillRunningAfter = ts + tragain.Processes.Default.Streams.stdout = Testers.ContainsExpression("Could Not Connect", "Curl attempt should have succeeded") + + # No name checking for the sig-only permissive override for bad_bar + ts.Disk.diags_log.Content += Testers.ContainsExpression( + r"Core server certificate verification failed for \(random3.com\). Action=Continue", "Permissive can connect") + ts.Disk.diags_log.Content += Testers.ContainsExpression( + r"Core server certificate verification failed for \(random2.com\). Action=Terminate", "Enforced cannot connect") + urtest.execute() diff --git a/tests/uranium_tests/tls/test_tls_verify_base.py b/tests/uranium_tests/tls/test_tls_verify_base.py new file mode 100644 index 00000000000..ee082d2ddcc --- /dev/null +++ b/tests/uranium_tests/tls/test_tls_verify_base.py @@ -0,0 +1,161 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_tls_verify_base(urtest: UraniumTest) -> None: + ''' + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + urtest.Summary = ''' + Test tls server certificate verification options + ''' + + # Define default ATS + ts = urtest.MakeATSProcess("ts", enable_tls=True) + server_foo = urtest.MakeOriginServer( + "server_foo", + ssl=True, + options={ + "--key": "{0}/signed-foo.key".format(urtest.RunDirectory), + "--cert": "{0}/signed-foo.pem".format(urtest.RunDirectory) + }) + server_bar = urtest.MakeOriginServer( + "server_bar", + ssl=True, + options={ + "--key": "{0}/signed-bar.key".format(urtest.RunDirectory), + "--cert": "{0}/signed-bar.pem".format(urtest.RunDirectory) + }) + server = urtest.MakeOriginServer("server", ssl=True) + + request_foo_header = {"headers": "GET / HTTP/1.1\r\nHost: foo.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + request_bad_foo_header = {"headers": "GET / HTTP/1.1\r\nHost: badfoo.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + request_bar_header = {"headers": "GET / HTTP/1.1\r\nHost: bar.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + request_bad_bar_header = {"headers": "GET / HTTP/1.1\r\nHost: badbar.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + server_foo.addResponse("sessionlog.json", request_foo_header, response_header) + server_foo.addResponse("sessionlog.json", request_bad_foo_header, response_header) + server_bar.addResponse("sessionlog.json", request_bar_header, response_header) + server_bar.addResponse("sessionlog.json", request_bad_bar_header, response_header) + + # add ssl materials like key, certificates for the server + ts.addSSLfile("ssl/signed-foo.pem") + ts.addSSLfile("ssl/signed-foo.key") + ts.addSSLfile("ssl/signed-bar.pem") + ts.addSSLfile("ssl/signed-bar.key") + ts.addSSLfile("ssl/server.pem") + ts.addSSLfile("ssl/server.key") + ts.addSSLfile("ssl/signer.pem") + ts.addSSLfile("ssl/signer.key") + + ts.Disk.remap_config.AddLine('map / https://127.0.0.1:{0}'.format(server.Variables.SSL_Port)) + ts.Disk.remap_config.AddLine('map https://foo.com/ https://127.0.0.1:{0}'.format(server_foo.Variables.SSL_Port)) + ts.Disk.remap_config.AddLine('map https://bad_foo.com/ https://127.0.0.1:{0}'.format(server_foo.Variables.SSL_Port)) + ts.Disk.remap_config.AddLine('map https://bar.com/ https://127.0.0.1:{0}'.format(server_bar.Variables.SSL_Port)) + ts.Disk.remap_config.AddLine('map https://bad_bar.com/ https://127.0.0.1:{0}'.format(server_bar.Variables.SSL_Port)) + + ts.Disk.ssl_multicert_yaml.AddLines( + """ + ssl_multicert: + - dest_ip: "*" + ssl_cert_name: server.pem + ssl_key_name: server.key + """.split("\n")) + + # Case 1, global config policy=permissive properties=signature + # override for foo.com policy=enforced properties=all + ts.Disk.records_config.update( + { + 'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir), + 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts.Variables.SSLDir), + 'proxy.config.ssl.client.CA.cert.path': '{0}'.format(ts.Variables.SSLDir), + 'proxy.config.ssl.client.CA.cert.filename': 'signer.pem', + 'proxy.config.url_remap.pristine_host_hdr': 1, + 'proxy.config.exec_thread.autoconfig.scale': 1.0, + 'proxy.config.ssl.client.sni_policy': 'host', + 'proxy.config.ssl.client.verify.server.policy': 'PERMISSIVE' + }) + + ts.Disk.sni_yaml.AddLines( + [ + 'sni:', + '- fqdn: bar.com', + ' verify_server_policy: ENFORCED', + ' verify_server_properties: ALL', + '- fqdn: bad_bar.com', + ' verify_server_policy: ENFORCED', + ' verify_server_properties: ALL', + ]) + + tr = urtest.AddTestRun("Permissive-Test") + tr.Setup.Copy("ssl/signed-foo.key") + tr.Setup.Copy("ssl/signed-foo.pem") + tr.Setup.Copy("ssl/signed-bar.key") + tr.Setup.Copy("ssl/signed-bar.pem") + tr.MakeCurlCommand("-v -k -H \"host: foo.com\" https://127.0.0.1:{0}".format(ts.Variables.ssl_port), ts=ts) + tr.ReturnCode = 0 + tr.Processes.Default.StartBefore(server_foo) + tr.Processes.Default.StartBefore(server_bar) + tr.Processes.Default.StartBefore(server) + tr.Processes.Default.StartBefore(urtest.Processes.ts) + tr.StillRunningAfter = server + tr.StillRunningAfter = ts + tr.Processes.Default.Streams.stdout = Testers.ExcludesExpression("Could Not Connect", "Curl attempt should have succeeded") + + tr = urtest.AddTestRun("Permissive-Test with logged failure") + tr.MakeCurlCommand("-v -k -H \"host: random.com\" https://127.0.0.1:{0}".format(ts.Variables.ssl_port), ts=ts) + tr.ReturnCode = 0 + tr.StillRunningAfter = server + tr.StillRunningAfter = ts + tr.Processes.Default.Streams.stdout = Testers.ExcludesExpression("Could Not Connect", "Curl attempt should have succeeded") + + tr2 = urtest.AddTestRun("Override-enforcing-Test") + tr2.MakeCurlCommand("-v -k -H \"host: bar.com\" https://127.0.0.1:{0}".format(ts.Variables.ssl_port), ts=ts) + tr2.ReturnCode = 0 + tr2.StillRunningAfter = server + tr2.StillRunningAfter = ts + tr2.Processes.Default.Streams.stdout = Testers.ExcludesExpression("Could Not Connect", "Curl attempt should have succeeded") + + tr3 = urtest.AddTestRun("Override-enforcing-Test-fail-name-check") + tr3.MakeCurlCommand("-v -k -H \"host: bad_bar.com\" https://127.0.0.1:{0}".format(ts.Variables.ssl_port), ts=ts) + tr3.Processes.Default.Streams.stdout = Testers.ContainsExpression("Could Not Connect", "Curl attempt should have failed") + tr3.ReturnCode = 0 + tr3.StillRunningAfter = server + tr3.StillRunningAfter = ts + + # Over riding the built in ERROR check since we expect tr3 to fail + ts.Disk.diags_log.Content = Testers.ContainsExpression( + r"WARNING: SNI \(bad_bar.com\) not in certificate. Action=Terminate", "Make sure bad_bar name checked failed.") + ts.Disk.diags_log.Content += Testers.ContainsExpression( + r"WARNING: SNI \(random.com\) not in certificate. Action=Continue ", "Permissive failure for random") + urtest.execute() diff --git a/tests/uranium_tests/tls/test_tls_verify_ca_override.py b/tests/uranium_tests/tls/test_tls_verify_ca_override.py new file mode 100644 index 00000000000..e70d908350c --- /dev/null +++ b/tests/uranium_tests/tls/test_tls_verify_ca_override.py @@ -0,0 +1,160 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_tls_verify_ca_override(urtest: UraniumTest) -> None: + ''' + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + urtest.Summary = ''' + Test tls server certificate verification options. Exercise conf_remap for ca bundle path and file. + ''' + + # Define default ATS + ts = urtest.MakeATSProcess("ts") + server1 = urtest.MakeOriginServer( + "server1", + ssl=True, + options={ + "--key": "{0}/signed-foo.key".format(urtest.RunDirectory), + "--cert": "{0}/signed-foo.pem".format(urtest.RunDirectory) + }) + server2 = urtest.MakeOriginServer( + "server2", + ssl=True, + options={ + "--key": "{0}/signed-foo.key".format(urtest.RunDirectory), + "--cert": "{0}/signed2-foo.pem".format(urtest.RunDirectory) + }) + + request_foo_header = {"headers": "GET / HTTP/1.1\r\nHost: foo.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + request_bad_foo_header = {"headers": "GET / HTTP/1.1\r\nHost: bad_foo.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + request_bar_header = {"headers": "GET / HTTP/1.1\r\nHost: bar.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + request_bad_bar_header = {"headers": "GET / HTTP/1.1\r\nHost: bad_bar.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + server1.addResponse("sessionlog.json", request_foo_header, response_header) + server1.addResponse("sessionlog.json", request_bad_foo_header, response_header) + server2.addResponse("sessionlog.json", request_bar_header, response_header) + server2.addResponse("sessionlog.json", request_bad_bar_header, response_header) + + # add ssl materials like key, certificates for the server + ts.addSSLfile("ssl/signed-foo.pem") + ts.addSSLfile("ssl/signed2-foo.pem") + ts.addSSLfile("ssl/signed-foo.key") + ts.addSSLfile("ssl/server.pem") + ts.addSSLfile("ssl/server.key") + ts.addSSLfile("ssl/signer.pem") + ts.addSSLfile("ssl/signer.key") + ts.addSSLfile("ssl/signer2.pem") + ts.addSSLfile("ssl/signer2.key") + + def ca_cert_overrides(filename): + return ( + f'@pparam=proxy.config.ssl.client.CA.cert.path={ts.Variables.SSLDir} ' + f'@pparam=proxy.config.ssl.client.CA.cert.filename={filename}') + + ts.Disk.remap_config.AddLine( + f'map /case1 https://127.0.0.1:{server1.Variables.SSL_Port}/ ' + f'@plugin=conf_remap.so {ca_cert_overrides("signer.pem")}') + ts.Disk.remap_config.AddLine( + f'map /badcase1 https://127.0.0.1:{server1.Variables.SSL_Port}/ ' + f'@plugin=conf_remap.so {ca_cert_overrides("signer2.pem")}') + ts.Disk.remap_config.AddLine( + f'map /case2 https://127.0.0.1:{server2.Variables.SSL_Port}/ ' + f'@plugin=conf_remap.so {ca_cert_overrides("signer2.pem")}') + ts.Disk.remap_config.AddLine( + f'map /badcase2 https://127.0.0.1:{server2.Variables.SSL_Port}/ ' + f'@plugin=conf_remap.so {ca_cert_overrides("signer.pem")}') + + ts.Disk.ssl_multicert_yaml.AddLines( + """ + ssl_multicert: + - dest_ip: "*" + ssl_cert_name: server.pem + ssl_key_name: server.key + """.split("\n")) + + # Case 1, global config policy=permissive properties=signature + # override for foo.com policy=enforced properties=all + ts.Disk.records_config.update( + { + 'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir), + 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts.Variables.SSLDir), + # set global policy + 'proxy.config.ssl.client.verify.server.policy': 'ENFORCED', + 'proxy.config.ssl.client.verify.server.properties': 'SIGNATURE', + 'proxy.config.ssl.client.CA.cert.path': '/tmp', + 'proxy.config.ssl.client.CA.cert.filename': '{0}/signer.pem'.format(ts.Variables.SSLDir), + 'proxy.config.exec_thread.autoconfig.scale': 1.0, + 'proxy.config.url_remap.pristine_host_hdr': 1 + }) + + # Should succeed + tr = urtest.AddTestRun("Use correct ca bundle for server 1") + tr.MakeCurlCommand('-k -H \"host: foo.com\" http://127.0.0.1:{0}/case1'.format(ts.Variables.port), ts=ts) + tr.ReturnCode = 0 + tr.Setup.Copy("ssl/signed-foo.key") + tr.Setup.Copy("ssl/signed-foo.pem") + tr.Setup.Copy("ssl/signed2-foo.pem") + tr.Processes.Default.StartBefore(server1) + tr.Processes.Default.StartBefore(server2) + tr.Processes.Default.StartBefore(urtest.Processes.ts) + tr.StillRunningAfter = server1 + tr.StillRunningAfter = ts + # Should succeed. No message + tr.Processes.Default.Streams.stdout = Testers.ExcludesExpression("Could Not Connect", "Curl attempt should have succeeded") + + tr2 = urtest.AddTestRun("Use incorrect ca bundle for server 1") + tr2.MakeCurlCommand("-k -H \"host: bar.com\" http://127.0.0.1:{0}/badcase1".format(ts.Variables.port), ts=ts) + tr2.ReturnCode = 0 + tr2.StillRunningAfter = server1 + tr2.StillRunningAfter = ts + # Should succeed, but will be message in log about name mismatch + tr2.Processes.Default.Streams.stdout = Testers.ContainsExpression("Could Not Connect", "Curl attempt should have succeeded") + + tr2 = urtest.AddTestRun("Use correct ca bundle for server 2") + tr2.MakeCurlCommand("-k -H \"host: random.com\" http://127.0.0.1:{0}/case2".format(ts.Variables.port), ts=ts) + tr2.ReturnCode = 0 + tr2.StillRunningAfter = server2 + tr2.StillRunningAfter = ts + # Should succeed, but will be message in log about signature + tr2.Processes.Default.Streams.stdout = Testers.ExcludesExpression("Could Not Connect", "Curl attempt should have succeeded") + + tr3 = urtest.AddTestRun("User incorrect ca bundle for server 2") + tr3.MakeCurlCommand("-k -H \"host: foo.com\" http://127.0.0.1:{0}/badcase2".format(ts.Variables.port), ts=ts) + tr3.ReturnCode = 0 + tr3.StillRunningAfter = server2 + tr3.StillRunningAfter = ts + # Should succeed. No error messages + tr3.Processes.Default.Streams.stdout = Testers.ContainsExpression("Could Not Connect", "Curl attempt should have succeeded") + urtest.execute() diff --git a/tests/uranium_tests/tls/test_tls_verify_not_pristine.py b/tests/uranium_tests/tls/test_tls_verify_not_pristine.py new file mode 100644 index 00000000000..49b8ed3fdac --- /dev/null +++ b/tests/uranium_tests/tls/test_tls_verify_not_pristine.py @@ -0,0 +1,131 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_tls_verify_not_pristine(urtest: UraniumTest) -> None: + ''' + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + urtest.Summary = ''' + Test tls server certificate verification without a pristine host header + ''' + + # Define default ATS + ts = urtest.MakeATSProcess("ts", enable_tls=True) + server_foo = urtest.MakeOriginServer( + "server_foo", + ssl=True, + options={ + "--key": "{0}/signed-foo.key".format(urtest.RunDirectory), + "--cert": "{0}/signed-foo.pem".format(urtest.RunDirectory) + }) + server = urtest.MakeOriginServer("server", ssl=True) + dns = urtest.MakeDNServer("dns") + + request_foo_header = {"headers": "GET / HTTP/1.1\r\nHost: foo.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + request_bad_foo_header = {"headers": "GET / HTTP/1.1\r\nHost: badfoo.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + server_foo.addResponse("sessionlog.json", request_foo_header, response_header) + server_foo.addResponse("sessionlog.json", request_bad_foo_header, response_header) + + # add ssl materials like key, certificates for the server + ts.addSSLfile("ssl/signed-foo.pem") + ts.addSSLfile("ssl/signed-foo.key") + ts.addSSLfile("ssl/signed-bar.pem") + ts.addSSLfile("ssl/signed-bar.key") + ts.addSSLfile("ssl/server.pem") + ts.addSSLfile("ssl/server.key") + ts.addSSLfile("ssl/signer.pem") + ts.addSSLfile("ssl/signer.key") + + ts.Disk.remap_config.AddLine( + 'map https://bar.com:{0}/ https://foo.com:{1}'.format(ts.Variables.ssl_port, server_foo.Variables.SSL_Port)) + ts.Disk.remap_config.AddLine( + 'map https://foo.com:{0}/ https://bar.com:{1}'.format(ts.Variables.ssl_port, server_foo.Variables.SSL_Port)) + + ts.Disk.ssl_multicert_yaml.AddLines( + """ + ssl_multicert: + - dest_ip: "*" + ssl_cert_name: server.pem + ssl_key_name: server.key + """.split("\n")) + + # Case 1, global config policy=permissive properties=signature + # override for foo.com policy=enforced properties=all + ts.Disk.records_config.update( + { + 'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir), + 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts.Variables.SSLDir), + # set global policy + 'proxy.config.ssl.client.verify.server.policy': 'ENFORCED', + 'proxy.config.ssl.client.verify.server.properties': 'ALL', + 'proxy.config.ssl.client.CA.cert.path': '{0}'.format(ts.Variables.SSLDir), + 'proxy.config.ssl.client.CA.cert.filename': 'signer.pem', + 'proxy.config.url_remap.pristine_host_hdr': 0, + 'proxy.config.dns.nameservers': '127.0.0.1:{0}'.format(dns.Variables.Port), + 'proxy.config.exec_thread.autoconfig.scale': 1.0, + 'proxy.config.dns.resolv_conf': 'NULL' + }) + + dns.addRecords(records={"foo.com.": ["127.0.0.1"]}) + dns.addRecords(records={"bar.com.": ["127.0.0.1"]}) + + # bar.com in. foo.com out. Should verify + tr = urtest.AddTestRun("Enforced-Good-Test") + tr.Setup.Copy("ssl/signed-foo.key") + tr.Setup.Copy("ssl/signed-foo.pem") + tr.Setup.Copy("ssl/signed-bar.key") + tr.Setup.Copy("ssl/signed-bar.pem") + tr.MakeCurlCommand("-v --resolve 'bar.com:{0}:127.0.0.1' -k https://bar.com:{0}".format(ts.Variables.ssl_port), ts=ts) + tr.ReturnCode = 0 + tr.Processes.Default.StartBefore(server_foo) + tr.Processes.Default.StartBefore(dns) + tr.Processes.Default.StartBefore(urtest.Processes.ts) + tr.StillRunningAfter = server + tr.StillRunningAfter = ts + tr.Processes.Default.Streams.stdout = Testers.ExcludesExpression("Could Not Connect", "Curl attempt should have succeeded") + + # foo.com in. bar.com out. Should not verify + tr2 = urtest.AddTestRun("Enforced-bad-test") + tr2.MakeCurlCommand("-v -k --resolve 'foo.com:{0}:127.0.0.1' https://foo.com:{0}".format(ts.Variables.ssl_port), ts=ts) + tr2.ReturnCode = 0 + tr2.StillRunningAfter = server + tr2.StillRunningAfter = ts + tr2.Processes.Default.Streams.stdout = Testers.ContainsExpression("Could Not Connect", "Curl attempt should not have succeeded") + + # Over riding the built in ERROR check since we expect tr3 to fail + ts.Disk.diags_log.Content = Testers.ExcludesExpression("verification failed", "Make sure the signatures didn't fail") + ts.Disk.diags_log.Content += Testers.ContainsExpression( + r"WARNING: SNI \(bar.com\) not in certificate", "Make sure bad_bar name checked failed.") + urtest.execute() diff --git a/tests/uranium_tests/tls/test_tls_verify_override.py b/tests/uranium_tests/tls/test_tls_verify_override.py new file mode 100644 index 00000000000..da762740e2a --- /dev/null +++ b/tests/uranium_tests/tls/test_tls_verify_override.py @@ -0,0 +1,298 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_tls_verify_override(urtest: UraniumTest) -> None: + ''' + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + urtest.Summary = ''' + Test tls server certificate verification options. Exercise conf_remap + ''' + + # Define default ATS + ts = urtest.MakeATSProcess("ts") + server_foo = urtest.MakeOriginServer( + "server_foo", + ssl=True, + options={ + "--key": "{0}/signed-foo.key".format(urtest.RunDirectory), + "--cert": "{0}/signed-foo.pem".format(urtest.RunDirectory) + }) + server_bar = urtest.MakeOriginServer( + "server_bar", + ssl=True, + options={ + "--key": "{0}/signed-bar.key".format(urtest.RunDirectory), + "--cert": "{0}/signed-bar.pem".format(urtest.RunDirectory) + }) + server = urtest.MakeOriginServer("server", ssl=True) + + dns = urtest.MakeDNServer("dns") + + request_foo_header = {"headers": "GET / HTTP/1.1\r\nHost: foo.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + request_bad_foo_header = {"headers": "GET / HTTP/1.1\r\nHost: bad_foo.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + request_bar_header = {"headers": "GET / HTTP/1.1\r\nHost: bar.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + request_bad_bar_header = {"headers": "GET / HTTP/1.1\r\nHost: bad_bar.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + server_foo.addResponse("sessionlog.json", request_foo_header, response_header) + server_foo.addResponse("sessionlog.json", request_bad_foo_header, response_header) + server_bar.addResponse("sessionlog.json", request_bar_header, response_header) + server_bar.addResponse("sessionlog.json", request_bad_bar_header, response_header) + + # add ssl materials like key, certificates for the server + ts.addSSLfile("ssl/signed-foo.pem") + ts.addSSLfile("ssl/signed-foo.key") + ts.addSSLfile("ssl/signed-bar.pem") + ts.addSSLfile("ssl/signed-bar.key") + ts.addSSLfile("ssl/server.pem") + ts.addSSLfile("ssl/server.key") + ts.addSSLfile("ssl/signer.pem") + ts.addSSLfile("ssl/signer.key") + + ts.Disk.remap_config.AddLine('map http://foo.com/basictobar https://bar.com:{0}'.format(server_bar.Variables.SSL_Port)) + ts.Disk.remap_config.AddLine('map http://foo.com/basic https://foo.com:{0}'.format(server_foo.Variables.SSL_Port)) + ts.Disk.remap_config.AddLine( + 'map http://foo.com/override https://foo.com:{0} @plugin=conf_remap.so @pparam=proxy.config.ssl.client.verify.server.policy=ENFORCED' + .format(server_foo.Variables.SSL_Port)) + ts.Disk.remap_config.AddLine('map http://bar.com/basic https://bar.com:{0}'.format(server_foo.Variables.SSL_Port)) + ts.Disk.remap_config.AddLine( + 'map http://bar.com/overridedisabled https://bar.com:{0} @plugin=conf_remap.so @pparam=proxy.config.ssl.client.verify.server.policy=DISABLED' + .format(server_foo.Variables.SSL_Port)) + ts.Disk.remap_config.AddLine( + 'map http://bad_bar.com/overridedisabled https://bar.com:{0} @plugin=conf_remap.so @pparam=proxy.config.ssl.client.verify.server.policy=DISABLED' + .format(server_foo.Variables.SSL_Port)) + ts.Disk.remap_config.AddLine( + 'map http://bar.com/overridesignature https://bar.com:{0} @plugin=conf_remap.so @pparam=proxy.config.ssl.client.verify.server.properties=SIGNATURE @plugin=conf_remap.so @pparam=proxy.config.ssl.client.verify.server.policy=ENFORCED' + .format(server_foo.Variables.SSL_Port)) + ts.Disk.remap_config.AddLine( + 'map http://bar.com/overridenone https://bar.com:{0} @plugin=conf_remap.so @pparam=proxy.config.ssl.client.verify.server.properties=NONE @plugin=conf_remap.so @pparam=proxy.config.ssl.client.verify.server.policy=ENFORCED' + .format(server_foo.Variables.SSL_Port)) + ts.Disk.remap_config.AddLine( + 'map http://bar.com/overrideenforced https://bar.com:{0} @plugin=conf_remap.so @pparam=proxy.config.ssl.client.verify.server.policy=ENFORCED' + .format(server_foo.Variables.SSL_Port)) + ts.Disk.remap_config.AddLine('map /basic https://random.com:{0}'.format(server.Variables.SSL_Port)) + ts.Disk.remap_config.AddLine( + 'map /overrideenforce https://127.0.0.1:{0} @plugin=conf_remap.so @pparam=proxy.config.ssl.client.verify.server.policy=ENFORCED' + .format(server.Variables.SSL_Port)) + ts.Disk.remap_config.AddLine( + 'map /overridename https://127.0.0.1:{0} @plugin=conf_remap.so @pparam=proxy.config.ssl.client.verify.server.properties=NAME' + .format(server.Variables.SSL_Port)) + ts.Disk.remap_config.AddLine( + 'map /snipolicyfooremap https://foo.com:{0} @plugin=conf_remap.so @pparam=proxy.config.ssl.client.verify.server.properties=NAME @plugin=conf_remap.so @pparam=proxy.config.ssl.client.verify.server.policy=ENFORCED @plugin=conf_remap.so @pparam=proxy.config.ssl.client.sni_policy=remap' + .format(server_bar.Variables.SSL_Port)) + ts.Disk.remap_config.AddLine( + 'map /snipolicyfoohost https://foo.com:{0} @plugin=conf_remap.so @pparam=proxy.config.ssl.client.verify.server.properties=NAME @plugin=conf_remap.so @pparam=proxy.config.ssl.client.verify.server.policy=ENFORCED @plugin=conf_remap.so @pparam=proxy.config.ssl.client.sni_policy=host' + .format(server_bar.Variables.SSL_Port)) + ts.Disk.remap_config.AddLine( + 'map /snipolicybarremap https://bar.com:{0} @plugin=conf_remap.so @pparam=proxy.config.ssl.client.verify.server.properties=NAME @plugin=conf_remap.so @pparam=proxy.config.ssl.client.verify.server.policy=ENFORCED @plugin=conf_remap.so @pparam=proxy.config.ssl.client.sni_policy=remap' + .format(server_bar.Variables.SSL_Port)) + ts.Disk.remap_config.AddLine( + 'map /snipolicybarhost https://bar.com:{0} @plugin=conf_remap.so @pparam=proxy.config.ssl.client.verify.server.properties=NAME @plugin=conf_remap.so @pparam=proxy.config.ssl.client.verify.server.policy=ENFORCED @plugin=conf_remap.so @pparam=proxy.config.ssl.client.sni_policy=host' + .format(server_bar.Variables.SSL_Port)) + + ts.Disk.ssl_multicert_yaml.AddLines( + """ + ssl_multicert: + - dest_ip: "*" + ssl_cert_name: server.pem + ssl_key_name: server.key + """.split("\n")) + + # Case 1, global config policy=permissive properties=signature + # override for foo.com policy=enforced properties=all + ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'ssl', + 'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir), + 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts.Variables.SSLDir), + # set global policy + 'proxy.config.ssl.client.verify.server.policy': 'PERMISSIVE', + 'proxy.config.ssl.client.verify.server.properties': 'ALL', + 'proxy.config.ssl.client.CA.cert.path': '{0}'.format(ts.Variables.SSLDir), + 'proxy.config.ssl.client.CA.cert.filename': 'signer.pem', + 'proxy.config.url_remap.pristine_host_hdr': 1, + 'proxy.config.dns.nameservers': '127.0.0.1:{0}'.format(dns.Variables.Port), + 'proxy.config.dns.resolv_conf': 'NULL', + 'proxy.config.exec_thread.autoconfig.scale': 1.0, + 'proxy.config.ssl.client.sni_policy': 'remap', + }) + + dns.addRecords(records={"foo.com.": ["127.0.0.1"]}) + dns.addRecords(records={"bar.com.": ["127.0.0.1"]}) + dns.addRecords(records={"random.com.": ["127.0.0.1"]}) + + # Should succeed without message + tr = urtest.AddTestRun("default-permissive-success") + tr.Setup.Copy("ssl/signed-foo.key") + tr.Setup.Copy("ssl/signed-foo.pem") + tr.Setup.Copy("ssl/signed-bar.key") + tr.Setup.Copy("ssl/signed-bar.pem") + tr.MakeCurlCommand('-v -k -H \"host: foo.com\" http://127.0.0.1:{0}/basic'.format(ts.Variables.port), ts=ts) + tr.ReturnCode = 0 + tr.Processes.Default.StartBefore(dns) + tr.Processes.Default.StartBefore(server_foo) + tr.Processes.Default.StartBefore(server_bar) + tr.Processes.Default.StartBefore(server) + tr.Processes.Default.StartBefore(urtest.Processes.ts) + tr.StillRunningAfter = server + tr.StillRunningAfter = ts + # Should succeed. No message + tr.Processes.Default.Streams.All = Testers.ExcludesExpression("Could Not Connect", "Curl attempt should have succeeded") + tr.Processes.Default.Streams.All += Testers.ContainsExpression("200 OK", "Curl attempt should have succeeded") + + tr2 = urtest.AddTestRun("default-permissive-fail") + tr2.MakeCurlCommand("-v -k -H \"host: bar.com\" http://127.0.0.1:{0}/basic".format(ts.Variables.port), ts=ts) + tr2.ReturnCode = 0 + tr2.StillRunningAfter = server + tr2.StillRunningAfter = ts + # Should succeed, but will be message in log about name mismatch + tr2.Processes.Default.Streams.All = Testers.ExcludesExpression("Could Not Connect", "Curl attempt should have succeeded") + tr2.Processes.Default.Streams.All += Testers.ContainsExpression("200 OK", "Curl attempt should have succeeded") + + tr2 = urtest.AddTestRun("default-permissive-fail2") + tr2.MakeCurlCommand("-v -k -H \"host: random.com\" http://127.0.0.1:{0}/basic".format(ts.Variables.port), ts=ts) + tr2.ReturnCode = 0 + tr2.StillRunningAfter = server + tr2.StillRunningAfter = ts + # Should succeed, but will be message in log about signature + tr2.Processes.Default.Streams.All = Testers.ExcludesExpression("Could Not Connect", "Curl attempt should have succeeded") + tr2.Processes.Default.Streams.All += Testers.ContainsExpression("404 Not Found", "Curl attempt should have succeeded") + + tr3 = urtest.AddTestRun("default-foo-to-bar") + tr3.MakeCurlCommand("-k -v -H \"host: foo.com\" http://127.0.0.1:{0}/basictobar".format(ts.Variables.port), ts=ts) + tr3.ReturnCode = 0 + tr3.StillRunningAfter = server + tr3.StillRunningAfter = ts + # Should succeed. No error messages + tr3.Processes.Default.Streams.All = Testers.ExcludesExpression("Could Not Connect", "Curl attempt should have succeeded") + tr3.Processes.Default.Streams.All += Testers.ContainsExpression("200 OK", "Curl attempt should have succeeded") + + tr3 = urtest.AddTestRun("override-foo") + tr3.MakeCurlCommand("-k -v -H \"host: foo.com\" http://127.0.0.1:{0}/override".format(ts.Variables.port), ts=ts) + tr3.ReturnCode = 0 + tr3.StillRunningAfter = server + tr3.StillRunningAfter = ts + # Should succeed. No error messages + tr3.Processes.Default.Streams.All = Testers.ExcludesExpression("Could Not Connect", "Curl attempt should have succeeded") + tr3.Processes.Default.Streams.All += Testers.ContainsExpression("200 OK", "Curl attempt should have succeeded") + + tr4 = urtest.AddTestRun("override-bar-disabled") + tr4.MakeCurlCommand("-k -v -H \"host: bad_bar.com\" http://127.0.0.1:{0}/overridedisabled".format(ts.Variables.port), ts=ts) + tr4.ReturnCode = 0 + tr4.StillRunningAfter = server + tr4.StillRunningAfter = ts + # Succeed. No error messages + tr4.Processes.Default.Streams.All = Testers.ExcludesExpression("Could Not Connect", "Curl attempt should have succeeded") + tr4.Processes.Default.Streams.All += Testers.ContainsExpression("200 OK", "Curl attempt should have succeeded") + + tr5 = urtest.AddTestRun("override-bar-signature-enforced") + tr5.MakeCurlCommand("-k -v -H \"host: bar.com\" http://127.0.0.1:{0}/overridesignature".format(ts.Variables.port), ts=ts) + tr5.ReturnCode = 0 + tr5.Processes.Default.Streams.All = Testers.ExcludesExpression("Could Not Connect", "Curl attempt should have succeeded") + tr5.Processes.Default.Streams.All += Testers.ContainsExpression("200 OK", "Curl attempt should have succeeded") + tr5.StillRunningAfter = server + tr5.StillRunningAfter = ts + + tr5a = urtest.AddTestRun("override-bar-none-permissive") + tr5a.MakeCurlCommand("-k -v -H \"host: bar.com\" http://127.0.0.1:{0}/overridenone".format(ts.Variables.port), ts=ts) + tr5a.ReturnCode = 0 + tr5a.Processes.Default.Streams.All = Testers.ExcludesExpression("Could Not Connect", "Curl attempt should have succeeded") + tr5a.Processes.Default.Streams.All += Testers.ContainsExpression("200 OK", "Curl attempt should have succeeded") + tr5a.StillRunningAfter = server + tr5a.StillRunningAfter = ts + + tr6 = urtest.AddTestRun("override-bar-enforced") + tr6.MakeCurlCommand("-v -k -H \"host: bar.com\" http://127.0.0.1:{0}/overrideenforced".format(ts.Variables.port), ts=ts) + tr6.ReturnCode = 0 + # Should fail + tr6.Processes.Default.Streams.All = Testers.ContainsExpression("Could Not Connect", "Curl attempt should have failed") + tr6.StillRunningAfter = server + tr6.StillRunningAfter = ts + + # Should succeed + tr = urtest.AddTestRun("foo-to-bar-sni-policy-remap") + tr.MakeCurlCommand("-v -k -H \"host: foo.com\" http://127.0.0.1:{0}/snipolicybarremap".format(ts.Variables.port), ts=ts) + tr.ReturnCode = 0 + tr.StillRunningAfter = server + tr.StillRunningAfter = ts + tr.Processes.Default.Streams.All = Testers.ExcludesExpression("Could not connect", "Curl attempt should succeed") + tr.Processes.Default.Streams.All += Testers.ContainsExpression("200 OK", "Curl attempt should have succeeded") + + # Should fail + tr = urtest.AddTestRun("foo-to-bar-sni-policy-host") + tr.MakeCurlCommand("-v -k -H \"host: foo.com\" http://127.0.0.1:{0}/snipolicybarhost".format(ts.Variables.port), ts=ts) + tr.ReturnCode = 0 + tr.StillRunningAfter = server + tr.StillRunningAfter = ts + tr.Processes.Default.Streams.All = Testers.ContainsExpression("Could not connect", "Curl attempt should fail") + + # Should fail + tr = urtest.AddTestRun("bar-to-foo-sni-policy-remap") + tr.MakeCurlCommand("-v -k -H \"host: bar.com\" http://127.0.0.1:{0}/snipolicyfooremap".format(ts.Variables.port), ts=ts) + tr.ReturnCode = 0 + tr.StillRunningAfter = server + tr.StillRunningAfter = ts + tr.Processes.Default.Streams.stdout = Testers.ContainsExpression("Could not connect", "Curl attempt should fail") + + # Should succeed + tr = urtest.AddTestRun("bar-to-foo-sni-policy-host") + tr.MakeCurlCommand("-v -k -H \"host: bar.com\" http://127.0.0.1:{0}/snipolicyfoohost".format(ts.Variables.port), ts=ts) + tr.ReturnCode = 0 + tr.StillRunningAfter = server + tr.StillRunningAfter = ts + tr.Processes.Default.Streams.stdout = Testers.ExcludesExpression("Could not connect", "Curl attempt should succeed") + + # Over riding the built in ERROR check since we expect some cases to fail + + # checks on random.com should fail with message only + ts.Disk.diags_log.Content = Testers.ContainsExpression( + r"WARNING: Core server certificate verification failed for \(random.com\). Action=Continue Error=self.signed certificate server=random.com\(127.0.0.1\) depth=0", + "Warning for self signed certificate") + # permissive failure for bar.com + ts.Disk.diags_log.Content += Testers.ContainsExpression( + r"WARNING: SNI \(bar.com\) not in certificate. Action=Continue server=bar.com\(127.0.0.1\)", + "Warning on missing name for bar.com") + # name check failure for random.com + ts.Disk.diags_log.Content += Testers.ContainsExpression( + r"WARNING: SNI \(random.com\) not in certificate. Action=Continue server=random.com\(127.0.0.1\)", + "Warning on missing name for random.com") + # name check failure for bar.com + ts.Disk.diags_log.Content += Testers.ContainsExpression( + r"WARNING: SNI \(bar.com\) not in certificate. Action=Terminate server=bar.com\(127.0.0.1\)", + "Failure on missing name for bar.com") + # See if the explicitly set default sni_policy of remap works. + ts.Disk.diags_log.Content += Testers.ExcludesExpression( + r"WARNING: SNI \(foo.com\) not in certificate. Action=Continue", "Warning on missing name for foo.com") + urtest.execute() diff --git a/tests/uranium_tests/tls/test_tls_verify_override_base.py b/tests/uranium_tests/tls/test_tls_verify_override_base.py new file mode 100644 index 00000000000..9c89d6e30b7 --- /dev/null +++ b/tests/uranium_tests/tls/test_tls_verify_override_base.py @@ -0,0 +1,285 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_tls_verify_override_base(urtest: UraniumTest) -> None: + ''' + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + urtest.Summary = ''' + Test tls server certificate verification options. Exercise conf_remap + ''' + + # Define default ATS + ts = urtest.MakeATSProcess("ts", enable_tls=True) + server_foo = urtest.MakeOriginServer( + "server_foo", + ssl=True, + options={ + "--key": "{0}/signed-foo.key".format(urtest.RunDirectory), + "--cert": "{0}/signed-foo.pem".format(urtest.RunDirectory) + }) + server_bar = urtest.MakeOriginServer( + "server_bar", + ssl=True, + options={ + "--key": "{0}/signed-bar.key".format(urtest.RunDirectory), + "--cert": "{0}/signed-bar.pem".format(urtest.RunDirectory) + }) + server = urtest.MakeOriginServer("server", ssl=True) + + dns = urtest.MakeDNServer("dns") + + request_foo_header = {"headers": "GET / HTTP/1.1\r\nHost: foo.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + request_bad_foo_header = {"headers": "GET / HTTP/1.1\r\nHost: bad_foo.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + request_bar_header = {"headers": "GET / HTTP/1.1\r\nHost: bar.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + request_bad_bar_header = {"headers": "GET / HTTP/1.1\r\nHost: bad_bar.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + server_foo.addResponse("sessionlog.json", request_foo_header, response_header) + server_foo.addResponse("sessionlog.json", request_bad_foo_header, response_header) + server_bar.addResponse("sessionlog.json", request_bar_header, response_header) + server_bar.addResponse("sessionlog.json", request_bad_bar_header, response_header) + + # add ssl materials like key, certificates for the server + ts.addSSLfile("ssl/signed-foo.pem") + ts.addSSLfile("ssl/signed-foo.key") + ts.addSSLfile("ssl/signed-bar.pem") + ts.addSSLfile("ssl/signed-bar.key") + ts.addSSLfile("ssl/server.pem") + ts.addSSLfile("ssl/server.key") + ts.addSSLfile("ssl/signer.pem") + ts.addSSLfile("ssl/signer.key") + + ts.Disk.remap_config.AddLine('map http://foo.com/basic https://foo.com:{0}'.format(server_foo.Variables.SSL_Port)) + ts.Disk.remap_config.AddLine( + 'map http://foo.com/override https://foo.com:{0} @plugin=conf_remap.so @pparam=proxy.config.ssl.client.verify.server.policy=ENFORCED' + .format(server_foo.Variables.SSL_Port)) + ts.Disk.remap_config.AddLine('map http://bar.com/basic https://bar.com:{0}'.format(server_foo.Variables.SSL_Port)) + ts.Disk.remap_config.AddLine( + 'map http://bar.com/overridedisabled https://bar.com:{0} @plugin=conf_remap.so @pparam=proxy.config.ssl.client.verify.server.policy=DISABLED' + .format(server_foo.Variables.SSL_Port)) + ts.Disk.remap_config.AddLine( + 'map http://bar.com/overridesignature https://bar.com:{0} @plugin=conf_remap.so @pparam=proxy.config.ssl.client.verify.server.properties=SIGNATURE @plugin=conf_remap.so @pparam=proxy.config.ssl.client.verify.server.policy=ENFORCED' + .format(server_foo.Variables.SSL_Port)) + ts.Disk.remap_config.AddLine( + 'map http://bar.com/overrideenforced https://bar.com:{0} @plugin=conf_remap.so @pparam=proxy.config.ssl.client.verify.server.policy=ENFORCED' + .format(server_foo.Variables.SSL_Port)) + ts.Disk.remap_config.AddLine('map /basic https://127.0.0.1:{0}'.format(server.Variables.SSL_Port)) + ts.Disk.remap_config.AddLine( + 'map /overrideenforce https://127.0.0.1:{0} @plugin=conf_remap.so @pparam=proxy.config.ssl.client.verify.server.policy=ENFORCED' + .format(server.Variables.SSL_Port)) + ts.Disk.remap_config.AddLine( + 'map /overridename https://127.0.0.1:{0} @plugin=conf_remap.so @pparam=proxy.config.ssl.client.verify.server.properties=NAME' + .format(server.Variables.SSL_Port)) + ts.Disk.remap_config.AddLine( + 'map /snipolicyfooremap https://foo.com:{0} @plugin=conf_remap.so @pparam=proxy.config.ssl.client.verify.server.properties=NAME @plugin=conf_remap.so @pparam=proxy.config.ssl.client.verify.server.policy=ENFORCED @plugin=conf_remap.so @pparam=proxy.config.ssl.client.sni_policy=remap' + .format(server_bar.Variables.SSL_Port)) + ts.Disk.remap_config.AddLine( + 'map /snipolicyfoohost https://foo.com:{0} @plugin=conf_remap.so @pparam=proxy.config.ssl.client.verify.server.properties=NAME @plugin=conf_remap.so @pparam=proxy.config.ssl.client.verify.server.policy=ENFORCED @plugin=conf_remap.so @pparam=proxy.config.ssl.client.sni_policy=host' + .format(server_bar.Variables.SSL_Port)) + ts.Disk.remap_config.AddLine( + 'map /snipolicyfooservername https://foo.com:{0} @plugin=conf_remap.so @pparam=proxy.config.ssl.client.verify.server.properties=NAME @plugin=conf_remap.so @pparam=proxy.config.ssl.client.verify.server.policy=ENFORCED @plugin=conf_remap.so @pparam=proxy.config.ssl.client.sni_policy=server_name' + .format(server_bar.Variables.SSL_Port)) + ts.Disk.remap_config.AddLine( + 'map /snipolicybarremap https://bar.com:{0} @plugin=conf_remap.so @pparam=proxy.config.ssl.client.verify.server.properties=NAME @plugin=conf_remap.so @pparam=proxy.config.ssl.client.verify.server.policy=ENFORCED @plugin=conf_remap.so @pparam=proxy.config.ssl.client.sni_policy=remap' + .format(server_bar.Variables.SSL_Port)) + ts.Disk.remap_config.AddLine( + 'map /snipolicybarhost https://bar.com:{0} @plugin=conf_remap.so @pparam=proxy.config.ssl.client.verify.server.properties=NAME @plugin=conf_remap.so @pparam=proxy.config.ssl.client.verify.server.policy=ENFORCED @plugin=conf_remap.so @pparam=proxy.config.ssl.client.sni_policy=host' + .format(server_bar.Variables.SSL_Port)) + ts.Disk.remap_config.AddLine( + 'map /snipolicybarservername https://bar.com:{0} @plugin=conf_remap.so @pparam=proxy.config.ssl.client.verify.server.properties=NAME @plugin=conf_remap.so @pparam=proxy.config.ssl.client.verify.server.policy=ENFORCED @plugin=conf_remap.so @pparam=proxy.config.ssl.client.sni_policy=server_name' + .format(server_bar.Variables.SSL_Port)) + + ts.Disk.ssl_multicert_yaml.AddLines( + """ + ssl_multicert: + - dest_ip: "*" + ssl_cert_name: server.pem + ssl_key_name: server.key + """.split("\n")) + + # Case 1, global config policy=permissive properties=signature + # override for foo.com policy=enforced properties=all + ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'ssl', + 'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir), + 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts.Variables.SSLDir), + 'proxy.config.ssl.client.CA.cert.path': '{0}'.format(ts.Variables.SSLDir), + 'proxy.config.ssl.client.CA.cert.filename': 'signer.pem', + 'proxy.config.url_remap.pristine_host_hdr': 1, + 'proxy.config.exec_thread.autoconfig.scale': 1.0, + 'proxy.config.dns.nameservers': '127.0.0.1:{0}'.format(dns.Variables.Port), + 'proxy.config.dns.resolv_conf': 'NULL', + 'proxy.config.ssl.client.verify.server.policy': 'PERMISSIVE', + 'proxy.config.http.connect.down.policy': 1, # Don't count TLS failures when deciding whehter the server is down. + }) + + dns.addRecords(records={"foo.com.": ["127.0.0.1"]}) + dns.addRecords(records={"bar.com.": ["127.0.0.1"]}) + + # Should succeed without message + tr = urtest.AddTestRun("default-permissive-success") + tr.Setup.Copy("ssl/signed-foo.key") + tr.Setup.Copy("ssl/signed-foo.pem") + tr.Setup.Copy("ssl/signed-bar.key") + tr.Setup.Copy("ssl/signed-bar.pem") + tr.MakeCurlCommand('-k -H \"host: foo.com\" http://127.0.0.1:{0}/basic'.format(ts.Variables.port), ts=ts) + tr.ReturnCode = 0 + tr.Processes.Default.StartBefore(dns) + tr.Processes.Default.StartBefore(server_foo) + tr.Processes.Default.StartBefore(server_bar) + tr.Processes.Default.StartBefore(server) + tr.Processes.Default.StartBefore(urtest.Processes.ts) + tr.StillRunningAfter = server + tr.StillRunningAfter = ts + # Should succeed. No message + tr.Processes.Default.Streams.stdout = Testers.ExcludesExpression("Could Not Connect", "Curl attempt should have succeeded") + + tr2 = urtest.AddTestRun("default-permissive-fail") + tr2.MakeCurlCommand("-k -H \"host: bar.com\" http://127.0.0.1:{0}/basic".format(ts.Variables.port), ts=ts) + tr2.ReturnCode = 0 + tr2.StillRunningAfter = server + tr2.StillRunningAfter = ts + # Should succeed, but will be message in log about name mismatch + tr2.Processes.Default.Streams.stdout = Testers.ExcludesExpression("Could Not Connect", "Curl attempt should have succeeded") + + tr2 = urtest.AddTestRun("default-permissive-fail2") + tr2.MakeCurlCommand("-k -H \"host: random.com\" http://127.0.0.1:{0}/basic".format(ts.Variables.port), ts=ts) + tr2.ReturnCode = 0 + tr2.StillRunningAfter = server + tr2.StillRunningAfter = ts + # Should succeed, but will be message in log about signature + tr2.Processes.Default.Streams.stdout = Testers.ExcludesExpression("Could Not Connect", "Curl attempt should have succeeded") + + tr3 = urtest.AddTestRun("override-foo") + tr3.MakeCurlCommand("-k -H \"host: foo.com\" http://127.0.0.1:{0}/override".format(ts.Variables.port), ts=ts) + tr3.ReturnCode = 0 + tr3.StillRunningAfter = server + tr3.StillRunningAfter = ts + # Should succeed. No error messages + tr3.Processes.Default.Streams.stdout = Testers.ExcludesExpression("Could Not Connect", "Curl attempt should have succeeded") + + tr4 = urtest.AddTestRun("override-bar-disabled") + tr4.MakeCurlCommand("-k -H \"host: bad_bar.com\" http://127.0.0.1:{0}/overridedisabled".format(ts.Variables.port), ts=ts) + tr4.ReturnCode = 0 + tr4.StillRunningAfter = server + tr4.StillRunningAfter = ts + # Succeed. No error messages + tr4.Processes.Default.Streams.stdout = Testers.ExcludesExpression("Could Not Connect", "Curl attempt should have succeeded") + + tr5 = urtest.AddTestRun("override-bar-signature-enforced") + tr5.MakeCurlCommand("-k -H \"host: bar.com\" http://127.0.0.1:{0}/overridesignature".format(ts.Variables.port), ts=ts) + tr5.ReturnCode = 0 + tr5.Processes.Default.Streams.stdout = Testers.ExcludesExpression("Could Not Connect", "Curl attempt should have succeeded") + tr5.StillRunningAfter = server + tr5.StillRunningAfter = ts + + tr6 = urtest.AddTestRun("override-bar-enforced") + tr6.MakeCurlCommand("-k -H \"host: bar.com\" http://127.0.0.1:{0}/overrideenforced".format(ts.Variables.port), ts=ts) + tr6.ReturnCode = 0 + # Should fail + tr6.Processes.Default.Streams.stdout = Testers.ContainsExpression("Could Not Connect", "Curl attempt should have failed") + tr6.StillRunningAfter = server + tr6.StillRunningAfter = ts + + # Should succeed + tr = urtest.AddTestRun("foo-to-bar-sni-policy-remap") + tr.MakeCurlCommand("-k -H \"host: foo.com\" http://127.0.0.1:{0}/snipolicybarremap".format(ts.Variables.port), ts=ts) + tr.ReturnCode = 0 + tr.StillRunningAfter = server + tr.StillRunningAfter = ts + tr.Processes.Default.Streams.stdout = Testers.ExcludesExpression("Could not connect", "Curl attempt should succeed") + + # Should fail + tr = urtest.AddTestRun("foo-to-bar-sni-policy-host") + tr.MakeCurlCommand("-k -H \"host: foo.com\" http://127.0.0.1:{0}/snipolicybarhost".format(ts.Variables.port), ts=ts) + tr.ReturnCode = 0 + tr.StillRunningAfter = server + tr.StillRunningAfter = ts + tr.Processes.Default.Streams.stdout = Testers.ContainsExpression("Could not connect", "Curl attempt should fail") + + # Should fail + tr = urtest.AddTestRun("foo-to-bar-sni-policy-servername") + tr.MakeCurlCommand( + "-k --resolve foo.com:{0}:127.0.0.1 https://foo.com:{0}/snipolicybarservername".format(ts.Variables.ssl_port), ts=ts) + tr.ReturnCode = 0 + tr.StillRunningAfter = server + tr.StillRunningAfter = ts + tr.Processes.Default.Streams.stdout = Testers.ContainsExpression("Could not connect", "Curl attempt should fail") + + # Should fail + tr = urtest.AddTestRun("bar-to-foo-sni-policy-remap") + tr.MakeCurlCommand("-k -H \"host: bar.com\" http://127.0.0.1:{0}/snipolicyfooremap".format(ts.Variables.port), ts=ts) + tr.ReturnCode = 0 + tr.StillRunningAfter = server + tr.StillRunningAfter = ts + tr.Processes.Default.Streams.stdout = Testers.ContainsExpression("Could not connect", "Curl attempt should fail") + + # Should succeed + tr = urtest.AddTestRun("bar-to-foo-sni-policy-host") + tr.MakeCurlCommand("-k -H \"host: bar.com\" http://127.0.0.1:{0}/snipolicyfoohost".format(ts.Variables.port), ts=ts) + tr.ReturnCode = 0 + tr.StillRunningAfter = server + tr.StillRunningAfter = ts + tr.Processes.Default.Streams.stdout = Testers.ExcludesExpression("Could not connect", "Curl attempt should succeed") + + # Should succeed + tr = urtest.AddTestRun("bar-to-foo-sni-policy-servername") + tr.MakeCurlCommand( + "-k --resolve bar.com:{0}:127.0.0.1 https://bar.com:{0}/snipolicyfooservername".format(ts.Variables.ssl_port), ts=ts) + tr.ReturnCode = 0 + tr.StillRunningAfter = server + tr.StillRunningAfter = ts + tr.Processes.Default.Streams.stdout = Testers.ExcludesExpression("Could not connect", "Curl attempt should succeed") + + # Over riding the built in ERROR check since we expect some cases to fail + + # checks on random.com should fail with message only + ts.Disk.diags_log.Content = Testers.ContainsExpression( + r"WARNING: Core server certificate verification failed for \(random.com\). Action=Continue Error=self.signed certificate server=127.0.0.1\(127.0.0.1\) depth=0", + "Warning for self signed certificate") + # permissive failure for bar.com + ts.Disk.diags_log.Content += Testers.ContainsExpression( + r"WARNING: SNI \(bar.com\) not in certificate. Action=Continue server=bar.com\(127.0.0.1\)", + "Warning on missing name for bar.com") + # name check failure for random.com + ts.Disk.diags_log.Content += Testers.ContainsExpression( + r"WARNING: SNI \(random.com\) not in certificate. Action=Continue server=127.0.0.1\(127.0.0.1\)", + "Warning on missing name for random.com") + # name check failure for bar.com + ts.Disk.diags_log.Content += Testers.ContainsExpression( + r"WARNING: SNI \(bar.com\) not in certificate. Action=Terminate server=bar.com\(127.0.0.1\)", + "Failure on missing name for bar.com") + urtest.execute() diff --git a/tests/uranium_tests/tls/test_tls_verify_override_sni.py b/tests/uranium_tests/tls/test_tls_verify_override_sni.py new file mode 100644 index 00000000000..d6ea297447a --- /dev/null +++ b/tests/uranium_tests/tls/test_tls_verify_override_sni.py @@ -0,0 +1,180 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_tls_verify_override_sni(urtest: UraniumTest) -> None: + ''' + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + urtest.Summary = ''' + Test tls server certificate verification options. Exercise conf_remap + ''' + + # Define default ATS + ts = urtest.MakeATSProcess("ts") + cafile = "{0}/signer.pem".format(urtest.RunDirectory) + + server_foo = urtest.MakeOriginServer( + "server_foo", + ssl=True, + options={ + "--key": "{0}/signed-foo.key".format(urtest.RunDirectory), + "--cert": "{0}/signed-foo.pem".format(urtest.RunDirectory), + "--clientCA": cafile, + "--clientverify": "" + }, + clientcert="{0}/signed-bar.pem".format(urtest.RunDirectory), + clientkey="{0}/signed-bar.key".format(urtest.RunDirectory)) + server_bar = urtest.MakeOriginServer( + "server_bar", + ssl=True, + options={ + "--key": "{0}/signed-foo.key".format(urtest.RunDirectory), + "--cert": "{0}/signed-foo.pem".format(urtest.RunDirectory), + "--clientCA": cafile, + "--clientverify": "" + }, + clientcert="{0}/signed-bar.pem".format(urtest.RunDirectory), + clientkey="{0}/signed-bar.key".format(urtest.RunDirectory)) + + dns = urtest.MakeDNServer("dns") + + request_foo_header = {"headers": "GET / HTTP/1.1\r\nHost: foo.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + request_bad_foo_header = {"headers": "GET / HTTP/1.1\r\nHost: bad_foo.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + request_bar_header = {"headers": "GET / HTTP/1.1\r\nHost: bar.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + request_bad_bar_header = {"headers": "GET / HTTP/1.1\r\nHost: bad_bar.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + server_foo.addResponse("sessionlog.json", request_foo_header, response_header) + server_foo.addResponse("sessionlog.json", request_bad_foo_header, response_header) + server_bar.addResponse("sessionlog.json", request_bar_header, response_header) + server_bar.addResponse("sessionlog.json", request_bad_bar_header, response_header) + + server_bar.Setup.Copy("ssl/signer.pem") + server_bar.Setup.Copy("ssl/signer2.pem") + server_foo.Setup.Copy("ssl/signer.pem") + server_foo.Setup.Copy("ssl/signer2.pem") + + # add ssl materials like key, certificates for the server + ts.addSSLfile("ssl/signed-foo.pem") + ts.addSSLfile("ssl/signed-foo.key") + ts.addSSLfile("ssl/signed-bar.pem") + ts.addSSLfile("ssl/signed-bar.key") + ts.addSSLfile("ssl/server.pem") + ts.addSSLfile("ssl/server.key") + ts.addSSLfile("ssl/signer.pem") + ts.addSSLfile("ssl/signer.key") + + ts.Disk.remap_config.AddLine('map http://foo.com/defaultbar https://bar.com:{0}'.format(server_bar.Variables.SSL_Port)) + ts.Disk.remap_config.AddLine('map http://foo.com/default https://foo.com:{0}'.format(server_foo.Variables.SSL_Port)) + ts.Disk.remap_config.AddLine( + 'map http://foo.com/overridepolicy https://bar.com:{0} @plugin=conf_remap.so @pparam=proxy.config.ssl.client.verify.server.policy=ENFORCED' + .format(server_foo.Variables.SSL_Port)) + ts.Disk.remap_config.AddLine( + 'map http://foo.com/overrideproperties https://bar.com:{0} @plugin=conf_remap.so @pparam=proxy.config.ssl.client.verify.server.properties=SIGNATURE' + .format(server_foo.Variables.SSL_Port)) + + ts.Disk.ssl_multicert_yaml.AddLines( + """ + ssl_multicert: + - dest_ip: "*" + ssl_cert_name: server.pem + ssl_key_name: server.key + """.split("\n")) + + # global config policy=permissive properties=all + ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'ssl', + 'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir), + 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts.Variables.SSLDir), + # set global policy + 'proxy.config.ssl.client.verify.server.policy': 'PERMISSIVE', + 'proxy.config.ssl.client.verify.server.properties': 'ALL', + 'proxy.config.ssl.client.CA.cert.path': '{0}'.format(ts.Variables.SSLDir), + 'proxy.config.ssl.client.CA.cert.filename': 'signer.pem', + 'proxy.config.url_remap.pristine_host_hdr': 1, + 'proxy.config.dns.nameservers': '127.0.0.1:{0}'.format(dns.Variables.Port), + 'proxy.config.dns.resolv_conf': 'NULL', + 'proxy.config.exec_thread.autoconfig.scale': 1.0, + 'proxy.config.ssl.client.sni_policy': 'remap' + }) + + ts.Disk.sni_yaml.AddLines( + [ + 'sni:', + '- fqdn: bar.com', + ' client_cert: "{0}/signed-foo.pem"'.format(ts.Variables.SSLDir), + ' client_key: "{0}/signed-foo.key"'.format(ts.Variables.SSLDir), + ]) + + dns.addRecords(records={"foo.com.": ["127.0.0.1"]}) + dns.addRecords(records={"bar.com.": ["127.0.0.1"]}) + dns.addRecords(records={"random.com.": ["127.0.0.1"]}) + + # Should succeed with message + # exercise default settings + tr = urtest.AddTestRun("default-permissive-success") + tr.Setup.Copy("ssl/signed-foo.key") + tr.Setup.Copy("ssl/signed-foo.pem") + tr.Setup.Copy("ssl/signed-bar.key") + tr.Setup.Copy("ssl/signed-bar.pem") + tr.MakeCurlCommand('-k -H \"host: foo.com\" http://127.0.0.1:{0}/defaultbar'.format(ts.Variables.port), ts=ts) + tr.ReturnCode = 0 + tr.Processes.Default.StartBefore(dns) + tr.Processes.Default.StartBefore(server_foo) + tr.Processes.Default.StartBefore(server_bar) + tr.Processes.Default.StartBefore(urtest.Processes.ts) + tr.StillRunningAfter = ts + tr.Processes.Default.Streams.stdout = Testers.ExcludesExpression("Could Not Connect", "Curl attempt should have succeeded") + + # should fail. Exercise the override + tr2 = urtest.AddTestRun("policy-override-fail") + tr2.MakeCurlCommand("-k -H \"host: foo.com\" http://127.0.0.1:{0}/overridepolicy".format(ts.Variables.port), ts=ts) + tr2.ReturnCode = 0 + tr2.StillRunningAfter = ts + tr2.Processes.Default.Streams.stdout = Testers.ContainsExpression("Could Not Connect", "Curl attempt should fail") + + # should succeed with an error message + tr2 = urtest.AddTestRun("properties-override-permissive") + tr2.MakeCurlCommand("-k -H \"host: foo.com\" http://127.0.0.1:{0}/overrideproperties".format(ts.Variables.port), ts=ts) + tr2.ReturnCode = 0 + tr2.StillRunningAfter = ts + tr2.Processes.Default.Streams.stdout = Testers.ExcludesExpression("Could Not Connect", "Curl attempt should have succeeded") + + # Over riding the built in ERROR check since we expect some cases to fail + ts.Disk.diags_log.Content = Testers.ContainsExpression( + r"WARNING: SNI \(bar.com\) not in certificate. Action=Continue server=bar.com", "Warning for mismatch name not enforcing") + ts.Disk.diags_log.Content += Testers.ContainsExpression( + r" WARNING: SNI \(bar.com\) not in certificate. Action=Terminate server=bar.com", "Warning for enforcing mismatch") + urtest.execute() diff --git a/tests/gold_tests/tls/tls_accept_timeout_crash.replay.yaml b/tests/uranium_tests/tls/tls_accept_timeout_crash.replay.yaml similarity index 100% rename from tests/gold_tests/tls/tls_accept_timeout_crash.replay.yaml rename to tests/uranium_tests/tls/tls_accept_timeout_crash.replay.yaml diff --git a/tests/gold_tests/tls/tls_client_alpn_configuration.replay.yaml b/tests/uranium_tests/tls/tls_client_alpn_configuration.replay.yaml similarity index 100% rename from tests/gold_tests/tls/tls_client_alpn_configuration.replay.yaml rename to tests/uranium_tests/tls/tls_client_alpn_configuration.replay.yaml diff --git a/tests/gold_tests/tls/tls_engine_abort.py b/tests/uranium_tests/tls/tls_engine_abort.py similarity index 100% rename from tests/gold_tests/tls/tls_engine_abort.py rename to tests/uranium_tests/tls/tls_engine_abort.py diff --git a/tests/gold_tests/tls/tls_flow_control_client.py b/tests/uranium_tests/tls/tls_flow_control_client.py similarity index 100% rename from tests/gold_tests/tls/tls_flow_control_client.py rename to tests/uranium_tests/tls/tls_flow_control_client.py diff --git a/tests/gold_tests/tls/tls_post_abort_client.py b/tests/uranium_tests/tls/tls_post_abort_client.py similarity index 100% rename from tests/gold_tests/tls/tls_post_abort_client.py rename to tests/uranium_tests/tls/tls_post_abort_client.py diff --git a/tests/gold_tests/tls/tls_post_abort_origin.py b/tests/uranium_tests/tls/tls_post_abort_origin.py similarity index 100% rename from tests/gold_tests/tls/tls_post_abort_origin.py rename to tests/uranium_tests/tls/tls_post_abort_origin.py diff --git a/tests/gold_tests/tls/tls_record_size_client.py b/tests/uranium_tests/tls/tls_record_size_client.py similarity index 100% rename from tests/gold_tests/tls/tls_record_size_client.py rename to tests/uranium_tests/tls/tls_record_size_client.py diff --git a/tests/gold_tests/tls/tls_reload_under_load_client.py b/tests/uranium_tests/tls/tls_reload_under_load_client.py similarity index 100% rename from tests/gold_tests/tls/tls_reload_under_load_client.py rename to tests/uranium_tests/tls/tls_reload_under_load_client.py diff --git a/tests/gold_tests/tls/tls_renegotiation_allowed_client.py b/tests/uranium_tests/tls/tls_renegotiation_allowed_client.py similarity index 100% rename from tests/gold_tests/tls/tls_renegotiation_allowed_client.py rename to tests/uranium_tests/tls/tls_renegotiation_allowed_client.py diff --git a/tests/gold_tests/tls/tls_renegotiation_client.py b/tests/uranium_tests/tls/tls_renegotiation_client.py similarity index 100% rename from tests/gold_tests/tls/tls_renegotiation_client.py rename to tests/uranium_tests/tls/tls_renegotiation_client.py diff --git a/tests/gold_tests/tls/tls_session_key_logging.replay.yaml b/tests/uranium_tests/tls/tls_session_key_logging.replay.yaml similarity index 100% rename from tests/gold_tests/tls/tls_session_key_logging.replay.yaml rename to tests/uranium_tests/tls/tls_session_key_logging.replay.yaml diff --git a/tests/gold_tests/tls/tls_sni_with_port.replay.yaml b/tests/uranium_tests/tls/tls_sni_with_port.replay.yaml similarity index 100% rename from tests/gold_tests/tls/tls_sni_with_port.replay.yaml rename to tests/uranium_tests/tls/tls_sni_with_port.replay.yaml diff --git a/tests/gold_tests/tls_hooks/gold/cert-1.gold b/tests/uranium_tests/tls_hooks/gold/cert-1.gold similarity index 100% rename from tests/gold_tests/tls_hooks/gold/cert-1.gold rename to tests/uranium_tests/tls_hooks/gold/cert-1.gold diff --git a/tests/gold_tests/tls_hooks/gold/client-hello-1.gold b/tests/uranium_tests/tls_hooks/gold/client-hello-1.gold similarity index 100% rename from tests/gold_tests/tls_hooks/gold/client-hello-1.gold rename to tests/uranium_tests/tls_hooks/gold/client-hello-1.gold diff --git a/tests/gold_tests/tls_hooks/gold/preaccept-1.gold b/tests/uranium_tests/tls_hooks/gold/preaccept-1.gold similarity index 100% rename from tests/gold_tests/tls_hooks/gold/preaccept-1.gold rename to tests/uranium_tests/tls_hooks/gold/preaccept-1.gold diff --git a/tests/gold_tests/tls_hooks/gold/sni-1.gold b/tests/uranium_tests/tls_hooks/gold/sni-1.gold similarity index 100% rename from tests/gold_tests/tls_hooks/gold/sni-1.gold rename to tests/uranium_tests/tls_hooks/gold/sni-1.gold diff --git a/tests/gold_tests/tls_hooks/gold/ts-cert-1-im-2.gold b/tests/uranium_tests/tls_hooks/gold/ts-cert-1-im-2.gold similarity index 100% rename from tests/gold_tests/tls_hooks/gold/ts-cert-1-im-2.gold rename to tests/uranium_tests/tls_hooks/gold/ts-cert-1-im-2.gold diff --git a/tests/gold_tests/tls_hooks/gold/ts-cert-1.gold b/tests/uranium_tests/tls_hooks/gold/ts-cert-1.gold similarity index 100% rename from tests/gold_tests/tls_hooks/gold/ts-cert-1.gold rename to tests/uranium_tests/tls_hooks/gold/ts-cert-1.gold diff --git a/tests/gold_tests/tls_hooks/gold/ts-cert-2.gold b/tests/uranium_tests/tls_hooks/gold/ts-cert-2.gold similarity index 100% rename from tests/gold_tests/tls_hooks/gold/ts-cert-2.gold rename to tests/uranium_tests/tls_hooks/gold/ts-cert-2.gold diff --git a/tests/gold_tests/tls_hooks/gold/ts-cert-im-1.gold b/tests/uranium_tests/tls_hooks/gold/ts-cert-im-1.gold similarity index 100% rename from tests/gold_tests/tls_hooks/gold/ts-cert-im-1.gold rename to tests/uranium_tests/tls_hooks/gold/ts-cert-im-1.gold diff --git a/tests/gold_tests/tls_hooks/gold/ts-client-hello-1.gold b/tests/uranium_tests/tls_hooks/gold/ts-client-hello-1.gold similarity index 100% rename from tests/gold_tests/tls_hooks/gold/ts-client-hello-1.gold rename to tests/uranium_tests/tls_hooks/gold/ts-client-hello-1.gold diff --git a/tests/gold_tests/tls_hooks/gold/ts-client-hello-2.gold b/tests/uranium_tests/tls_hooks/gold/ts-client-hello-2.gold similarity index 100% rename from tests/gold_tests/tls_hooks/gold/ts-client-hello-2.gold rename to tests/uranium_tests/tls_hooks/gold/ts-client-hello-2.gold diff --git a/tests/gold_tests/tls_hooks/gold/ts-client-hello-delayed-1.gold b/tests/uranium_tests/tls_hooks/gold/ts-client-hello-delayed-1.gold similarity index 100% rename from tests/gold_tests/tls_hooks/gold/ts-client-hello-delayed-1.gold rename to tests/uranium_tests/tls_hooks/gold/ts-client-hello-delayed-1.gold diff --git a/tests/gold_tests/tls_hooks/gold/ts-close-out-close.gold b/tests/uranium_tests/tls_hooks/gold/ts-close-out-close.gold similarity index 100% rename from tests/gold_tests/tls_hooks/gold/ts-close-out-close.gold rename to tests/uranium_tests/tls_hooks/gold/ts-close-out-close.gold diff --git a/tests/gold_tests/tls_hooks/gold/ts-out-delay-start-2.gold b/tests/uranium_tests/tls_hooks/gold/ts-out-delay-start-2.gold similarity index 100% rename from tests/gold_tests/tls_hooks/gold/ts-out-delay-start-2.gold rename to tests/uranium_tests/tls_hooks/gold/ts-out-delay-start-2.gold diff --git a/tests/gold_tests/tls_hooks/gold/ts-out-start-close-2.gold b/tests/uranium_tests/tls_hooks/gold/ts-out-start-close-2.gold similarity index 100% rename from tests/gold_tests/tls_hooks/gold/ts-out-start-close-2.gold rename to tests/uranium_tests/tls_hooks/gold/ts-out-start-close-2.gold diff --git a/tests/gold_tests/tls_hooks/gold/ts-preaccept-1.gold b/tests/uranium_tests/tls_hooks/gold/ts-preaccept-1.gold similarity index 100% rename from tests/gold_tests/tls_hooks/gold/ts-preaccept-1.gold rename to tests/uranium_tests/tls_hooks/gold/ts-preaccept-1.gold diff --git a/tests/gold_tests/tls_hooks/gold/ts-preaccept-2.gold b/tests/uranium_tests/tls_hooks/gold/ts-preaccept-2.gold similarity index 100% rename from tests/gold_tests/tls_hooks/gold/ts-preaccept-2.gold rename to tests/uranium_tests/tls_hooks/gold/ts-preaccept-2.gold diff --git a/tests/gold_tests/tls_hooks/gold/ts-preaccept-delayed-1-immdate-2.gold b/tests/uranium_tests/tls_hooks/gold/ts-preaccept-delayed-1-immdate-2.gold similarity index 100% rename from tests/gold_tests/tls_hooks/gold/ts-preaccept-delayed-1-immdate-2.gold rename to tests/uranium_tests/tls_hooks/gold/ts-preaccept-delayed-1-immdate-2.gold diff --git a/tests/gold_tests/tls_hooks/gold/ts-preaccept-delayed-1.gold b/tests/uranium_tests/tls_hooks/gold/ts-preaccept-delayed-1.gold similarity index 100% rename from tests/gold_tests/tls_hooks/gold/ts-preaccept-delayed-1.gold rename to tests/uranium_tests/tls_hooks/gold/ts-preaccept-delayed-1.gold diff --git a/tests/gold_tests/tls_hooks/gold/ts-preaccept1-sni1-cert1.gold b/tests/uranium_tests/tls_hooks/gold/ts-preaccept1-sni1-cert1.gold similarity index 100% rename from tests/gold_tests/tls_hooks/gold/ts-preaccept1-sni1-cert1.gold rename to tests/uranium_tests/tls_hooks/gold/ts-preaccept1-sni1-cert1.gold diff --git a/tests/gold_tests/tls_hooks/gold/ts-sni-1.gold b/tests/uranium_tests/tls_hooks/gold/ts-sni-1.gold similarity index 100% rename from tests/gold_tests/tls_hooks/gold/ts-sni-1.gold rename to tests/uranium_tests/tls_hooks/gold/ts-sni-1.gold diff --git a/tests/gold_tests/tls_hooks/gold/ts-sni-2.gold b/tests/uranium_tests/tls_hooks/gold/ts-sni-2.gold similarity index 100% rename from tests/gold_tests/tls_hooks/gold/ts-sni-2.gold rename to tests/uranium_tests/tls_hooks/gold/ts-sni-2.gold diff --git a/tests/uranium_tests/tls_hooks/test_tls_hooks.py b/tests/uranium_tests/tls_hooks/test_tls_hooks.py new file mode 100644 index 00000000000..ac21a9e3abb --- /dev/null +++ b/tests/uranium_tests/tls_hooks/test_tls_hooks.py @@ -0,0 +1,101 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_tls_hooks(urtest: UraniumTest) -> None: + ''' + Test 1 preaccept callback (without delay) + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import os + import re + + urtest.Summary = ''' + Test different combinations of TLS handshake hooks to ensure they are applied consistently. + ''' + + ts = urtest.MakeATSProcess("ts", enable_tls=True) + server = urtest.MakeOriginServer("server") + request_header = {"headers": "GET / HTTP/1.1\r\nHost: www.example.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + # desired response form the origin server + response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + server.addResponse("sessionlog.json", request_header, response_header) + + ts.addDefaultSSLFiles() + + ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.show_location': 0, + 'proxy.config.diags.debug.tags': 'ssl_hook_test', + 'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir), + 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts.Variables.SSLDir), + 'proxy.config.ssl.TLSv1_3.enabled': 0, + }) + + ts.Disk.ssl_multicert_yaml.AddLines( + """ + ssl_multicert: + - dest_ip: "*" + ssl_cert_name: server.pem + ssl_key_name: server.key + """.split("\n")) + + ts.Disk.remap_config.AddLine( + 'map https://example.com:{0} http://127.0.0.1:{1}'.format(ts.Variables.ssl_port, server.Variables.Port)) + + urtest.PrepareTestPlugin(os.path.join(urtest.Variables.AtsTestPluginsDir, 'ssl_hook_test.so'), ts, '-preaccept=1') + + tr = urtest.AddTestRun("Test one preaccept hook") + tr.Processes.Default.StartBefore(server) + tr.Processes.Default.StartBefore(urtest.Processes.ts) + tr.StillRunningAfter = ts + tr.StillRunningAfter = server + tr.MakeCurlCommand('-v -k -H \'host:example.com:{0}\' https://127.0.0.1:{0}'.format(ts.Variables.ssl_port), ts=ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.stdout = "gold/preaccept-1.gold" + tr.Processes.Default.Streams.All = Testers.ExcludesExpression( + "TLSv1.3 (IN), TLS handshake, Finished (20):", "Should not negotiate a TLSv1.3 connection") + + ts.Disk.traffic_out.Content = "gold/ts-preaccept-1.gold" + + # the preaccept may get triggered twice because the test framework creates a TCP connection before handing off to traffic_server + preacceptstring = "Pre accept callback 0" + ts.Disk.traffic_out.Content = Testers.ContainsExpression( + r"\A(?:(?!{0}).)*{0}.*({0})?(?!.*{0}).*\Z".format(preacceptstring), + "Pre accept message appears only once or twice", + reflags=re.S | re.M) + + tr.Processes.Default.TimeOut = 15 + tr.TimeOut = 15 + urtest.execute() diff --git a/tests/uranium_tests/tls_hooks/test_tls_hooks10.py b/tests/uranium_tests/tls_hooks/test_tls_hooks10.py new file mode 100644 index 00000000000..d4539f086f2 --- /dev/null +++ b/tests/uranium_tests/tls_hooks/test_tls_hooks10.py @@ -0,0 +1,89 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_tls_hooks10(urtest: UraniumTest) -> None: + ''' + Test one delayed cert callback and two immediate cert callbacks + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import os + + urtest.Summary = ''' + Test different combinations of TLS handshake hooks to ensure they are applied consistently. + ''' + + ts = urtest.MakeATSProcess("ts", enable_tls=True) + server = urtest.MakeOriginServer("server") + request_header = {"headers": "GET / HTTP/1.1\r\nHost: www.example.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + # desired response form the origin server + response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + server.addResponse("sessionlog.json", request_header, response_header) + + ts.addDefaultSSLFiles() + + ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.show_location': 0, + 'proxy.config.diags.debug.tags': 'ssl_hook_test', + 'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir), + 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts.Variables.SSLDir), + }) + + ts.Disk.ssl_multicert_yaml.AddLines( + """ + ssl_multicert: + - dest_ip: "*" + ssl_cert_name: server.pem + ssl_key_name: server.key + """.split("\n")) + + ts.Disk.remap_config.AddLine( + 'map https://example.com:{0} http://127.0.0.1:{1}'.format(ts.Variables.ssl_port, server.Variables.Port)) + + urtest.PrepareTestPlugin(os.path.join(urtest.Variables.AtsTestPluginsDir, 'ssl_hook_test.so'), ts, '-cert=1 -i=2') + + tr = urtest.AddTestRun("Test a combination of delayed and immediate cert hooks") + tr.Processes.Default.StartBefore(server) + tr.Processes.Default.StartBefore(urtest.Processes.ts) + tr.StillRunningAfter = ts + tr.StillRunningAfter = server + tr.MakeCurlCommand('-k -H \'host:example.com:{0}\' https://127.0.0.1:{0}'.format(ts.Variables.ssl_port), ts=ts) + tr.Processes.Default.ReturnCode = 0 + + ts.Disk.traffic_out.Content = "gold/ts-cert-1-im-2.gold" + + tr.Processes.Default.TimeOut = 15 + tr.TimeOut = 15 + urtest.execute() diff --git a/tests/uranium_tests/tls_hooks/test_tls_hooks11.py b/tests/uranium_tests/tls_hooks/test_tls_hooks11.py new file mode 100644 index 00000000000..92f4b8e09ee --- /dev/null +++ b/tests/uranium_tests/tls_hooks/test_tls_hooks11.py @@ -0,0 +1,96 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_tls_hooks11(urtest: UraniumTest) -> None: + ''' + Test one delayed preaccept callback + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import os + import re + + urtest.Summary = ''' + Test different combinations of TLS handshake hooks to ensure they are applied consistently. + ''' + + ts = urtest.MakeATSProcess("ts", enable_tls=True) + server = urtest.MakeOriginServer("server") + request_header = {"headers": "GET / HTTP/1.1\r\nHost: www.example.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + # desired response form the origin server + response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + server.addResponse("sessionlog.json", request_header, response_header) + + ts.addDefaultSSLFiles() + + ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.show_location': 0, + 'proxy.config.diags.debug.tags': 'ssl_hook_test', + 'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir), + 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts.Variables.SSLDir), + }) + + ts.Disk.ssl_multicert_yaml.AddLines( + """ + ssl_multicert: + - dest_ip: "*" + ssl_cert_name: server.pem + ssl_key_name: server.key + """.split("\n")) + + ts.Disk.remap_config.AddLine( + 'map https://example.com:{0} http://127.0.0.1:{1}'.format(ts.Variables.ssl_port, server.Variables.Port)) + + urtest.PrepareTestPlugin(os.path.join(urtest.Variables.AtsTestPluginsDir, 'ssl_hook_test.so'), ts, '-d=1') + + tr = urtest.AddTestRun("Test one delayed preaccept hook") + tr.Processes.Default.StartBefore(server) + tr.Processes.Default.StartBefore(urtest.Processes.ts) + tr.StillRunningAfter = ts + tr.StillRunningAfter = server + tr.MakeCurlCommand('-k -H \'host:example.com:{0}\' https://127.0.0.1:{0}'.format(ts.Variables.ssl_port), ts=ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.stdout = "gold/preaccept-1.gold" + + ts.Disk.traffic_out.Content = "gold/ts-preaccept-delayed-1.gold" + + preacceptstring = "Pre accept delay callback 0" + ts.Disk.traffic_out.Content = Testers.ContainsExpression( + r"\A(?:(?!{0}).)*{0}.*({0})?(?!.*{0}).*\Z".format(preacceptstring), + "Pre accept message appears only once or twice", + reflags=re.S | re.M) + tr.Processes.Default.TimeOut = 15 + tr.TimeOut = 15 + urtest.execute() diff --git a/tests/uranium_tests/tls_hooks/test_tls_hooks12.py b/tests/uranium_tests/tls_hooks/test_tls_hooks12.py new file mode 100644 index 00000000000..57749fd1c2e --- /dev/null +++ b/tests/uranium_tests/tls_hooks/test_tls_hooks12.py @@ -0,0 +1,89 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_tls_hooks12(urtest: UraniumTest) -> None: + ''' + Test a combination of delayed and immediate preaccept callbacks + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import os + + urtest.Summary = ''' + Test different combinations of TLS handshake hooks to ensure they are applied consistently. + ''' + + ts = urtest.MakeATSProcess("ts", enable_tls=True) + server = urtest.MakeOriginServer("server") + request_header = {"headers": "GET / HTTP/1.1\r\nHost: www.example.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + # desired response form the origin server + response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + server.addResponse("sessionlog.json", request_header, response_header) + + ts.addDefaultSSLFiles() + + ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.show_location': 0, + 'proxy.config.diags.debug.tags': 'ssl_hook_test', + 'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir), + 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts.Variables.SSLDir), + }) + + ts.Disk.ssl_multicert_yaml.AddLines( + """ + ssl_multicert: + - dest_ip: "*" + ssl_cert_name: server.pem + ssl_key_name: server.key + """.split("\n")) + + ts.Disk.remap_config.AddLine( + 'map https://example.com:{0} http://127.0.0.1:{1}'.format(ts.Variables.ssl_port, server.Variables.Port)) + + urtest.PrepareTestPlugin(os.path.join(urtest.Variables.AtsTestPluginsDir, 'ssl_hook_test.so'), ts, '-p=2 -d=1') + + tr = urtest.AddTestRun("Test combination of delayed and immediate preaccept hook2") + tr.Processes.Default.StartBefore(server) + tr.Processes.Default.StartBefore(urtest.Processes.ts) + tr.StillRunningAfter = ts + tr.StillRunningAfter = server + tr.MakeCurlCommand('-k -H \'host:example.com:{0}\' https://127.0.0.1:{0}'.format(ts.Variables.ssl_port), ts=ts) + tr.Processes.Default.ReturnCode = 0 + + ts.Disk.traffic_out.Content = "gold/ts-preaccept-delayed-1-immdate-2.gold" + + tr.Processes.Default.TimeOut = 15 + tr.TimeOut = 15 + urtest.execute() diff --git a/tests/uranium_tests/tls_hooks/test_tls_hooks13.py b/tests/uranium_tests/tls_hooks/test_tls_hooks13.py new file mode 100644 index 00000000000..9678116a5ea --- /dev/null +++ b/tests/uranium_tests/tls_hooks/test_tls_hooks13.py @@ -0,0 +1,89 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_tls_hooks13(urtest: UraniumTest) -> None: + ''' + Test one outbound start and two outbound close + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import os + + urtest.Summary = ''' + Test different combinations of TLS handshake hooks to ensure they are applied consistently. + ''' + + ts = urtest.MakeATSProcess("ts", enable_tls=True) + server = urtest.MakeOriginServer("server", ssl=True) + request_header = {"headers": "GET / HTTP/1.1\r\nHost: www.example.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + # desired response form the origin server + response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + server.addResponse("sessionlog.json", request_header, response_header) + + ts.addDefaultSSLFiles() + + ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.show_location': 0, + 'proxy.config.diags.debug.tags': 'ssl_hook_test', + 'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir), + 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts.Variables.SSLDir), + }) + + ts.Disk.ssl_multicert_yaml.AddLines( + """ + ssl_multicert: + - dest_ip: "*" + ssl_cert_name: server.pem + ssl_key_name: server.key + """.split("\n")) + + ts.Disk.remap_config.AddLine( + 'map https://example.com:{0} https://127.0.0.1:{1}'.format(ts.Variables.ssl_port, server.Variables.SSL_Port)) + + urtest.PrepareTestPlugin(os.path.join(urtest.Variables.AtsTestPluginsDir, 'ssl_hook_test.so'), ts, '-out_start=1 -out_close=2') + + tr = urtest.AddTestRun("Test outbound start and close") + tr.Processes.Default.StartBefore(server) + tr.Processes.Default.StartBefore(urtest.Processes.ts) + tr.StillRunningAfter = ts + tr.StillRunningAfter = server + tr.MakeCurlCommand('-k -H \'host:example.com:{0}\' https://127.0.0.1:{0}'.format(ts.Variables.ssl_port), ts=ts) + tr.Processes.Default.ReturnCode = 0 + + ts.Disk.traffic_out.Content = "gold/ts-out-start-close-2.gold" + + tr.Processes.Default.TimeOut = 15 + tr.TimeOut = 15 + urtest.execute() diff --git a/tests/uranium_tests/tls_hooks/test_tls_hooks14.py b/tests/uranium_tests/tls_hooks/test_tls_hooks14.py new file mode 100644 index 00000000000..9fa3a5e4a63 --- /dev/null +++ b/tests/uranium_tests/tls_hooks/test_tls_hooks14.py @@ -0,0 +1,90 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_tls_hooks14(urtest: UraniumTest) -> None: + ''' + Test two outbound start delayed hooks + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import os + + urtest.Summary = ''' + Test different combinations of TLS handshake hooks to ensure they are applied consistently. + ''' + + ts = urtest.MakeATSProcess("ts", enable_tls=True) + server = urtest.MakeOriginServer("server", ssl=True) + request_header = {"headers": "GET / HTTP/1.1\r\nHost: www.example.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + # desired response form the origin server + response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + server.addResponse("sessionlog.json", request_header, response_header) + + ts.addDefaultSSLFiles() + + ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.show_location': 0, + 'proxy.config.diags.debug.tags': 'ssl_hook_test', + 'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir), + 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts.Variables.SSLDir), + 'proxy.config.ssl.client.verify.server.policy': 'PERMISSIVE', + }) + + ts.Disk.ssl_multicert_yaml.AddLines( + """ + ssl_multicert: + - dest_ip: "*" + ssl_cert_name: server.pem + ssl_key_name: server.key + """.split("\n")) + + ts.Disk.remap_config.AddLine( + 'map https://example.com:{0} https://127.0.0.1:{1}'.format(ts.Variables.ssl_port, server.Variables.SSL_Port)) + + urtest.PrepareTestPlugin(os.path.join(urtest.Variables.AtsTestPluginsDir, 'ssl_hook_test.so'), ts, '-out_start_delay=2') + + tr = urtest.AddTestRun("Test outbound delay start") + tr.Processes.Default.StartBefore(server) + tr.Processes.Default.StartBefore(urtest.Processes.ts) + tr.StillRunningAfter = ts + tr.StillRunningAfter = server + tr.MakeCurlCommand('-k -H \'host:example.com:{0}\' https://127.0.0.1:{0}'.format(ts.Variables.ssl_port), ts=ts) + tr.Processes.Default.ReturnCode = 0 + + ts.Disk.traffic_out.Content = "gold/ts-out-delay-start-2.gold" + + tr.Processes.Default.TimeOut = 15 + tr.TimeOut = 15 + urtest.execute() diff --git a/tests/uranium_tests/tls_hooks/test_tls_hooks15.py b/tests/uranium_tests/tls_hooks/test_tls_hooks15.py new file mode 100644 index 00000000000..33c1850aa91 --- /dev/null +++ b/tests/uranium_tests/tls_hooks/test_tls_hooks15.py @@ -0,0 +1,89 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_tls_hooks15(urtest: UraniumTest) -> None: + ''' + Test one delayed preaccept callback + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import os + + urtest.Summary = ''' + Test different combinations of TLS handshake hooks to ensure they are applied consistently. + ''' + + ts = urtest.MakeATSProcess("ts", enable_tls=True) + server = urtest.MakeOriginServer("server", ssl=True) + request_header = {"headers": "GET / HTTP/1.1\r\nHost: www.example.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + # desired response form the origin server + response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + server.addResponse("sessionlog.json", request_header, response_header) + + ts.addDefaultSSLFiles() + + ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.show_location': 0, + 'proxy.config.diags.debug.tags': 'ssl_hook_test', + 'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir), + 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts.Variables.SSLDir), + }) + + ts.Disk.ssl_multicert_yaml.AddLines( + """ + ssl_multicert: + - dest_ip: "*" + ssl_cert_name: server.pem + ssl_key_name: server.key + """.split("\n")) + + ts.Disk.remap_config.AddLine( + 'map https://example.com:{0} https://127.0.0.1:{1}'.format(ts.Variables.ssl_port, server.Variables.SSL_Port)) + + urtest.PrepareTestPlugin(os.path.join(urtest.Variables.AtsTestPluginsDir, 'ssl_hook_test.so'), ts, '-close=2 -out_close=1') + + tr = urtest.AddTestRun("Test one delayed preaccept hook") + tr.Processes.Default.StartBefore(server) + tr.Processes.Default.StartBefore(urtest.Processes.ts) + tr.StillRunningAfter = ts + tr.StillRunningAfter = server + tr.MakeCurlCommand('-k -H \'host:example.com:{0}\' https://127.0.0.1:{0}'.format(ts.Variables.ssl_port), ts=ts) + tr.Processes.Default.ReturnCode = 0 + + ts.Disk.traffic_out.Content = "gold/ts-close-out-close.gold" + + tr.Processes.Default.TimeOut = 15 + tr.TimeOut = 15 + urtest.execute() diff --git a/tests/uranium_tests/tls_hooks/test_tls_hooks16.py b/tests/uranium_tests/tls_hooks/test_tls_hooks16.py new file mode 100644 index 00000000000..1a19af70015 --- /dev/null +++ b/tests/uranium_tests/tls_hooks/test_tls_hooks16.py @@ -0,0 +1,97 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_tls_hooks16(urtest: UraniumTest) -> None: + ''' + Test single immediate client hello hook + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import os + import re + + urtest.Summary = ''' + Test different combinations of TLS handshake hooks to ensure they are applied consistently. + ''' + + urtest.SkipUnless(Condition.HasOpenSSLVersion("1.1.1")) + + ts = urtest.MakeATSProcess("ts", enable_tls=True) + server = urtest.MakeOriginServer("server") + request_header = {"headers": "GET / HTTP/1.1\r\nHost: www.example.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + # desired response form the origin server + response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + server.addResponse("sessionlog.json", request_header, response_header) + + ts.addDefaultSSLFiles() + + ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.show_location': 0, + 'proxy.config.diags.debug.tags': 'ssl_hook_test', + 'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir), + 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts.Variables.SSLDir), + }) + + ts.Disk.ssl_multicert_yaml.AddLines( + """ + ssl_multicert: + - dest_ip: "*" + ssl_cert_name: server.pem + ssl_key_name: server.key + """.split("\n")) + + ts.Disk.remap_config.AddLine( + 'map https://example.com:{1} http://127.0.0.1:{0}'.format(server.Variables.Port, ts.Variables.ssl_port)) + + urtest.PrepareTestPlugin(os.path.join(urtest.Variables.AtsTestPluginsDir, 'ssl_hook_test.so'), ts, '-client_hello_imm=1') + + tr = urtest.AddTestRun("Test one immediate client hello hook") + tr.Processes.Default.StartBefore(server) + tr.Processes.Default.StartBefore(urtest.Processes.ts) + tr.StillRunningAfter = ts + tr.StillRunningAfter = server + tr.MakeCurlCommand('-k -H \'host:example.com:{0}\' https://127.0.0.1:{0}'.format(ts.Variables.ssl_port), ts=ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.stdout = "gold/client-hello-1.gold" + + ts.Disk.traffic_out.Content = "gold/ts-client-hello-1.gold" + + snistring = "Client Hello callback 0" + ts.Disk.traffic_out.Content = Testers.ContainsExpression( + r"\A(?:(?!{0}).)*{0}(?!.*{0}).*\Z".format(snistring), "Client Hello message appears only once", reflags=re.S | re.M) + + tr.Processes.Default.TimeOut = 15 + tr.TimeOut = 15 + urtest.execute() diff --git a/tests/uranium_tests/tls_hooks/test_tls_hooks17.py b/tests/uranium_tests/tls_hooks/test_tls_hooks17.py new file mode 100644 index 00000000000..b09ee128830 --- /dev/null +++ b/tests/uranium_tests/tls_hooks/test_tls_hooks17.py @@ -0,0 +1,97 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_tls_hooks17(urtest: UraniumTest) -> None: + ''' + Test single delayed client hello hook + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import os + import re + + urtest.Summary = ''' + Test different combinations of TLS handshake hooks to ensure they are applied consistently. + ''' + + urtest.SkipUnless(Condition.HasOpenSSLVersion("1.1.1"),) + + ts = urtest.MakeATSProcess("ts", enable_tls=True) + server = urtest.MakeOriginServer("server") + request_header = {"headers": "GET / HTTP/1.1\r\nHost: www.example.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + # desired response form the origin server + response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + server.addResponse("sessionlog.json", request_header, response_header) + + ts.addDefaultSSLFiles() + + ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.show_location': 0, + 'proxy.config.diags.debug.tags': 'ssl_hook_test', + 'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir), + 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts.Variables.SSLDir), + }) + + ts.Disk.ssl_multicert_yaml.AddLines( + """ + ssl_multicert: + - dest_ip: "*" + ssl_cert_name: server.pem + ssl_key_name: server.key + """.split("\n")) + + ts.Disk.remap_config.AddLine( + 'map https://example.com:{1} http://127.0.0.1:{0}'.format(server.Variables.Port, ts.Variables.ssl_port)) + + urtest.PrepareTestPlugin(os.path.join(urtest.Variables.AtsTestPluginsDir, 'ssl_hook_test.so'), ts, '-client_hello=1 -close=1') + + tr = urtest.AddTestRun("Test one delayed client hello hook") + tr.Processes.Default.StartBefore(server) + tr.Processes.Default.StartBefore(urtest.Processes.ts) + tr.StillRunningAfter = ts + tr.StillRunningAfter = server + tr.MakeCurlCommand('-k -H \'host:example.com:{0}\' https://127.0.0.1:{0}'.format(ts.Variables.ssl_port), ts=ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.stdout = "gold/client-hello-1.gold" + + ts.Disk.traffic_out.Content = "gold/ts-client-hello-delayed-1.gold" + + snistring = "Client Hello callback 0" + ts.Disk.traffic_out.Content = Testers.ContainsExpression( + r"\A(?:(?!{0}).)*{0}(?!.*{0}).*\Z".format(snistring), "Client Hello message appears only once", reflags=re.S | re.M) + + tr.Processes.Default.TimeOut = 15 + tr.TimeOut = 15 + urtest.execute() diff --git a/tests/uranium_tests/tls_hooks/test_tls_hooks18.py b/tests/uranium_tests/tls_hooks/test_tls_hooks18.py new file mode 100644 index 00000000000..19d1956fc76 --- /dev/null +++ b/tests/uranium_tests/tls_hooks/test_tls_hooks18.py @@ -0,0 +1,100 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_tls_hooks18(urtest: UraniumTest) -> None: + ''' + Test two delayed client hello callbacks + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import os + import re + + urtest.Summary = ''' + Test different combinations of TLS handshake hooks to ensure they are applied consistently. + ''' + + urtest.SkipUnless(Condition.HasOpenSSLVersion("1.1.1"),) + + ts = urtest.MakeATSProcess("ts", enable_tls=True) + server = urtest.MakeOriginServer("server") + request_header = {"headers": "GET / HTTP/1.1\r\nHost: www.example.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + # desired response form the origin server + response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + server.addResponse("sessionlog.json", request_header, response_header) + + ts.addDefaultSSLFiles() + + ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.show_location': 0, + 'proxy.config.diags.debug.tags': 'ssl_hook_test', + 'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir), + 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts.Variables.SSLDir), + }) + + ts.Disk.ssl_multicert_yaml.AddLines( + """ + ssl_multicert: + - dest_ip: "*" + ssl_cert_name: server.pem + ssl_key_name: server.key + """.split("\n")) + + ts.Disk.remap_config.AddLine( + 'map https://example.com:{1} http://127.0.0.1:{0}'.format(server.Variables.Port, ts.Variables.ssl_port)) + + urtest.PrepareTestPlugin(os.path.join(urtest.Variables.AtsTestPluginsDir, 'ssl_hook_test.so'), ts, '-client_hello=2 -close=1') + + tr = urtest.AddTestRun("Test two client hello hooks") + tr.Processes.Default.StartBefore(server) + tr.Processes.Default.StartBefore(urtest.Processes.ts) + tr.StillRunningAfter = ts + tr.StillRunningAfter = server + tr.MakeCurlCommand('-k -H \'host:example.com:{0}\' https://127.0.0.1:{0}'.format(ts.Variables.ssl_port), ts=ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.stdout = "gold/preaccept-1.gold" + + ts.Disk.traffic_out.Content = "gold/ts-client-hello-2.gold" + + certstring0 = "Client Hello callback 0" + certstring1 = "Client Hello callback 1" + ts.Disk.traffic_out.Content = Testers.ContainsExpression( + r"\A(?:(?!{0}).)*{0}(?!.*{0}).*\Z".format(certstring0), "Cert message appears only once", reflags=re.S | re.M) + ts.Disk.traffic_out.Content = Testers.ContainsExpression( + r"\A(?:(?!{0}).)*{0}(?!.*{0}).*\Z".format(certstring1), "Cert message appears only once", reflags=re.S | re.M) + + tr.Processes.Default.TimeOut = 15 + tr.TimeOut = 15 + urtest.execute() diff --git a/tests/uranium_tests/tls_hooks/test_tls_hooks2.py b/tests/uranium_tests/tls_hooks/test_tls_hooks2.py new file mode 100644 index 00000000000..ebed2b23e59 --- /dev/null +++ b/tests/uranium_tests/tls_hooks/test_tls_hooks2.py @@ -0,0 +1,95 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_tls_hooks2(urtest: UraniumTest) -> None: + ''' + Test single SNI hook + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import os + import re + + urtest.Summary = ''' + Test different combinations of TLS handshake hooks to ensure they are applied consistently. + ''' + + ts = urtest.MakeATSProcess("ts", enable_tls=True) + server = urtest.MakeOriginServer("server") + request_header = {"headers": "GET / HTTP/1.1\r\nHost: www.example.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + # desired response form the origin server + response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + server.addResponse("sessionlog.json", request_header, response_header) + + ts.addDefaultSSLFiles() + + ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.show_location': 0, + 'proxy.config.diags.debug.tags': 'ssl_hook_test', + 'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir), + 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts.Variables.SSLDir), + }) + + ts.Disk.ssl_multicert_yaml.AddLines( + """ + ssl_multicert: + - dest_ip: "*" + ssl_cert_name: server.pem + ssl_key_name: server.key + """.split("\n")) + + ts.Disk.remap_config.AddLine( + 'map https://example.com:{0} http://127.0.0.1:{1}'.format(ts.Variables.ssl_port, server.Variables.Port)) + + urtest.PrepareTestPlugin(os.path.join(urtest.Variables.AtsTestPluginsDir, 'ssl_hook_test.so'), ts, '-sni=1') + + tr = urtest.AddTestRun("Test one sni hook") + tr.Processes.Default.StartBefore(server) + tr.Processes.Default.StartBefore(urtest.Processes.ts) + tr.StillRunningAfter = ts + tr.StillRunningAfter = server + tr.MakeCurlCommand('-k -H \'host:example.com:{0}\' https://127.0.0.1:{0}'.format(ts.Variables.ssl_port), ts=ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.stdout = "gold/preaccept-1.gold" + + ts.Disk.traffic_out.Content = "gold/ts-sni-1.gold" + + snistring = "SNI callback 0" + ts.Disk.traffic_out.Content = Testers.ContainsExpression( + r"\A(?:(?!{0}).)*{0}(?!.*{0}).*\Z".format(snistring), "SNI message appears only once", reflags=re.S | re.M) + + tr.Processes.Default.TimeOut = 15 + tr.TimeOut = 15 + urtest.execute() diff --git a/tests/uranium_tests/tls_hooks/test_tls_hooks3.py b/tests/uranium_tests/tls_hooks/test_tls_hooks3.py new file mode 100644 index 00000000000..3f06713bd8a --- /dev/null +++ b/tests/uranium_tests/tls_hooks/test_tls_hooks3.py @@ -0,0 +1,95 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_tls_hooks3(urtest: UraniumTest) -> None: + ''' + Test single delayed cert callback + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import os + import re + + urtest.Summary = ''' + Test different combinations of TLS handshake hooks to ensure they are applied consistently. + ''' + + ts = urtest.MakeATSProcess("ts", enable_tls=True) + server = urtest.MakeOriginServer("server") + request_header = {"headers": "GET / HTTP/1.1\r\nHost: www.example.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + # desired response form the origin server + response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + server.addResponse("sessionlog.json", request_header, response_header) + + ts.addDefaultSSLFiles() + + ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.show_location': 0, + 'proxy.config.diags.debug.tags': 'ssl_hook_test', + 'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir), + 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts.Variables.SSLDir), + }) + + ts.Disk.ssl_multicert_yaml.AddLines( + """ + ssl_multicert: + - dest_ip: "*" + ssl_cert_name: server.pem + ssl_key_name: server.key + """.split("\n")) + + ts.Disk.remap_config.AddLine( + 'map https://example.com:{0} http://127.0.0.1:{1}'.format(ts.Variables.ssl_port, server.Variables.Port)) + + urtest.PrepareTestPlugin(os.path.join(urtest.Variables.AtsTestPluginsDir, 'ssl_hook_test.so'), ts, '-cert=1') + + tr = urtest.AddTestRun("Test one cert hook") + tr.Processes.Default.StartBefore(server) + tr.Processes.Default.StartBefore(urtest.Processes.ts) + tr.StillRunningAfter = ts + tr.StillRunningAfter = server + tr.MakeCurlCommand('-k -H \'host:example.com:{0}\' https://127.0.0.1:{0}'.format(ts.Variables.ssl_port), ts=ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.stdout = "gold/preaccept-1.gold" + + ts.Disk.traffic_out.Content = "gold/ts-cert-1.gold" + + certstring = "Cert callback 0" + ts.Disk.traffic_out.Content = Testers.ContainsExpression( + r"\A(?:(?!{0}).)*{0}(?!.*{0}).*\Z".format(certstring), "Cert message appears only once", reflags=re.S | re.M) + + tr.Processes.Default.TimeOut = 15 + tr.TimeOut = 15 + urtest.execute() diff --git a/tests/uranium_tests/tls_hooks/test_tls_hooks4.py b/tests/uranium_tests/tls_hooks/test_tls_hooks4.py new file mode 100644 index 00000000000..c80437d1398 --- /dev/null +++ b/tests/uranium_tests/tls_hooks/test_tls_hooks4.py @@ -0,0 +1,104 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_tls_hooks4(urtest: UraniumTest) -> None: + ''' + Test 1 preaccept, 1 sni, and 1 cert callback (with delay) + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import os + import re + + urtest.Summary = ''' + Test different combinations of TLS handshake hooks to ensure they are applied consistently. + ''' + + ts = urtest.MakeATSProcess("ts", enable_tls=True) + server = urtest.MakeOriginServer("server") + request_header = {"headers": "GET / HTTP/1.1\r\nHost: www.example.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + # desired response form the origin server + response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + server.addResponse("sessionlog.json", request_header, response_header) + + ts.addDefaultSSLFiles() + + ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.show_location': 0, + 'proxy.config.diags.debug.tags': 'ssl_hook_test', + 'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir), + 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts.Variables.SSLDir), + }) + + ts.Disk.ssl_multicert_yaml.AddLines( + """ + ssl_multicert: + - dest_ip: "*" + ssl_cert_name: server.pem + ssl_key_name: server.key + """.split("\n")) + + ts.Disk.remap_config.AddLine( + 'map https://example.com:{0} http://127.0.0.1:{1}'.format(ts.Variables.ssl_port, server.Variables.Port)) + + urtest.PrepareTestPlugin( + os.path.join(urtest.Variables.AtsTestPluginsDir, 'ssl_hook_test.so'), ts, '-cert=1 -sni=1 -preaccept=1') + + tr = urtest.AddTestRun("Test one sni, one preaccept, and one cert hook") + tr.Processes.Default.StartBefore(server) + tr.Processes.Default.StartBefore(urtest.Processes.ts) + tr.StillRunningAfter = ts + tr.StillRunningAfter = server + tr.MakeCurlCommand('-k -H \'host:example.com:{0}\' https://127.0.0.1:{0}'.format(ts.Variables.ssl_port), ts=ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.stdout = "gold/preaccept-1.gold" + + ts.Disk.traffic_out.Content = "gold/ts-preaccept1-sni1-cert1.gold" + snistring = "SNI callback 0" + preacceptstring = "Pre accept callback 0" + certstring = "Cert callback 0" + ts.Disk.traffic_out.Content = Testers.ContainsExpression( + r"\A(?:(?!{0}).)*{0}(?!.*{0}).*\Z".format(snistring), "SNI message appears only once", reflags=re.S | re.M) + # the preaccept may get triggered twice because the test framework creates a TCP connection before handing off to traffic_server + ts.Disk.traffic_out.Content += Testers.ContainsExpression( + r"\A(?:(?!{0}).)*{0}.*({0})?(?!.*{0}).*\Z".format(preacceptstring), + "Pre accept message appears only once or twice", + reflags=re.S | re.M) + ts.Disk.traffic_out.Content += Testers.ContainsExpression( + r"\A(?:(?!{0}).)*{0}(?!.*{0}).*\Z".format(certstring), "Cert message appears only once", reflags=re.S | re.M) + + tr.Processes.Default.TimeOut = 15 + tr.TimeOut = 15 + urtest.execute() diff --git a/tests/uranium_tests/tls_hooks/test_tls_hooks6.py b/tests/uranium_tests/tls_hooks/test_tls_hooks6.py new file mode 100644 index 00000000000..c54f36407ea --- /dev/null +++ b/tests/uranium_tests/tls_hooks/test_tls_hooks6.py @@ -0,0 +1,103 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_tls_hooks6(urtest: UraniumTest) -> None: + ''' + Test 1 preaccept callback (without delay) two times + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import os + import re + + urtest.Summary = ''' + Test different combinations of TLS handshake hooks to ensure they are applied consistently. + ''' + + ts = urtest.MakeATSProcess("ts", enable_tls=True) + server = urtest.MakeOriginServer("server") + request_header = {"headers": "GET / HTTP/1.1\r\nHost: www.example.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + # desired response form the origin server + response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + server.addResponse("sessionlog.json", request_header, response_header) + + ts.addDefaultSSLFiles() + + ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.show_location': 0, + 'proxy.config.diags.debug.tags': 'ssl_hook_test', + 'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir), + 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts.Variables.SSLDir), + }) + + ts.Disk.ssl_multicert_yaml.AddLines( + """ + ssl_multicert: + - dest_ip: "*" + ssl_cert_name: server.pem + ssl_key_name: server.key + """.split("\n")) + + ts.Disk.remap_config.AddLine( + 'map https://example.com:{0} http://127.0.0.1:{1}'.format(ts.Variables.ssl_port, server.Variables.Port)) + + urtest.PrepareTestPlugin(os.path.join(urtest.Variables.AtsTestPluginsDir, 'ssl_hook_test.so'), ts, '-preaccept=2') + + tr = urtest.AddTestRun("Test two preaccept hooks") + tr.Processes.Default.StartBefore(server) + tr.Processes.Default.StartBefore(urtest.Processes.ts) + tr.StillRunningAfter = ts + tr.StillRunningAfter = server + tr.MakeCurlCommand('-k -H \'host:example.com:{0}\' https://127.0.0.1:{0}'.format(ts.Variables.ssl_port), ts=ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.stdout = "gold/preaccept-1.gold" + + ts.Disk.traffic_out.Content = "gold/ts-preaccept-2.gold" + + # the preaccept may get triggered twice because the test framework creates a TCP connection before handing off to traffic_server + preacceptstring0 = "Pre accept callback 0" + preacceptstring1 = "Pre accept callback 1" + ts.Disk.traffic_out.Content = Testers.ContainsExpression( + r"\A(?:(?!{0}).)*{0}.*({0})?(?!.*{0}).*\Z".format(preacceptstring0), + "Pre accept message appears only once or twice", + reflags=re.S | re.M) + ts.Disk.traffic_out.Content = Testers.ContainsExpression( + r"\A(?:(?!{0}).)*{0}.*({0})?(?!.*{0}).*\Z".format(preacceptstring1), + "Pre accept message appears only once or twice", + reflags=re.S | re.M) + + tr.Processes.Default.TimeOut = 15 + tr.TimeOut = 15 + urtest.execute() diff --git a/tests/uranium_tests/tls_hooks/test_tls_hooks7.py b/tests/uranium_tests/tls_hooks/test_tls_hooks7.py new file mode 100644 index 00000000000..85ff56661da --- /dev/null +++ b/tests/uranium_tests/tls_hooks/test_tls_hooks7.py @@ -0,0 +1,98 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_tls_hooks7(urtest: UraniumTest) -> None: + ''' + Test two SNI hooks + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import os + import re + + urtest.Summary = ''' + Test different combinations of TLS handshake hooks to ensure they are applied consistently. + ''' + + ts = urtest.MakeATSProcess("ts", enable_tls=True) + server = urtest.MakeOriginServer("server") + request_header = {"headers": "GET / HTTP/1.1\r\nHost: www.example.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + # desired response form the origin server + response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + server.addResponse("sessionlog.json", request_header, response_header) + + ts.addDefaultSSLFiles() + + ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.show_location': 0, + 'proxy.config.diags.debug.tags': 'ssl_hook_test', + 'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir), + 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts.Variables.SSLDir), + }) + + ts.Disk.ssl_multicert_yaml.AddLines( + """ + ssl_multicert: + - dest_ip: "*" + ssl_cert_name: server.pem + ssl_key_name: server.key + """.split("\n")) + + ts.Disk.remap_config.AddLine( + 'map https://example.com:{0} http://127.0.0.1:{1}'.format(ts.Variables.ssl_port, server.Variables.Port)) + + urtest.PrepareTestPlugin(os.path.join(urtest.Variables.AtsTestPluginsDir, 'ssl_hook_test.so'), ts, '-sni=2') + + tr = urtest.AddTestRun("Test two sni hooks") + tr.Processes.Default.StartBefore(server) + tr.Processes.Default.StartBefore(urtest.Processes.ts) + tr.StillRunningAfter = ts + tr.StillRunningAfter = server + tr.MakeCurlCommand('-k -H \'host:example.com:{0}\' https://127.0.0.1:{0}'.format(ts.Variables.ssl_port), ts=ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.stdout = "gold/preaccept-1.gold" + + ts.Disk.traffic_out.Content = "gold/ts-sni-2.gold" + + snistring0 = "SNI callback 0" + snistring1 = "SNI callback 1" + ts.Disk.traffic_out.Content = Testers.ContainsExpression( + r"\A(?:(?!{0}).)*{0}(?!.*{0}).*\Z".format(snistring0), "SNI message appears only once", reflags=re.S | re.M) + ts.Disk.traffic_out.Content = Testers.ContainsExpression( + r"\A(?:(?!{0}).)*{0}(?!.*{0}).*\Z".format(snistring1), "SNI message appears only once", reflags=re.S | re.M) + + tr.Processes.Default.TimeOut = 15 + tr.TimeOut = 15 + urtest.execute() diff --git a/tests/uranium_tests/tls_hooks/test_tls_hooks8.py b/tests/uranium_tests/tls_hooks/test_tls_hooks8.py new file mode 100644 index 00000000000..729ad482a65 --- /dev/null +++ b/tests/uranium_tests/tls_hooks/test_tls_hooks8.py @@ -0,0 +1,98 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_tls_hooks8(urtest: UraniumTest) -> None: + ''' + Test two delayed cert callbacks + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import os + import re + + urtest.Summary = ''' + Test different combinations of TLS handshake hooks to ensure they are applied consistently. + ''' + + ts = urtest.MakeATSProcess("ts", enable_tls=True) + server = urtest.MakeOriginServer("server") + request_header = {"headers": "GET / HTTP/1.1\r\nHost: www.example.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + # desired response form the origin server + response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + server.addResponse("sessionlog.json", request_header, response_header) + + ts.addDefaultSSLFiles() + + ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.show_location': 0, + 'proxy.config.diags.debug.tags': 'ssl_hook_test', + 'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir), + 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts.Variables.SSLDir), + }) + + ts.Disk.ssl_multicert_yaml.AddLines( + """ + ssl_multicert: + - dest_ip: "*" + ssl_cert_name: server.pem + ssl_key_name: server.key + """.split("\n")) + + ts.Disk.remap_config.AddLine( + 'map https://example.com:{0} http://127.0.0.1:{1}'.format(ts.Variables.ssl_port, server.Variables.Port)) + + urtest.PrepareTestPlugin(os.path.join(urtest.Variables.AtsTestPluginsDir, 'ssl_hook_test.so'), ts, '-cert=2') + + tr = urtest.AddTestRun("Test two cert hooks") + tr.Processes.Default.StartBefore(server) + tr.Processes.Default.StartBefore(urtest.Processes.ts) + tr.StillRunningAfter = ts + tr.StillRunningAfter = server + tr.MakeCurlCommand('-k -H \'host:example.com:{0}\' https://127.0.0.1:{0}'.format(ts.Variables.ssl_port), ts=ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.stdout = "gold/preaccept-1.gold" + + ts.Disk.traffic_out.Content = "gold/ts-cert-2.gold" + + certstring0 = "Cert callback 0" + certstring1 = "Cert callback 1" + ts.Disk.traffic_out.Content = Testers.ContainsExpression( + r"\A(?:(?!{0}).)*{0}(?!.*{0}).*\Z".format(certstring0), "Cert message appears only once", reflags=re.S | re.M) + ts.Disk.traffic_out.Content = Testers.ContainsExpression( + r"\A(?:(?!{0}).)*{0}(?!.*{0}).*\Z".format(certstring1), "Cert message appears only once", reflags=re.S | re.M) + + tr.Processes.Default.TimeOut = 15 + tr.TimeOut = 15 + urtest.execute() diff --git a/tests/uranium_tests/tls_hooks/test_tls_hooks9.py b/tests/uranium_tests/tls_hooks/test_tls_hooks9.py new file mode 100644 index 00000000000..20b90b7ce14 --- /dev/null +++ b/tests/uranium_tests/tls_hooks/test_tls_hooks9.py @@ -0,0 +1,94 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_tls_hooks9(urtest: UraniumTest) -> None: + ''' + Test one immediate cert callback + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import os + import re + + urtest.Summary = ''' + Test different combinations of TLS handshake hooks to ensure they are applied consistently. + ''' + + ts = urtest.MakeATSProcess("ts", enable_tls=True) + server = urtest.MakeOriginServer("server") + request_header = {"headers": "GET / HTTP/1.1\r\nHost: www.example.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + # desired response form the origin server + response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + server.addResponse("sessionlog.json", request_header, response_header) + + ts.addDefaultSSLFiles() + + ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.show_location': 0, + 'proxy.config.diags.debug.tags': 'ssl_hook_test', + 'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir), + 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts.Variables.SSLDir), + }) + + ts.Disk.ssl_multicert_yaml.AddLines( + """ + ssl_multicert: + - dest_ip: "*" + ssl_cert_name: server.pem + ssl_key_name: server.key + """.split("\n")) + + ts.Disk.remap_config.AddLine( + 'map https://example.com:{0} http://127.0.0.1:{1}'.format(ts.Variables.ssl_port, server.Variables.Port)) + + urtest.PrepareTestPlugin(os.path.join(urtest.Variables.AtsTestPluginsDir, 'ssl_hook_test.so'), ts, '-i=1') + + tr = urtest.AddTestRun("Test one immediate cert hooks") + tr.Processes.Default.StartBefore(server) + tr.Processes.Default.StartBefore(urtest.Processes.ts) + tr.StillRunningAfter = ts + tr.StillRunningAfter = server + tr.MakeCurlCommand('-k -H \'host:example.com:{0}\' https://127.0.0.1:{0}'.format(ts.Variables.ssl_port), ts=ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.stdout = "gold/preaccept-1.gold" + + ts.Disk.traffic_out.Content = "gold/ts-cert-im-1.gold" + + certstring0 = "Cert callback 0" + ts.Disk.traffic_out.Content = Testers.ContainsExpression( + r"\A(?:(?!{0}).)*{0}(?!.*{0}).*\Z".format(certstring0), "Cert message appears only once", reflags=re.S | re.M) + tr.Processes.Default.TimeOut = 15 + tr.TimeOut = 15 + urtest.execute() diff --git a/tests/uranium_tests/tls_hooks/test_tls_hooks_close_while_parked.py b/tests/uranium_tests/tls_hooks/test_tls_hooks_close_while_parked.py new file mode 100644 index 00000000000..1111cd86da9 --- /dev/null +++ b/tests/uranium_tests/tls_hooks/test_tls_hooks_close_while_parked.py @@ -0,0 +1,110 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_tls_hooks_close_while_parked(urtest: UraniumTest) -> None: + ''' + Verify that a plugin's VCONN_CLOSE hook runs when the connection closes while it is + parked in a TLS handshake hook. + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import os + + urtest.Summary = ''' + A connection that closes while parked in a TLS handshake hook must still deliver + TS_VCONN_CLOSE_HOOK to the plugin. + ''' + + urtest.SkipUnless(Condition.HasOpenSSLVersion("1.1.1"),) + + ts = urtest.MakeATSProcess("ts", enable_tls=True) + server = urtest.MakeOriginServer("server") + server.addResponse( + "sessionlog.json", { + "headers": "GET / HTTP/1.1\r\nHost: www.example.com\r\n\r\n", + "timestamp": "1469733493.993", + "body": "" + }, { + "headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", + "timestamp": "1469733493.993", + "body": "" + }) + + ts.addDefaultSSLFiles() + + ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.show_location': 0, + 'proxy.config.diags.debug.tags': 'ssl_hook_test', + # Fire the handshake timeout while the plugin still has the handshake parked (2s park). + 'proxy.config.ssl.handshake_timeout_in': 1, + 'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir), + 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts.Variables.SSLDir), + }) + + ts.Disk.ssl_multicert_yaml.AddLines( + """ + ssl_multicert: + - dest_ip: "*" + ssl_cert_name: server.pem + ssl_key_name: server.key + """.split("\n")) + + ts.Disk.remap_config.AddLine( + 'map https://example.com:{1} http://127.0.0.1:{0}'.format(server.Variables.Port, ts.Variables.ssl_port)) + + # The delayed client hello callback parks the handshake for 2 seconds before it reenables. + urtest.PrepareTestPlugin(os.path.join(urtest.Variables.AtsTestPluginsDir, 'ssl_hook_test.so'), ts, '-client_hello=1 -close=1') + + # Give up after 1 second, which is inside the 2 second park, so the connection closes while it + # is still suspended in the client hello hook. curl reports operation timed out (exit 28). + tr = urtest.AddTestRun("Client disconnects while parked in the client hello hook") + tr.Processes.Default.StartBefore(server) + tr.Processes.Default.StartBefore(urtest.Processes.ts) + tr.StillRunningAfter = ts + tr.StillRunningAfter = server + tr.MakeCurlCommand('-k --max-time 1 -H \'host:example.com:{0}\' https://127.0.0.1:{0}'.format(ts.Variables.ssl_port), ts=ts) + tr.Processes.Default.ReturnCode = 28 + tr.Processes.Default.TimeOut = 15 + tr.TimeOut = 15 + + # The handshake really was parked. + ts.Disk.traffic_out.Content = Testers.ContainsExpression( + "Client Hello callback 0", "the handshake parked in the client hello hook") + + # The close hook must still fire, with the correct event. Before the fix, callHooks() advanced + # curHook within the client hello hook list instead of the close hook list, so this never ran. + ts.Disk.traffic_out.Content += Testers.ContainsExpression( + "Close callback 0 .* - event is good", "the close hook ran for the parked connection") + urtest.execute() diff --git a/tests/gold_tests/traffic_ctl/convert_plugin_config/gold/basic.yaml b/tests/uranium_tests/traffic_ctl/convert_plugin_config/gold/basic.yaml similarity index 100% rename from tests/gold_tests/traffic_ctl/convert_plugin_config/gold/basic.yaml rename to tests/uranium_tests/traffic_ctl/convert_plugin_config/gold/basic.yaml diff --git a/tests/gold_tests/traffic_ctl/convert_plugin_config/gold/commented.yaml b/tests/uranium_tests/traffic_ctl/convert_plugin_config/gold/commented.yaml similarity index 100% rename from tests/gold_tests/traffic_ctl/convert_plugin_config/gold/commented.yaml rename to tests/uranium_tests/traffic_ctl/convert_plugin_config/gold/commented.yaml diff --git a/tests/gold_tests/traffic_ctl/convert_plugin_config/gold/quoted.yaml b/tests/uranium_tests/traffic_ctl/convert_plugin_config/gold/quoted.yaml similarity index 100% rename from tests/gold_tests/traffic_ctl/convert_plugin_config/gold/quoted.yaml rename to tests/uranium_tests/traffic_ctl/convert_plugin_config/gold/quoted.yaml diff --git a/tests/gold_tests/traffic_ctl/convert_plugin_config/gold/skip_disabled.yaml b/tests/uranium_tests/traffic_ctl/convert_plugin_config/gold/skip_disabled.yaml similarity index 100% rename from tests/gold_tests/traffic_ctl/convert_plugin_config/gold/skip_disabled.yaml rename to tests/uranium_tests/traffic_ctl/convert_plugin_config/gold/skip_disabled.yaml diff --git a/tests/gold_tests/traffic_ctl/convert_plugin_config/legacy_config/basic.config b/tests/uranium_tests/traffic_ctl/convert_plugin_config/legacy_config/basic.config similarity index 100% rename from tests/gold_tests/traffic_ctl/convert_plugin_config/legacy_config/basic.config rename to tests/uranium_tests/traffic_ctl/convert_plugin_config/legacy_config/basic.config diff --git a/tests/gold_tests/traffic_ctl/convert_plugin_config/legacy_config/commented.config b/tests/uranium_tests/traffic_ctl/convert_plugin_config/legacy_config/commented.config similarity index 100% rename from tests/gold_tests/traffic_ctl/convert_plugin_config/legacy_config/commented.config rename to tests/uranium_tests/traffic_ctl/convert_plugin_config/legacy_config/commented.config diff --git a/tests/gold_tests/traffic_ctl/convert_plugin_config/legacy_config/quoted.config b/tests/uranium_tests/traffic_ctl/convert_plugin_config/legacy_config/quoted.config similarity index 100% rename from tests/gold_tests/traffic_ctl/convert_plugin_config/legacy_config/quoted.config rename to tests/uranium_tests/traffic_ctl/convert_plugin_config/legacy_config/quoted.config diff --git a/tests/uranium_tests/traffic_ctl/convert_plugin_config/test_convert_plugin_config.py b/tests/uranium_tests/traffic_ctl/convert_plugin_config/test_convert_plugin_config.py new file mode 100644 index 00000000000..dc25b3ec4b4 --- /dev/null +++ b/tests/uranium_tests/traffic_ctl/convert_plugin_config/test_convert_plugin_config.py @@ -0,0 +1,89 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_convert_plugin_config(urtest: UraniumTest) -> None: + ''' + Test the traffic_ctl config convert plugin_config command. + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + urtest.Summary = 'Test traffic_ctl config convert plugin_config command.' + + ts = urtest.MakeATSProcess("ts", enable_cache=False) + + # Test 1: Basic plugin.config conversion. + tr = urtest.AddTestRun("Test basic plugin.config conversion") + tr.Setup.Copy('legacy_config/basic.config') + tr.Processes.Default.Command = 'traffic_ctl config convert plugin_config basic.config -' + tr.Processes.Default.Streams.stdout = "gold/basic.yaml" + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Env = ts.Env + tr.Processes.Default.StartBefore(ts) + tr.StillRunningAfter = ts + + # Test 2: Commented-out lines become enabled: false. + tr = urtest.AddTestRun("Test commented lines converted to disabled entries") + tr.Setup.Copy('legacy_config/commented.config') + tr.Processes.Default.Command = 'traffic_ctl config convert plugin_config commented.config -' + tr.Processes.Default.Streams.stdout = "gold/commented.yaml" + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Env = ts.Env + tr.StillRunningAfter = ts + + # Test 3: Quoted arguments. + tr = urtest.AddTestRun("Test plugin.config with quoted arguments") + tr.Setup.Copy('legacy_config/quoted.config') + tr.Processes.Default.Command = 'traffic_ctl config convert plugin_config quoted.config -' + tr.Processes.Default.Streams.stdout = "gold/quoted.yaml" + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Env = ts.Env + tr.StillRunningAfter = ts + + # Test 4: Output to file instead of stdout. + tr = urtest.AddTestRun("Test output to file") + tr.Setup.Copy('legacy_config/basic.config') + tr.Processes.Default.Command = 'traffic_ctl config convert plugin_config basic.config generated.yaml > /dev/null && cat generated.yaml' + tr.Processes.Default.Streams.stdout = "gold/basic.yaml" + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Env = ts.Env + tr.StillRunningAfter = ts + + # Test 5: --skip-disabled omits commented-out plugins from output. + tr = urtest.AddTestRun("Test --skip-disabled drops disabled entries") + tr.Setup.Copy('legacy_config/commented.config') + tr.Processes.Default.Command = 'traffic_ctl config convert plugin_config --skip-disabled commented.config -' + tr.Processes.Default.Streams.stdout = "gold/skip_disabled.yaml" + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Env = ts.Env + tr.StillRunningAfter = ts + urtest.execute() diff --git a/tests/gold_tests/traffic_ctl/convert_ssl_multicert/gold/basic.yaml b/tests/uranium_tests/traffic_ctl/convert_ssl_multicert/gold/basic.yaml similarity index 100% rename from tests/gold_tests/traffic_ctl/convert_ssl_multicert/gold/basic.yaml rename to tests/uranium_tests/traffic_ctl/convert_ssl_multicert/gold/basic.yaml diff --git a/tests/gold_tests/traffic_ctl/convert_ssl_multicert/gold/full.yaml b/tests/uranium_tests/traffic_ctl/convert_ssl_multicert/gold/full.yaml similarity index 100% rename from tests/gold_tests/traffic_ctl/convert_ssl_multicert/gold/full.yaml rename to tests/uranium_tests/traffic_ctl/convert_ssl_multicert/gold/full.yaml diff --git a/tests/gold_tests/traffic_ctl/convert_ssl_multicert/gold/quoted.yaml b/tests/uranium_tests/traffic_ctl/convert_ssl_multicert/gold/quoted.yaml similarity index 100% rename from tests/gold_tests/traffic_ctl/convert_ssl_multicert/gold/quoted.yaml rename to tests/uranium_tests/traffic_ctl/convert_ssl_multicert/gold/quoted.yaml diff --git a/tests/gold_tests/traffic_ctl/convert_ssl_multicert/legacy_config/basic.config b/tests/uranium_tests/traffic_ctl/convert_ssl_multicert/legacy_config/basic.config similarity index 100% rename from tests/gold_tests/traffic_ctl/convert_ssl_multicert/legacy_config/basic.config rename to tests/uranium_tests/traffic_ctl/convert_ssl_multicert/legacy_config/basic.config diff --git a/tests/gold_tests/traffic_ctl/convert_ssl_multicert/legacy_config/full.config b/tests/uranium_tests/traffic_ctl/convert_ssl_multicert/legacy_config/full.config similarity index 100% rename from tests/gold_tests/traffic_ctl/convert_ssl_multicert/legacy_config/full.config rename to tests/uranium_tests/traffic_ctl/convert_ssl_multicert/legacy_config/full.config diff --git a/tests/gold_tests/traffic_ctl/convert_ssl_multicert/legacy_config/quoted.config b/tests/uranium_tests/traffic_ctl/convert_ssl_multicert/legacy_config/quoted.config similarity index 100% rename from tests/gold_tests/traffic_ctl/convert_ssl_multicert/legacy_config/quoted.config rename to tests/uranium_tests/traffic_ctl/convert_ssl_multicert/legacy_config/quoted.config diff --git a/tests/uranium_tests/traffic_ctl/convert_ssl_multicert/test_convert_ssl_multicert.py b/tests/uranium_tests/traffic_ctl/convert_ssl_multicert/test_convert_ssl_multicert.py new file mode 100644 index 00000000000..e01aba446e0 --- /dev/null +++ b/tests/uranium_tests/traffic_ctl/convert_ssl_multicert/test_convert_ssl_multicert.py @@ -0,0 +1,81 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_convert_ssl_multicert(urtest: UraniumTest) -> None: + ''' + Test the traffic_ctl config convert ssl_multicert command. + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + urtest.Summary = 'Test traffic_ctl config convert ssl_multicert command.' + + # Create an ATS process to get the environment with PATH set correctly. + ts = urtest.MakeATSProcess("ts", enable_cache=False) + + # Test 1: Basic config conversion. + tr = urtest.AddTestRun("Test basic ssl_multicert.config conversion") + tr.Setup.Copy('legacy_config/basic.config') + tr.Processes.Default.Command = 'traffic_ctl config convert ssl_multicert basic.config -' + tr.Processes.Default.Streams.stdout = "gold/basic.yaml" + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Env = ts.Env + tr.Processes.Default.StartBefore(ts) + tr.StillRunningAfter = ts + + # Test 2: Full config with all options. + tr = urtest.AddTestRun("Test full ssl_multicert.config conversion with all options") + tr.Setup.Copy('legacy_config/full.config') + tr.Processes.Default.Command = 'traffic_ctl config convert ssl_multicert full.config -' + tr.Processes.Default.Streams.stdout = "gold/full.yaml" + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Env = ts.Env + tr.StillRunningAfter = ts + + # Test 3: Config with quoted values. + tr = urtest.AddTestRun("Test ssl_multicert.config with quoted values") + tr.Setup.Copy('legacy_config/quoted.config') + tr.Processes.Default.Command = 'traffic_ctl config convert ssl_multicert quoted.config -' + tr.Processes.Default.Streams.stdout = "gold/quoted.yaml" + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Env = ts.Env + tr.StillRunningAfter = ts + + # Test 4: Output to file. + tr = urtest.AddTestRun("Test output to file") + tr.Setup.Copy('legacy_config/basic.config') + tr.Processes.Default.Command = 'traffic_ctl config convert ssl_multicert basic.config generated.yaml > /dev/null && cat generated.yaml' + tr.Processes.Default.Streams.stdout = "gold/basic.yaml" + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Env = ts.Env + tr.StillRunningAfter = ts + urtest.execute() diff --git a/tests/gold_tests/traffic_ctl/convert_storage/gold/basic.yaml b/tests/uranium_tests/traffic_ctl/convert_storage/gold/basic.yaml similarity index 100% rename from tests/gold_tests/traffic_ctl/convert_storage/gold/basic.yaml rename to tests/uranium_tests/traffic_ctl/convert_storage/gold/basic.yaml diff --git a/tests/gold_tests/traffic_ctl/convert_storage/gold/exclusive.yaml b/tests/uranium_tests/traffic_ctl/convert_storage/gold/exclusive.yaml similarity index 100% rename from tests/gold_tests/traffic_ctl/convert_storage/gold/exclusive.yaml rename to tests/uranium_tests/traffic_ctl/convert_storage/gold/exclusive.yaml diff --git a/tests/gold_tests/traffic_ctl/convert_storage/gold/no_volumes.yaml b/tests/uranium_tests/traffic_ctl/convert_storage/gold/no_volumes.yaml similarity index 100% rename from tests/gold_tests/traffic_ctl/convert_storage/gold/no_volumes.yaml rename to tests/uranium_tests/traffic_ctl/convert_storage/gold/no_volumes.yaml diff --git a/tests/gold_tests/traffic_ctl/convert_storage/legacy_config/basic.storage.config b/tests/uranium_tests/traffic_ctl/convert_storage/legacy_config/basic.storage.config similarity index 100% rename from tests/gold_tests/traffic_ctl/convert_storage/legacy_config/basic.storage.config rename to tests/uranium_tests/traffic_ctl/convert_storage/legacy_config/basic.storage.config diff --git a/tests/gold_tests/traffic_ctl/convert_storage/legacy_config/basic.volume.config b/tests/uranium_tests/traffic_ctl/convert_storage/legacy_config/basic.volume.config similarity index 100% rename from tests/gold_tests/traffic_ctl/convert_storage/legacy_config/basic.volume.config rename to tests/uranium_tests/traffic_ctl/convert_storage/legacy_config/basic.volume.config diff --git a/tests/gold_tests/traffic_ctl/convert_storage/legacy_config/exclusive.storage.config b/tests/uranium_tests/traffic_ctl/convert_storage/legacy_config/exclusive.storage.config similarity index 100% rename from tests/gold_tests/traffic_ctl/convert_storage/legacy_config/exclusive.storage.config rename to tests/uranium_tests/traffic_ctl/convert_storage/legacy_config/exclusive.storage.config diff --git a/tests/gold_tests/traffic_ctl/convert_storage/legacy_config/exclusive.volume.config b/tests/uranium_tests/traffic_ctl/convert_storage/legacy_config/exclusive.volume.config similarity index 100% rename from tests/gold_tests/traffic_ctl/convert_storage/legacy_config/exclusive.volume.config rename to tests/uranium_tests/traffic_ctl/convert_storage/legacy_config/exclusive.volume.config diff --git a/tests/gold_tests/traffic_ctl/convert_storage/legacy_config/no_volumes.storage.config b/tests/uranium_tests/traffic_ctl/convert_storage/legacy_config/no_volumes.storage.config similarity index 100% rename from tests/gold_tests/traffic_ctl/convert_storage/legacy_config/no_volumes.storage.config rename to tests/uranium_tests/traffic_ctl/convert_storage/legacy_config/no_volumes.storage.config diff --git a/tests/uranium_tests/traffic_ctl/convert_storage/test_convert_storage.py b/tests/uranium_tests/traffic_ctl/convert_storage/test_convert_storage.py new file mode 100644 index 00000000000..6b8e6719a47 --- /dev/null +++ b/tests/uranium_tests/traffic_ctl/convert_storage/test_convert_storage.py @@ -0,0 +1,89 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_convert_storage(urtest: UraniumTest) -> None: + ''' + Test the traffic_ctl config convert storage command. + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + urtest.Summary = 'Test traffic_ctl config convert storage command.' + + # Create an ATS process to get the environment with PATH set correctly. + ts = urtest.MakeATSProcess("ts", enable_cache=False) + + # Test 1: Basic conversion (spans only, no volume=N annotations). + tr = urtest.AddTestRun("Test basic storage.config + volume.config conversion") + tr.Setup.Copy('legacy_config/basic.storage.config') + tr.Setup.Copy('legacy_config/basic.volume.config') + tr.Processes.Default.Command = \ + 'traffic_ctl config convert storage basic.storage.config basic.volume.config -' + tr.Processes.Default.Streams.stdout = "gold/basic.yaml" + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Env = ts.Env + tr.Processes.Default.StartBefore(ts) + tr.StillRunningAfter = ts + + # Test 2: Exclusive volume assignments (volume=N per span line). + tr = urtest.AddTestRun("Test storage.config with exclusive volume=N span assignments") + tr.Setup.Copy('legacy_config/exclusive.storage.config') + tr.Setup.Copy('legacy_config/exclusive.volume.config') + tr.Processes.Default.Command = \ + 'traffic_ctl config convert storage exclusive.storage.config exclusive.volume.config -' + tr.Processes.Default.Streams.stdout = "gold/exclusive.yaml" + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Env = ts.Env + tr.StillRunningAfter = ts + + # Test 3: Spans only, no volume.config (missing volume.config is treated as empty). + tr = urtest.AddTestRun("Test storage.config with no volume.config (spans only)") + tr.Setup.Copy('legacy_config/no_volumes.storage.config') + tr.Processes.Default.Command = \ + 'traffic_ctl config convert storage no_volumes.storage.config /nonexistent/volume.config -' + tr.Processes.Default.Streams.stdout = "gold/no_volumes.yaml" + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Env = ts.Env + tr.StillRunningAfter = ts + + # Test 4: Output to file. + tr = urtest.AddTestRun("Test output to file") + tr.Setup.Copy('legacy_config/basic.storage.config') + tr.Setup.Copy('legacy_config/basic.volume.config') + tr.Processes.Default.Command = \ + 'traffic_ctl config convert storage basic.storage.config basic.volume.config generated.yaml' \ + ' > /dev/null && cat generated.yaml' + tr.Processes.Default.Streams.stdout = "gold/basic.yaml" + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Env = ts.Env + tr.StillRunningAfter = ts + urtest.execute() diff --git a/tests/gold_tests/traffic_ctl/gold/describe.gold b/tests/uranium_tests/traffic_ctl/gold/describe.gold similarity index 100% rename from tests/gold_tests/traffic_ctl/gold/describe.gold rename to tests/uranium_tests/traffic_ctl/gold/describe.gold diff --git a/tests/gold_tests/traffic_ctl/gold/diff.gold b/tests/uranium_tests/traffic_ctl/gold/diff.gold similarity index 100% rename from tests/gold_tests/traffic_ctl/gold/diff.gold rename to tests/uranium_tests/traffic_ctl/gold/diff.gold diff --git a/tests/gold_tests/traffic_ctl/gold/diff_yaml.gold b/tests/uranium_tests/traffic_ctl/gold/diff_yaml.gold similarity index 100% rename from tests/gold_tests/traffic_ctl/gold/diff_yaml.gold rename to tests/uranium_tests/traffic_ctl/gold/diff_yaml.gold diff --git a/tests/gold_tests/traffic_ctl/gold/match.gold b/tests/uranium_tests/traffic_ctl/gold/match.gold similarity index 100% rename from tests/gold_tests/traffic_ctl/gold/match.gold rename to tests/uranium_tests/traffic_ctl/gold/match.gold diff --git a/tests/gold_tests/traffic_ctl/gold/t1_yaml.gold b/tests/uranium_tests/traffic_ctl/gold/t1_yaml.gold similarity index 100% rename from tests/gold_tests/traffic_ctl/gold/t1_yaml.gold rename to tests/uranium_tests/traffic_ctl/gold/t1_yaml.gold diff --git a/tests/gold_tests/traffic_ctl/gold/t2_yaml.gold b/tests/uranium_tests/traffic_ctl/gold/t2_yaml.gold similarity index 100% rename from tests/gold_tests/traffic_ctl/gold/t2_yaml.gold rename to tests/uranium_tests/traffic_ctl/gold/t2_yaml.gold diff --git a/tests/gold_tests/traffic_ctl/gold/t3_yaml.gold b/tests/uranium_tests/traffic_ctl/gold/t3_yaml.gold similarity index 100% rename from tests/gold_tests/traffic_ctl/gold/t3_yaml.gold rename to tests/uranium_tests/traffic_ctl/gold/t3_yaml.gold diff --git a/tests/gold_tests/traffic_ctl/gold/t4_yaml.gold b/tests/uranium_tests/traffic_ctl/gold/t4_yaml.gold similarity index 100% rename from tests/gold_tests/traffic_ctl/gold/t4_yaml.gold rename to tests/uranium_tests/traffic_ctl/gold/t4_yaml.gold diff --git a/tests/uranium_tests/traffic_ctl/remap_inc/test_remap_inc.py b/tests/uranium_tests/traffic_ctl/remap_inc/test_remap_inc.py new file mode 100644 index 00000000000..8f08312fd81 --- /dev/null +++ b/tests/uranium_tests/traffic_ctl/remap_inc/test_remap_inc.py @@ -0,0 +1,81 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_remap_inc(urtest: UraniumTest) -> None: + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + urtest.Summary = ''' + Test traffic_ctl config reload with remap.config .include directive + ''' + + urtest.ContinueOnFail = False + + # Define ATS and configure + ts = urtest.MakeATSProcess("ts", enable_cache=False) + nameserver = urtest.MakeDNServer("dns", default='127.0.0.1') + + ts.Disk.File(ts.Variables.CONFIGDIR + "/test.inc", id="test_cfg", typename="ats:config") + ts.Disk.test_cfg.AddLine( + "map http://example.two/ http://yada.com/ " + "@plugin=conf_remap.so @pparam=proxy.config.url_remap.pristine_host_hdr=1") + + ts.Disk.remap_config.AddLine("map http://example.one/ http://yada.com/") + ts.Disk.remap_config.AddLine(".include test.inc") + ts.Disk.remap_config.AddLine("map http://example.three/ http://yada.com/") + + # minimal configuration + ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'regex_remap|url_rewrite|plugin_factory', + 'proxy.config.dns.nameservers': f"127.0.0.1:{nameserver.Variables.Port}", + }) + + tr = urtest.AddTestRun("Start TS, then update test.inc") + tr.Processes.Default.StartBefore(urtest.Processes.ts) + tr.Processes.Default.StartBefore(nameserver) + test_inc_path = ts.Variables.CONFIGDIR + "/test.inc" + tr.Processes.Default.Command = ( + f"rm -f {test_inc_path} ; " + f"echo 'map http://example.four/ http://localhost/ @plugin=generator.so' > {test_inc_path}") + tr.Processes.Default.ReturnCode = 0 + tr.StillRunningAfter = ts + + tr = urtest.AddConfigReload(ts, expect_tasks=["remap.config"], description="Reload remap.config") + tr.StillRunningAfter = ts + + tr = urtest.AddTestRun("Get response from generator") + tr.MakeCurlCommand(f'--proxy 127.0.0.1:{ts.Variables.port} http://example.four/nocache/5', ts=ts) + tr.Processes.Default.ReturnCode = 0 + tr.StillRunningAfter = ts + tr.Processes.Default.Streams.All = Testers.ContainsExpression("xxxxx", "Contains generated text") + tr.Processes.Default.Streams.All += Testers.ExcludesExpression("xxxxxx", "Not too much data") + urtest.execute() diff --git a/tests/gold_tests/traffic_ctl/show_ssl_multicert/gold/show_json.gold b/tests/uranium_tests/traffic_ctl/show_ssl_multicert/gold/show_json.gold similarity index 100% rename from tests/gold_tests/traffic_ctl/show_ssl_multicert/gold/show_json.gold rename to tests/uranium_tests/traffic_ctl/show_ssl_multicert/gold/show_json.gold diff --git a/tests/gold_tests/traffic_ctl/show_ssl_multicert/gold/show_yaml.gold b/tests/uranium_tests/traffic_ctl/show_ssl_multicert/gold/show_yaml.gold similarity index 100% rename from tests/gold_tests/traffic_ctl/show_ssl_multicert/gold/show_yaml.gold rename to tests/uranium_tests/traffic_ctl/show_ssl_multicert/gold/show_yaml.gold diff --git a/tests/uranium_tests/traffic_ctl/show_ssl_multicert/test_show_ssl_multicert.py b/tests/uranium_tests/traffic_ctl/show_ssl_multicert/test_show_ssl_multicert.py new file mode 100644 index 00000000000..2d6a31985c2 --- /dev/null +++ b/tests/uranium_tests/traffic_ctl/show_ssl_multicert/test_show_ssl_multicert.py @@ -0,0 +1,110 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_show_ssl_multicert(urtest: UraniumTest) -> None: + ''' + Test the traffic_ctl config ssl-multicert show command. + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + urtest.Summary = 'Test traffic_ctl config ssl-multicert show command.' + + class ShowSSLMulticert: + + def __init__(self): + self.setup_ts() + self.setup_show_default() + self.setup_show_json() + self.setup_show_yaml() + + def setup_ts(self): + self._ts = urtest.MakeATSProcess("ts", enable_cache=False, enable_tls=True) + self._ts.addDefaultSSLFiles() + self._ts.Disk.ssl_multicert_yaml.AddLines( + """ + ssl_multicert: + - ssl_cert_name: server.pem + dest_ip: "*" + ssl_key_name: server.key + """.split("\n")) + self._ts.Disk.records_config.update( + { + 'proxy.config.ssl.server.cert.path': f'{self._ts.Variables.SSLDir}', + 'proxy.config.ssl.server.private_key.path': f'{self._ts.Variables.SSLDir}', + }) + + def setup_show_default(self): + tr = urtest.AddTestRun("Test ssl-multicert show (default YAML format)") + tr.Processes.Default.Command = 'traffic_ctl config ssl-multicert show' + tr.Processes.Default.Streams.stdout = "gold/show_yaml.gold" + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Env = self._ts.Env + tr.Processes.Default.StartBefore(self._ts) + tr.StillRunningAfter = self._ts + + def setup_show_json(self): + # Test with explicit --json flag. + tr = urtest.AddTestRun("Test ssl-multicert show --json") + tr.Processes.Default.Command = 'traffic_ctl config ssl-multicert show --json' + tr.Processes.Default.Streams.stdout = "gold/show_json.gold" + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Env = self._ts.Env + tr.StillRunningAfter = self._ts + + # Test with short -j flag. + tr = urtest.AddTestRun("Test ssl-multicert show -j") + tr.Processes.Default.Command = 'traffic_ctl config ssl-multicert show -j' + tr.Processes.Default.Streams.stdout = "gold/show_json.gold" + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Env = self._ts.Env + tr.StillRunningAfter = self._ts + + def setup_show_yaml(self): + # Test with --yaml flag. + tr = urtest.AddTestRun("Test ssl-multicert show --yaml") + tr.Processes.Default.Command = 'traffic_ctl config ssl-multicert show --yaml' + tr.Processes.Default.Streams.stdout = "gold/show_yaml.gold" + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Env = self._ts.Env + tr.StillRunningAfter = self._ts + + # Test with short -y flag. + tr = urtest.AddTestRun("Test ssl-multicert show -y") + tr.Processes.Default.Command = 'traffic_ctl config ssl-multicert show -y' + tr.Processes.Default.Streams.stdout = "gold/show_yaml.gold" + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Env = self._ts.Env + tr.StillRunningAfter = self._ts + + ShowSSLMulticert() + urtest.execute() diff --git a/tests/uranium_tests/traffic_ctl/test_traffic_ctl_config_output.py b/tests/uranium_tests/traffic_ctl/test_traffic_ctl_config_output.py new file mode 100644 index 00000000000..3786a429f03 --- /dev/null +++ b/tests/uranium_tests/traffic_ctl/test_traffic_ctl_config_output.py @@ -0,0 +1,162 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_traffic_ctl_config_output(urtest: UraniumTest) -> None: + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + import sys + import os + + # To include util classes + sys.path.insert(0, f'{urtest.TestDirectory}') + + from traffic_ctl_test_utils import Make_traffic_ctl + # import ruamel.yaml Uncomment only when GoldFilePathFor is used. + + urtest.Summary = ''' + Test traffic_ctl config output responses. + ''' + + urtest.ContinueOnFail = True + + records_yaml = ''' + udp: + threads: 1 + diags: + debug: + enabled: 1 + tags: rpc + throttling_interval_msec: 0 + ''' + + traffic_ctl = Make_traffic_ctl(urtest, records_yaml) + + ##### CONFIG GET + + # Test 0: YAML output + traffic_ctl.config().get("proxy.config.diags.debug.tags").as_records().validate_with_goldfile("t1_yaml.gold") + # Test 1: Default output + traffic_ctl.config().get("proxy.config.diags.debug.enabled").validate_with_text("proxy.config.diags.debug.enabled: 1") + # Test 2: Default output with default. + traffic_ctl.config().get("proxy.config.diags.debug.tags").with_default() \ + .validate_with_text("proxy.config.diags.debug.tags: rpc # default http|dns") + + # Test 3: Now same output test but with defaults, traffic_ctl supports adding default value + # when using --records. + traffic_ctl.config().get("proxy.config.diags.debug.tags").as_records().with_default().validate_with_goldfile("t2_yaml.gold") + # Test 4: + traffic_ctl.config().get( + "proxy.config.diags.debug.tags proxy.config.diags.debug.enabled proxy.config.diags.debug.throttling_interval_msec" + ).as_records().with_default().validate_with_goldfile("t3_yaml.gold") + + ##### CONFIG MATCH + # Test 5: + traffic_ctl.config().match("threads").with_default().validate_with_goldfile("match.gold") + + # Test 6: The idea is to check the traffic_ctl yaml emitter when a value starts with the + # same prefix of a node like: + # diags: + # logfile: + # logfile_perm: rw-r--r-- + # + # traffic_ctl have a special logic to deal with cases like this, so better test it. + traffic_ctl.config().match("diags.logfile").as_records().validate_with_goldfile("t4_yaml.gold") + + ##### CONFIG DIFF + # Test 7: + traffic_ctl.config().diff().validate_with_goldfile("diff.gold") + # Test 8: + traffic_ctl.config().diff().as_records().validate_with_goldfile("diff_yaml.gold") + + ##### CONFIG DESCRIBE + # Test 9: don't really care about values, but just output and that the command actually went through + traffic_ctl.config().describe("proxy.config.http.server_ports").validate_with_goldfile("describe.gold") + + ##### CONFIG RESET + # Test 10: Reset a single modified record (proxy.config.diags.debug.tags is set to "rpc" in records_yaml, + # default is "http|dns", so it should be reset) + traffic_ctl.config().reset("proxy.config.diags.debug.tags").validate_with_text( + "Set proxy.config.diags.debug.tags, please wait 10 seconds for traffic server to sync " + "configuration, restart is not required") + # Test 11: Validate the record was reset to its default value + traffic_ctl.config().get("proxy.config.diags.debug.tags").validate_with_text("proxy.config.diags.debug.tags: http|dns") + + # Test 12: Reset records matching a partial path (proxy.config.diags) + # First set the record back to non-default for this test + traffic_ctl.config().set("proxy.config.diags.debug.tags", "rpc").exec() + # Test 13: Resetting proxy.config.diags should reset all matching modified records under that path + traffic_ctl.config().reset("proxy.config.diags").validate_contains_all( + "Set proxy.config.diags.debug.tags", "Set proxy.config.diags.debug.enabled") + # Test 14: Validate the record was reset to its default value + traffic_ctl.config().get("proxy.config.diags.debug.tags").validate_with_text("proxy.config.diags.debug.tags: http|dns") + + # Test 15: Reset all records using "records" keyword + # First set the record back to non-default for this test + traffic_ctl.config().set("proxy.config.diags.debug.tags", "rpc").exec() + # Test 16: This will reset all modified records (including proxy.config.diags.debug.tags) + # Some may require restart, which is ok, we can use diff anyways as the records that needs + # restart will just change the value but won't have any effect. + traffic_ctl.config().reset("records").exec() + # Validate the diff + # Test 17: Validate the diff + traffic_ctl.config().diff().validate_with_text("") + # Test 18: Validate the record was reset to its default value + traffic_ctl.config().get("proxy.config.diags.debug.tags").validate_with_text("proxy.config.diags.debug.tags: http|dns") + + # # Test resetting when no records need resetting (all already at default) + # # Create a new instance with default values only + # traffic_ctl_default = Make_traffic_ctl(Test, None) + # traffic_ctl_default.config().reset("proxy.config.diags.debug.enabled").validate_with_text( + # "No records to reset (all matching records are already at default values)") + + ##### CONFIG RESET with YAML-style paths (records.* format) + # Test 19: Set a record to non-default first + traffic_ctl.config().set("proxy.config.diags.debug.tags", "yaml_test").exec() + # Test 20: Reset using YAML-style path (records.diags.debug.tags instead of proxy.config.diags.debug.tags) + traffic_ctl.config().reset("records.diags.debug.tags").validate_with_text( + "Set proxy.config.diags.debug.tags, please wait 10 seconds for traffic server to sync " + "configuration, restart is not required") + # Test 21: Validate the record was reset to its default value + traffic_ctl.config().get("proxy.config.diags.debug.tags").validate_with_text("proxy.config.diags.debug.tags: http|dns") + + # Test 22: Reset using YAML-style partial path (records.diags) + traffic_ctl.config().set("proxy.config.diags.debug.tags", "yaml_partial_test").exec() + traffic_ctl.config().set("proxy.config.diags.debug.enabled", "1").exec() + # Test 23: Reset using records.diags (YAML format) + traffic_ctl.config().reset("records.diags").validate_contains_all( + "Set proxy.config.diags.debug.tags", "Set proxy.config.diags.debug.enabled") + # Test 24: Validate record was reset + traffic_ctl.config().get("proxy.config.diags.debug.tags").validate_with_text("proxy.config.diags.debug.tags: http|dns") + + # Test 25: Make sure that the command returns an exit code of 2 + traffic_ctl.config().get("invalid.should.set.the.exit.code.to.2").validate_with_exit_code(2) + urtest.execute() diff --git a/tests/uranium_tests/traffic_ctl/test_traffic_ctl_config_reload.py b/tests/uranium_tests/traffic_ctl/test_traffic_ctl_config_reload.py new file mode 100644 index 00000000000..3735980ce15 --- /dev/null +++ b/tests/uranium_tests/traffic_ctl/test_traffic_ctl_config_reload.py @@ -0,0 +1,215 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_traffic_ctl_config_reload(urtest: UraniumTest) -> None: + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + import sys + import os + + # To include util classes + sys.path.insert(0, f'{urtest.TestDirectory}') + + from traffic_ctl_test_utils import Make_traffic_ctl + + # import ruamel.yaml Uncomment only when GoldFilePathFor is used. + + def touch(fname, times=None): + with open(fname, 'a'): + os.utime(fname, times) + + urtest.Summary = ''' + Test traffic_ctl config reload. + ''' + + urtest.ContinueOnFail = True + + records_config = ''' + udp: + threads: 1 + diags: + debug: + enabled: 1 + tags: rpc|config + throttling_interval_msec: 0 + ''' + + traffic_ctl = Make_traffic_ctl(urtest, records_config, Any(0, 2)) + # todo: we need to get the status just in json format + + #### CONFIG STATUS + + # Config status with no token, no reloads exist, should return error. + traffic_ctl.config().status().validate_with_text(""" + `` + Message: No reload tasks found, Code: 6005 + `` + """) + + traffic_ctl.config().status().token("test1").validate_with_text( + """ + `` + Message: Token 'test1' not found, Code: 6001 + `` + """) + + traffic_ctl.config().status().count("all").validate_with_text( + """ + `` + Message: No reload tasks found, Code: 6005 + `` + """) + + traffic_ctl.config().status().token("test1").count("all").validate_with_text( + """ + `` + You can't use both --token and --count options together. Ignoring --count + `` + Message: Token 'test1' not found, Code: 6001 + `` + """) + ##### CONFIG RELOAD + + # basic reload, no params. no existing reload in progress, we expect this to start a new reload. + traffic_ctl.config().reload().validate_with_text( + "\u2714 Reload scheduled [``]\n\n Monitor : traffic_ctl config reload -t `` -m\n Details : traffic_ctl config reload -t `` -s -l" + ) + + # basic reload, but traffic_ctl should create and wait for the details, showing the newly created + # reload and some details. + traffic_ctl.config().reload().show_details().validate_contains_all( + "Reload scheduled", "Waiting for details", "Reload [success]") + + # Now we try with a token, this should start a new reload with the given token. + token = "testtoken_1234" + traffic_ctl.config().reload().token(token).validate_with_text( + f"\u2714 Reload scheduled [{token}]\n\n Monitor : traffic_ctl config reload -t {token} -m\n Details : traffic_ctl config reload -t {token} -s -l" + ) + + # traffic_ctl config status should show the last reload, same as the above. + traffic_ctl.config().status().token(token).validate_contains_all("success", "testtoken_1234") + + # Now we try again, with same token, this should fail as the token already exists. + traffic_ctl.config().reload().token(token).validate_with_text( + f"\u2717 Token '{token}' already in use\n\n Status : traffic_ctl config status -t {token}\n Retry : traffic_ctl config reload" + ) + + # Modify ip_allow.yaml and validate the reload status. + + tr = urtest.AddTestRun("touch file to trigger ip_allow reload") + tr.Processes.Default.Command = f"touch {os.path.join(traffic_ctl._ts.Variables.CONFIGDIR, 'ip_allow.yaml')} && sleep 1" + tr.Processes.Default.ReturnCode = 0 + + traffic_ctl.config().reload().token("reload_ip_allow").show_details().validate_contains_all( + "reload_ip_allow", "success", "ip_allow.yaml") + + ##### FORCE RELOAD + + # Force reload should work even if we just did a reload + traffic_ctl.config().reload().force().validate_contains_all("Reload scheduled") + + ##### INLINE DATA RELOAD + + # Test inline data with -d flag (config not registered, expect error but no stuck task) + # Use --force to avoid "reload in progress" conflict + tr = urtest.AddTestRun("Inline data reload with unregistered config") + tr.DelayStart = 5 # Wait for previous reload to complete + tr.Processes.Default.Command = f'traffic_ctl config reload --force -d "unknown_cfg: {{foo: bar}}"' + tr.Processes.Default.Env = traffic_ctl._ts.Env + tr.Processes.Default.ReturnCode = Any(0, 1, 2) + tr.StillRunningAfter = traffic_ctl._ts + tr.Processes.Default.Streams.All.Content = Testers.ContainsExpression( + r'not registered|No configs were scheduled', "Should report config not registered") + + # Verify no stuck task - new reload should work immediately after + traffic_ctl.config().reload().token("after_inline_test").validate_with_text( + "\u2714 Reload scheduled [after_inline_test]\n\n Monitor : traffic_ctl config reload -t after_inline_test -m\n Details : traffic_ctl config reload -t after_inline_test -s -l" + ) + + ##### MULTI-KEY FILE RELOAD + + # Create a multi-key config file + tr = urtest.AddTestRun("Create multi-key config file") + multi_config_path = os.path.join(traffic_ctl._ts.Variables.CONFIGDIR, 'multi_test.yaml') + tr.Processes.Default.Command = f'''cat > {multi_config_path} << 'EOF' + # Multiple config keys in one file + config_a: + foo: bar + config_b: + baz: qux + EOF''' + tr.Processes.Default.Env = traffic_ctl._ts.Env + tr.Processes.Default.ReturnCode = 0 + tr.StillRunningAfter = traffic_ctl._ts + + # Test reload with multi-key file using data_file() + tr = urtest.AddTestRun("Multi-key file reload") + tr.DelayStart = 5 # Wait for previous reload to complete + tr.Processes.Default.Command = f'traffic_ctl config reload --force --data @{multi_config_path}' + tr.Processes.Default.Env = traffic_ctl._ts.Env + tr.Processes.Default.ReturnCode = Any(0, 1, 2) + tr.StillRunningAfter = traffic_ctl._ts + tr.Processes.Default.Streams.All.Content = Testers.ContainsExpression( + r'not registered|No configs were scheduled|error', "Should process multi-key file") + + ##### FORCE WITH INLINE DATA + + # Force reload with inline data + tr = urtest.AddTestRun("Force reload with inline data") + tr.DelayStart = 1 + tr.Processes.Default.Command = f'traffic_ctl config reload --force --data "test_config: {{key: value}}"' + tr.Processes.Default.Env = traffic_ctl._ts.Env + tr.Processes.Default.ReturnCode = Any(0, 1, 2) + tr.StillRunningAfter = traffic_ctl._ts + tr.Processes.Default.Streams.All.Content = Testers.ContainsExpression( + r'not registered|No configs were scheduled|scheduled', "Should handle force with inline data") + + ##### EXIT CODE TESTS + # Exit codes: 0 = success, 2 = error, 75 = temporary failure / in-progress (EX_TEMPFAIL from sysexits.h) + + # Test: Successful reload should return exit code 0 + traffic_ctl.config().reload().token("exit_code_ok").validate_with_exit_code(0) + + # Test: Successful reload with --monitor should return exit code 0 + traffic_ctl.config().reload().token("exit_code_monitor_ok").monitor().initial_wait(0.5).validate_with_exit_code(0) + + # Test: Token already in use should return exit code 2 (CTRL_EX_ERROR) + traffic_ctl.config().reload().token("exit_code_ok").validate_with_exit_code(2) + + # NOTE: Exit code 75 (CTRL_EX_TEMPFAIL / RELOAD_IN_PROGRESS) is not tested here + # because the reload completes almost instantly in the test environment, making it + # impossible to reliably trigger the "in progress" window with a second concurrent + # request. Testing this path would require a test plugin that artificially delays + # reload processing. The exit code 75 paths are validated by code review: + # 1. Server returns RELOAD_IN_PROGRESS when a second reload is attempted while one is active. + # 2. Ctrl+C during --monitor sets CTRL_EX_TEMPFAIL when the reload hasn't finished. + urtest.execute() diff --git a/tests/uranium_tests/traffic_ctl/test_traffic_ctl_server_debug.py b/tests/uranium_tests/traffic_ctl/test_traffic_ctl_server_debug.py new file mode 100644 index 00000000000..f67b8d82e76 --- /dev/null +++ b/tests/uranium_tests/traffic_ctl/test_traffic_ctl_server_debug.py @@ -0,0 +1,101 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_traffic_ctl_server_debug(urtest: UraniumTest) -> None: + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import sys + + # To include util classes + sys.path.insert(0, f'{urtest.TestDirectory}') + + from traffic_ctl_test_utils import Make_traffic_ctl + + urtest.Summary = ''' + Test traffic_ctl server debug enable/disable commands. + ''' + + urtest.ContinueOnFail = True + + records_yaml = ''' + diags: + debug: + enabled: 0 + tags: xyz + ''' + + traffic_ctl = Make_traffic_ctl(urtest, records_yaml) + + ###### + # Test 1: Enable debug with tags + traffic_ctl.server().debug().enable(tags="http").exec() + # Test 2: Verify debug is enabled and tags are set + traffic_ctl.config().get("proxy.config.diags.debug.enabled").validate_with_text("proxy.config.diags.debug.enabled: 1") + # Test 3: Verify tags are set + traffic_ctl.config().get("proxy.config.diags.debug.tags").validate_with_text("proxy.config.diags.debug.tags: http") + + # Test 4: Disable debug + traffic_ctl.server().debug().disable().exec() + # Test 5: Verify debug is disabled + traffic_ctl.config().get("proxy.config.diags.debug.enabled").validate_with_text("proxy.config.diags.debug.enabled: 0") + + # Test 6: Enable debug with new tags (replace mode) + traffic_ctl.server().debug().enable(tags="cache").exec() + # Test 7: Verify tags are replaced + traffic_ctl.config().get("proxy.config.diags.debug.tags").validate_with_text("proxy.config.diags.debug.tags: cache") + + # Test 8: Enable debug with append mode - should combine with existing tags + traffic_ctl.server().debug().enable(tags="http", append=True).exec() + # Test 9: Verify tags are appended + traffic_ctl.config().get("proxy.config.diags.debug.tags").validate_with_text("proxy.config.diags.debug.tags: cache|http") + + # Test 10: Append another tag + traffic_ctl.server().debug().enable(tags="dns", append=True).exec() + # Test 11: Verify all tags are present + traffic_ctl.config().get("proxy.config.diags.debug.tags").validate_with_text("proxy.config.diags.debug.tags: cache|http|dns") + + # Test 12: Disable and verify + traffic_ctl.server().debug().disable().exec() + # Test 13: Verify debug is disabled + traffic_ctl.config().get("proxy.config.diags.debug.enabled").validate_with_text("proxy.config.diags.debug.enabled: 0") + + # Test 14: Verify --append requires --tags (should fail with error) + # This tests the ArgParser requires() functionality + tr = urtest.AddTestRun("test --append without --tags") + tr.Processes.Default.Env = traffic_ctl._ts.Env + tr.Processes.Default.Command = "traffic_ctl server debug enable --append" + tr.Processes.Default.ReturnCode = 64 # EX_USAGE - command line usage error + tr.Processes.Default.Streams.All = Testers.ContainsExpression( + "Option \'--append\' requires \'--tags\' to be specified", "Should show error that --append requires --tags") + tr.StillRunningAfter = traffic_ctl._ts + urtest.execute() diff --git a/tests/uranium_tests/traffic_ctl/test_traffic_ctl_server_output.py b/tests/uranium_tests/traffic_ctl/test_traffic_ctl_server_output.py new file mode 100644 index 00000000000..919b12c803f --- /dev/null +++ b/tests/uranium_tests/traffic_ctl/test_traffic_ctl_server_output.py @@ -0,0 +1,86 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_traffic_ctl_server_output(urtest: UraniumTest) -> None: + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import sys + import os + + # To include util classes + sys.path.insert(0, f'{urtest.TestDirectory}') + + from traffic_ctl_test_utils import Make_traffic_ctl + + urtest.Summary = ''' + Test traffic_ctl different commands. + ''' + + urtest.ContinueOnFail = True + + records_yaml = ''' + exec_thread: + autoconfig: + enabled: 0 + limit: 4 + ''' + + urtest.Summary = 'Basic test for traffic_ctl server command features.' + + traffic_ctl = Make_traffic_ctl(urtest, records_yaml) + ###### + # traffic_ctl server status + traffic_ctl.server().status().validate_json_contains( + initialized_done='true', is_ssl_handshaking_stopped='false', is_draining='false', is_event_system_shut_down='false') + # Drain ats so we can check the output. + traffic_ctl.server().drain().exec() + + # After the drain, server status should reflect this change. + traffic_ctl.server().status().validate_json_contains(initialized_done='true', is_draining='true') + + # Get basic and empty connection tracker info. + traffic_ctl.rpc().invoke( + handler="get_connection_tracker_info", params='"table: both"').validate_result_with_text( + '{"outbound": {"count": "0", "list": []}, "inbound": {"count": "0", "list": []}}') + # default = outbound only + traffic_ctl.rpc().invoke( + handler="get_connection_tracker_info").validate_result_with_text('{"outbound": {"count": "0", "list": []}}') + # request inbound only + traffic_ctl.rpc().invoke( + handler="get_connection_tracker_info", + params='"table: inbound"').validate_result_with_text('{"inbound": {"count": "0", "list": []}}') + # request outbound only + traffic_ctl.rpc().invoke( + handler="get_connection_tracker_info", + params='"table: outbound"').validate_result_with_text('{"outbound": {"count": "0", "list": []}}') + urtest.execute() diff --git a/tests/uranium_tests/traffic_ctl/test_traffic_ctl_set_read_only.py b/tests/uranium_tests/traffic_ctl/test_traffic_ctl_set_read_only.py new file mode 100644 index 00000000000..de4625a6556 --- /dev/null +++ b/tests/uranium_tests/traffic_ctl/test_traffic_ctl_set_read_only.py @@ -0,0 +1,130 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_traffic_ctl_set_read_only(urtest: UraniumTest) -> None: + ''' + Verify that the management RPC refuses to write records registered as + RECA_READ_ONLY. + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + from jsonrpc import Request, Response + + urtest.Summary = ''' + Verify that records registered with RECA_READ_ONLY cannot be modified through + the management RPC backing "traffic_ctl config set" + (admin_config_set_records). + ''' + + urtest.ContinueOnFail = True + + # proxy.config.thread.max_heartbeat_mseconds is registered as RECA_READ_ONLY + # in src/records/RecordsConfig.cc with a default of 60. RECA_READ_ONLY = 2 + # in the RecAccessT enum (see include/records/RecDefs.h). + READ_ONLY_RECORD = "proxy.config.thread.max_heartbeat_mseconds" + DEFAULT_VALUE = "60" + ATTEMPTED_VALUE = "999" + RECA_READ_ONLY = "2" + RECT_CONFIG_BIT = "1" # bit value in the rec_types filter + + # RecordError::RECORD_READ_ONLY in src/mgmt/rpc/handlers/common/RecordsUtils.h + # (assigned as Codes::RECORD + offset). This is the per-tier code that the + # JSONRPC response must surface in error.data[*].code so programmatic + # clients can branch on the access tier without parsing the message text. + RECORD_READ_ONLY_CODE = 2009 + + ts = urtest.MakeATSProcess("ts") + + def lookup_request(): + """Build a JSONRPC request that fetches the read-only record.""" + return Request.admin_lookup_records([{"record_name": READ_ONLY_RECORD, "rec_types": [RECT_CONFIG_BIT]}]) + + def assert_record_at_default(resp: Response): + """Validate the looked-up record is RECA_READ_ONLY and at its default value.""" + if resp.is_error(): + return (False, f"unexpected error: {resp.error_as_str()}") + + records = resp.result.get('recordList', []) + if len(records) != 1: + return (False, f"expected exactly 1 record, got {len(records)}") + + rec = records[0]['record'] + if rec.get('record_name') != READ_ONLY_RECORD: + return (False, f"record_name {rec.get('record_name')!r} != {READ_ONLY_RECORD!r}") + if rec.get('current_value') != DEFAULT_VALUE: + return (False, f"current_value {rec.get('current_value')!r} != {DEFAULT_VALUE!r}") + + access = rec.get('config_meta', {}).get('access_type') + if str(access) != RECA_READ_ONLY: + return (False, f"access_type {access!r} != {RECA_READ_ONLY!r} (record is not RECA_READ_ONLY)") + + return (True, "record is RECA_READ_ONLY and at the registered default") + + def assert_set_was_rejected(resp: Response): + """Validate the set attempt produced the per-tier not-writable error code.""" + if not resp.is_error(): + return (False, f"set should have failed but returned a result: {resp.result!r}") + # Validate the structured error code rather than the message text so the + # test stays meaningful if the error wording is ever rephrased and so + # that any regression to the generic Codes::RECORD (2000) is caught. + if not resp.contains_nested_error(code=RECORD_READ_ONLY_CODE): + return (False, f"expected nested error code {RECORD_READ_ONLY_CODE} (RECORD_READ_ONLY); got: {resp.error_as_str()}") + return (True, f"set was refused with code {RECORD_READ_ONLY_CODE} (RECORD_READ_ONLY)") + + # Step 0: confirm the record is registered as RECA_READ_ONLY and starts at + # its registered default value. This anchors the rest of the test against + # the registration in RecordsConfig.cc -- if someone reclassifies the record + # the test fails noisily here instead of silently exercising a permissive + # write path. + tr = urtest.AddTestRun("Confirm record is RECA_READ_ONLY and at default") + tr.Processes.Default.StartBefore(ts) + tr.AddJsonRPCClientRequest(ts, lookup_request()) + tr.Processes.Default.Streams.stdout = Testers.CustomJSONRPCResponse(assert_record_at_default) + + # Step 1: attempt to write the record via the management RPC. The handler + # must reject the request with the "Record is not writable" error. + tr = urtest.AddTestRun("Attempt to set the RECA_READ_ONLY record (must be refused)") + tr.AddJsonRPCClientRequest( + ts, Request.admin_config_set_records([{ + "record_name": READ_ONLY_RECORD, + "record_value": ATTEMPTED_VALUE, + }])) + tr.Processes.Default.Streams.stdout = Testers.CustomJSONRPCResponse(assert_set_was_rejected) + + # Step 2: re-look-up the record. Even if step 1's response had been + # misleading, the record must still hold its default value -- this is the + # assertion that catches the underlying bug at the storage level. + tr = urtest.AddTestRun("Confirm record was not modified") + tr.AddJsonRPCClientRequest(ts, lookup_request()) + tr.Processes.Default.Streams.stdout = Testers.CustomJSONRPCResponse(assert_record_at_default) + urtest.execute() diff --git a/tests/gold_tests/traffic_ctl/traffic_ctl_test_utils.py b/tests/uranium_tests/traffic_ctl/traffic_ctl_test_utils.py similarity index 100% rename from tests/gold_tests/traffic_ctl/traffic_ctl_test_utils.py rename to tests/uranium_tests/traffic_ctl/traffic_ctl_test_utils.py diff --git a/tests/gold_tests/tunnel/dumb_proxy.py b/tests/uranium_tests/tunnel/dumb_proxy.py similarity index 100% rename from tests/gold_tests/tunnel/dumb_proxy.py rename to tests/uranium_tests/tunnel/dumb_proxy.py diff --git a/tests/uranium_tests/tunnel/test_tunnel_transform.py b/tests/uranium_tests/tunnel/test_tunnel_transform.py new file mode 100644 index 00000000000..15f01f169d7 --- /dev/null +++ b/tests/uranium_tests/tunnel/test_tunnel_transform.py @@ -0,0 +1,215 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_tunnel_transform(urtest: UraniumTest) -> None: + ''' + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import os + import subprocess + import sys + from ports import get_port + import re + + urtest.Summary = ''' + Test the reported type of HTTP transactions and tunnels + ''' + + urtest.SkipIf(Condition.CurlUsingUnixDomainSocket()) + + # Define default ATS. Disable the cache to simplify the test. + ts = urtest.MakeATSProcess("ts", enable_cache=False, enable_tls=True) + ts.addSSLfile("../tls/ssl/server.pem") + ts.addSSLfile("../tls/ssl/server.key") + + server = urtest.MakeOriginServer("server", ssl=True) + server2 = urtest.MakeOriginServer("server2") + + urtest.testName = "" + request_tunnel_header = {"headers": "GET / HTTP/1.1\r\nHost: tunnel-test\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + # expected response from the origin server + response_tunnel_header = { + "headers": "HTTP/1.1 200 OK\r\nConnection: close\r\nContent-Length:0\r\n\r\n", + "timestamp": "1469733493.993", + "body": "" + } + + urtest.PrepareTestPlugin(os.path.join(urtest.Variables.AtsTestPluginsDir, 'tunnel_transform.so'), ts) + + # add response to the server dictionary + server.addResponse("sessionfile.log", request_tunnel_header, response_tunnel_header) + ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 0, + 'proxy.config.diags.debug.tags': 'http|test', + 'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir), + 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts.Variables.SSLDir), + 'proxy.config.ssl.client.verify.server.policy': 'PERMISSIVE', + 'proxy.config.http.connect_ports': '{0}'.format(server.Variables.SSL_Port) + }) + + ts.Disk.ssl_multicert_yaml.AddLines( + """ + ssl_multicert: + - dest_ip: "*" + ssl_cert_name: server.pem + ssl_key_name: server.key + """.split("\n")) + + ts.Disk.sni_yaml.AddLines([ + 'sni:', + '- fqdn: tunnel-test', + " tunnel_route: localhost:{0}".format(server.Variables.SSL_Port), + ]) + ts.addPrivateConnectAllowYaml() + + # Set up simple forwarding proxy to keep track of TLS bytes for both + # directions + tr = urtest.AddTestRun("Run dumb proxy and send tunnel request.") + tr.Setup.CopyAs('dumb_proxy.py', tr.RunDirectory) + dumb_proxy = tr.Processes.Process(f'dumb-proxy') + proxy_port = get_port(dumb_proxy, "listening_port") + dumb_proxy.Command = f'{sys.executable} dumb_proxy.py --listening_port {proxy_port} --forwarding_port {ts.Variables.ssl_port}' + dumb_proxy.StartBefore(urtest.Processes.ts) + dumb_proxy.Ready = When.PortOpenv4(proxy_port) + dumb_proxy.ReturnCode = 0 + # Record the log file path for later verification. + proxy_output = dumb_proxy.Streams.stdout.AbsPath + + # Add connection close to ensure that the client connection closes promptly after completing the transaction + cmd_tunnel = '-k --http1.1 -H "Connection: close" -vs --resolve "tunnel-test:{0}:127.0.0.1" https://tunnel-test:{0}/'.format( + proxy_port) + + # Send the tunnel request + tr.Processes.Default.Env = ts.Env + tr.MakeCurlCommand(cmd_tunnel, ts=ts) + tr.Processes.Default.ReturnCode = 0 + tr.TimeOut = 10 + tr.Processes.Default.StartBefore(server, ready=When.PortOpen(server.Variables.SSL_Port)) + tr.Processes.Default.StartBefore(urtest.Processes.ts) + tr.Processes.Default.StartBefore(dumb_proxy) + tr.StillRunningAfter = ts + tr.StillRunningAfter = server + + # Signal that all the curl processes have completed + tr = urtest.AddTestRun("Curl Done") + tr.DelayStart = 2 # Delaying a couple seconds to make sure the global continuation's lock contention resolves. + tr.Processes.Default.Command = "traffic_ctl plugin msg done done" + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Env = ts.Env + tr.StillRunningAfter = ts + tr.StillRunningAfter = server + + # Parking this as a ready tester on a meaningless process + # To stall the test runs that check for the stats until the + # stats have propagated and are ready to read. + + def make_done_stat_ready(tsenv): + + def done_stat_ready(process, hasRunFor, **kw): + retval = subprocess.run( + "traffic_ctl metric get tunnel_transform.test.done", + shell=True, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + env=tsenv) + return b'1' in retval.stdout + + return done_stat_ready + + # number of sessions/transactions opened and closed are equal + tr = urtest.AddTestRun("Check type errors") + server2.StartupTimeout = 60 + # Again, here the important thing is the ready function not the server2 process + tr.Processes.Default.StartBefore(server2, ready=make_done_stat_ready(ts.Env)) + tr.Processes.Default.Command = 'traffic_ctl metric get tunnel_transform.error' + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Env = ts.Env + tr.Processes.Default.Streams.All = Testers.ContainsExpression( + 'tunnel_transform.error 0', 'incorrect statistic return, or possible error.') + tr.StillRunningAfter = ts + tr.StillRunningAfter = server + + def get_expected_bytes(path_to_proxy_output, key): + # Construct the regex pattern. + pattern = re.compile(rf'{re.escape(key)}:\s+(\d+)') + with open(path_to_proxy_output, 'r') as file: + log_content = file.read() + bytes_transferred = 0 + match = pattern.search(log_content) + if match: + bytes_transferred = int(match.group(1)) + return bytes_transferred + + def check_byte_count(plugin_metric_path, proxy_output_path, key): + expected_bytes = get_expected_bytes(proxy_output_path, key) + f = open(plugin_metric_path, 'r') + content = f.read() + values = content.split() + f.close() + if len(values) == 2: + val = int(values[1]) + return val == expected_bytes, "Check byte count", "Byte count does not match the expected value" + else: + return False, "Check byte count", "Unexpected metrics output format" + + tr = urtest.AddTestRun("Fetch bytes sent") + tr.Processes.Default.Command = "traffic_ctl metric get tunnel_transform.ua.bytes_sent" + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Env = ts.Env + tr.StillRunningAfter = ts + tr.StillRunningAfter = server + + path1 = tr.Processes.Default.Streams.stdout.AbsPath + + tr2 = urtest.AddTestRun("Check the input bytes sent and fetch outptut bytes sent") + tr2.Processes.Default.Command = 'traffic_ctl metric get tunnel_transform.os.bytes_sent' + tr2.Processes.Default.ReturnCode = 0 + tr2.Processes.Default.Env = ts.Env + tr2.StillRunningAfter = ts + tr2.StillRunningAfter = server + tr2.Processes.Default.Streams.stdout = Testers.Lambda( + lambda info, tester: check_byte_count(path1, proxy_output, 'client-to-server')) + + path2 = tr2.Processes.Default.Streams.stdout.AbsPath + + tr = urtest.AddTestRun("Check that output bytes sent") + tr.Processes.Default.Command = 'echo foo' + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Env = ts.Env + tr.StillRunningAfter = ts + tr.StillRunningAfter = server + tr.Processes.Default.Streams.stdout = Testers.Lambda( + lambda info, tester: check_byte_count(path2, proxy_output, 'server-to-client')) + urtest.execute() diff --git a/tests/uranium_tests/tunnel/test_txn_type.py b/tests/uranium_tests/tunnel/test_txn_type.py new file mode 100644 index 00000000000..f5f372f6353 --- /dev/null +++ b/tests/uranium_tests/tunnel/test_txn_type.py @@ -0,0 +1,197 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_txn_type(urtest: UraniumTest) -> None: + ''' + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + import os + import subprocess + + urtest.Summary = ''' + Test the reported type of HTTP transactions and tunnels + ''' + + urtest.SkipIf(Condition.CurlUsingUnixDomainSocket()) + + # Define default ATS. Disable the cache to simplify the test. + ts = urtest.MakeATSProcess("ts", enable_cache=False, enable_tls=True) + ts.addSSLfile("../tls/ssl/server.pem") + ts.addSSLfile("../tls/ssl/server.key") + + server = urtest.MakeOriginServer("server", ssl=True) + server2 = urtest.MakeOriginServer("server2") + + urtest.testName = "" + request_header = {"headers": "GET / HTTP/1.1\r\nHost: http-test\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + # expected response from the origin server + response_header = { + "headers": "HTTP/1.1 200 OK\r\nConnection: close\r\nContent-Length:0\r\n\r\n", + "timestamp": "1469733493.993", + "body": "" + } + request_tunnel_header = {"headers": "GET / HTTP/1.1\r\nHost: tunnel-test\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + # expected response from the origin server + response_tunnel_header = { + "headers": "HTTP/1.1 200 OK\r\nConnection: close\r\nContent-Length:0\r\n\r\n", + "timestamp": "1469733493.993", + "body": "" + } + + urtest.PrepareTestPlugin(os.path.join(urtest.Variables.AtsTestPluginsDir, 'hook_tunnel_plugin.so'), ts) + + # add response to the server dictionary + server.addResponse("sessionfile.log", request_header, response_header) + server.addResponse("sessionfile.log", request_tunnel_header, response_tunnel_header) + ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 0, + 'proxy.config.diags.debug.tags': 'http|test', + 'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir), + 'proxy.config.ssl.server.private_key.path': '{0}'.format(ts.Variables.SSLDir), + 'proxy.config.ssl.client.verify.server.policy': 'PERMISSIVE', + 'proxy.config.http.connect_ports': '{0}'.format(server.Variables.SSL_Port) + }) + + ts.Disk.ssl_multicert_yaml.AddLines( + """ + ssl_multicert: + - dest_ip: "*" + ssl_cert_name: server.pem + ssl_key_name: server.key + """.split("\n")) + + ts.Disk.remap_config.AddLine( + 'map https://http-test:{0}/ https://127.0.0.1:{1}/'.format(ts.Variables.ssl_port, server.Variables.SSL_Port)) + + ts.Disk.sni_yaml.AddLines([ + 'sni:', + '- fqdn: tunnel-test', + " tunnel_route: localhost:{0}".format(server.Variables.SSL_Port), + ]) + ts.addPrivateConnectAllowYaml(methods='[ CONNECT, GET ]') + + # Add connection close to ensure that the client connection closes promptly after completing the transaction + cmd_http = '-k --http1.1 -H "Connection: close" -vs --resolve "http-test:{0}:127.0.0.1" https://http-test:{0}/'.format( + ts.Variables.ssl_port) + cmd_tunnel = '-k --http1.1 -H "Connection: close" -vs --resolve "tunnel-test:{0}:127.0.0.1" https://tunnel-test:{0}/'.format( + ts.Variables.ssl_port) + cmd_connect = '-k --http1.1 -H "Connection: close" -vs --resolve "connect-proxy:{0}:127.0.0.1" -x http://connect-proxy:{0} --resolve "http-test:{1}:127.0.0.1" https://http-test:{1}/'.format( + ts.Variables.port, ts.Variables.ssl_port) + + # Send the http request + tr = urtest.AddTestRun("send http request") + tr.Processes.Default.Env = ts.Env + tr.MakeCurlCommand(cmd_http, ts=ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.StartBefore(server, ready=When.PortOpen(server.Variables.SSL_Port)) + tr.Processes.Default.StartBefore(urtest.Processes.ts) + tr.StillRunningAfter = ts + tr.StillRunningAfter = server + + # Send the tunnel request + tr = urtest.AddTestRun("send tunnel request") + tr.Processes.Default.Env = ts.Env + tr.MakeCurlCommand(cmd_tunnel, ts=ts) + tr.Processes.Default.ReturnCode = 0 + tr.StillRunningAfter = ts + tr.StillRunningAfter = server + + # Send the connect request + # while the connect method will set up a tunnel, it is processed in ATS as a + # transaction rather than a blind tunnel directly. Plugs can differentiate on the + # method to determine whether a connect tunnel will be set up + tr = urtest.AddTestRun("send connect request") + tr.Processes.Default.Env = ts.Env + tr.MakeCurlCommand(cmd_connect, ts=ts) + tr.Processes.Default.ReturnCode = 0 + tr.StillRunningAfter = ts + tr.StillRunningAfter = server + + # Signal that all the curl processes have completed + tr = urtest.AddTestRun("Curl Done") + tr.DelayStart = 2 # Delaying a couple seconds to make sure the global continuation's lock contention resolves. + tr.Processes.Default.Command = "traffic_ctl plugin msg done done" + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Env = ts.Env + tr.StillRunningAfter = ts + tr.StillRunningAfter = server + + # Parking this as a ready tester on a meaningless process + # To stall the test runs that check for the stats until the + # stats have propagated and are ready to read. + + def make_done_stat_ready(tsenv): + + def done_stat_ready(process, hasRunFor, **kw): + retval = subprocess.run( + "traffic_ctl metric get txn_type_verify.test.done", + shell=True, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + env=tsenv) + return b'1' in retval.stdout + + return done_stat_ready + + # number of sessions/transactions opened and closed are equal + tr = urtest.AddTestRun("Check type errors") + server2.StartupTimeout = 60 + # Again, here the important thing is the ready function not the server2 process + tr.Processes.Default.StartBefore(server2, ready=make_done_stat_ready(ts.Env)) + tr.Processes.Default.Command = 'traffic_ctl metric get txn_type_verify.error' + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Env = ts.Env + tr.Processes.Default.Streams.All = Testers.ContainsExpression( + 'txn_type_verify.error 0', 'incorrect statistic return, or possible error.') + tr.StillRunningAfter = ts + tr.StillRunningAfter = server + + tr = urtest.AddTestRun("Check for tunnel start") + tr.Processes.Default.Command = 'traffic_ctl metric get txn_type_verify.tunnel.start' + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Env = ts.Env + tr.Processes.Default.Streams.stdout = Testers.ContainsExpression( + "txn_type_verify.tunnel.start 1", 'Should have a tunnel start.') + tr.StillRunningAfter = ts + tr.StillRunningAfter = server + + tr = urtest.AddTestRun("Check for http request") + tr.Processes.Default.Command = 'traffic_ctl metric get txn_type_verify.http.req' + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Env = ts.Env + tr.Processes.Default.Streams.stdout = Testers.ContainsExpression("txn_type_verify.http.req 2", 'Should have two http requests.') + tr.StillRunningAfter = ts + tr.StillRunningAfter = server + urtest.execute() diff --git a/tests/uranium_tests/uds/test_uds_socket_perm.py b/tests/uranium_tests/uds/test_uds_socket_perm.py new file mode 100644 index 00000000000..dae166a0a0c --- /dev/null +++ b/tests/uranium_tests/uds/test_uds_socket_perm.py @@ -0,0 +1,76 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_uds_socket_perm(urtest: UraniumTest) -> None: + '''Verify the listening UDS socket mode honors uds-perm and defaults to 0666.''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + urtest.Summary = ''' + Verify proxy.config.http.server_ports honors the uds-perm option on UDS + listeners and defaults to 0666. + ''' + + def assert_socket_mode(socket_path: str, expected_octal: str) -> str: + return ( + f'python3 -c "import os, stat, sys; ' + f'sys.exit(0 if stat.S_IMODE(os.stat(sys.argv[1]).st_mode) == int(sys.argv[2], 8) else 1)" ' + f'{socket_path} {expected_octal}') + + # + # Default UDS permission should be 0666 (no uds-perm option specified). + # + ts_default = urtest.MakeATSProcess("ts_default") + ts_default.Disk.records_config.update( + { + 'proxy.config.http.server_ports': f"{ts_default.Variables.port} {ts_default.Variables.uds_path}", + }) + + tr = urtest.AddTestRun("UDS default permission is 0666") + tr.Processes.Default.Command = assert_socket_mode(ts_default.Variables.uds_path, '0666') + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.StartBefore(ts_default) + + # + # uds-perm=0660 should be honored. + # + ts_custom = urtest.MakeATSProcess("ts_custom") + ts_custom.Disk.records_config.update( + { + 'proxy.config.http.server_ports': f"{ts_custom.Variables.port} {ts_custom.Variables.uds_path}:uds-perm=0660", + }) + + tr = urtest.AddTestRun("UDS custom permission 0660 honored") + tr.Processes.Default.Command = assert_socket_mode(ts_custom.Variables.uds_path, '0660') + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.StartBefore(ts_custom) + urtest.execute() diff --git a/tests/uranium_tests/url/test_uri.py b/tests/uranium_tests/url/test_uri.py new file mode 100644 index 00000000000..e49df863cdb --- /dev/null +++ b/tests/uranium_tests/url/test_uri.py @@ -0,0 +1,56 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When + + +def test_uri(urtest: UraniumTest) -> None: + ''' + Verify correct URI parsing behavior. + ''' + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License 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. + + urtest.Summary = ''' + Verify correct URI parsing behavior. + ''' + + ts = urtest.MakeATSProcess("ts") + replay_file = "uri.replay.yaml" + server = urtest.MakeVerifierServerProcess("server", replay_file) + ts.Disk.records_config.update({ + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'http|cache|url', + }) + ts.Disk.remap_config.AddLine('map / http://127.0.0.1:{0}'.format(server.Variables.http_port)) + tr = urtest.AddTestRun("Verify correct URI parsing behavior.") + tr.Processes.Default.StartBefore(server) + tr.Processes.Default.StartBefore(ts) + tr.AddVerifierClientProcess("client", replay_file, http_ports=[ts.Variables.port]) + urtest.execute() diff --git a/tests/gold_tests/url/uri.replay.yaml b/tests/uranium_tests/url/uri.replay.yaml similarity index 100% rename from tests/gold_tests/url/uri.replay.yaml rename to tests/uranium_tests/url/uri.replay.yaml diff --git a/tests/gold_tests/pluginTest/txn_box/basic/txn_box_cmp.test.py b/tests/urtest.sh old mode 100644 new mode 100755 similarity index 68% rename from tests/gold_tests/pluginTest/txn_box/basic/txn_box_cmp.test.py rename to tests/urtest.sh index df0f08671e9..c5b9a865a9b --- a/tests/gold_tests/pluginTest/txn_box/basic/txn_box_cmp.test.py +++ b/tests/urtest.sh @@ -1,4 +1,4 @@ -# @file +#!/bin/bash # # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file @@ -15,12 +15,8 @@ # 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. -Test.Summary = ''' -Comparison checks. -''' -Test.SkipUnless(Condition.PluginExists("txn_box.so")) +set -u -tr = Test.TxnBoxTestAndRun("Test comparisons", "cmp.replay.yaml", config_path='Auto', config_key="meta.txn_box.global") -ts = tr.Variables.TS -ts.Disk.records_config.update({'proxy.config.diags.debug.enabled': 1, 'proxy.config.diags.debug.tags': 'txn_box'}) +test_directory=$(cd "$(dirname "$0")" && pwd) +exec python3 "${test_directory}/uranium_testkit/runner.py" source "$(dirname "${test_directory}")" "$@" diff --git a/tests/gold_tests/pluginTest/txn_box/basic/txn_box_ip-addr.test.py b/tests/urtest.sh.in old mode 100644 new mode 100755 similarity index 59% rename from tests/gold_tests/pluginTest/txn_box/basic/txn_box_ip-addr.test.py rename to tests/urtest.sh.in index 3686ec001c3..d6d232ddc2a --- a/tests/gold_tests/pluginTest/txn_box/basic/txn_box_ip-addr.test.py +++ b/tests/urtest.sh.in @@ -1,4 +1,4 @@ -# @file +#!/bin/bash # # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file @@ -15,20 +15,21 @@ # 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. -# -# Copyright 2020, Verizon Media -Test.Summary = ''' -Comparison checks. -''' +set -u -Test.SkipUnless(Condition.PluginExists("txn_box.so")) +configured_curl_uds="@CURL_UDS_FLAG@" +curl_uds_argument=() +if [ -n "${configured_curl_uds}" ]; then + curl_uds_argument=(--configured-curl-uds) +fi -tr = Test.TxnBoxTestAndRun("Test IP address", "ip-addr.replay.yaml", config_path='Auto', config_key="meta.txn_box.global") -ts = tr.Variables.TS -ts.Disk.records_config.update( - { - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'txn_box', - 'proxy.config.http.insert_request_via_str': 0 - }) +exec python3 "@CMAKE_CURRENT_SOURCE_DIR@/uranium_testkit/runner.py" configured \ + "--source-root=@CMAKE_SOURCE_DIR@" \ + "--build-root=@CMAKE_BINARY_DIR@" \ + "--install-prefix=@CMAKE_INSTALL_PREFIX@" \ + "--verifier-bin=@PROXY_VERIFIER_PATH@" \ + "--sandbox=@URTEST_SANDBOX@" \ + "--project-directory=@CMAKE_CURRENT_BINARY_DIR@" \ + "${curl_uds_argument[@]}" \ + -- @URTEST_OPTIONS@ "$@" diff --git a/tests/uv.lock b/tests/uv.lock new file mode 100644 index 00000000000..cefeff56d88 --- /dev/null +++ b/tests/uv.lock @@ -0,0 +1,1233 @@ +version = 1 +revision = 3 +requires-python = ">=3.11" + +[[package]] +name = "aioquic" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "cryptography" }, + { name = "pylsqpack" }, + { name = "pyopenssl" }, + { name = "service-identity" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6b/0c/858bb02e0ff96b40735b09ed7be25690197851e4c1bcde51af3348c851fc/aioquic-1.3.0.tar.gz", hash = "sha256:28d070b2183e3e79afa9d4e7bd558960d0d53aeb98bc0cf0a358b279ba797c92", size = 181923, upload-time = "2025-10-11T09:16:30.91Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/41/9a6cf092f2d21768091969dccd4723270f4cd8138d00097160d9c8eabeb8/aioquic-1.3.0-cp310-abi3-macosx_10_9_x86_64.whl", hash = "sha256:59da070ff0f55a54f5623c9190dbc86638daa0bcf84bbdb11ebe507abc641435", size = 1922701, upload-time = "2025-10-11T09:16:10.971Z" }, + { url = "https://files.pythonhosted.org/packages/9e/ea/ac91850a3e6c915802d8c0ee782f966ddfaeed9f870696c1cdb98b25c9a1/aioquic-1.3.0-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:48590fa38ec13f01a3d4e44fb3cfd373661094c9c7248f3c54d2d9512b6c3469", size = 2240281, upload-time = "2025-10-11T09:16:12.895Z" }, + { url = "https://files.pythonhosted.org/packages/a8/65/383f3b3921e1d6b9b757bff3c805c24f7180eda690aecb5e8df50eb7b028/aioquic-1.3.0-cp310-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:019b16580d53541b5d77b4a44a61966921156554fad2536d74895713c800caa5", size = 2752433, upload-time = "2025-10-11T09:16:14.724Z" }, + { url = "https://files.pythonhosted.org/packages/b9/00/66f9a2f95db35ccbe1d9384d44beae28072fceec6ca0ffa29f6c640516c2/aioquic-1.3.0-cp310-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:396e5f53f6ddb27713d9b5bb11d8f0f842e42857b7e671c5ae203bf618528550", size = 2445180, upload-time = "2025-10-11T09:16:17.136Z" }, + { url = "https://files.pythonhosted.org/packages/d5/7a/f020815b9fa6ea9b83354deb213b90a25fd01466f5a8e517e1c0e672be8c/aioquic-1.3.0-cp310-abi3-manylinux_2_28_i686.whl", hash = "sha256:4098afc6337adf19bdb54474f6c37983988e7bfa407892a277259c32eb664b00", size = 2361800, upload-time = "2025-10-11T09:16:18.685Z" }, + { url = "https://files.pythonhosted.org/packages/87/be/a141aafe8984ed380e610397d606a9d9818ef30ce352aa9ede048a966d81/aioquic-1.3.0-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:48292279a248422b6289fffd82159eba8d8b35ff4b1f660b9f74ff85e10ca265", size = 2797515, upload-time = "2025-10-11T09:16:20.451Z" }, + { url = "https://files.pythonhosted.org/packages/52/50/b421e7aedff4a96840bf8734c2c11c18a8434c780c0cb59dff7f0906cee8/aioquic-1.3.0-cp310-abi3-musllinux_1_2_i686.whl", hash = "sha256:0538acdfbf839d87b175676664737c248cd51f1a2295c5fef8e131ddde478a86", size = 2388628, upload-time = "2025-10-11T09:16:21.661Z" }, + { url = "https://files.pythonhosted.org/packages/bc/f4/3c674f4608883e7fc7212f067c599d1321b0c5dd45bda5c77ab5a1e73924/aioquic-1.3.0-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a8881239801279188e33ced6f9849cedf033325a48a6f44d7e55e583abc555a3", size = 2465059, upload-time = "2025-10-11T09:16:23.474Z" }, + { url = "https://files.pythonhosted.org/packages/23/f2/7b1908feffb29b89d2f6d4adc583e83543cd559676354f85c5b4b77a6428/aioquic-1.3.0-cp310-abi3-win32.whl", hash = "sha256:ba30016244e45d9222fdd1fbd4e8b0e5f6811e81a5d0643475ad7024a537274a", size = 1326532, upload-time = "2025-10-11T09:16:25.971Z" }, + { url = "https://files.pythonhosted.org/packages/82/45/4e47404984d65ee31cc9e1370f1fbc4e8c92b25da71f61429dbdba437246/aioquic-1.3.0-cp310-abi3-win_amd64.whl", hash = "sha256:2d7957ba14a6c5efcc14fdc685ccda7ecf0ad048c410a2bdcad1b63bf9527e8e", size = 1675068, upload-time = "2025-10-11T09:16:27.258Z" }, + { url = "https://files.pythonhosted.org/packages/43/60/a8cb5f85c5a6a3cc630124a45644ca5a0ab3eecae2df558b6e0ab7847e1c/aioquic-1.3.0-cp310-abi3-win_arm64.whl", hash = "sha256:9d15a89213d38cbc4679990fa5151af8ea02655a1d6ce5ec972b0a6af74d5f1c", size = 1234825, upload-time = "2025-10-11T09:16:28.994Z" }, +] + +[[package]] +name = "ats-urtest" +version = "0.1.0" +source = { virtual = "." } +dependencies = [ + { name = "aioquic" }, + { name = "charset-normalizer" }, + { name = "dnslib" }, + { name = "eventlet" }, + { name = "grpcio" }, + { name = "grpcio-tools" }, + { name = "gunicorn" }, + { name = "h2" }, + { name = "hpack" }, + { name = "hyperframe" }, + { name = "jsonschema" }, + { name = "microserver" }, + { name = "prometheus-client" }, + { name = "psutil" }, + { name = "pyopenssl" }, + { name = "pytest" }, + { name = "pytest-xdist" }, + { name = "python-jose" }, + { name = "pyyaml" }, + { name = "requests" }, + { name = "traffic-replay" }, +] + +[package.optional-dependencies] +dev = [ + { name = "pyflakes" }, +] + +[package.metadata] +requires-dist = [ + { name = "aioquic", specifier = "==1.3.0" }, + { name = "charset-normalizer" }, + { name = "dnslib" }, + { name = "eventlet" }, + { name = "grpcio" }, + { name = "grpcio-tools" }, + { name = "gunicorn" }, + { name = "h2" }, + { name = "hpack" }, + { name = "hyperframe" }, + { name = "jsonschema" }, + { name = "microserver", specifier = ">=1.0.8" }, + { name = "prometheus-client" }, + { name = "psutil" }, + { name = "pyflakes", marker = "extra == 'dev'" }, + { name = "pyopenssl" }, + { name = "pytest", specifier = ">=8.0" }, + { name = "pytest-xdist", specifier = ">=3.6" }, + { name = "python-jose" }, + { name = "pyyaml" }, + { name = "requests" }, + { name = "traffic-replay" }, +] +provides-extras = ["dev"] + +[[package]] +name = "attrs" +version = "26.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9a/8e/82a0fe20a541c03148528be8cac2408564a6c9a0cc7e9171802bc1d26985/attrs-26.1.0.tar.gz", hash = "sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32", size = 952055, upload-time = "2026-03-19T14:22:25.026Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl", hash = "sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309", size = 67548, upload-time = "2026-03-19T14:22:23.645Z" }, +] + +[[package]] +name = "certifi" +version = "2026.7.22" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a3/c2/24167ea9858356b47a87a50d39908bfdb72ceeefe0041586e704e5376b3a/certifi-2026.7.22.tar.gz", hash = "sha256:741e2c3b351ddf169a738da9f2c048608ff7f2c5cc02f1ebc6b118bb090d5d55", size = 138112, upload-time = "2026-07-22T03:35:12.644Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/a7/71ac2cff56fec219ed242bb11b8efb69fcc4bec75db06fb7bfe35de520e6/certifi-2026.7.22-py3-none-any.whl", hash = "sha256:62f22742b58a1a33014a2b6b706588a8d7e2a88ae7bd1a6ebe8c992928483775", size = 136983, upload-time = "2026-07-22T03:35:11.276Z" }, +] + +[[package]] +name = "cffi" +version = "2.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pycparser", marker = "implementation_name != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9e/ef/008a1939e372c06329a3fce4279c02f328488f3526744906eeec3da7ad5f/cffi-2.1.1.tar.gz", hash = "sha256:dd31f52ea1086513bb9df30f8fcee9b8918323ae067a3d5b78bc826a000712be", size = 530807, upload-time = "2026-08-03T21:21:18.939Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/70/d2/16d99a0c4948febc0ebd133a13b2f688ff7f8cb04da971e1128872ce0c03/cffi-2.1.1-cp311-cp311-macosx_10_15_x86_64.whl", hash = "sha256:c8d2c9fd1f2d16f780d15127abb050d13d1a76c03a4bd87d7e4980e45e511e12", size = 183838, upload-time = "2026-08-03T21:19:29.637Z" }, + { url = "https://files.pythonhosted.org/packages/cd/95/31b535a9f0220ae9f357de4a08d57ce89cb417653c2fd9f075f50822a388/cffi-2.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:398aff33cee2767e3e781d2554c54bd0dff386bb437581e0d8011fde1a942ec1", size = 184168, upload-time = "2026-08-03T21:19:30.764Z" }, + { url = "https://files.pythonhosted.org/packages/ad/5a/4707a0dc1f203f5dde5a907b0d4e3c25d71120241048bd5bc6f1bb9d4e71/cffi-2.1.1-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:154852545011f779917b11c78db2358d095da62a9a172b78ad0a583ee5adc0d0", size = 211805, upload-time = "2026-08-03T21:19:31.867Z" }, + { url = "https://files.pythonhosted.org/packages/ad/66/c19feabb28485b6e0bbaaafa90837a1ef5d302e90f2178bd33f17a49879b/cffi-2.1.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3311ed60d36f83378794e1009ac6258bafbf81f7888b4caa7b35a521e3f95813", size = 218716, upload-time = "2026-08-03T21:19:32.896Z" }, + { url = "https://files.pythonhosted.org/packages/a7/92/500760486c8baab49a7a8a58ba7fc3355ec3974b454b8a09e528efde9e1d/cffi-2.1.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6e192623c49c94421616a5778fba35cf0d5a8d000650c1967ef4448ee5cdd990", size = 205569, upload-time = "2026-08-03T21:19:34.142Z" }, + { url = "https://files.pythonhosted.org/packages/a5/a7/a67c733254d6e7373f7822f8082d8d6beade791e0cf12a7611f376fa61c7/cffi-2.1.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:a6e721d4b0e45d5b65e87534470e67b18dcd092c83f68fba09f152b9cbc061af", size = 204907, upload-time = "2026-08-03T21:19:35.174Z" }, + { url = "https://files.pythonhosted.org/packages/f7/a4/4399daaf8f7dfee9d7c3327fdb0426ee041cc63edc358b93911ceb2bfc7a/cffi-2.1.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:34e261f78cb6ceaaa36f42f2613f4380d94d9c759a9c73c769ee6e0247364632", size = 217807, upload-time = "2026-08-03T21:19:36.286Z" }, + { url = "https://files.pythonhosted.org/packages/28/f7/dabe6da2466ecbd82dc62e7342dc6b1065dad990c06f00f0ede9ebf2a0ed/cffi-2.1.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7225e4514edb64eb6740324353e0da0711954fd8d7da4576755b1c6e09b697cd", size = 221252, upload-time = "2026-08-03T21:19:37.416Z" }, + { url = "https://files.pythonhosted.org/packages/ce/87/616202d8e51342c07d2534c510111c4cc37201775ce8f60802c9335d1edd/cffi-2.1.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:df913725b79db7bcf03448f36b7bf8815363417d5b58deecf9305e3e30f0f21a", size = 214214, upload-time = "2026-08-03T21:19:38.507Z" }, + { url = "https://files.pythonhosted.org/packages/b4/c6/ab025d75d2c26c19b087c0124e75ee31cb65032f4fe345d356d8c507ab97/cffi-2.1.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f5cfbc5fe74540d335175b656c725d74d90e3730c626d92575eea35029d9afaa", size = 219408, upload-time = "2026-08-03T21:19:39.809Z" }, + { url = "https://files.pythonhosted.org/packages/db/e2/7e8109f65445bdc673a7b54f02c677de462db75674220fd1335efc8eb598/cffi-2.1.1-cp311-cp311-win32.whl", hash = "sha256:f8ec5e643a9a937f64e1999eb9f75d072263751912dc5cd06d3c85f8f44be7c3", size = 174470, upload-time = "2026-08-03T21:19:41.246Z" }, + { url = "https://files.pythonhosted.org/packages/73/c0/77ba02423c2f7d7091143c45cd49e0e6575c4c1967394bb542bd923a9b74/cffi-2.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:42f6930c31dc7f50732c9ae793c2786c7b6b044195967bbdde40bb9be81c4cc0", size = 185096, upload-time = "2026-08-03T21:19:42.615Z" }, + { url = "https://files.pythonhosted.org/packages/7c/47/9f1f85f9672ceda4984dc6c4f8824e8558992a2972c3d3c81fb8eb28d4ba/cffi-2.1.1-cp311-cp311-win_arm64.whl", hash = "sha256:c7659f22557c5a0bc4855cd635f55edec690cc008a40768527762cb9fb263455", size = 179941, upload-time = "2026-08-03T21:19:43.747Z" }, + { url = "https://files.pythonhosted.org/packages/10/69/43965eccfdead3b9220015fd1320e117be8c6ed01a62ffab76eeb752f5d5/cffi-2.1.1-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:c8c69575568085ba0b1b10c0249d779a214aea6f6522e949a0fc9fb0fcb449d0", size = 184821, upload-time = "2026-08-03T21:19:44.887Z" }, + { url = "https://files.pythonhosted.org/packages/54/7d/16e5a096677b5e313ca80cd5e5170efa3ea44624a82bb111925522da64b1/cffi-2.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f81b3b8f3d4e343550fa4baa0e479bba9f2d29ce9c2e9b51d1ce1718d7442fcf", size = 184719, upload-time = "2026-08-03T21:19:46.129Z" }, + { url = "https://files.pythonhosted.org/packages/56/e6/8941622732edec876dd17d0453dce07317ae96db34f2ec1436c9d3785986/cffi-2.1.1-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:811bd1e21d32de12efca32393a0ab3f5133b54fce9bd44b8bd77ab07da14bf6a", size = 214799, upload-time = "2026-08-03T21:19:47.218Z" }, + { url = "https://files.pythonhosted.org/packages/44/de/f98430906df1545ffde0d543dd124a7a439bc2cd32b36b9c53f805df7333/cffi-2.1.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:68e62fe11f30d5ca8289242866f0a5291402d8529ca2178ab8afc5c9694ae890", size = 222389, upload-time = "2026-08-03T21:19:48.331Z" }, + { url = "https://files.pythonhosted.org/packages/6a/5b/717f1526b9957b34456313c31645c5b82b8fb5c3fe9e4752999be7128bfc/cffi-2.1.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:4a7c934f7360e8cd64fe9efadcbd10c7c6364f531e432b9a4bf5ccbc9e0e8b50", size = 210249, upload-time = "2026-08-03T21:19:49.543Z" }, + { url = "https://files.pythonhosted.org/packages/64/b3/f8aa4f3e34986c7e4ec45072d1b1b9dd295b6b18007b45518d79726dd725/cffi-2.1.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:3143d81e29e1e20a9ce10901ec369012947876596f75a222235965f2b7ae832e", size = 208775, upload-time = "2026-08-03T21:19:50.918Z" }, + { url = "https://files.pythonhosted.org/packages/b1/db/dceb9dd5b231e1da801793f8acc9f3c52a7e1afe40bb1aae37e02b0faad5/cffi-2.1.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c1453022f490d2459a11819d83ad1d586e9ff65a12ac3e705ffebd46d3685dcf", size = 221822, upload-time = "2026-08-03T21:19:52.054Z" }, + { url = "https://files.pythonhosted.org/packages/a0/d2/6cd24ae3be000a634109c247d1475d62e5616d0dc78c82770942ec384248/cffi-2.1.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:208f941bb9d18e768138677f0a6d2ce01f590df56043dda1df1535ac57c88517", size = 225232, upload-time = "2026-08-03T21:19:53.109Z" }, + { url = "https://files.pythonhosted.org/packages/cb/52/3fa190537004dd7f0ab860a6dc7c0175b8667f68d1e618a46f5498d30250/cffi-2.1.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:210019b6c7cf07f081b4c54635c8cf744377001350e29cc0f81c4377b4797735", size = 223597, upload-time = "2026-08-03T21:19:54.515Z" }, + { url = "https://files.pythonhosted.org/packages/80/fb/0bb75b7039588c074b37ae99f40d9bfddf990ecb2fbc346ebccd2e56b9be/cffi-2.1.1-cp312-cp312-win32.whl", hash = "sha256:046bfc24911b37851ee1b51aab8bffe713d89c68c6a057b09484ce9fd5f69b4e", size = 175292, upload-time = "2026-08-03T21:19:55.566Z" }, + { url = "https://files.pythonhosted.org/packages/d9/79/615cc094e2fb508cade7de88d3b4f6c4ec2bab695c97bce9153dc65aadf5/cffi-2.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:f53e442b08449d42821fa4a4fba000095af9f62742a500f978a9f557ec44339a", size = 185919, upload-time = "2026-08-03T21:19:56.89Z" }, + { url = "https://files.pythonhosted.org/packages/70/c6/d0ea84713fe46b243a436a18fcd47d639732747e21635c8a27191b06dc30/cffi-2.1.1-cp312-cp312-win_arm64.whl", hash = "sha256:7bde5e4cc5c10140859842b9d383af292b22639a4dffb725314baf45968cef80", size = 180093, upload-time = "2026-08-03T21:19:58.155Z" }, + { url = "https://files.pythonhosted.org/packages/9d/f4/035513d4117049066b4779dc3b7c0c0fdad175fa13731c9f4003f1cd1478/cffi-2.1.1-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:b5bdfd1c873d4e093aabc0ca84c4ca6dbc4f752afb5c86f146d9742580c9da2e", size = 194248, upload-time = "2026-08-03T21:19:59.399Z" }, + { url = "https://files.pythonhosted.org/packages/76/af/2aeb4dbb5fc41a04161ae9ff1518de7cec08e164f44a8ce6a4cf7fd2cd1d/cffi-2.1.1-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:31348097ff5bbe827ccc41795d4dd099d9f0625e7def00ee653c137a490c2a6c", size = 196908, upload-time = "2026-08-03T21:20:00.746Z" }, + { url = "https://files.pythonhosted.org/packages/a7/46/2e5fdde8555706dd98139a910ca11be02809f3f605ce956f655d0214e100/cffi-2.1.1-cp313-cp313-macosx_10_15_x86_64.whl", hash = "sha256:9d2055050ea716bd38b7f7f1579c275386646b4894c155a3e2f3cd62ed41b7c6", size = 184805, upload-time = "2026-08-03T21:20:02.02Z" }, + { url = "https://files.pythonhosted.org/packages/55/41/4c7042f317b9217502988f0873af87e16ad606dc20f84e546e3e6ce9764c/cffi-2.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:19ee6127ee34de7d83ce3d371ebc5ed91addbdcc39f9ab15ce4eb35a4e534971", size = 184764, upload-time = "2026-08-03T21:20:03.141Z" }, + { url = "https://files.pythonhosted.org/packages/43/1f/1c3d90d91811c8f86ced9ed637956c54bfe5b79ca98fe976d7f8c8979f6b/cffi-2.1.1-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:6a8dddef476fab96d066d578fc88526767b836ab5ab21754e1d5bf3879c31c7c", size = 214722, upload-time = "2026-08-03T21:20:04.377Z" }, + { url = "https://files.pythonhosted.org/packages/37/6f/3b5ce4c3b2192d250f04908f2bfd91ef34552ec8f7716a5d4abdb8d67bb2/cffi-2.1.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f16c709686a78c727bbbf059f92b0bf41c6fc60deec706d2dc19f529175a6125", size = 222369, upload-time = "2026-08-03T21:20:05.544Z" }, + { url = "https://files.pythonhosted.org/packages/02/10/4b3c75dde3d9663c9e02ba05c2668b954f671d4bbe346413ca8c696b295a/cffi-2.1.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:fcd22650c908d7b7da162bbfaab594a1227a15d1643a98c68b122ac642fa2264", size = 210175, upload-time = "2026-08-03T21:20:06.75Z" }, + { url = "https://files.pythonhosted.org/packages/df/62/14f74b9543e605d17701dc797b815958b8bb70b7624ce1b832ddad48ed6c/cffi-2.1.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:aa9511c62d14da7aacc9b4bf51f3f697a621e83b2d6919008243c3aad168eea3", size = 208670, upload-time = "2026-08-03T21:20:08.04Z" }, + { url = "https://files.pythonhosted.org/packages/95/95/86342356ff5953b3fb06f7ef7c5bee212d45e770abc7218d451b9148313c/cffi-2.1.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a931079504ecc49efed7744c476a5c343a92fabf66dec2db95edb1b2fdc770e2", size = 221824, upload-time = "2026-08-03T21:20:09.274Z" }, + { url = "https://files.pythonhosted.org/packages/eb/ff/7b3429ff53aafe931ed8a5fc69f481bbef7ba6de87ddcbb63d08f483f613/cffi-2.1.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a2d7755bef5a12ed488f4ef1f1b69ee9191d7396083b755a5d2295f6edb4768b", size = 225148, upload-time = "2026-08-03T21:20:10.7Z" }, + { url = "https://files.pythonhosted.org/packages/34/34/a95870b9221e09cf4f2ce3178b1a210abdfe63a1bd357da940418d7b8d15/cffi-2.1.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e0bcb7e0f677f543555d2adff3bf19c05f66cdb4796e5ff602442ab2fe3c4ef7", size = 223564, upload-time = "2026-08-03T21:20:12.165Z" }, + { url = "https://files.pythonhosted.org/packages/70/ea/839b50531021a647fb5e929f72cf97bc1ff702b5472166164b5b6e76b851/cffi-2.1.1-cp313-cp313-win32.whl", hash = "sha256:334644fbac4eff73d985a17a91226df55d0f394160c4cfb880e084c8f7161cac", size = 175263, upload-time = "2026-08-03T21:20:13.559Z" }, + { url = "https://files.pythonhosted.org/packages/60/a6/8b149b2c3f2e11aaa1618ef64500b45f50f22c57a977a4dff1aff1f91042/cffi-2.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:1aa5645c30469b09530c4ebca77ebf8f17618293c58f8549cb1a543a50236e7d", size = 185688, upload-time = "2026-08-03T21:20:14.69Z" }, + { url = "https://files.pythonhosted.org/packages/01/9a/11f687cb39d6a3504060d5242f04f48c735afb4d3d533958a20594890cb2/cffi-2.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:63bbfd5ded17c4840ac07cd8f1c21ba9d9708141f840b324f422f41b207e3973", size = 180078, upload-time = "2026-08-03T21:20:15.917Z" }, + { url = "https://files.pythonhosted.org/packages/d3/7b/d6bbf82b8b96e7391438898c42f5bd96dd02030fd5b64937d248220003e2/cffi-2.1.1-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:7dbb61fe3a7699468030f71bbe5f8a0e326a151daa91beb11a6fc1f980c55e1c", size = 194064, upload-time = "2026-08-03T21:20:17.148Z" }, + { url = "https://files.pythonhosted.org/packages/94/e6/bcc91b283be94735e268487a054004f0aa19947b6348fa367db53230abc8/cffi-2.1.1-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:f24fb43132a4c6b4cb4eb029492919b2db645be6808d738f244fd146c03c32cb", size = 196720, upload-time = "2026-08-03T21:20:18.268Z" }, + { url = "https://files.pythonhosted.org/packages/d9/99/c4b0c17cacdc9c3b8f280026286a9826d6a208c0f047591a3c3ce99b91fd/cffi-2.1.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:d28630f5854ab07ab1fd4aba756de52326c82e6be15d414b12793f1975048b54", size = 184964, upload-time = "2026-08-03T21:20:19.708Z" }, + { url = "https://files.pythonhosted.org/packages/b3/a9/9db617d05d7367c1ad0ab00b3aa6e6f9281edd689b4ee9ea0e5a84e89c97/cffi-2.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:661c298b4821edebead0c91edd2b00374d67ad7c5a1f7a91d4442633b79d6a72", size = 184962, upload-time = "2026-08-03T21:20:20.833Z" }, + { url = "https://files.pythonhosted.org/packages/67/b8/b42132ca113dc567d37684437b46ca1dafc885902b02a110a02d5b511857/cffi-2.1.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:58acb8ab8e295e6c5ea12f888cbb13cf21511ef2a3303a23f4325c29d17fe5c1", size = 222328, upload-time = "2026-08-03T21:20:22.118Z" }, + { url = "https://files.pythonhosted.org/packages/80/10/c5c0cbf0a657aecf59ef511409734230bf556f05a0d6c9eed7aa5c0a0166/cffi-2.1.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:456a61fa52d579ebf9df2e9552ead5129855dbaff6c1e5a9b1bc408809bdc062", size = 209985, upload-time = "2026-08-03T21:20:23.401Z" }, + { url = "https://files.pythonhosted.org/packages/d5/6c/bfa0b87b03b9238148beca990292843c9396ba069b54496596594173de7b/cffi-2.1.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:a4f00aa42f75d6e4595e8866e748cc1705adc0cddfeb2ca86d0d03993d63ba03", size = 208530, upload-time = "2026-08-03T21:20:24.628Z" }, + { url = "https://files.pythonhosted.org/packages/e9/02/4e7d553a7ac4b4238b38b3c1b80d486e9d4436f8d2acbf87a0997fe3f402/cffi-2.1.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b0431303acaea1089ad4b3e9ce4e6518193def1118d4073ca848635ee4ea2e96", size = 221525, upload-time = "2026-08-03T21:20:25.758Z" }, + { url = "https://files.pythonhosted.org/packages/82/1d/a4aaf9babd75acb4d5f223bff71533bee748dd770a382619a798960ee9ba/cffi-2.1.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:64faea20f4e2613363a1a9b9c7dd73058f3ecd00133a511e72ad7c511658f527", size = 225053, upload-time = "2026-08-03T21:20:26.985Z" }, + { url = "https://files.pythonhosted.org/packages/81/10/5dc0e7bdd18e22107054288283380fc97a06ae3f1656a106908d666a3c88/cffi-2.1.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5c58fe613dc5e5336357eff555824a314d8e43282600435c8d1cb6a7a2fedd13", size = 223213, upload-time = "2026-08-03T21:20:28.277Z" }, + { url = "https://files.pythonhosted.org/packages/0b/e9/d0061c364cde06ee43168a0d076ac1da512cbc380d44767b844ba34fe2b6/cffi-2.1.1-cp314-cp314-win32.whl", hash = "sha256:1a18a57b58cfb21fc28d72e876acf10eaed67a1ed96226f92af4df681d571c4c", size = 177682, upload-time = "2026-08-03T21:20:44.288Z" }, + { url = "https://files.pythonhosted.org/packages/a7/06/1c3e01e3ba14c39f6d10bfbac52753b7e22259e38088e5cfe1d704918690/cffi-2.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:3222ba5d678f80a030e6afbcc33dc1ae5cb45facabb61cee2c7016b8432fde48", size = 187949, upload-time = "2026-08-03T21:20:45.623Z" }, + { url = "https://files.pythonhosted.org/packages/87/5b/da4e39efe18eeb89cf580ea9cfc66b6a7c3eadb808fc0cc1d3a295cb5a5d/cffi-2.1.1-cp314-cp314-win_arm64.whl", hash = "sha256:ab36d55f9ed2d067327667c2fea18dda018eb628dd6347aa01dda6cf1f5d3836", size = 182947, upload-time = "2026-08-03T21:20:46.955Z" }, + { url = "https://files.pythonhosted.org/packages/23/59/40338bf421c5accea1d45158170c87006ef1cd371b05c077e76476949728/cffi-2.1.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:7750c6449dff7864bb9bb27ddfb0267756189201a3afc911d82b3caacd70dfc3", size = 188504, upload-time = "2026-08-03T21:20:29.495Z" }, + { url = "https://files.pythonhosted.org/packages/7d/47/5ecf1023850036e674c77ec4de86182d309ae344e39e7cba984b7df5d647/cffi-2.1.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0beceaabe56af686895136a2de78db54ecd8e4046b236b8fd6d6cb61389e9bf2", size = 188259, upload-time = "2026-08-03T21:20:31.291Z" }, + { url = "https://files.pythonhosted.org/packages/2a/9c/92934c3bea9f785b23eba304538c0b4d37a2a96d2431eb3a1bc87a11aa19/cffi-2.1.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:49cbc70e6542d4ccccb936558d1064a8012541e78f821f955cff24e357776c94", size = 223864, upload-time = "2026-08-03T21:20:32.571Z" }, + { url = "https://files.pythonhosted.org/packages/4d/45/ba4c93527bc38616a8bd36488acb69a2212d60486794f0c1f318949bbb76/cffi-2.1.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:e2d65b31f36619cda3999b78b2aa9632e76b78448e7a56fc4240824200e7c4fc", size = 211538, upload-time = "2026-08-03T21:20:33.808Z" }, + { url = "https://files.pythonhosted.org/packages/80/e9/b6ef565e452acb932fb0cb5443f44a78efbd1233e566f02b5a83855e9115/cffi-2.1.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:28907ab9bfb6aa13184cfc17c6b8e1023c5ab6fd7076d8c20a35e59fe04f8f29", size = 210688, upload-time = "2026-08-03T21:20:34.974Z" }, + { url = "https://files.pythonhosted.org/packages/9a/95/eff5f0cee78d2eabc7eebffec40d3fc1876b5f3c95582e018bb4b99601f2/cffi-2.1.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:51b31d1c98274844cfd7838ce00bfc27c7423a4dc00fc0772fc3331c2cc90676", size = 223803, upload-time = "2026-08-03T21:20:36.564Z" }, + { url = "https://files.pythonhosted.org/packages/fa/01/579d39fb8bef00a335a23d83757b44feb24cd6345a2c451b64cb67b9c362/cffi-2.1.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5e7cecbaadb83884793e05828cee59b210b24583b9c7425d0ba6a754fe22eb4e", size = 226763, upload-time = "2026-08-03T21:20:37.816Z" }, + { url = "https://files.pythonhosted.org/packages/8d/b0/0b44f47c60b01b57b6e2bbd92343f13a85a1d93bc46ccf6e47e244acd99c/cffi-2.1.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:25792eac27877609e7bb06d42ff88278a6624fff2ba9bbb523c09616b117e80f", size = 225688, upload-time = "2026-08-03T21:20:38.959Z" }, + { url = "https://files.pythonhosted.org/packages/eb/d2/3b7176cb570a1d3e27faf67b72f591af508036e0d8b2be2ef9af9e8c84bb/cffi-2.1.1-cp314-cp314t-win32.whl", hash = "sha256:8ef53b2de9bcb9197d31854256575d59dbac0cba72ac627bb291ef5eceb74be4", size = 182868, upload-time = "2026-08-03T21:20:40.388Z" }, + { url = "https://files.pythonhosted.org/packages/56/78/31f00c1bcd97c9bbf55f1bfdf5bc809a5de8887473e90bb9960dca825e80/cffi-2.1.1-cp314-cp314t-win_amd64.whl", hash = "sha256:616f097f2fe415bc92a247f02e11f634e1f9e9a83d327e3c915c15089c87869e", size = 194104, upload-time = "2026-08-03T21:20:41.725Z" }, + { url = "https://files.pythonhosted.org/packages/7b/1b/58496f2ed0a35de575250c02a43ab3cc2c04d494a88fed31c1cabc0fd176/cffi-2.1.1-cp314-cp314t-win_arm64.whl", hash = "sha256:ad2c86c495b899d862ea0f4b42891b8713a3bd45dd4105c7fd51c2a72f39f3a5", size = 186402, upload-time = "2026-08-03T21:20:43.042Z" }, + { url = "https://files.pythonhosted.org/packages/c1/8f/9ebe220eab48a093d1a5a5e339ab0dc7316eef3bb04d63c42f0251b61f50/cffi-2.1.1-cp315-cp315-ios_13_0_arm64_iphoneos.whl", hash = "sha256:dddad92b554513a31f272570678ba307fb9f618f05e3d4a5eacafff9eae03e1d", size = 194043, upload-time = "2026-08-03T21:20:48.179Z" }, + { url = "https://files.pythonhosted.org/packages/ff/69/844bad3ece306c4782c2ecb93597035b6690d48704b803914c199da1e8b3/cffi-2.1.1-cp315-cp315-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:da0e573f9f97159390c89d9f1a9e41908b66d408cc5b58d08cf3847d844c531b", size = 196737, upload-time = "2026-08-03T21:20:49.457Z" }, + { url = "https://files.pythonhosted.org/packages/1b/8a/af668013284634733f02d683458a0728739c7d6ddb5e14cb0c20832266fe/cffi-2.1.1-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:fb92203a88b3d3053034db775110081c49d28be6551923805e039924093761e4", size = 184933, upload-time = "2026-08-03T21:20:50.639Z" }, + { url = "https://files.pythonhosted.org/packages/0c/75/2f5207ff6d1a613133b23a5203cc0c2a628313b5eb3974d7956ae3c57950/cffi-2.1.1-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:2ae64be792b8966f2c69538199728b290e34726562896df1e5dc8ffd8d8188e8", size = 185002, upload-time = "2026-08-03T21:20:52.173Z" }, + { url = "https://files.pythonhosted.org/packages/e2/31/9e1313b0a6e30e91b3b3d3fff51ae99c857c07738e3afcce1f7334e1b7ab/cffi-2.1.1-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:507a24c282e0f42f8ed737cf048572cbf580468da5555764a8331735e9c736b6", size = 222271, upload-time = "2026-08-03T21:20:53.462Z" }, + { url = "https://files.pythonhosted.org/packages/50/e3/f6234a833e6e08c7007003074723c406559eecf9b48dfc97471e5a8eb7a0/cffi-2.1.1-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:246fa40ce8645a614ff682e0b70f37134e460eaf93a775e0cbe3cca585a67a80", size = 209919, upload-time = "2026-08-03T21:20:54.783Z" }, + { url = "https://files.pythonhosted.org/packages/0d/fc/5f74e293fced6edb51af3a46c4ccf6c23c9943774ecb375ddbd522c76add/cffi-2.1.1-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:471cee653ae88de62096552e6d24ccb4a5adb8c8c9f10b5054d0122c15bf2779", size = 208529, upload-time = "2026-08-03T21:20:56.066Z" }, + { url = "https://files.pythonhosted.org/packages/44/16/29e6d01b388bef055ecd6ca8244b3f4d336bd09e92d5d892187b9601084e/cffi-2.1.1-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:aeae0e330c9f6acd681f647d46cefd30c29f93e3392882e792e82080c9691399", size = 221630, upload-time = "2026-08-03T21:20:57.336Z" }, + { url = "https://files.pythonhosted.org/packages/a4/18/fa7f1f6857d5eb88a4ca99ffcbfb7c387a287ccc154c64a73e86314745d7/cffi-2.1.1-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:42a494cee34437f05546455144f2b5d9ac09b1face62bcfce597d2e521066688", size = 225134, upload-time = "2026-08-03T21:20:58.675Z" }, + { url = "https://files.pythonhosted.org/packages/e0/9f/e8e3dfa04a1b4c241f8c91faacad872b4d4efd051d49764ad4e2fd4b9fea/cffi-2.1.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:cc572dace3f60ef98d7b12ff411d20f5362feb31a0439eab0085bbfd349982d7", size = 223197, upload-time = "2026-08-03T21:20:59.968Z" }, + { url = "https://files.pythonhosted.org/packages/f8/7e/8debeb04f1ab9fe2a6963964cd6f1aaf7192627b83926586a6a4e089c9fa/cffi-2.1.1-cp315-cp315-win32.whl", hash = "sha256:4f42141fc14250de6dde5ee7ea4432be017252d91f19c5ad043c084cea629cac", size = 177683, upload-time = "2026-08-03T21:21:14.901Z" }, + { url = "https://files.pythonhosted.org/packages/e0/31/5158704cc474ab65c1647932e88be78dc0873f47130e253be38bcaf13d01/cffi-2.1.1-cp315-cp315-win_amd64.whl", hash = "sha256:e6e8cff14d6fb0be70a09c0bdc58096f501952d04624ebf867e0e56da2df8960", size = 187897, upload-time = "2026-08-03T21:21:16.108Z" }, + { url = "https://files.pythonhosted.org/packages/cc/4b/b3a2da8570c704ffc0f9762cdc3ec0f02c8573798e0b5cf7f11c82bbb70f/cffi-2.1.1-cp315-cp315-win_arm64.whl", hash = "sha256:27350daa11d4f10c540e6e89dada4c54feb7256ad03e9a4dc075ebad7ba360d1", size = 182935, upload-time = "2026-08-03T21:21:17.271Z" }, + { url = "https://files.pythonhosted.org/packages/d0/ef/5443574510a1207e6f6bc38ba6e1f1de36cb48fef07b2728bb896a21f430/cffi-2.1.1-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:c26608d2222fb1e94487e4a387d85f13eb55d5ed725cb25a0c589ac4ee60e7bc", size = 188464, upload-time = "2026-08-03T21:21:01.163Z" }, + { url = "https://files.pythonhosted.org/packages/7e/ae/a56fa8c4686ad50e148fcbc8d3ae0d03915ff5c30d795058988c24118cef/cffi-2.1.1-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:4be96343e422f2dfcd12ab5c9f5aebe03f82f737c6bffeca6830b3875cb44aab", size = 188262, upload-time = "2026-08-03T21:21:02.382Z" }, + { url = "https://files.pythonhosted.org/packages/53/b2/6187f46f2912276a3ae284076109cc5c8680482f11f766ccf26db4a86427/cffi-2.1.1-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:937c0052c05a31ca1daf18de3158eed4dbfcb9cc107adbea227728d647be701e", size = 223779, upload-time = "2026-08-03T21:21:03.553Z" }, + { url = "https://files.pythonhosted.org/packages/8a/f6/c3ad28bd19f77047a03084424fbd4cbe997303267c14423737324be0385d/cffi-2.1.1-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:df423d40ee8654634421812bc3b196da3f9bd7d32929da813f8394c4348a5358", size = 211520, upload-time = "2026-08-03T21:21:04.863Z" }, + { url = "https://files.pythonhosted.org/packages/a0/cd/ccac9013a5bd9fd764de118674ab9c805b5ca10c19270d90ee273f8b2240/cffi-2.1.1-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:a730a083190634c65cca36ba5f489531576ebd79bcd5c8e172130f6453127231", size = 210673, upload-time = "2026-08-03T21:21:06.223Z" }, + { url = "https://files.pythonhosted.org/packages/52/86/2976131c639aead931c5bee5aba67e4b09fbeb8018b6f282f70803f923a7/cffi-2.1.1-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:363e05fa78e15116c3c32c210ee36884fd6b9afa6d440e47112c3bd511d64cb6", size = 223835, upload-time = "2026-08-03T21:21:07.539Z" }, + { url = "https://files.pythonhosted.org/packages/ac/0c/33a7aeab2f9c76918c52e084beb39c570db3588133412929e8ec06fab90b/cffi-2.1.1-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:770de9db11e84213beec501cfcaa013b019820ca881e03344dea5844f7876d94", size = 226705, upload-time = "2026-08-03T21:21:08.774Z" }, + { url = "https://files.pythonhosted.org/packages/e3/26/2cde30fdde421130bfc18f70395731a6e6b2053c6a1978a5258ff04e72fa/cffi-2.1.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:7da0c5eff80f0197f3b3d1232ec5a682a9325f4ae9016a78f5f5ca35f9ced1f5", size = 225539, upload-time = "2026-08-03T21:21:09.911Z" }, + { url = "https://files.pythonhosted.org/packages/6d/cd/a361394c94b2129d604bb846f624a8e88255a3ee33129c434a00d715e64f/cffi-2.1.1-cp315-cp315t-win32.whl", hash = "sha256:06c72bb76605a4b0cd0aad6930b69d4baf7dd5d806cfc409b824191099700e66", size = 182707, upload-time = "2026-08-03T21:21:11.226Z" }, + { url = "https://files.pythonhosted.org/packages/9b/b5/ba2b299993c26577d529b6ae29841f9e15b9fcf004d65f423f4fcf94ade9/cffi-2.1.1-cp315-cp315t-win_amd64.whl", hash = "sha256:d9c275eaacd24aa73f94ffd6de08fc3f932424d8b6c376f4bed7cde376fe7bc3", size = 193772, upload-time = "2026-08-03T21:21:12.39Z" }, + { url = "https://files.pythonhosted.org/packages/aa/29/35e016098c814cd93de9cd320c66b5bfba14dc6ecedd3cb518fa7c408c69/cffi-2.1.1-cp315-cp315t-win_arm64.whl", hash = "sha256:d18e5ac0f2f03f4f518d3e23db0f0cad7faa1da8620e9c09461d443bbf6e6692", size = 186360, upload-time = "2026-08-03T21:21:13.636Z" }, +] + +[[package]] +name = "charset-normalizer" +version = "3.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cb/31/4971872b3ed8715346231fb6eb4da8fcba65a4143c189db151ee28a2812b/charset_normalizer-3.5.0.tar.gz", hash = "sha256:49bd5feb59b0bf3cbf6ebcf4352e371c95b9da9bacd4449f8b64d0ad2c10a26e", size = 169295, upload-time = "2026-08-12T14:35:31.624Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/50/42/71e4e3bfe59202feef062c68487f54c6adf501cfbe087ecd93e3cd597fea/charset_normalizer-3.5.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:e46a37ea7fcf9ae01d71b2e5ece19f1565987f3e308394b829197cbefc061f92", size = 349110, upload-time = "2026-08-12T14:32:07.832Z" }, + { url = "https://files.pythonhosted.org/packages/0f/dd/fd3386d0fbd358d3b5c7a2fa5bf312afe6159b04fafeb67d39fa971d7448/charset_normalizer-3.5.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1cdfed4d7a59333c8220c67dd3be4e7a6c887b67453a64394022dcc919570add", size = 250773, upload-time = "2026-08-12T14:32:09.113Z" }, + { url = "https://files.pythonhosted.org/packages/17/ad/4901a66d6d3b17f1096725d7e50266132c16555aa6a70047fe1cf262b4b2/charset_normalizer-3.5.0-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:9491f594859b68052edebd69e05fb045055a713b57a67974e6c1553b4e503c39", size = 240229, upload-time = "2026-08-12T14:32:10.43Z" }, + { url = "https://files.pythonhosted.org/packages/43/b7/1d790e0425e0f4c99e9b89a3956a94a6d2d0c6f01b2a5eca93d8f082d5ac/charset_normalizer-3.5.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:420b19411959eec115063229536788e6b32d0a7fa907d6b940317919120d702d", size = 280757, upload-time = "2026-08-12T14:32:11.628Z" }, + { url = "https://files.pythonhosted.org/packages/44/bb/4b8c8086c67636e52d6354ad17697f54a00b40041ca53dc765737e21709b/charset_normalizer-3.5.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a565303d118ea3b94a4b6c076bf568069726be414e43b06d58f7070b076ce11d", size = 276174, upload-time = "2026-08-12T14:32:12.808Z" }, + { url = "https://files.pythonhosted.org/packages/25/fa/690a11924c40c766d258d2b74d817cc5efe7bbcfceeedc5c0f35256d7524/charset_normalizer-3.5.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:815f143a91983ba3041bba066e492ae3c42de523fb1c699685a1abf3313b7d1b", size = 261808, upload-time = "2026-08-12T14:32:14.138Z" }, + { url = "https://files.pythonhosted.org/packages/53/2d/4a6945eff0c8f684e3f5b7b978644ab18ba9198da060dbaa1d9206bc6cc9/charset_normalizer-3.5.0-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c5e981a5ac8641381efe6f0029467500661616a530d27bc6eedfe45f840599f8", size = 259922, upload-time = "2026-08-12T14:32:15.485Z" }, + { url = "https://files.pythonhosted.org/packages/60/4d/10ac7e07bbf7ea569effeb9524e32f345f7e643800b20d538fb4706eab4e/charset_normalizer-3.5.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1a573e1e428f93908e79e04b349717f400e720f2f82285f0aaaf3ee0ff7f4c79", size = 252315, upload-time = "2026-08-12T14:32:16.764Z" }, + { url = "https://files.pythonhosted.org/packages/13/bf/1ddacf7aa7da12097f229a6a4e71a70200937a621b3617f6dc819fb99a66/charset_normalizer-3.5.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:32e6d56dd825205f81e5c45bcebb4df6a11fb2bbf4969a01ef156d6ced90c224", size = 240600, upload-time = "2026-08-12T14:32:17.956Z" }, + { url = "https://files.pythonhosted.org/packages/c2/88/293444d48c8d86fe51552262117134a9fc666d68cbbbc9b8c1b2b35a29be/charset_normalizer-3.5.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:30ae26a1adcd943690dcbbc47f28be762bae9e08ad7442b78c86b1c0dd5a626c", size = 280788, upload-time = "2026-08-12T14:32:19.1Z" }, + { url = "https://files.pythonhosted.org/packages/12/ed/0e34e40584f51eda38d4a5daf25fd8586366347efd4b2470dbf64710e778/charset_normalizer-3.5.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:5f51a19dc52197a20218b05ec5336d0c6b3b09935f838724722032c8d45dc91a", size = 258425, upload-time = "2026-08-12T14:32:20.207Z" }, + { url = "https://files.pythonhosted.org/packages/72/b2/c1b1c27f6f0ef35b21a8bdb592854bbf7f219629db3477f74c0b1380e0ed/charset_normalizer-3.5.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:6e44bc2780516b3df986d6fe33103c7080cd9dcd5576fe3cb4b0f64309c8f22b", size = 277437, upload-time = "2026-08-12T14:32:21.345Z" }, + { url = "https://files.pythonhosted.org/packages/b2/bd/a1b7d959a37847675adb2f5978f81703306dd78df4fefb82ee5a1cc5e37f/charset_normalizer-3.5.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:7faa47b56070b3dd6f4898ed28528843ab130d53266cb9948d9b1f3bb1a5c5e8", size = 262947, upload-time = "2026-08-12T14:32:22.613Z" }, + { url = "https://files.pythonhosted.org/packages/c1/fd/a76a2d7e639bfc6a9c869371e34f28231eaca7d7126ec19895aa38eedb51/charset_normalizer-3.5.0-cp311-cp311-win32.whl", hash = "sha256:830c04a49998b5ed58c8b642c65b7b26419397f52392a64121ba9fd0e95e7f9f", size = 181313, upload-time = "2026-08-12T14:32:23.736Z" }, + { url = "https://files.pythonhosted.org/packages/97/84/6fc03e802578df41a2ab9b6a1f26657fb92e32285a603ad5852a4a4f68c1/charset_normalizer-3.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:8cb9b6892b53bd6d11fa4cde3dbee020b1f0b6656be1fbaa1ec0d4324a7839db", size = 206197, upload-time = "2026-08-12T14:32:24.938Z" }, + { url = "https://files.pythonhosted.org/packages/0a/27/7208360ff1901607359869fcc45ca0989d597f3e30777ba30c7254170587/charset_normalizer-3.5.0-cp311-cp311-win_arm64.whl", hash = "sha256:2403b489c103e9a18c835863fc6dd54361355c8291d4cafdb37492b683440b9b", size = 184925, upload-time = "2026-08-12T14:32:26.123Z" }, + { url = "https://files.pythonhosted.org/packages/6d/3c/045ea64ea5a550870dd8ab60b2242870328d53f17d2be593b4f9f3121474/charset_normalizer-3.5.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:98820e1ceb25c6df7a80c4fd8efa59cb121f99bc7c4c1693ad94a2caff5b311d", size = 343861, upload-time = "2026-08-12T14:32:27.254Z" }, + { url = "https://files.pythonhosted.org/packages/fc/1b/7502be709db899d5b4801509829188b3a5a10969411da9c846115a5f1b70/charset_normalizer-3.5.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:608553f476fca509537e804c4a71f5eb166ce63b75141f89c2c686ce1aa36956", size = 237550, upload-time = "2026-08-12T14:32:28.385Z" }, + { url = "https://files.pythonhosted.org/packages/ca/ce/66392661375148d9455c17bee25509a54e28c39969e34befa48ec8777936/charset_normalizer-3.5.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6753de11eef42f1c321b26d682957d92c7f7bbce6530f34bbe0f9291dd37cc6f", size = 229673, upload-time = "2026-08-12T14:32:29.668Z" }, + { url = "https://files.pythonhosted.org/packages/6a/64/f58c32a8d4ecf55b82ee61ee9aa6a664d4afcd36c72feb4c926fd6fe9af8/charset_normalizer-3.5.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0f76dc0a47f94cb9b69d86f01e477f4b0371ca70208b9ccea7e063c41eed9046", size = 260768, upload-time = "2026-08-12T14:32:30.891Z" }, + { url = "https://files.pythonhosted.org/packages/b1/ea/36c90e59a96386174377e855479ec154221ef001e96637e0b23be92489c4/charset_normalizer-3.5.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c387c6bf91b4774e359a48a179e2872b8e8bf741e4fde06ba8d1665eb9a4760a", size = 257880, upload-time = "2026-08-12T14:32:32.175Z" }, + { url = "https://files.pythonhosted.org/packages/23/35/5b85772eb82528ef22ba29487ad544a7049dfd27f35b1a5a55dbc0843048/charset_normalizer-3.5.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:14f6904a3cf870abf044df3a8c4924ac6c8ef77e9896586fd37e73ae96cff2af", size = 247547, upload-time = "2026-08-12T14:32:33.495Z" }, + { url = "https://files.pythonhosted.org/packages/b6/14/ba11a99c2a22ab04c2d5383a700b378cb463a78ab15f36444cabc10cd671/charset_normalizer-3.5.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0cce46dd29d73e135e8087b96eb62a4aca6d69391b7f97808c6588ebed3178f3", size = 243326, upload-time = "2026-08-12T14:32:34.794Z" }, + { url = "https://files.pythonhosted.org/packages/4c/4a/cadba3f2400b45aa1d62a4ae0298bf58a3b30b1158baf15c338c7ce5b601/charset_normalizer-3.5.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b476cdb63df22da2b91837593380be3ddbe406f36c506c1c91d80e7196b66288", size = 238820, upload-time = "2026-08-12T14:32:35.984Z" }, + { url = "https://files.pythonhosted.org/packages/47/41/d5188b9342d75b72c2b05d3ee373f01a691397e770f001ee05e3b37925f5/charset_normalizer-3.5.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:1f56ce84b317ef2a59d7d3461891c7597c79247d2192bb8114c68a1a1debfcc0", size = 231661, upload-time = "2026-08-12T14:32:37.191Z" }, + { url = "https://files.pythonhosted.org/packages/13/5f/df38fa972c4e945c3d8cee2bc4e610613af522fd359c7dc7a74c419f0278/charset_normalizer-3.5.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:9ce0f885239357379d92fd9a5fddbe20f0e30e0527c29ba69f8e99eeb1304a76", size = 261459, upload-time = "2026-08-12T14:32:38.369Z" }, + { url = "https://files.pythonhosted.org/packages/a5/d7/043ff7720067a3beee05523465ac9c1c846c68b7884930dd483f72ee5ab6/charset_normalizer-3.5.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:96ae7ab5d8155fde927aa0864fbc8ba3cc4fde6d41ab0c7cea9d6012b4978603", size = 242300, upload-time = "2026-08-12T14:32:39.505Z" }, + { url = "https://files.pythonhosted.org/packages/19/f6/33980b7b802a048e546a6d9ad2ea783a6cf6b10a86aaccd10db462d8b913/charset_normalizer-3.5.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:bf91921009025e96ce57a03ced6d14604fc3baf0530351638e9504a55da6fa3b", size = 259101, upload-time = "2026-08-12T14:32:41.02Z" }, + { url = "https://files.pythonhosted.org/packages/a6/b7/0d19bde844bff9165377c1da9ef3c4792a4c24bd49b5b7094d9e6f6ab58b/charset_normalizer-3.5.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0b2e44e6d42d1a4ff78ccc219a93c5449105d10b16198d1aea581080df8073f9", size = 249246, upload-time = "2026-08-12T14:32:42.47Z" }, + { url = "https://files.pythonhosted.org/packages/1c/cc/2c34fdfaacdf0e96e880ef562cbf80a9b5f8ea97e0dd9e57ba348a9c65cf/charset_normalizer-3.5.0-cp312-cp312-win32.whl", hash = "sha256:deb99535e9bf0bea8e274c6413eb939a21be35a3f492678dba4d5b1f4d70f142", size = 178025, upload-time = "2026-08-12T14:32:43.909Z" }, + { url = "https://files.pythonhosted.org/packages/76/d0/c34dbd1df23bcbdc1b5d2f48256340d72fa747f1eb03924a9d2fa35ed85b/charset_normalizer-3.5.0-cp312-cp312-win_amd64.whl", hash = "sha256:e54dd1a66fa4bce0ccaf0db9dde336e49b3eec646dc4c1c0991279369d373a14", size = 200143, upload-time = "2026-08-12T14:32:45.254Z" }, + { url = "https://files.pythonhosted.org/packages/c0/ea/4e6bf1465d60c3d8f488d5bde140d0cb91cd23ccab0dc2895cc6c6982047/charset_normalizer-3.5.0-cp312-cp312-win_arm64.whl", hash = "sha256:b8ea208b304587d47931b36481342d20336e0d338ab052f8b4305926482598d6", size = 180046, upload-time = "2026-08-12T14:32:46.545Z" }, + { url = "https://files.pythonhosted.org/packages/1d/be/cc7b7b6fc41984902c0d31b06f5d9297e67705c1dae9352608e5540fad09/charset_normalizer-3.5.0-cp313-cp313-android_24_arm64_v8a.whl", hash = "sha256:5c23fa4f6eccdd601949cb00f3988c01d64e671d8faba356397971077022e144", size = 211050, upload-time = "2026-08-12T14:32:47.81Z" }, + { url = "https://files.pythonhosted.org/packages/d3/ae/e3ec8f17313609f43f7b323012fdb1ee37b83432277ca4eceba83e00366c/charset_normalizer-3.5.0-cp313-cp313-android_24_x86_64.whl", hash = "sha256:07f6f42b5a6325df35b458004fb5f9f29bf502d89287a33c7cdef3590e31de0f", size = 222768, upload-time = "2026-08-12T14:32:49.027Z" }, + { url = "https://files.pythonhosted.org/packages/c5/50/9f9c0d7ccc1512d49e27a0e7c12c58ec71dfe91698fa4326f058c33e1f1b/charset_normalizer-3.5.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:8efc3f1563ed431882dd0dc0411b5f8ace1b1b89074981deaf6bd8af77dbe1bc", size = 193907, upload-time = "2026-08-12T14:32:50.414Z" }, + { url = "https://files.pythonhosted.org/packages/fd/1d/cfe7b745ef7f4c3b7214581955b5a0869ba2ac551a58fc11036281ae167c/charset_normalizer-3.5.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:368eb2fc9482158b3a3386e8f01fa61f479c968e9a19ceab8f0188b86b312991", size = 197135, upload-time = "2026-08-12T14:32:51.605Z" }, + { url = "https://files.pythonhosted.org/packages/28/55/30fafdcfca9ba616bc394240545e4cd52f4f66dea43ded81b7d2d5274fde/charset_normalizer-3.5.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:826a295a039178479a325be1ae60eded1f0b10f7dda749df59e2440de8f61d64", size = 339892, upload-time = "2026-08-12T14:32:52.82Z" }, + { url = "https://files.pythonhosted.org/packages/f4/08/bdca5fc2bdc36ee443673dc7d12b23885a5a7b282bef85a1a4c3b325b40e/charset_normalizer-3.5.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:70ff1c16eb0eb5ee6bb12739292347f981a5ba764cc4df1bc2e69b0405d4ac3b", size = 239439, upload-time = "2026-08-12T14:32:54.058Z" }, + { url = "https://files.pythonhosted.org/packages/d1/9e/506c8d7a7722bba7c8cdd78c1b5ef23bda92bfbe0b3e28ea84673d519a0f/charset_normalizer-3.5.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3cfdab178a4add5483e26a9bb1c16d8018ccf39b4be7a3aea6c3979e6828f2ee", size = 227896, upload-time = "2026-08-12T14:32:55.326Z" }, + { url = "https://files.pythonhosted.org/packages/0d/1a/dd828f2b1d6f4bf10821b9a74d866be05ffcdbfcddfc501d6fe6428762a7/charset_normalizer-3.5.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6083d10a846218502d664375b9448508d9fa580bd834567423156c6abfbe899d", size = 262548, upload-time = "2026-08-12T14:32:56.484Z" }, + { url = "https://files.pythonhosted.org/packages/be/81/196d26f6bd78b93e0d451b69082a71027ceeddd4b0be9170b81bb038f824/charset_normalizer-3.5.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0f211c21aa316cb6e2662e54a1194633a79d98a50a876addacfce7ba5b34b09f", size = 259986, upload-time = "2026-08-12T14:32:57.661Z" }, + { url = "https://files.pythonhosted.org/packages/3a/6a/5b964a1eb0f9075ecd45083eeb21aaec215334f98bac3d400302ea73875d/charset_normalizer-3.5.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3b08ebf9488c7ff5eff038e48e6ea938178dfd9dcc8598b5ca941e4ae27b20be", size = 249853, upload-time = "2026-08-12T14:32:59.123Z" }, + { url = "https://files.pythonhosted.org/packages/c8/b4/aee3a9d82edd0e931091ef3e9f03e46491ae3590e96e998d0975dadbe17c/charset_normalizer-3.5.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6c95450fce59f00c6d08eff6572ec2e736e5054c9450253afd5748f8416f2eb9", size = 244217, upload-time = "2026-08-12T14:33:00.341Z" }, + { url = "https://files.pythonhosted.org/packages/22/3e/33f72ca11c1b619b220fd9f35905ebd171cbd0e0470f2357e467b9e861ee/charset_normalizer-3.5.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5780a29823e1d2bec69b7a104ead4195a43f3e97782efaedbf1f79a0157af715", size = 241307, upload-time = "2026-08-12T14:33:01.589Z" }, + { url = "https://files.pythonhosted.org/packages/78/27/6029dccba958621c7f3a65136f87c5512d712aef9e890f09512cc171bd03/charset_normalizer-3.5.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:054420b5db984971d886e5e4e2c37c760ae6682aedbd066687ff0949d9ed5f08", size = 233305, upload-time = "2026-08-12T14:33:02.866Z" }, + { url = "https://files.pythonhosted.org/packages/18/d7/691c967be459153fe9faf49bf78bc95639ef8bf6dd008f38cc6389a349eb/charset_normalizer-3.5.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:d016dc857136c726958102c3b8a3986acdc65ace6fbf12cfdc09cc4bfa2935b2", size = 263465, upload-time = "2026-08-12T14:33:04.166Z" }, + { url = "https://files.pythonhosted.org/packages/7c/2d/9202221be5c90b2a835924191e362690ed8dc8c7d6606100c2bd03fe0f8c/charset_normalizer-3.5.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:f2ce3d39fb4a9d674e6639dd5d3146b2e273475d2260f10163228d66fc04433d", size = 245060, upload-time = "2026-08-12T14:33:05.325Z" }, + { url = "https://files.pythonhosted.org/packages/b3/9a/298772fd0a0cbccadf36451a1cd7eef4b66a11e99b4a7f6fafc47cc62c75/charset_normalizer-3.5.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:a5613a3a82c974227bde18f03409e30c467f8065cb56d822e3eb83708a5f223d", size = 261091, upload-time = "2026-08-12T14:33:06.475Z" }, + { url = "https://files.pythonhosted.org/packages/82/3b/1a11fe66e555dbe2f5714ade6ba74fa29edc9155d9cf1001d4d6ed096aa7/charset_normalizer-3.5.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fded2e82ff082e5d8e017e2ddcc1411bd8cb83b8585097fc401ef574f756b888", size = 251857, upload-time = "2026-08-12T14:33:07.784Z" }, + { url = "https://files.pythonhosted.org/packages/17/fc/73b817e8af3f1d25ec5cf458d405abba5a144cf9812238a61530f5eac186/charset_normalizer-3.5.0-cp313-cp313-pyemscripten_2025_0_wasm32.whl", hash = "sha256:8f006866047c6ec4b627ec144b1e0bbc7427cb31fd7c08d19897d0ac9032af3d", size = 139745, upload-time = "2026-08-12T14:33:09.123Z" }, + { url = "https://files.pythonhosted.org/packages/b3/fb/ddb66303c86f7dc5043a457dad9fa82b4d6d0cb97094f9bcdde21693fd58/charset_normalizer-3.5.0-cp313-cp313-win32.whl", hash = "sha256:196e270c4e80827b5072eed7d6aa661d133afada94fe366669f9609e718d305e", size = 177217, upload-time = "2026-08-12T14:33:10.305Z" }, + { url = "https://files.pythonhosted.org/packages/fb/88/6018cc8d76ea2b7cb02918f37e23e86c261d1a102713d7e88d2cfb8b211c/charset_normalizer-3.5.0-cp313-cp313-win_amd64.whl", hash = "sha256:72982d9958a42f8132bf2d6b90214ed66477295ef1188731f98ae3511c6eeb5a", size = 198896, upload-time = "2026-08-12T14:33:11.51Z" }, + { url = "https://files.pythonhosted.org/packages/20/2e/04c0bbfc8d9abf91959f7a3d207d45cbf63a8116984caae2381890019bb5/charset_normalizer-3.5.0-cp313-cp313-win_arm64.whl", hash = "sha256:0b373bab0b867b68b8eb249da9478cab9181a42993437cd2f5dba5fb0b4fbd1b", size = 179193, upload-time = "2026-08-12T14:33:12.726Z" }, + { url = "https://files.pythonhosted.org/packages/43/14/d098868dac5ff27e0258f548b1c74c6484be528384965d8fcf8fc6a4011d/charset_normalizer-3.5.0-cp314-cp314-android_24_arm64_v8a.whl", hash = "sha256:d95244906ed69d0f79f190893c65e336c15959003e21449256dc05c001b52ea2", size = 211664, upload-time = "2026-08-12T14:33:14.153Z" }, + { url = "https://files.pythonhosted.org/packages/e7/da/a944b32a46601ae5a4c3499e8d64ecd14fe82313f00da74dcdf00273a0b4/charset_normalizer-3.5.0-cp314-cp314-android_24_x86_64.whl", hash = "sha256:d788e2ded0c4c47efa4d73cfe59eaf975ee32f425219873d2cb3e3fbaa00f636", size = 224375, upload-time = "2026-08-12T14:33:15.472Z" }, + { url = "https://files.pythonhosted.org/packages/f7/db/eabb5996be2f529744755e7b2fc9396eff4a64961f034e7fd49d54b9afb2/charset_normalizer-3.5.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:f9f91d3e8382900f3a68fa0ce94294479de9cd2de6bc0c70acd0f0dfd511836b", size = 194364, upload-time = "2026-08-12T14:33:16.607Z" }, + { url = "https://files.pythonhosted.org/packages/78/65/4ad3c5be108930310d8003f5602861d5b89f728293b9f09c3a4837f7ba10/charset_normalizer-3.5.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:d54625cbf4e6b60bf0639728cb8b4cb541e340f6d7cafae5806051a40ddf4c45", size = 197643, upload-time = "2026-08-12T14:33:17.88Z" }, + { url = "https://files.pythonhosted.org/packages/3d/39/8fee3201b98d52289be60a775797d69be05a04fb6cfb48c1587dad33e649/charset_normalizer-3.5.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:1f99a8c3a1da5d955edbad18208b3d627bdd54c48a6e739fa877bdca98c686d6", size = 341384, upload-time = "2026-08-12T14:33:19.239Z" }, + { url = "https://files.pythonhosted.org/packages/a7/dd/9e757101d1f76c35c0643684ba499ac3a181fb2b264c68174bf727d627e8/charset_normalizer-3.5.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bf1e75dc07a3850b53d1e5f75e04d3ae12afe56284be7821771eaa2466350c73", size = 241637, upload-time = "2026-08-12T14:33:20.619Z" }, + { url = "https://files.pythonhosted.org/packages/eb/e4/7857023015400bc4aa0a82fbcca29fa2dc7ec25f971a130764cb2dc7a589/charset_normalizer-3.5.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:ac5a9cc079c67d75f4ddf343276031879eadbb333d1bb231cce297b8d7b9aae8", size = 226170, upload-time = "2026-08-12T14:33:21.773Z" }, + { url = "https://files.pythonhosted.org/packages/7d/ae/8b52935b304f7b6bbf33151ed2b75266b09aa4b6f8f04230d948885b2577/charset_normalizer-3.5.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0dfe83c1b4d00abbf433998117a14f56a5c2bc68226c0d331709eed0d1ce539b", size = 265093, upload-time = "2026-08-12T14:33:22.999Z" }, + { url = "https://files.pythonhosted.org/packages/dc/78/6e838f6bb059f2c0afc60a4e7f294252f043c254656ad4114c50302cae4d/charset_normalizer-3.5.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e4e8fa586df2208ef040684751345f10f503834a757c9a74ecd19c1a2f9b1ccd", size = 262789, upload-time = "2026-08-12T14:33:24.214Z" }, + { url = "https://files.pythonhosted.org/packages/c2/08/189b27e51fddc9d6b3695331da0e31792c1d88b953ad854e57f06e9b2cc8/charset_normalizer-3.5.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:82cc5835997ec78afe293a192e385099355770a7db94b2fb1239d36b32796f1c", size = 250580, upload-time = "2026-08-12T14:33:25.707Z" }, + { url = "https://files.pythonhosted.org/packages/ac/55/64854e99b25841f83e8e37d9df2f3d1f96f693439f80e5fabd542a7e47ab/charset_normalizer-3.5.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:19e52bda45086df8a4be4bb5910af6f5d9d3b538c78712c8ae09ef10b85bf458", size = 245008, upload-time = "2026-08-12T14:33:26.971Z" }, + { url = "https://files.pythonhosted.org/packages/4f/01/7720c904fa635d4260b4dced6029cf3d298c57b26741365d5a8d28c54043/charset_normalizer-3.5.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:3418edd0ecb72a0a3861cf72f31be0ad9b7fe338ce2b58fb5cc80b9aeb792700", size = 243892, upload-time = "2026-08-12T14:33:28.237Z" }, + { url = "https://files.pythonhosted.org/packages/70/50/7bfcb327631d4870c720872b548745f6ec8baa044d51c21b5d1d32ac4e3a/charset_normalizer-3.5.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:125ee619611019471b177c70bc3e9d4cda9fad7e01d93523501d3b188df0193a", size = 230996, upload-time = "2026-08-12T14:33:29.511Z" }, + { url = "https://files.pythonhosted.org/packages/24/51/40c45d6d940c04005ed721aa54bdebf1ebb2930f8a2ae537e8d60484fb27/charset_normalizer-3.5.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:a3ad0e3da22852533858663848608f3f24c0d35e5cde415a4903476f2b4c88ec", size = 265834, upload-time = "2026-08-12T14:33:30.689Z" }, + { url = "https://files.pythonhosted.org/packages/eb/d4/ef7a227ef89d215b47f9df79c3966610b17faa13bb2f236989207a631622/charset_normalizer-3.5.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:4ebebb410bc517e1d284c52a123e82704b21e4e7e26a21ebecf7439d0647b8a3", size = 245544, upload-time = "2026-08-12T14:33:31.859Z" }, + { url = "https://files.pythonhosted.org/packages/37/a9/a4ca9156964ded61c7718eba410ce11be2fd2b263fda4bcf08367b6578cd/charset_normalizer-3.5.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:91f9f7c151e772acebe489eaec96e96a2877202d7dd144e3f96b8676881715a0", size = 264110, upload-time = "2026-08-12T14:33:33.13Z" }, + { url = "https://files.pythonhosted.org/packages/38/6a/838364bb8702229c6e5f8b23f80ff0f052a12dfaf3113a12fd6acbe92a44/charset_normalizer-3.5.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:401ea6e7af9e7852ed818f64714b579c1935482049670847ca3bd7ba45dc63fb", size = 252303, upload-time = "2026-08-12T14:33:34.98Z" }, + { url = "https://files.pythonhosted.org/packages/9c/5f/d88032edce951f499a2321cf7ae0d35a043c74be12bc22d81084cc7afbcc/charset_normalizer-3.5.0-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:f7496aed56b06325a1ad419c5bf23c6dd042558e874f71dd1b958f3e255f3053", size = 139964, upload-time = "2026-08-12T14:33:36.195Z" }, + { url = "https://files.pythonhosted.org/packages/37/ae/1c4a46b6b00d1c34d2ee355ef99ad6173674166800d1af0f05f85028d513/charset_normalizer-3.5.0-cp314-cp314-win32.whl", hash = "sha256:606a86c1c3196f3738de39a67a7490bbd61cb31c0e0436070bd0c6a48170b38e", size = 179790, upload-time = "2026-08-12T14:33:37.356Z" }, + { url = "https://files.pythonhosted.org/packages/01/51/f94dcf34fa8eba48c1fb89b6490a5f1426e19488fe5f38aac6c648c99057/charset_normalizer-3.5.0-cp314-cp314-win_amd64.whl", hash = "sha256:ec6c464cf45867f66a2273e2214d9199a8fbad5cb95ca0fd45f6a2fe1d9d2cf4", size = 203723, upload-time = "2026-08-12T14:33:38.639Z" }, + { url = "https://files.pythonhosted.org/packages/9f/ba/91d386870b5d9e4b0d8c4034f63877cc2e47b99c81ef05f3e6d42bf9a53f/charset_normalizer-3.5.0-cp314-cp314-win_arm64.whl", hash = "sha256:dc28949de1bb5f7f30a46f15d74ce7ac5aaa63e03c5de04d68f571c7423af834", size = 183423, upload-time = "2026-08-12T14:33:39.899Z" }, + { url = "https://files.pythonhosted.org/packages/f1/c9/534ecb17b7fb95f9052c4a44cf316316a27d4a8f73e8475ff55e778dcdd7/charset_normalizer-3.5.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:68b7e84ae8239a94f8d2c8f3f3a3a81bcde54805ec8f42a34de927d155688ec6", size = 368967, upload-time = "2026-08-12T14:33:41.093Z" }, + { url = "https://files.pythonhosted.org/packages/3c/b2/ad7c3242d7fe55cd55126c22c65cb1b49779782cdf8932fd01d12232d86a/charset_normalizer-3.5.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:58ca5dc0a0ef99f2801ec0574214c978e9574055bc783830bbb6e7433218609f", size = 239478, upload-time = "2026-08-12T14:33:42.428Z" }, + { url = "https://files.pythonhosted.org/packages/7e/62/77f0b850048e430fc350ec58876b0c020f5c8d0d3956fd1a4d6ae2fa292f/charset_normalizer-3.5.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6c57af4084c10cb3286688d65e4c654190ff5edcbc2411d08cdca0a8a44c59a1", size = 227036, upload-time = "2026-08-12T14:33:43.635Z" }, + { url = "https://files.pythonhosted.org/packages/f0/ba/47d951e1a51dddbaad0a1410baf49fb1d897ceb00281568f1183b79bce9a/charset_normalizer-3.5.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f1619a3cc174a7e3963dd34348e6fceb6e50db0ddeb0031bd7c73a58286454fa", size = 260772, upload-time = "2026-08-12T14:33:44.96Z" }, + { url = "https://files.pythonhosted.org/packages/b0/61/8c7ff4c81b2a88271126acf4b83ab3e31f6d63868b0f01d331eaa0f9cb67/charset_normalizer-3.5.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1328cc57dd4372be1265f68232cee890e087416e3e6e93e6ffb32c2bad4d36a4", size = 259273, upload-time = "2026-08-12T14:33:46.185Z" }, + { url = "https://files.pythonhosted.org/packages/e1/fd/36129689be08dc287b951306946657ff70d76e287dd57018861f86d0e474/charset_normalizer-3.5.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:06f4fb62a9139bef056b8b2da6773c94c2f259f90e4b8e53b166f3d0372d7cf6", size = 248086, upload-time = "2026-08-12T14:33:47.54Z" }, + { url = "https://files.pythonhosted.org/packages/cf/f8/bcae67f994c8fd31dda445e5ebf84045823c31443fe46f0e9ee6aca99aa0/charset_normalizer-3.5.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:478650a70a750d75d5add401606c77f77069c32e4ba2c9131dc6cee566962ca0", size = 242671, upload-time = "2026-08-12T14:33:48.746Z" }, + { url = "https://files.pythonhosted.org/packages/61/92/0472cdad1061c2f0e4d3aee29973eb6e81bb8fe256ff2860cf115b15f1c9/charset_normalizer-3.5.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:48920bf6fe83eb2226756ac623fa54940487154eb18f80889d5735cf234965c0", size = 241311, upload-time = "2026-08-12T14:33:50.152Z" }, + { url = "https://files.pythonhosted.org/packages/f2/89/04a03de5d27c77c624d9fcf6287073754bd438df1b58cb7d030c57c2824d/charset_normalizer-3.5.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:168a0cb536b5123a77bc42ecf5e0bf6f923d0d9ae43c42a14eb0677c19ac6c19", size = 229898, upload-time = "2026-08-12T14:33:51.523Z" }, + { url = "https://files.pythonhosted.org/packages/42/a2/639c4278adcb7ed1f4db608dd9ac19b6774fa2285a96b1c0bdb9c124ccbd/charset_normalizer-3.5.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:f278e131afa96a3622cef9211c406ea2ad1b68eb06f8837cd443684a40e0ae50", size = 262852, upload-time = "2026-08-12T14:33:52.924Z" }, + { url = "https://files.pythonhosted.org/packages/9d/95/02e34c97bedfd0c5574efb9179c850591acc7f967ba039ed8dd29d332b73/charset_normalizer-3.5.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:d6100f877d2ed95f0856a3fde25334153add94bf2224c43f45f88e7039262aaa", size = 242913, upload-time = "2026-08-12T14:33:54.18Z" }, + { url = "https://files.pythonhosted.org/packages/a3/64/0946aeab6462dad9f160a50dfb4704d3f58a5ee708f085abc2105fbbff0c/charset_normalizer-3.5.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:4c440122e1ea68b1f8b44a631ebf49c39180f6869b1da22d76e8a724208ec6e9", size = 257938, upload-time = "2026-08-12T14:33:55.802Z" }, + { url = "https://files.pythonhosted.org/packages/79/77/36787d41ead124746506a4425c729f4f17c68280af8a6a5baa0a598cae86/charset_normalizer-3.5.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e5f834965c2fe589837bac1002e07e25734ff70381903ccd95b3d649e22bfa40", size = 249467, upload-time = "2026-08-12T14:33:57.081Z" }, + { url = "https://files.pythonhosted.org/packages/65/10/d9f6c5589cd24198d4ce6cd2948191c18e657272f433e5a00d258d9f5c22/charset_normalizer-3.5.0-cp314-cp314t-win32.whl", hash = "sha256:076cf9d3f3c7e410295c09d96355cf3b1bcae74990034d80e4371e20fe1ba4c6", size = 190624, upload-time = "2026-08-12T14:33:58.449Z" }, + { url = "https://files.pythonhosted.org/packages/6c/81/43e0584a802051a22c725795ebe1df78263abc7de858eef6cdc9b36637e9/charset_normalizer-3.5.0-cp314-cp314t-win_amd64.whl", hash = "sha256:3288a560dc3114d5d2ebe309b1ef43f8af355eafe25856832415c2a8196c9db3", size = 215902, upload-time = "2026-08-12T14:33:59.753Z" }, + { url = "https://files.pythonhosted.org/packages/30/f3/af6a1160fef0eac4510d035241e11eccf78e5350e4cd4de79e79fe02a5e5/charset_normalizer-3.5.0-cp314-cp314t-win_arm64.whl", hash = "sha256:a284c36b9c6616bf0a8aa4aabba668a0c75ba65ccf40a79868aeaa69ad996897", size = 193452, upload-time = "2026-08-12T14:34:01.017Z" }, + { url = "https://files.pythonhosted.org/packages/42/a4/dee470afb7a55c4f78b6fef37306c51fed17ebf94dbe530798c91d394350/charset_normalizer-3.5.0-cp315-cp315-macosx_10_15_universal2.whl", hash = "sha256:c38d1e9bc2073b0984d2099ea647fd7f6c0d8f83a1e14e0cd32926f16e4c44ce", size = 341595, upload-time = "2026-08-12T14:34:02.4Z" }, + { url = "https://files.pythonhosted.org/packages/6a/32/9c3126dc429c6d9d7f79c52681a7c4453ed20a26267c9a8275d7ab620aba/charset_normalizer-3.5.0-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c9f45186390aee4d1f26f723c615b67df346766c3b16df000d84d6e374f06757", size = 242177, upload-time = "2026-08-12T14:34:03.741Z" }, + { url = "https://files.pythonhosted.org/packages/4b/9d/5b616a887301ff4cc0916b39ba44257390d3da80deeed6e8b6f2f26b14a8/charset_normalizer-3.5.0-cp315-cp315-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:f0fde5e5100c735b2274ab898f0742a5dcde492796296cfbe7e0ad6a4cd1a396", size = 236730, upload-time = "2026-08-12T14:34:04.991Z" }, + { url = "https://files.pythonhosted.org/packages/be/b4/d6d3e70be93ebe5fabef65e4c7ac113e1d1705cbaeb5fb72467e713aca17/charset_normalizer-3.5.0-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3d00e18e7bbf47e332ab63903d18bae31efc701b1d8cca0382b97784a621fc44", size = 265158, upload-time = "2026-08-12T14:34:06.235Z" }, + { url = "https://files.pythonhosted.org/packages/30/e7/3f1fafa87e2643257474f9c4eec609f2193a61d907dce7dd4f3f2390ebd5/charset_normalizer-3.5.0-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5b81980668800dd1c69faad8aea6e85a8cee0e13bcd3bba7671695ff16260293", size = 262931, upload-time = "2026-08-12T14:34:07.511Z" }, + { url = "https://files.pythonhosted.org/packages/0a/df/ebeb224a949d91829e5e114c6b64372a3c792b00762a9e951ce416f3a32d/charset_normalizer-3.5.0-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9bb3e0d1345b9c0fe73673ea656375f38a78ec679c2edeae0c24800f04798a85", size = 251388, upload-time = "2026-08-12T14:34:08.949Z" }, + { url = "https://files.pythonhosted.org/packages/a0/64/9a6ce2e7acc5cf1b4636f78f82e89ff581e06a0216a40678b28bd4d832c4/charset_normalizer-3.5.0-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:2401f7671242e921e604f609d429f6b282ea4ca787a6ffd22ed7372011ddb9d1", size = 251821, upload-time = "2026-08-12T14:34:10.138Z" }, + { url = "https://files.pythonhosted.org/packages/f1/b1/6e69b8056f615e5ccff6b91ca16db2d47922251f016821a300c115267fef/charset_normalizer-3.5.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:96720f2aeed3434bc48f4d52fbad64ecc820cfed88915d664780ed9ba09ede78", size = 244507, upload-time = "2026-08-12T14:34:11.488Z" }, + { url = "https://files.pythonhosted.org/packages/0f/34/02c15d6a0aa6b934dcdc136b111da63ae857b9fd51cf5505b0736337c2eb/charset_normalizer-3.5.0-cp315-cp315-musllinux_1_2_armv7l.whl", hash = "sha256:c455829625df983f716cbaecbba77f2d1dc2e0e0ed1638c059cece15a279344b", size = 240951, upload-time = "2026-08-12T14:34:12.991Z" }, + { url = "https://files.pythonhosted.org/packages/ae/15/0fe893d3e1c7d111280bd6c4bd4c1e431487a1124a1bcbce78dfeda3a3a8/charset_normalizer-3.5.0-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:c41b067eddcfa5ee6b1169c287605be7fb6b0ea22bba6474c5bb978a668def4f", size = 266162, upload-time = "2026-08-12T14:34:14.232Z" }, + { url = "https://files.pythonhosted.org/packages/55/ea/eca03527307670f5d102c295671a800c404ca958cf94fefd10fc963a72f0/charset_normalizer-3.5.0-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:e31786a947b136329bfdc458c82c06d4ec539b4a4436b7da4df4aafc9902ee80", size = 251835, upload-time = "2026-08-12T14:34:15.48Z" }, + { url = "https://files.pythonhosted.org/packages/03/a8/fee5633081e595fe9e191df6f215106c791ad596eddf5e41e39b8ea0f2e2/charset_normalizer-3.5.0-cp315-cp315-musllinux_1_2_s390x.whl", hash = "sha256:c5c6d47a865147e0ae3322ce92e7fb52ba3169d94b447deda56897ea2aa6fac9", size = 264314, upload-time = "2026-08-12T14:34:16.679Z" }, + { url = "https://files.pythonhosted.org/packages/cf/fb/17f47ae6ca35b562fb6e6f4b05f7aec6034217353eb4a23aaa3566dc7340/charset_normalizer-3.5.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:c75191e3c8052045179646cb40e280800a4e0bdfda34d9c949c2f268d44e80e4", size = 253194, upload-time = "2026-08-12T14:34:17.975Z" }, + { url = "https://files.pythonhosted.org/packages/59/88/f2b0f7ebb92493e925889ff29239b3b0073ffafd91230dbfc69e5cf9389c/charset_normalizer-3.5.0-cp315-cp315-win32.whl", hash = "sha256:83b62410bd36bb1178a7d563e2ee0cf21eb1c980c912ab99c2c78f06227f1731", size = 179800, upload-time = "2026-08-12T14:34:19.409Z" }, + { url = "https://files.pythonhosted.org/packages/e2/f0/afb5bfdea52fd943b1960403847a276b8e900c6e4cd6a38752321b4eda64/charset_normalizer-3.5.0-cp315-cp315-win_amd64.whl", hash = "sha256:e3b9eaa99a6d8c9ace4cd303915947ef55088d4cd87c6676874f98c5c03aa040", size = 203726, upload-time = "2026-08-12T14:34:20.656Z" }, + { url = "https://files.pythonhosted.org/packages/fc/71/219783eb691aa2ec879c0e521afdfe2b826f9678eed51b9c039d03e0db2b/charset_normalizer-3.5.0-cp315-cp315-win_arm64.whl", hash = "sha256:fec352b793cdc183cc9e7e0b6c10fd7bff38ec54ba44cc43599b9b56f7f3db2e", size = 183428, upload-time = "2026-08-12T14:34:21.975Z" }, + { url = "https://files.pythonhosted.org/packages/d1/d0/14aef3b9f80f2593c039d897e89034635b9eb0eb44b6ce5173bbd79ff338/charset_normalizer-3.5.0-cp315-cp315t-macosx_10_15_universal2.whl", hash = "sha256:c9bde7a960720c8b8e1b5ef7afaa0c9a2f3b55c44abd635b2b29dd066b298e3a", size = 368728, upload-time = "2026-08-12T14:34:23.221Z" }, + { url = "https://files.pythonhosted.org/packages/10/fc/b249466ddbbeffa448b6597631e9091d1f01b5132ff8e7a0e21a6eb72b63/charset_normalizer-3.5.0-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f8cd1283a9fe6c2065c807e9d5da81afe5e1e004caef39adc0d8ae86dd883698", size = 240925, upload-time = "2026-08-12T14:34:24.504Z" }, + { url = "https://files.pythonhosted.org/packages/2b/b9/c17e72aaa1b3e1ca6c184e8025cf138ed492d01a54f85286ff7d31253a4b/charset_normalizer-3.5.0-cp315-cp315t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:1c010dd86d3f4c4433c9634d33ce8147393b270dfa54f217f965540b8ae8e075", size = 234932, upload-time = "2026-08-12T14:34:25.822Z" }, + { url = "https://files.pythonhosted.org/packages/18/d7/f84ef0966bbe216f71029e34e7fa425a16b1682e2a40265e679dedf2b655/charset_normalizer-3.5.0-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5e68229977b2dea28e7061c0c0630a23f2f9f6e9c6fb38d77d3d6dbfe3768b74", size = 261733, upload-time = "2026-08-12T14:34:27.112Z" }, + { url = "https://files.pythonhosted.org/packages/3b/73/3e887fa0781a395339355ed934ab6561ceb5bb52574160f070224039c630/charset_normalizer-3.5.0-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a60773eb5fda796e6e6f76b9c152d270fe59f9788a51a6ff8ba44082d8548ae4", size = 258460, upload-time = "2026-08-12T14:34:28.431Z" }, + { url = "https://files.pythonhosted.org/packages/b3/81/52ebd9849bf9e35d0b21fff115cb6543162a8e1f2f564e8f87121a336b8c/charset_normalizer-3.5.0-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d9419f44e568f7fafcdc0b3b5c766a2364e705a9b34fb8a56b431e0d1f3f4258", size = 249894, upload-time = "2026-08-12T14:34:29.698Z" }, + { url = "https://files.pythonhosted.org/packages/85/f3/9366492b8a5fe0187de282e001d61345740cf79eb4a5f20181d769be02b5/charset_normalizer-3.5.0-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:75e243abbb528c1a774390ed71e3f868a9f37b1373442e4bbadd401cfc505ff4", size = 249540, upload-time = "2026-08-12T14:34:30.96Z" }, + { url = "https://files.pythonhosted.org/packages/0b/af/28bb5e5dbd3e67cb9196a62781ac2b6d79492f4fc7a069b6ca7d6d6c8d58/charset_normalizer-3.5.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:54c963ce6404e52255b737e8a06d356fc762d59096ae566203a67cf2b7d050f2", size = 242734, upload-time = "2026-08-12T14:34:32.481Z" }, + { url = "https://files.pythonhosted.org/packages/26/d6/7ccfa62b53b40fc06b2d3504825aa400764740bd10cf248fdc4272441b93/charset_normalizer-3.5.0-cp315-cp315t-musllinux_1_2_armv7l.whl", hash = "sha256:63ea0cc840c66670183578c2630d138c0e944aeadfc33f25173ee240f5db780d", size = 239580, upload-time = "2026-08-12T14:34:33.916Z" }, + { url = "https://files.pythonhosted.org/packages/0b/82/71c0c9b046697b8da66b3acefa8d5f92d00a9ef433ad7c3522b971d0369a/charset_normalizer-3.5.0-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:6c06875a1d4a7537bef70f659b55c6b55b9a47ec3ba8f2db610350c2d9915e6e", size = 263281, upload-time = "2026-08-12T14:34:35.333Z" }, + { url = "https://files.pythonhosted.org/packages/d6/01/d027583c869f40ba980c1c76994adbd522c360a6327e72beb44d7c267385/charset_normalizer-3.5.0-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:4253da1b4456b633651a8d59eb1dc7a8a8fa38241014dd7c217b353e547ae394", size = 250027, upload-time = "2026-08-12T14:34:36.991Z" }, + { url = "https://files.pythonhosted.org/packages/2a/e9/6475d739e0ec8bb1236e06263dc3affaffdf947d8114ad27024932f325da/charset_normalizer-3.5.0-cp315-cp315t-musllinux_1_2_s390x.whl", hash = "sha256:f044cb1cf44012184715f46584658993b5fee9344d71c4b0c455a17a299730c0", size = 257547, upload-time = "2026-08-12T14:34:38.277Z" }, + { url = "https://files.pythonhosted.org/packages/a5/60/d1f502fcaa048a2aca3ab80bfef8407659c131e4f1792fa805fec14b4960/charset_normalizer-3.5.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:a17864853f7c518ae7d4b368af98f427f9396805476af40af8698560f09d7d97", size = 251718, upload-time = "2026-08-12T14:34:39.562Z" }, + { url = "https://files.pythonhosted.org/packages/c3/69/76343dcf4381a698807ff8a20d89f66bbdd9f6222b0b17740f77ab764335/charset_normalizer-3.5.0-cp315-cp315t-win32.whl", hash = "sha256:9e726478d7a213847860219d74665a6892a643ac93b8f76580f6cf9ed39996b7", size = 190757, upload-time = "2026-08-12T14:34:41.053Z" }, + { url = "https://files.pythonhosted.org/packages/e1/ea/d18147626a1667cc773c42104ab155a4ca5d6d4d174b7a35e01062213ea5/charset_normalizer-3.5.0-cp315-cp315t-win_amd64.whl", hash = "sha256:d7229a99120c6c2792d96f4857c2648ce5530e93667a2c2388c5ef69a6b84775", size = 215431, upload-time = "2026-08-12T14:34:42.501Z" }, + { url = "https://files.pythonhosted.org/packages/47/21/4869598aae0872d94faa5933918a4fe37ab2c5af9d095786e241f9506fed/charset_normalizer-3.5.0-cp315-cp315t-win_arm64.whl", hash = "sha256:527e28a5e751d9e11369b9c5f9ab35c748eb9c109101920c7deb40d6eadf8d03", size = 193205, upload-time = "2026-08-12T14:34:43.781Z" }, + { url = "https://files.pythonhosted.org/packages/5b/f3/7b523d807cb5e73562ef8acf21d39cdb9d704955327362c781bc3478a73d/charset_normalizer-3.5.0-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:5a4ee37248dfac25107c758bda99d545ce73e60b44d2dd39e4a2bb9f2831e9f5", size = 330840, upload-time = "2026-08-12T14:34:45.06Z" }, + { url = "https://files.pythonhosted.org/packages/f0/de/fc68978fe78ca97063c96d764e41ff92ca639948f319271e0ff450e577a2/charset_normalizer-3.5.0-cp37-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a864bdcacd8bff58bb4845304e031f821a3ec64b2b7259f2d409cd49c9e59ca3", size = 251862, upload-time = "2026-08-12T14:34:46.58Z" }, + { url = "https://files.pythonhosted.org/packages/a9/cb/82b41a0ab7fb1a88065f1d78ad32696ad88ea3fe8e25b8189d08833938de/charset_normalizer-3.5.0-cp37-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:84b736e3b391601bc47b86da381c749c0f894e9191aaca9f31f30c2632206df3", size = 239484, upload-time = "2026-08-12T14:34:47.869Z" }, + { url = "https://files.pythonhosted.org/packages/e8/0c/19608b631f4538f908098d4a2d56a8f79a665e27cc58e9d90479761a9227/charset_normalizer-3.5.0-cp37-abi3-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6abb1f356fb865baeb6ebc3fadd843e9a96fbf49b9adcca55037f3cceccb7438", size = 230602, upload-time = "2026-08-12T14:34:49.265Z" }, + { url = "https://files.pythonhosted.org/packages/29/db/f648eb30e14eba301aed61e11672156f137905c1bdbb530151abe8065943/charset_normalizer-3.5.0-cp37-abi3-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1d366548d2ee28a8cfdcc4296363978cc644a728333be9824d2de4652e83df0a", size = 259208, upload-time = "2026-08-12T14:34:50.632Z" }, + { url = "https://files.pythonhosted.org/packages/d3/e0/ed2c8bdbac484d69614d6993143aeb6cb0f4dd1561c883402517b623c8ef/charset_normalizer-3.5.0-cp37-abi3-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d672f329ae504ee240eb39b6effb3318aa8e7e8924c0ce8eee5760b3fad98539", size = 253659, upload-time = "2026-08-12T14:34:52.11Z" }, + { url = "https://files.pythonhosted.org/packages/32/08/b4907cb9ec5b521d9d024ced13611240b86ef065c2eb15b3ad2334dc9940/charset_normalizer-3.5.0-cp37-abi3-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c54036a518748b6c02e666f6d46c3817561998fb904c3be25b56fb4fe3dc5706", size = 248821, upload-time = "2026-08-12T14:34:53.399Z" }, + { url = "https://files.pythonhosted.org/packages/12/b2/e2d1abcfbc05822f0030869efb4e9f8a3658e13b4821796d4b62da917327/charset_normalizer-3.5.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:22a1889f1c9b752c63c36758a0c2145458e3cadb20fced7a0790002e9dd12b26", size = 240271, upload-time = "2026-08-12T14:34:55.09Z" }, + { url = "https://files.pythonhosted.org/packages/dc/f9/4ba127ad610542fa3eabfa41c45bf12d357860a815b3566374ec0188e213/charset_normalizer-3.5.0-cp37-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:b7eb3eab5c646d3de7dcb14a7c9caebace5249c5767da39e1761cb1576e521a3", size = 232155, upload-time = "2026-08-12T14:34:56.543Z" }, + { url = "https://files.pythonhosted.org/packages/01/68/40613182366d00bd6dbd5f6c84a926cbd120960e038a8269e9ae7d782762/charset_normalizer-3.5.0-cp37-abi3-musllinux_1_2_ppc64le.whl", hash = "sha256:2080aa129a28267984cdc902898993d788c995c384e285d0d19199f56760d52e", size = 259674, upload-time = "2026-08-12T14:34:57.815Z" }, + { url = "https://files.pythonhosted.org/packages/0a/53/4574a14fa4c9de4a6c9f31725354bfa40b67f653e6d594ce1654f9a41b32/charset_normalizer-3.5.0-cp37-abi3-musllinux_1_2_riscv64.whl", hash = "sha256:fd68c825548a611158230e2f9222e210ceb2e3391995c0aa5865cbdf3ab4bd49", size = 246122, upload-time = "2026-08-12T14:34:59.337Z" }, + { url = "https://files.pythonhosted.org/packages/5a/02/bd8030d13d92c058ca7b2b9615bbb3169569e144db64d65c149cd45abf5e/charset_normalizer-3.5.0-cp37-abi3-musllinux_1_2_s390x.whl", hash = "sha256:d8a9316f4da85e937242642b537c6d55d7e9287dd38e5634732f8233932aff45", size = 255221, upload-time = "2026-08-12T14:35:00.71Z" }, + { url = "https://files.pythonhosted.org/packages/77/9d/10ecd3bcbe2666b3d4d4026c97b48f73990682815db516052a1e8f4a31c5/charset_normalizer-3.5.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:d90254c8f609338c53ec180fcd4c4f9c16502e238e3fc88ca7fd4c2f38d445b8", size = 253450, upload-time = "2026-08-12T14:35:02.217Z" }, + { url = "https://files.pythonhosted.org/packages/e4/0f/d044c4872c0938a84f87b5027a698c0e61bacfc5c3551a4e749ca9b7bc5c/charset_normalizer-3.5.0-cp37-abi3-win32.whl", hash = "sha256:8b3e9e29b8b07cc461b9ce7768db7693a93979d0dadf22046f6f3555ded2f516", size = 173594, upload-time = "2026-08-12T14:35:03.845Z" }, + { url = "https://files.pythonhosted.org/packages/10/6b/6046773901f1944b9a89436351529811ee958afc7b774563be9d74a6f0c3/charset_normalizer-3.5.0-cp37-abi3-win_amd64.whl", hash = "sha256:0c8953d9d1617794cfc40d81179571c9ba3805dd029623a15c93f1fb70e60a74", size = 198959, upload-time = "2026-08-12T14:35:05.187Z" }, + { url = "https://files.pythonhosted.org/packages/ab/a6/b57708ac92aefc8e8389d51d5178129b81f03196da61ee2c23e687b8178a/charset_normalizer-3.5.0-cp37-abi3-win_arm64.whl", hash = "sha256:562d24ca7797c1af8852994950c2e623a907b201fc4b0ed29e92af173d3828ca", size = 267055, upload-time = "2026-08-12T14:35:06.533Z" }, + { url = "https://files.pythonhosted.org/packages/22/c7/754d09943a616937df61e4ba367c409ded2a987e872972098d51a6fcf73b/charset_normalizer-3.5.0-py3-none-any.whl", hash = "sha256:993dfcbe75a85a3784abb5084f2c41b915767c90546fcc92803cffa28611baea", size = 67943, upload-time = "2026-08-12T14:35:30.363Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "cryptography" +version = "50.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/de/41/6cbdcf9142d00fe82836fbb51e503e58088575cf7a0fe1dbff6695bf0840/cryptography-50.0.0.tar.gz", hash = "sha256:eeac2acb5a20ed25e0ad6d1df9891a520b78b404266b6d11778f25d5d691a6c9", size = 880201, upload-time = "2026-07-31T14:25:10.11Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c5/5c/59086b4aac5e879d38ddbcf74e4be7ade89cebc3eb199a55da998c3bb46a/cryptography-50.0.0-cp311-abi3-macosx_11_0_arm64.whl", hash = "sha256:031e2d5dd4bb9caa3ca9c82e5a197fd8ae680232cee62603d1a813f3f07e3d03", size = 4001252, upload-time = "2026-07-31T14:23:33.331Z" }, + { url = "https://files.pythonhosted.org/packages/57/ef/8f2df13c7216bcad3e1c74e07f6e193d93e998e114f524a53877c9af27ad/cryptography-50.0.0-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fd9192b7b70c573d7f214eb1ae35e00d359f6f5e4b27c7e21e30de1fc6204645", size = 4719554, upload-time = "2026-07-31T14:23:35.611Z" }, + { url = "https://files.pythonhosted.org/packages/d9/41/029086c34d91052fc3b88bcc8056f709a7c915c7a23b235a54eb800b1c97/cryptography-50.0.0-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:06a32a980526a6ab9a4b9bf8f7385800791e2bb960903cb6b530e4817509a3b7", size = 4702130, upload-time = "2026-07-31T14:23:37.635Z" }, + { url = "https://files.pythonhosted.org/packages/7d/ff/b6ce0954962e7f7b969f850a883744197bb3910bdfd7b6da162eab7d9f68/cryptography-50.0.0-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:a1b30560f2acc95aa8b2e06e716a13dbfc97314747b80d9707e307f77b40d6b3", size = 4725244, upload-time = "2026-07-31T14:23:39.471Z" }, + { url = "https://files.pythonhosted.org/packages/06/1e/63a1027cb7fec360a182208e1b7767d5aa1fe57be3d6aa856e69a321edc0/cryptography-50.0.0-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:8d89f3976b10b4ce31118de72329025f70d2c6ead14a8217c5514dd2c6d5a78f", size = 5342265, upload-time = "2026-07-31T14:23:41.286Z" }, + { url = "https://files.pythonhosted.org/packages/6b/72/a1116d683a6d7ece94590013882515de087edf9ef0e6292aae615a44df73/cryptography-50.0.0-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:b42a28c1844fd9de8f3f7d540e36b66f3a9c83fceac7170ebc7a6a19edd9dcae", size = 4734609, upload-time = "2026-07-31T14:23:43.139Z" }, + { url = "https://files.pythonhosted.org/packages/15/37/36a9c479bbe49acea2636c7fd3360d20f7b7e079c300352011c44850b181/cryptography-50.0.0-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:900131fafd8aead39ac7dd3a7e833be754c17a95cfd91221636949fe4eb0aa8a", size = 4356517, upload-time = "2026-07-31T14:23:44.939Z" }, + { url = "https://files.pythonhosted.org/packages/32/98/8a151d64367204cbc63ec65d37502f1d9c53cf4bfc6ec3c532614dbec60d/cryptography-50.0.0-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:07949c449a1abcf60d1ee6e88956d89404c7df3c8258f46589e912988e551987", size = 4724529, upload-time = "2026-07-31T14:23:46.93Z" }, + { url = "https://files.pythonhosted.org/packages/22/f6/ec13b470172126464a86bf54d2294a46d29837fc51ba3e45d4047946fb5e/cryptography-50.0.0-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:f89831ef99dd7dd169ab06d63a831adb9e20a87aac6d380266bbda5823349169", size = 5299852, upload-time = "2026-07-31T14:23:48.851Z" }, + { url = "https://files.pythonhosted.org/packages/da/3a/f05e32c99d440c9bb891ea0e36c9091891e36be5a9a87ab2ee6ea20729f6/cryptography-50.0.0-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:82148ec5bddac30b51a5b3c1945075f896fa022cb93f8e4a01e9f6ee95292c5f", size = 4734462, upload-time = "2026-07-31T14:23:50.861Z" }, + { url = "https://files.pythonhosted.org/packages/ca/dc/bd72b26be8953f80625f63151efd38eee71c76ca6cf591c08ff34615a79e/cryptography-50.0.0-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1489e263a8048bb8b6a8bac662eb2d402ea5d2b7b4699b72f385f1e2772db105", size = 4852708, upload-time = "2026-07-31T14:23:52.715Z" }, + { url = "https://files.pythonhosted.org/packages/27/20/c930314a2ab476d15dec966ec87e2e9637bb02b06106b12c0396c57bb603/cryptography-50.0.0-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:7cec5b856506da6defb290f30c9ee687d5f5e8cb0bd3f6459dde43b0b4fa40ef", size = 5004179, upload-time = "2026-07-31T14:23:54.887Z" }, + { url = "https://files.pythonhosted.org/packages/32/2e/c9db68a0c4bfa28e310707527c0ee3a2bd254104d2e02e68f368e197aa4c/cryptography-50.0.0-cp311-abi3-win_amd64.whl", hash = "sha256:bd1c592e4d5974f0d08d4888e432157adba757c66da0246918e43677fafa2d30", size = 3840395, upload-time = "2026-07-31T14:23:56.677Z" }, + { url = "https://files.pythonhosted.org/packages/c3/fb/951032a3bf22a5697c83183fb6294a4843772947a70e616c57b3ff5f522e/cryptography-50.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:49e7d93abdbd2990caced757e5fade25302f719c3c8fb6e6fff2dde98999fc41", size = 3989258, upload-time = "2026-07-31T14:23:58.881Z" }, + { url = "https://files.pythonhosted.org/packages/d4/67/91eb047e69c5e845f2f14b8a2e4a1aab0f283cb885531e9e22c8adb176bc/cryptography-50.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:19736989797678c6af1e55cd49055cdbcb55d8f6b5583ac5335f933aba9101dc", size = 4700648, upload-time = "2026-07-31T14:24:00.702Z" }, + { url = "https://files.pythonhosted.org/packages/30/82/85f0f7425c856b9f96459411eb12e74ef72df9caf6f8f15bf23a33ff131f/cryptography-50.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:80b63928fa35083b33966ce1efb70e5b9607181e49dcd1c22c8c005e319f667f", size = 4682442, upload-time = "2026-07-31T14:24:02.538Z" }, + { url = "https://files.pythonhosted.org/packages/1a/28/b555a365adff1cca2fbe7b9e487d68a40de6bc67ff2cb587473eb43de0e7/cryptography-50.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:d58c3db7cd6eed54e6c06744db55456b65ebd7492ddeae9c1e93cfca7aa857d3", size = 4707596, upload-time = "2026-07-31T14:24:04.394Z" }, + { url = "https://files.pythonhosted.org/packages/72/d8/f52538140cc719df62a01cf87d1c7142318d235817109d6f4054d7c352d6/cryptography-50.0.0-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:df2a58a472f332225671c35b0a830208b86d004f82baa8530fa3782c85646533", size = 5314552, upload-time = "2026-07-31T14:24:06.31Z" }, + { url = "https://files.pythonhosted.org/packages/38/14/6120e5bd7c5aa022ad15424ba4d5c5269d0d9448ed4d55e492ea91e3c1c4/cryptography-50.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:11b74db56cdbe3cdee6e3f6982ecb70334fa10dce99ed58bf7894aaaa3b2a037", size = 4717113, upload-time = "2026-07-31T14:24:08.349Z" }, + { url = "https://files.pythonhosted.org/packages/fa/71/190bf38c3ee2e0f8efc9860ae100c9df4169742eef274b91e7aa1cb133b9/cryptography-50.0.0-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:f59e38625469987d7ef6d495323c55e7db6c212eaf6112267e0d3b565a2e9c9f", size = 4338580, upload-time = "2026-07-31T14:24:10.227Z" }, + { url = "https://files.pythonhosted.org/packages/3a/63/504ccfbbe61fd8aa983f7f146399cdf034c72c2fc55f5b2dfdcdcdb20c99/cryptography-50.0.0-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:ecfed7367f965a0328cfbdd70da860f15441f002f613185668c6e6ebf5a0ac11", size = 4707038, upload-time = "2026-07-31T14:24:12.169Z" }, + { url = "https://files.pythonhosted.org/packages/01/77/2cf79bbfc4d12ca106437a6e170d6aaa01a373e93093118aaaef0e801bd4/cryptography-50.0.0-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:9aa87839c383bdbab6ef865787a1fb877af8dd03464c4400322726feaaadfc6d", size = 5273110, upload-time = "2026-07-31T14:24:14.38Z" }, + { url = "https://files.pythonhosted.org/packages/e5/45/8aae2972c520145377ea3559a605a899bebe227bf070b33cdb445929a9b9/cryptography-50.0.0-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:6ba6a53445bd3cfa809ef3ef5f1589aa6ba08784a1d962bf47d0940e871dab1c", size = 4716439, upload-time = "2026-07-31T14:24:16.415Z" }, + { url = "https://files.pythonhosted.org/packages/7b/20/4fe50b619a48c2525cc46e2dbc1ac490708d704be5d467bdaac6dc955682/cryptography-50.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:3f5735ffe4996d28b809371756219f5354864902a3b9e7c0b9ee87041209fc9c", size = 4837383, upload-time = "2026-07-31T14:24:18.553Z" }, + { url = "https://files.pythonhosted.org/packages/92/91/3a31366e183343d3703f8995c095f5734676bd6938118047e50fcf279eb4/cryptography-50.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:1b4a266766514614f8aa60416e71f2fc6e575d36e7bdc90f644fadb2f4b75b95", size = 4985772, upload-time = "2026-07-31T14:24:20.385Z" }, + { url = "https://files.pythonhosted.org/packages/74/9a/02ffe35b2853d121689871eb5dce862092562b3a1ed5cc98f1aaed441506/cryptography-50.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:12b9c6996425c76ea6c457ace4f3073e715b8c545add07cd1a8f3a4f90691269", size = 3816291, upload-time = "2026-07-31T14:24:22.125Z" }, + { url = "https://files.pythonhosted.org/packages/03/37/73d005be173aff344af30e9fd2a576575cb2391a7101d9cd3842e1fa8cce/cryptography-50.0.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:ccdc4a71a4dabae05de219404f9f4abc38e3b58422177ff93d0da05967dafa07", size = 4036009, upload-time = "2026-07-31T14:24:24.122Z" }, + { url = "https://files.pythonhosted.org/packages/ff/c6/7a6202a534e32103a285b7834a120869557fe198d51d7cfe59754c8bda9c/cryptography-50.0.0-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:910e1d2668e7de9648f2bcee30e180db2a6b15c30f887d7c4c93ddf96e3992e3", size = 4745252, upload-time = "2026-07-31T14:24:26.118Z" }, + { url = "https://files.pythonhosted.org/packages/85/4f/0fa8c2f4428198f15d9ff8d63400e27afbf94ce833f6108da1eb3753f945/cryptography-50.0.0-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a91296cb61e8df6f86d0c19cc4068228da256bf59bf86049fbd821084565327f", size = 4728939, upload-time = "2026-07-31T14:24:27.994Z" }, + { url = "https://files.pythonhosted.org/packages/d1/63/54dd723490ba2dc09b299682c10b38db38f159728bcaae8c591b8af2f22d/cryptography-50.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:e722f16708d854fe924790e051061f6704a472c3bac347b6fd88033ea8dd0dc5", size = 4748483, upload-time = "2026-07-31T14:24:30.254Z" }, + { url = "https://files.pythonhosted.org/packages/1d/dd/7c77d26285cc7f6991efce64a0f5b4f9383bfa5dd8c5033003eaf7db4cdb/cryptography-50.0.0-cp39-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:d764dcf130c428ef66786f866dd750f53182bc608813489915e9fc106bb0c82f", size = 5367599, upload-time = "2026-07-31T14:24:32.457Z" }, + { url = "https://files.pythonhosted.org/packages/46/c9/f60aed34c013f317f92817b6c171c2d22a78270fa41109bd4b08af26b194/cryptography-50.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:105110f43a471dbd0060b9c9516cb8a6a79233631a04cc2ba16f28323ac6e025", size = 4762647, upload-time = "2026-07-31T14:24:34.599Z" }, + { url = "https://files.pythonhosted.org/packages/be/f3/f9a0173b139372c3a48ed98154b45cc6b9de17c789d5ab552e621c293609/cryptography-50.0.0-cp39-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:828743d939e9629bc267b8e2d08d8bb67cd4319c771a33d4b18b22dd8fb7440a", size = 4385197, upload-time = "2026-07-31T14:24:36.647Z" }, + { url = "https://files.pythonhosted.org/packages/d8/36/83bb81f6e569bc38e1e4a7bc80f29b46bb9601920bc455fc8e888f5d5742/cryptography-50.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:2a8183b489dc1f7f80f135780fadc1108f14b31b8a40411c7a5b17425f65f28b", size = 4748095, upload-time = "2026-07-31T14:24:39.493Z" }, + { url = "https://files.pythonhosted.org/packages/6b/16/d3008eff98c764979865834c3d386d4fd041b5f52e7f34fc29ac1a5eb515/cryptography-50.0.0-cp39-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:6e7d61120573a7f2cd94cc095f9e81f6967c61ccdf194285aa143ecec8e0b708", size = 5325948, upload-time = "2026-07-31T14:24:41.556Z" }, + { url = "https://files.pythonhosted.org/packages/9c/f8/d97f9603efda3888187bfdb893f26c41be4735c10631d05d284ee6b047c4/cryptography-50.0.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:37fdb0d0111f1e2ff07139dfb79f1b49531f8e213c46f1163dd7642979b58c47", size = 4762400, upload-time = "2026-07-31T14:24:43.636Z" }, + { url = "https://files.pythonhosted.org/packages/64/a2/4615c8f7d81a00b1d6e6afe19f694e1543582349fb5f4076f6cb5dc36485/cryptography-50.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:c87f62a3d3b9888ed0fdde100ec06aa61ca9cd44bad9057d1dff9a516b5f5bb9", size = 4878208, upload-time = "2026-07-31T14:24:45.522Z" }, + { url = "https://files.pythonhosted.org/packages/d2/1a/efcfb02f91407149a0dacffffab791f7e19bf6385f63b3666dc8b5e5c9c8/cryptography-50.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:65c2c3add92b45fd0709db8594536aea39c2a67af0e27ffcf049c498501140b7", size = 5037050, upload-time = "2026-07-31T14:24:47.697Z" }, + { url = "https://files.pythonhosted.org/packages/57/30/4a22984d4f1bdfb8c054f07a92bc176b97a3134cc1d6c4b3bffb1f3688b4/cryptography-50.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:d24fead1d4d076e1bfb006dcec392074a3cd8d7b4fc8a595aa64073b2b7a96ba", size = 3874135, upload-time = "2026-07-31T14:24:50.085Z" }, + { url = "https://files.pythonhosted.org/packages/9d/3e/e54cde8c01631a5a8226ccd617eab9e57fd5cfdad90f1a9e6bb570794631/cryptography-50.0.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:5e34edd123674534acd70147f0ca331eaa2c74e6325fb2028c886aa26ba0b68c", size = 3963170, upload-time = "2026-07-31T14:24:51.968Z" }, + { url = "https://files.pythonhosted.org/packages/01/b6/0b9e125e90f3d2dcf599a218a899cda7326a3158cfa258723f0b398b08f6/cryptography-50.0.0-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:8eb5e1172eb569ea8a872796576e6a67c276351728b6455d5beb01242b027c6a", size = 4692441, upload-time = "2026-07-31T14:24:53.743Z" }, + { url = "https://files.pythonhosted.org/packages/53/c9/a5151588710785a96d7bc4de27d4cd62f263bbbcb203cfe29df537eb6505/cryptography-50.0.0-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:910d11e1a385c654bf738bf3e6b8e6ed5de0f5610fcae2be9e5b398d8081d20e", size = 4699810, upload-time = "2026-07-31T14:24:55.746Z" }, + { url = "https://files.pythonhosted.org/packages/c7/1a/15b92b25eb6ce3089cd49377ae990a0f3ad485a510f968aed1f19dbdcdf2/cryptography-50.0.0-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:62598a8a57f815db4c6259a4e97d857dab56697e7de8e8ab02352ab74da1995d", size = 4691924, upload-time = "2026-07-31T14:24:58.082Z" }, + { url = "https://files.pythonhosted.org/packages/62/15/219075012ab13e8905f3cd572204f4acb4b111df787104346b9bc0cea789/cryptography-50.0.0-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:07479a1cb08219ab719147e742e76090c9c773321959bb94946fffdd397a6437", size = 4699593, upload-time = "2026-07-31T14:24:59.951Z" }, + { url = "https://files.pythonhosted.org/packages/8e/b5/c2c5fce26f0ee40d21bafe7f191d29a34b35a65ac4fe8a1191d1983612e9/cryptography-50.0.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:c99c003e088647b8a5b7c145d6f78c335f6348332b62e142d411c4b63d1460b9", size = 3813796, upload-time = "2026-07-31T14:25:02.298Z" }, +] + +[[package]] +name = "dnslib" +version = "0.9.26" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/71/269f74ef9bc8ca453af2e1768d4f4c8e7ef5f894d058d27fd1b69c754d7f/dnslib-0.9.26.tar.gz", hash = "sha256:be56857534390b2fbd02935270019bacc5e6b411d156cb3921ac55a7fb51f1a8", size = 82901, upload-time = "2025-03-03T09:17:32.606Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/16/93/167364f10194e374119b211e475ed8763d7591ae4f7001b304282dde5825/dnslib-0.9.26-py3-none-any.whl", hash = "sha256:e68719e633d761747c7e91bd241019ef5a2b61a63f56025939e144c841a70e0d", size = 64161, upload-time = "2025-03-03T09:17:31.47Z" }, +] + +[[package]] +name = "dnspython" +version = "2.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8c/8b/57666417c0f90f08bcafa776861060426765fdb422eb10212086fb811d26/dnspython-2.8.0.tar.gz", hash = "sha256:181d3c6996452cb1189c4046c61599b84a5a86e099562ffde77d26984ff26d0f", size = 368251, upload-time = "2025-09-07T18:58:00.022Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ba/5a/18ad964b0086c6e62e2e7500f7edc89e3faa45033c71c1893d34eed2b2de/dnspython-2.8.0-py3-none-any.whl", hash = "sha256:01d9bbc4a2d76bf0db7c1f729812ded6d912bd318d3b1cf81d30c0f845dbf3af", size = 331094, upload-time = "2025-09-07T18:57:58.071Z" }, +] + +[[package]] +name = "ecdsa" +version = "0.19.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/25/ca/8de7744cb3bc966c85430ca2d0fcaeea872507c6a4cf6e007f7fe269ed9d/ecdsa-0.19.2.tar.gz", hash = "sha256:62635b0ac1ca2e027f82122b5b81cb706edc38cd91c63dda28e4f3455a2bf930", size = 202432, upload-time = "2026-03-26T09:58:17.675Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/51/79/119091c98e2bf49e24ed9f3ae69f816d715d2904aefa6a2baa039a2ba0b0/ecdsa-0.19.2-py2.py3-none-any.whl", hash = "sha256:840f5dc5e375c68f36c1a7a5b9caad28f95daa65185c9253c0c08dd952bb7399", size = 150818, upload-time = "2026-03-26T09:58:15.808Z" }, +] + +[[package]] +name = "eventlet" +version = "0.41.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "dnspython" }, + { name = "greenlet" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/eb/e8/6a3a23a3b85ed129b21309c6eca873d833855e63879cd72b9dac20d9b76a/eventlet-0.41.2.tar.gz", hash = "sha256:721b86b77fca33a735598292022ac6feef99747bf48f52defdada6b572acd5af", size = 566270, upload-time = "2026-08-14T07:51:00.331Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/23/75/9681fa59c4b27d9e34e8be682d2fff3f5be87bf6552477f9a5d395a068c3/eventlet-0.41.2-py3-none-any.whl", hash = "sha256:6cae50e67fe6ae8bb7013e7fd4d8e0d0d20aeb9b3259b93f023c93eb6749631f", size = 364641, upload-time = "2026-08-14T07:50:58.838Z" }, +] + +[[package]] +name = "execnet" +version = "2.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bf/89/780e11f9588d9e7128a3f87788354c7946a9cbb1401ad38a48c4db9a4f07/execnet-2.1.2.tar.gz", hash = "sha256:63d83bfdd9a23e35b9c6a3261412324f964c2ec8dcd8d3c6916ee9373e0befcd", size = 166622, upload-time = "2025-11-12T09:56:37.75Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ab/84/02fc1827e8cdded4aa65baef11296a9bbe595c474f0d6d758af082d849fd/execnet-2.1.2-py3-none-any.whl", hash = "sha256:67fba928dd5a544b783f6056f449e5e3931a5c378b128bc18501f7ea79e296ec", size = 40708, upload-time = "2025-11-12T09:56:36.333Z" }, +] + +[[package]] +name = "greenlet" +version = "3.5.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0b/d8/7cc97c142388aef03f622e001c572c4f84e9252a439549d483f555771970/greenlet-3.5.5.tar.gz", hash = "sha256:adb4bae02e91a8e863e48b177e4014bdcac8a6b5e047ea1df687a61534b85e6c", size = 207585, upload-time = "2026-08-10T15:09:36.136Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4e/a3/07297917485ee2ca85bc3c8dc6ed85ad3fffcf424047fba62671dba68e97/greenlet-3.5.5-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:be63afcbbccfad3dd95a1ba12ada84dab2ef32031973d80b5b92df67fa763a61", size = 294165, upload-time = "2026-08-10T13:25:17.987Z" }, + { url = "https://files.pythonhosted.org/packages/db/51/6f732f9314cda54c5fd48a7620c7160f4f286967e8045ad94b9d66ce80b7/greenlet-3.5.5-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8a268024ce2d7d2b04694bf1594058981a9fa663d1df4b762dee499211ed7c1c", size = 613610, upload-time = "2026-08-10T14:14:33.829Z" }, + { url = "https://files.pythonhosted.org/packages/d8/c0/b27589e25d220289edcd4d582b2b17b83058d1a56d53d971b6ea1a34f10d/greenlet-3.5.5-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:35cbb8bf55ace57fbccb4fb8622c4521713acd8691e77f4696d416ea7ca527da", size = 625481, upload-time = "2026-08-10T14:27:23.647Z" }, + { url = "https://files.pythonhosted.org/packages/39/82/5c873dbb4fb001d22fbbd50e80d4c1b0181ddae106856132160f84b94e88/greenlet-3.5.5-cp311-cp311-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:abc8bc8d9f935cd685457545b6a53863a877fdc12c2c0f5ee9beee18d9db139c", size = 633329, upload-time = "2026-08-10T14:30:06.062Z" }, + { url = "https://files.pythonhosted.org/packages/51/2d/f2c928218ac52f26d7a2c188c171d1b7e728b23782cb3347e7b4fce1493a/greenlet-3.5.5-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:74cc6df89ec5302337adc9cf096221cbed2510fd444b0e0f1586cf0470740864", size = 624562, upload-time = "2026-08-10T13:40:48.064Z" }, + { url = "https://files.pythonhosted.org/packages/70/12/f7df98e72a8eb4a7edfec5a08d6d1a4ab53a52c95ba0b2ea6c10b8dd9bd0/greenlet-3.5.5-cp311-cp311-manylinux_2_39_riscv64.whl", hash = "sha256:3134291427bb0f3526e9d90311988caf336eb43730e95244997a4fb15f45144f", size = 428145, upload-time = "2026-08-10T14:30:01.071Z" }, + { url = "https://files.pythonhosted.org/packages/3e/4a/92fc51d5d35912f4f06eec037ba347985defd0be47463a010a325634d9d2/greenlet-3.5.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6d9b454c5fc48aeaa7c4337813dbf513a6870468e426438a04d922c6d0fe63db", size = 1584909, upload-time = "2026-08-10T14:15:04.343Z" }, + { url = "https://files.pythonhosted.org/packages/ac/58/ed98b80ac5738c149a5258544843c45601ade1fd70f61740cdaead6351b3/greenlet-3.5.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:03551ed792cb1b4fc0277a0c60dfd8c343894a0ba06fe60dcd22f568b433da39", size = 1651184, upload-time = "2026-08-10T13:40:28.879Z" }, + { url = "https://files.pythonhosted.org/packages/d8/be/b582ceb80cefdf9d8da34078714e4b12b3d16f509dee0f65e40a5cc8fc7d/greenlet-3.5.5-cp311-cp311-win_amd64.whl", hash = "sha256:ab3df3dffb58bf70564e93a5cec7941e4d9faa5a36cc4234a10d3131afe04f53", size = 323280, upload-time = "2026-08-10T13:26:07.495Z" }, + { url = "https://files.pythonhosted.org/packages/4d/18/5313c4c58598c38b0373c013e4ff2b3e6d258aaaa338f373335ebecdaddd/greenlet-3.5.5-cp311-cp311-win_arm64.whl", hash = "sha256:2b70a766135540c472ac1393d57c2e1b4a2eb85bf526a1e41e6d096173a8cee5", size = 307785, upload-time = "2026-08-10T13:28:34.874Z" }, + { url = "https://files.pythonhosted.org/packages/2e/7e/9ecd0285e3153532ae07aeb88063c43c72b4221cf0d4d123b02f3682e3ff/greenlet-3.5.5-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:49520f0c95a48b42cf55414b8e8479beb274ea70431afc33e3f79903c71f4380", size = 295809, upload-time = "2026-08-10T13:25:34.023Z" }, + { url = "https://files.pythonhosted.org/packages/35/73/60e4bbcc89252037b18087f2ec16405d5b2d5be42dde191bbf3667e96102/greenlet-3.5.5-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:55272212cbc5f43d1d723725ab931f1939969b7e9523882ca58b55061769d053", size = 611910, upload-time = "2026-08-10T14:14:35.18Z" }, + { url = "https://files.pythonhosted.org/packages/a4/17/cd5134be659cd4a443e7a61ae670dabec165a814c51162916d637b6dd38e/greenlet-3.5.5-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:655bca754a2ef4efcb0eb48a94d3f4593536d0f3d48f8ed44343c01d16a92f95", size = 624198, upload-time = "2026-08-10T14:27:25.229Z" }, + { url = "https://files.pythonhosted.org/packages/9b/30/87c212b5c684d0e72974f1063b7a9687631e8985902c06e1016542c874e7/greenlet-3.5.5-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6ca5d6ae0739e5764f2cfcfaa562ac5a990cbdaedca93251c5e3cf07c362371f", size = 629504, upload-time = "2026-08-10T14:30:07.967Z" }, + { url = "https://files.pythonhosted.org/packages/78/ac/5c5b959999b6f09c3026b5dfe171575bc3121c5236ce74f495096f25b203/greenlet-3.5.5-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:147b25a42e5ca5be3d42356e8f608b37af715a1c196e9bf9d1627f3341adfe1d", size = 621439, upload-time = "2026-08-10T13:40:49.391Z" }, + { url = "https://files.pythonhosted.org/packages/63/2c/eb487fafc9f50ffff2b1e0b697f70fb34bf150821c08ab225aacf5583a7e/greenlet-3.5.5-cp312-cp312-manylinux_2_39_riscv64.whl", hash = "sha256:1b5ed9162c0c098e0bbc2cf88a94f433c1b8926f831745252e099e5d83e17759", size = 432462, upload-time = "2026-08-10T14:30:02.309Z" }, + { url = "https://files.pythonhosted.org/packages/c8/8b/6acf112ed8aee499f25b4d6949820fb02ac950ff9c1f3d793bd5be0599f2/greenlet-3.5.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:27493374cff1d1b7919dc8126547f2aea582737e3046147b434b1e12de56389b", size = 1581342, upload-time = "2026-08-10T14:15:05.653Z" }, + { url = "https://files.pythonhosted.org/packages/b8/d7/734e5f198888876b42d7616ff6644c075baf6b8a2412deadd6b0e1b8b20c/greenlet-3.5.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:12e2ee66c2aba86133f10fd99d6a8856c6d351ffb7be0e4d52ef2cc5fbb705b2", size = 1645744, upload-time = "2026-08-10T13:40:30.353Z" }, + { url = "https://files.pythonhosted.org/packages/de/30/1f42b88dc587b5899ee50616ad56ee40cafaf225df4fb829f10183c62a5c/greenlet-3.5.5-cp312-cp312-win_amd64.whl", hash = "sha256:49ddacd36af37735fab103846f4ee4d18a492dde72730d1699c0c8ebe30d9f18", size = 324171, upload-time = "2026-08-10T13:28:44.472Z" }, + { url = "https://files.pythonhosted.org/packages/76/e5/4dee4d8d2e603fe5fdd7b444e63219f7b9bd852c60c6214511c7157cbe88/greenlet-3.5.5-cp312-cp312-win_arm64.whl", hash = "sha256:5f1b1ff4828cdc1aba4266aff814085d04a1d07959287219af021b838b265d52", size = 308362, upload-time = "2026-08-10T13:26:46.839Z" }, + { url = "https://files.pythonhosted.org/packages/fb/3d/8cef5f724ec0d4add2af8961d504535ec60c3cca9e464f6d03bdba29d85b/greenlet-3.5.5-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:b79fd2a5bc099b5e744f34c4c9a58954a5f4cb7529fb4b6e8446057d61b6edaa", size = 294730, upload-time = "2026-08-10T13:27:51.206Z" }, + { url = "https://files.pythonhosted.org/packages/88/4b/8e7aa3f514273aecff30a16ab1bac09ff54cfc7e6860fdd8058c37ff2499/greenlet-3.5.5-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:634cf15a233a949136879dd388e25d3296e16f3f1e217d2456797b8579ebc6ed", size = 614536, upload-time = "2026-08-10T14:14:36.589Z" }, + { url = "https://files.pythonhosted.org/packages/85/48/4e95e9dd5a8a397dc6a6345dd7f1935113d0fca4f85e89d3976da9cd988d/greenlet-3.5.5-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:499adea519f748407fc6806d20eedabac2884fd73b9f38d81236e190ba20dfef", size = 626924, upload-time = "2026-08-10T14:27:27.048Z" }, + { url = "https://files.pythonhosted.org/packages/0e/84/eaa476d6bf3816828d0d70e80dcc36bf30a058233bd889e707e693f6e860/greenlet-3.5.5-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f7278591501941bb2456af102bb9cd59aab48c6cfd6e2dd68fa1290bb0c49a42", size = 632726, upload-time = "2026-08-10T14:30:09.874Z" }, + { url = "https://files.pythonhosted.org/packages/89/5d/398a1c71fa7a277deeb376c999979de6786f08fc2d5747a0b9d6e11738dd/greenlet-3.5.5-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2eabb980975cba5b93a95f6f69287d05fc05ac955bfd6a320a7c083eeb52c0b0", size = 623906, upload-time = "2026-08-10T13:40:50.501Z" }, + { url = "https://files.pythonhosted.org/packages/d0/f2/0cc2849ede68579291e9c59b3ab6ec1958f98681cca5b14d8fc75bf674a4/greenlet-3.5.5-cp313-cp313-manylinux_2_39_riscv64.whl", hash = "sha256:4dfc7c4470354e7b09184d1a3a985761053a2fd694ddb5b5c80242afc2c8c90b", size = 434966, upload-time = "2026-08-10T14:30:03.729Z" }, + { url = "https://files.pythonhosted.org/packages/04/1b/745450fc5ea9e0cb17d840d248f284db3363de736d362c7d2d883e3eadba/greenlet-3.5.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:03115c2e0a371999bf8ae616aa8d653f96641d4705c457aebaa187276e9f7537", size = 1581430, upload-time = "2026-08-10T14:15:06.853Z" }, + { url = "https://files.pythonhosted.org/packages/d4/29/d51b296e3191bb15d3d81ec375af1909e4466c0f395d744ed475801798a9/greenlet-3.5.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4441153ffba21b90d3ca89fe3d31f5c093ae6c0bf0cfdfc98f54cde22f95b62e", size = 1645684, upload-time = "2026-08-10T13:40:32.133Z" }, + { url = "https://files.pythonhosted.org/packages/12/63/369f1a1625e64e9e31df3963c6044056e3fdfa3fa3fdba3c54ffefa6e987/greenlet-3.5.5-cp313-cp313-win_amd64.whl", hash = "sha256:95c5b1f4b3a193f8a0c2de4bfdcb48d119f7f1063941f1de1f2168051b3e52dd", size = 324075, upload-time = "2026-08-10T13:26:58.974Z" }, + { url = "https://files.pythonhosted.org/packages/45/78/649cb5c09d4d81f6dd1444e75474a7206784743283a21d24171562ac4899/greenlet-3.5.5-cp313-cp313-win_arm64.whl", hash = "sha256:1af90aa4bc129883b340cdd6957a3bc74f60528a4993bbd1f53aaebe1d9981cc", size = 308260, upload-time = "2026-08-10T13:27:50.795Z" }, + { url = "https://files.pythonhosted.org/packages/7f/8c/080e881fa2be95ff1ddbd6994b2bab3b1a78df3b3fcab39306011764fcc7/greenlet-3.5.5-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:d4a389a852e392a6366058651a20fa5ba40d979865aa81bea2ccbdc44805070d", size = 295309, upload-time = "2026-08-10T13:26:03.032Z" }, + { url = "https://files.pythonhosted.org/packages/25/cc/0ac614e6586c0e42d4cc281a5819150f4f43685744a4c5ff77139286409d/greenlet-3.5.5-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:70b157cd319873e8b544ddc2de158f55bbd0a9b0218c8ce9332039801518e328", size = 661185, upload-time = "2026-08-10T14:14:37.867Z" }, + { url = "https://files.pythonhosted.org/packages/5e/b9/6808725354be8ad305dfe5172377664fc9642d4fc043be246b3314cf4482/greenlet-3.5.5-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8bdfd1424abcf26832961e766570cae79efdb9599d709088c9cb6ef82b194926", size = 673419, upload-time = "2026-08-10T14:27:28.652Z" }, + { url = "https://files.pythonhosted.org/packages/eb/52/f005d579acde46c3d1cc3cab1c9f3d5708c8a3006a4120e8cf5da801afe9/greenlet-3.5.5-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d98ef6f92e67c6dbf299dbfd8facc1b0d2d9cedf91e325e73b3d0373fe4309d8", size = 677863, upload-time = "2026-08-10T14:30:11.663Z" }, + { url = "https://files.pythonhosted.org/packages/42/2e/40c509967da7f254680826a2fa0dd22138ec79946c70b97542d74cde8b43/greenlet-3.5.5-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:182de51c6b572a705f2fafaab2e783bcf7d2760940229dfe73086cbae037af3e", size = 670822, upload-time = "2026-08-10T13:40:51.833Z" }, + { url = "https://files.pythonhosted.org/packages/c4/8a/a75f8a2bdcef3c358a3147cdc9db3aa83755f0a038f766ab0bedb66f512c/greenlet-3.5.5-cp314-cp314-manylinux_2_39_riscv64.whl", hash = "sha256:159df1942d88e8f784cbb38d6f18bdb365cd11319cfbb3e89623de2b97892d53", size = 480554, upload-time = "2026-08-10T14:30:05.171Z" }, + { url = "https://files.pythonhosted.org/packages/2d/22/c3c2eee4a8fe191d6d1d183086c56133d646024e3d70bfd414829f64560b/greenlet-3.5.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8fec3f165dfe332e490c3247c0f6c23b0bfc45f06496ad7f00ddb00e3d35e4dc", size = 1628469, upload-time = "2026-08-10T14:15:08.11Z" }, + { url = "https://files.pythonhosted.org/packages/f7/87/25babd09b94cb1f03e71db815fde463f0262e40cfbd953d58a8d77311351/greenlet-3.5.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c6ce25fee6cabc8bf22cb8b52e642cbb821be5b9aec8094d07ff03378141b8e9", size = 1691952, upload-time = "2026-08-10T13:40:33.502Z" }, + { url = "https://files.pythonhosted.org/packages/2e/3d/5cc9701117ea4dc0eb7bf1f4f9b7888a6e2e5277ddfae095805ace50f2b6/greenlet-3.5.5-cp314-cp314-win_amd64.whl", hash = "sha256:7dffc5c859fe6059974df1e37d7923d654a83e2ae18fdd616994270e001115e1", size = 327458, upload-time = "2026-08-10T13:27:02.868Z" }, + { url = "https://files.pythonhosted.org/packages/a7/6b/594fa2de7fae7629168a404a4305d7d7e31a5742c50a801b1839543cb93d/greenlet-3.5.5-cp314-cp314-win_arm64.whl", hash = "sha256:5e2afcfc4d4305dd715809b03da5cbe437c8984f61d8917751eb5fe4aefa3e07", size = 311146, upload-time = "2026-08-10T13:27:25.046Z" }, + { url = "https://files.pythonhosted.org/packages/24/e0/50cd600b469e5734c72709b6b1838b6bc63f307b573c772c3132d6ecfe92/greenlet-3.5.5-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:0e5a7de979d764aea1f5b6e95cf92b5b37741b9823702041f34b126e7f690277", size = 305471, upload-time = "2026-08-10T13:26:20.568Z" }, + { url = "https://files.pythonhosted.org/packages/75/a3/77acd66dfc6387b5219b2080806c0cabb73c10eb1bb44b413c40a62015ba/greenlet-3.5.5-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fef01bd457f11fc158b130ca0027a3c365693280e8e231b65bdaf57999f39f5b", size = 672470, upload-time = "2026-08-10T14:14:39.058Z" }, + { url = "https://files.pythonhosted.org/packages/b9/71/0d178142dca3ec19f46fb2212ae73d30ad53b9d548dc64804086033a7089/greenlet-3.5.5-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5173a72310725a74afc82c164f0e52cb8ad0de62f2bb623f24f6c0cc07d80272", size = 679973, upload-time = "2026-08-10T14:27:30.072Z" }, + { url = "https://files.pythonhosted.org/packages/aa/ac/0d7887aa4bbfc9eba075cc428244dfc96f623478454d5ec81180d0d6bd5a/greenlet-3.5.5-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5e9ec2e7c98e895fcea0c5cc57b2606cf86ece6d0a56578f3eb225e2af4f0387", size = 681587, upload-time = "2026-08-10T14:30:13.519Z" }, + { url = "https://files.pythonhosted.org/packages/6e/31/46eb8567302eaf787abf88d09df014e14ae3baf460af1b8b0efdbd3efcd5/greenlet-3.5.5-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:44f08341873200ba8a60a8bc14ace3d91f1754f7fa7bc66157714a8cd420a476", size = 676634, upload-time = "2026-08-10T13:40:53.004Z" }, + { url = "https://files.pythonhosted.org/packages/4f/18/8d58ba1c429b0383e3219a3d0e0bba241d0444d8ed05b73349953c7d7c7b/greenlet-3.5.5-cp314-cp314t-manylinux_2_39_riscv64.whl", hash = "sha256:102817506f6090b5176c746a82603341a549b40e5c3d5b72a4c672228a918c41", size = 510175, upload-time = "2026-08-10T14:30:07.047Z" }, + { url = "https://files.pythonhosted.org/packages/a3/e9/b88bbf5b29970cb84172dc2c32aa3e5e579ceb94c808e81c826454138850/greenlet-3.5.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:d246c0db9a2513cd45f019ba178ea4d4d4705bd210ee465e2c15d76a1ab13874", size = 1637320, upload-time = "2026-08-10T14:15:09.317Z" }, + { url = "https://files.pythonhosted.org/packages/6d/8c/7631ed29cc6f0392f11830076e172ce4885e70b0bc2c1bce1731176d4b4e/greenlet-3.5.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:72507285b5caa1d17904a3f7c322ca780823a54170a0e04ec3f37bcc60d4db71", size = 1697412, upload-time = "2026-08-10T13:40:34.924Z" }, + { url = "https://files.pythonhosted.org/packages/da/0f/f7dd935f9c4cb1be49098770587f54d8a78518e55c89bce86c4fb4109057/greenlet-3.5.5-cp314-cp314t-win_amd64.whl", hash = "sha256:7805655781fb8f28a55d05fe57ed61f5f10f1892fb587673e3bb5264f28041f0", size = 331514, upload-time = "2026-08-10T13:29:20.611Z" }, + { url = "https://files.pythonhosted.org/packages/b7/e5/681b01f8fbc1b55232822f99e8f8afeb78a55a7c76a7bf9dbdc7ccb03a6d/greenlet-3.5.5-cp315-cp315-macosx_11_0_universal2.whl", hash = "sha256:c0db80fcd5b8aece93f66c64f78a786bbb6b96c5fe63ef5a5a4581ecf8bab206", size = 295975, upload-time = "2026-08-10T13:28:45.985Z" }, + { url = "https://files.pythonhosted.org/packages/11/f2/69b488cd9e7267bf4b0fe8cdebf25d8d6df680d21bdf41150d23e23d6652/greenlet-3.5.5-cp315-cp315-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b241c32f912ada659808d68e308c568baf577eebf757d15471472de0c18cfad", size = 666823, upload-time = "2026-08-10T14:14:40.222Z" }, + { url = "https://files.pythonhosted.org/packages/84/d4/d5bc2fdebbdda0c94555925ba79948b8395d75a7f6a36cc85dce5bab9f11/greenlet-3.5.5-cp315-cp315-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ef6a08349401d8eaf3cb12688ac8557de95788556b8631ef17555a4a173022c0", size = 677613, upload-time = "2026-08-10T14:27:31.543Z" }, + { url = "https://files.pythonhosted.org/packages/65/53/4e13642efc4d7ad6554ecb2242a5be42666b2e1a067323e88dfc0124a04b/greenlet-3.5.5-cp315-cp315-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:37faa97daccb6d9f4c2141ce3118d023c3c5506864a7d8bdf726f665018c1f76", size = 681436, upload-time = "2026-08-10T14:30:14.839Z" }, + { url = "https://files.pythonhosted.org/packages/bd/93/542d8a3a90f3b35c6ad8bf7e56a03010287f2cafa289a5b7985b5207db39/greenlet-3.5.5-cp315-cp315-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f2e3d061b8e13aec2f0441689b3c71b244a20e5d274a52cb0f7e31bd1d139552", size = 675930, upload-time = "2026-08-10T13:40:54.205Z" }, + { url = "https://files.pythonhosted.org/packages/cd/32/188447c9a468d6977d2989397226b0c6b65ab6f4cf943f931643328512fc/greenlet-3.5.5-cp315-cp315-manylinux_2_39_riscv64.whl", hash = "sha256:b18007dc2473a7942fd157366b55f01da6fed7ce85318591005b419e0a439474", size = 487404, upload-time = "2026-08-10T14:30:08.903Z" }, + { url = "https://files.pythonhosted.org/packages/52/b5/89c9f2e8460d71101037d47a1feed11928615a5edd42370be290e0657eeb/greenlet-3.5.5-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:9ab5f5b93655e77fe0d6c2dfd22b5eac751bb1f876d8ec21761b7c1fb9266007", size = 1633878, upload-time = "2026-08-10T14:15:10.693Z" }, + { url = "https://files.pythonhosted.org/packages/b8/60/297de93f3b02ac78a5e04d32bb8bbe3080f4a73d8ed95016561463b70618/greenlet-3.5.5-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:f0e5a21bd4452a88cf032fc43c4a5b307ab1380eacb63b5988f9c0317885e773", size = 1696597, upload-time = "2026-08-10T13:40:36.252Z" }, + { url = "https://files.pythonhosted.org/packages/18/25/54c6eaff4f337fb670215e89eb2d00d9499487b658e709d4b477be4a342e/greenlet-3.5.5-cp315-cp315-win_amd64.whl", hash = "sha256:469dbb0a78625642f4a626cfd0c6e8bccc0385b5e49189b6308bbe849ec88a8e", size = 327700, upload-time = "2026-08-10T13:28:06.752Z" }, + { url = "https://files.pythonhosted.org/packages/67/67/857e88a36301caa0e029870132c2478bd55d896630321432afab03a3115f/greenlet-3.5.5-cp315-cp315-win_arm64.whl", hash = "sha256:2d57406c3efd32d7a81e17a674314e8bd00792cdab49ea3228a49aa1bfb2e769", size = 311750, upload-time = "2026-08-10T13:34:08.815Z" }, + { url = "https://files.pythonhosted.org/packages/10/e2/3144c0a116067ac1e30457b0139a94d60d1d36a86e015de68e9ac87cb3bc/greenlet-3.5.5-cp315-cp315t-macosx_11_0_universal2.whl", hash = "sha256:68184dfcf50ccaa8e864770fe0633a7e27250ea9329f8192ef47ee9ecfd78e1c", size = 306387, upload-time = "2026-08-10T13:27:00.897Z" }, + { url = "https://files.pythonhosted.org/packages/5c/a1/cb4223a7e9b9f43b8807e8eb212358bfe2dfaa174a9ea2889eb1714dcba2/greenlet-3.5.5-cp315-cp315t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9ec0dc0e59dc9c61af5c47348365ccbbd7addfafe0a93b00336ff3da2907bdc6", size = 676472, upload-time = "2026-08-10T14:14:41.417Z" }, + { url = "https://files.pythonhosted.org/packages/9e/cd/a154b4498e5d8f12ada291cfb3b8d596eadde2177f5bf09a9be699d2a446/greenlet-3.5.5-cp315-cp315t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e604f58e35833fc46ef20302bcb314dddbfd3fcf33a4f936216d51dd678d63ae", size = 684238, upload-time = "2026-08-10T14:27:32.946Z" }, + { url = "https://files.pythonhosted.org/packages/ce/f4/e450a68a152f819491d8c7df6a8254e761d87e6a78759268961f8c5bd4dd/greenlet-3.5.5-cp315-cp315t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2888a3a38bc5ee5bb6c438372197152e815837e4fab7ed7a1f86ef18ffd58ad1", size = 686022, upload-time = "2026-08-10T14:30:15.96Z" }, + { url = "https://files.pythonhosted.org/packages/bf/bb/b0031d260c2968a3c87deebc51d80c64e499377f993aafe06ee3b7488cc2/greenlet-3.5.5-cp315-cp315t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:40239b5384f96da3963585cc6d7eaa9b56f8ae67e8d92cc82dd9e202fc847de3", size = 681246, upload-time = "2026-08-10T13:40:55.402Z" }, + { url = "https://files.pythonhosted.org/packages/18/23/17e63d6bf3b9c9b9dbea981b7f643a71f79603bdfb4f1c3a9cf353e22aed/greenlet-3.5.5-cp315-cp315t-manylinux_2_39_riscv64.whl", hash = "sha256:1e8d9391fe77f15649589a907cef972dbbd6352ef7ff7dc0492f658c0c26495f", size = 516951, upload-time = "2026-08-10T14:30:10.907Z" }, + { url = "https://files.pythonhosted.org/packages/9a/07/da554b71ab88e649da146e1065d86a48a5c5d92e50ab74ef41b504aa7f56/greenlet-3.5.5-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:a1eaccf5c3a1d3e46dead602c72e6836731e8e245c9de6a27764567b6b62d4c0", size = 1642735, upload-time = "2026-08-10T14:15:11.92Z" }, + { url = "https://files.pythonhosted.org/packages/78/76/26a3782a051677668af9d92beaa47cd87ba9dd5072f762961144a03dd4c6/greenlet-3.5.5-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:19e4e026fe20691f333b8eb1a3bc9625eceba8c3f9d62ec5a6f8581afbc6b5a5", size = 1700925, upload-time = "2026-08-10T13:40:37.656Z" }, + { url = "https://files.pythonhosted.org/packages/28/d9/fe7baf4190c2ae71f267efb9de21b3172bb35bc0ed1ef53dd6027d658e33/greenlet-3.5.5-cp315-cp315t-win_amd64.whl", hash = "sha256:712aee154f648bde84634654bb38bb78c69ac640c37a45c9effed800735049d8", size = 331829, upload-time = "2026-08-10T13:26:48.851Z" }, + { url = "https://files.pythonhosted.org/packages/df/af/419a4e383bd600858a9b67e9b280a60fdc383ee3f2fe5b6c0c1ef04e74d1/greenlet-3.5.5-cp315-cp315t-win_arm64.whl", hash = "sha256:7f049911ee81a16a03c33d5450d8d5867d27f596ca5fb201b86f4524e874468b", size = 315093, upload-time = "2026-08-10T13:29:34.949Z" }, +] + +[[package]] +name = "grpcio" +version = "1.83.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0c/98/304898ac4e04e2d5e4e4c2eadc178b1f2a16d5f4bc2f91306c87d64680b9/grpcio-1.83.0.tar.gz", hash = "sha256:7674587248fbbb2ac6e4eecf83a8a0f3d91a928f941de571acfd3a2f007fbc24", size = 13428824, upload-time = "2026-07-23T15:20:37.759Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d3/f6/3b781cd07a715ea5f5125ae264226e7fc4d87603d6d3955022cabfdc5da2/grpcio-1.83.0-cp311-cp311-linux_armv7l.whl", hash = "sha256:8ff0b8767ddd62704e0d9571c1890af08d84a3a689ebba1807e62519d0b3277f", size = 6338720, upload-time = "2026-07-23T15:19:13.177Z" }, + { url = "https://files.pythonhosted.org/packages/21/cc/d14833d15d5984e366f1b027fa78bd038c9b028c66880bffb0f5a4d25ee2/grpcio-1.83.0-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:4772402f43517b4824980be4b3b2274a81eec0004a70009473c31b340d43e223", size = 12178773, upload-time = "2026-07-23T15:19:15.401Z" }, + { url = "https://files.pythonhosted.org/packages/6b/98/8acbb416544e7871132d8e42a07ed70c802d70e6a16c6009e505a34d32a4/grpcio-1.83.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f4cee5fc86e84a0cf7ad1574b454c3320e087c07f55b7df5dc0ac6a873fb90c0", size = 6921203, upload-time = "2026-07-23T15:19:17.824Z" }, + { url = "https://files.pythonhosted.org/packages/45/9c/0fdbfaf4fc54e5c88f6bce4008a065092fe7fbc4460eb5617ae8b20fd505/grpcio-1.83.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:f5e822a7e7d03282f6ad225e710493c48b9057a353358344a5f7c42b2b37618d", size = 7648508, upload-time = "2026-07-23T15:19:19.685Z" }, + { url = "https://files.pythonhosted.org/packages/f3/ea/107b9dbb2ed3ad14dd774fd3dde7d29ff9938a6c198654becb2c3a0e9a6a/grpcio-1.83.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f5f410d7c2903eabb34789dfd6342eef04af1ad459943936b7e09a9f5bd417b9", size = 7079466, upload-time = "2026-07-23T15:19:21.478Z" }, + { url = "https://files.pythonhosted.org/packages/3b/06/9fa9941089e6fae83b060b6ce61c1e81053e52decae43197245f45e07d36/grpcio-1.83.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ee94a4016fdf8699fb1fd8a38652475ff677f1c72074cee44deeeb9a7e95e745", size = 7605583, upload-time = "2026-07-23T15:19:23.74Z" }, + { url = "https://files.pythonhosted.org/packages/a8/2f/f10fb56062dc2771c630827a82d9ad0ecd05cad572ea3b08d49f6631680a/grpcio-1.83.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:c6444666317338e903093c7c756e6cc88eee59f798cb8dd41e87725bf54e1617", size = 8637810, upload-time = "2026-07-23T15:19:25.536Z" }, + { url = "https://files.pythonhosted.org/packages/99/55/f84927258f6a1b6ea6dea661fdc6de859b35e560c96f3012d15ccd39f85e/grpcio-1.83.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:aa074041231f03959cb097dd5517b0677b8ea49215bae01d5710a7b69dd59969", size = 8008021, upload-time = "2026-07-23T15:19:27.863Z" }, + { url = "https://files.pythonhosted.org/packages/c9/6b/cdf72161397ccd29d4ca2192f641524536c9cf54ad948c9dd0e0e01138fa/grpcio-1.83.0-cp311-cp311-win32.whl", hash = "sha256:cb056f6e171c42639a50460b2929c82241fda51f71cf3dcdd68090fe45095a45", size = 4404376, upload-time = "2026-07-23T15:19:30.137Z" }, + { url = "https://files.pythonhosted.org/packages/df/ed/e0ffeb4c848699c194dc9fb6a29ab29bcb2b6aac8c416bf18c51bfe8242c/grpcio-1.83.0-cp311-cp311-win_amd64.whl", hash = "sha256:7416952ca770477990257206276999056f8316d79196f2f25942393e58a20b49", size = 5164469, upload-time = "2026-07-23T15:19:31.941Z" }, + { url = "https://files.pythonhosted.org/packages/15/2b/51e32514a4e9b715375c99721aadff0f24164cc2049b8269eda4de82a814/grpcio-1.83.0-cp312-cp312-linux_armv7l.whl", hash = "sha256:28f6c35ac8fcf10e4594f138e468f194360089dde40d126a7033e863fc479930", size = 6303167, upload-time = "2026-07-23T15:19:33.78Z" }, + { url = "https://files.pythonhosted.org/packages/39/33/b5b50fc2c6fbe350e04814047bb2d409feec7b36ef8b170254c050e06bc0/grpcio-1.83.0-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:33898e6a28e4ae598f1577cb1c4fec2a15c033d0ec52b9b45a09610dd045b9da", size = 12160538, upload-time = "2026-07-23T15:19:35.958Z" }, + { url = "https://files.pythonhosted.org/packages/7b/5f/734e72e7b9f79bcf0b2c270b8d3bca0e4ebb97a27a50d06240b145f6d41e/grpcio-1.83.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:6fb8a1dd0c6f0f931e69e9d0dc6d1c406ed2a44fa963414eafba07b7fb685d16", size = 6869310, upload-time = "2026-07-23T15:19:38.607Z" }, + { url = "https://files.pythonhosted.org/packages/a4/17/a1735f215b2a5cd43c38b79eac072ad197e61be9829905b6b29550abd0db/grpcio-1.83.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:2b5e75c34842cd9c1b95285ca395c6a569664b81e3ffa6b714125922942abaaf", size = 7613472, upload-time = "2026-07-23T15:19:40.645Z" }, + { url = "https://files.pythonhosted.org/packages/b2/78/c9e81f806ac704b6b145cb01628db398985b1f8dfdc10e23b55fb0902b3d/grpcio-1.83.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:aeb339838db07600481ef869507279b75326c75eac6d10f7afa62a0da1d2bcdd", size = 7040616, upload-time = "2026-07-23T15:19:42.349Z" }, + { url = "https://files.pythonhosted.org/packages/9a/ba/94cd5af859876049d340480acbb61a959096c84b567f215534faa78d0424/grpcio-1.83.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f47d62808b4c0a97b78bff88a6d4ca283a2a492b9a04a87d814af95ca3b9c19c", size = 7570491, upload-time = "2026-07-23T15:19:44.357Z" }, + { url = "https://files.pythonhosted.org/packages/3e/15/108d30d5a5c964312ae8b9cb0e8cc5b3c1cc68d8f757cca52b3565534d26/grpcio-1.83.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:62003babc444a606dcd1f009cd16391ce23669ae4ad6ec267a873da7937a69f5", size = 8605036, upload-time = "2026-07-23T15:19:46.454Z" }, + { url = "https://files.pythonhosted.org/packages/ea/23/3828ae13c3db8233d123ad612747665817b952d8a954f32390230b582336/grpcio-1.83.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1aa567f8c3f19850ffd5d2858c9a8ea7c80f0db6c01186b71eb31e923ec984f5", size = 7981587, upload-time = "2026-07-23T15:19:48.913Z" }, + { url = "https://files.pythonhosted.org/packages/17/5b/77af31228f55f55a2a5112bb0077ad0a1c4d23dbb0c2853a62475bbdcc14/grpcio-1.83.0-cp312-cp312-win32.whl", hash = "sha256:cb2906c61db4f9c64cc360054b5df70eeb81846228e9e56a4944bd415a63dadc", size = 4394004, upload-time = "2026-07-23T15:19:50.618Z" }, + { url = "https://files.pythonhosted.org/packages/c0/da/f706e39550e7a3732ce2b9c5926107a93d74a802775b19b642a6df27dc96/grpcio-1.83.0-cp312-cp312-win_amd64.whl", hash = "sha256:1c699bbb20f143c8f2bff219de578aa2dc1f919399d67dc702b038b986ee62df", size = 5158525, upload-time = "2026-07-23T15:19:52.246Z" }, + { url = "https://files.pythonhosted.org/packages/56/eb/135daaa713f32d33b8f99b4153b3f8dc3b2a124996ac15581bf9ebdad3c3/grpcio-1.83.0-cp313-cp313-linux_armv7l.whl", hash = "sha256:6662f3b1e07cc7493d437351860dc867bddc6a93c83ecf33bbfdaf0c217ab2d0", size = 6304480, upload-time = "2026-07-23T15:19:53.962Z" }, + { url = "https://files.pythonhosted.org/packages/8f/a1/121806ce69f23138dabe06aa595b0e5f1ae051a37e4c1954eed7d692c800/grpcio-1.83.0-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:74fe6f9e8a35c7dbf32255ee154d15e3e5338a81ed39173d079d594d2e544cd1", size = 12154419, upload-time = "2026-07-23T15:19:56.3Z" }, + { url = "https://files.pythonhosted.org/packages/b0/e8/d0389e09cd6b4c4d3089b92967ae4e3ffd64795bd349bf2f85cd6656d3da/grpcio-1.83.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:10b3fa0475eb572c9a81a6fe37fa16a9c500c0c91cfc148cac15692b7e3c2867", size = 6873200, upload-time = "2026-07-23T15:19:58.701Z" }, + { url = "https://files.pythonhosted.org/packages/f8/51/f464c1d211fa50d5adbabe1b2e519948d99c13757052bfc9ea7afa28e284/grpcio-1.83.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:5f20a988480b0f28207f057f7f7ae1313393c3cef0adcfeae8248f9947eaf881", size = 7618811, upload-time = "2026-07-23T15:20:00.733Z" }, + { url = "https://files.pythonhosted.org/packages/e8/c0/539fe0832f2dd6500a28f5263071623fb34e8d4867aec632ccf81bd21156/grpcio-1.83.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7bd82671b39065ba18cd536e9cd45b27ff649053f81ddd2c6a966d595067080f", size = 7042310, upload-time = "2026-07-23T15:20:02.675Z" }, + { url = "https://files.pythonhosted.org/packages/8c/ca/ccf617d37ffa72567fa8e005ec7090c99da922799be2fb9847c8b21ca18c/grpcio-1.83.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bc60215b5cb9fc8ca72942c498b551ac2305bd08f6ef8d4e3f0d21b64fbecd61", size = 7575412, upload-time = "2026-07-23T15:20:04.712Z" }, + { url = "https://files.pythonhosted.org/packages/eb/b9/fd8d5245f823a8e0fd35d90e20ea3aa4acd47f8d5318fa8df307df52dec6/grpcio-1.83.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:f1c3e5689d4b90987b1d72022bcfe866a9a3dc66197484cf856d96b6150e7f45", size = 8604248, upload-time = "2026-07-23T15:20:06.77Z" }, + { url = "https://files.pythonhosted.org/packages/14/1e/f37632fc11db72dfa4bba86c3a43e54358e53030df111ecae5e91a733ad6/grpcio-1.83.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a21cb4eeeba124443f399be2e8b624943cde864dcbe588cb42e5c483a52a906c", size = 7977458, upload-time = "2026-07-23T15:20:09.109Z" }, + { url = "https://files.pythonhosted.org/packages/93/b6/d70b69ae5c0cfc341b9ba474980e4ed99cbf05c0e4a14e9eee8cb73db0a5/grpcio-1.83.0-cp313-cp313-win32.whl", hash = "sha256:8fe04f1050a59f875601eb55d42b4f66946fe89817f967e34db1462ccd07dadf", size = 4393993, upload-time = "2026-07-23T15:20:11.017Z" }, + { url = "https://files.pythonhosted.org/packages/0f/13/45d4cccb555cf4c476226979bf3d2fd0b0254216f7564c3a053e35117efc/grpcio-1.83.0-cp313-cp313-win_amd64.whl", hash = "sha256:6e01ecd9d8ef280abe1365138a4dc318f9a5287f4cb1b41d07816f796653f735", size = 5159650, upload-time = "2026-07-23T15:20:12.979Z" }, + { url = "https://files.pythonhosted.org/packages/9c/60/f2cca8147ea213d3e43ae9158d03ad04e020fdf32ff027253e1fe93f921d/grpcio-1.83.0-cp314-cp314-linux_armv7l.whl", hash = "sha256:3f351629f6ae16ecc0ec3553e586a6763ffd9f6114044286d0cbec3e09241bfa", size = 6305607, upload-time = "2026-07-23T15:20:15.353Z" }, + { url = "https://files.pythonhosted.org/packages/d0/ab/d3874931d123a95e83a3ebf8aa04537988fb62425cedb8bf3cefc5ad41b2/grpcio-1.83.0-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:d05ff664100d429335b93c91b8b34ddf9e94a112205e7fa06dede309e44a4e4c", size = 12166617, upload-time = "2026-07-23T15:20:17.435Z" }, + { url = "https://files.pythonhosted.org/packages/92/ff/6f18f9426b69306f4e00a9add3b0ee2748da8aad53836ef80cab0d62d04f/grpcio-1.83.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7936f2a56cf04f6514705c0fedf400971de01b6aa1719327e4718f410a765e2b", size = 6880213, upload-time = "2026-07-23T15:20:19.98Z" }, + { url = "https://files.pythonhosted.org/packages/70/21/706d1147c6b93b98f179240c13991fbcc56880eba0c868abb1ad40d8a0a6/grpcio-1.83.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:b0a0be840e51b6b7ee9df9269770faf77bdf4b771053c257c21d12bad607714c", size = 7618335, upload-time = "2026-07-23T15:20:22.161Z" }, + { url = "https://files.pythonhosted.org/packages/74/04/1a8443c889115ec9e213a213e86bc93a71ee9088027e5befa09aaa0edd9d/grpcio-1.83.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:009667eaf3dcd5224c713589cdc98e7ca4ed0ff0b61132c6b276e930eb83a2df", size = 7043416, upload-time = "2026-07-23T15:20:24.209Z" }, + { url = "https://files.pythonhosted.org/packages/86/c6/94e0fee5b12bc1da1370185b680988db6f739d19b42d9959db01a7ea50bf/grpcio-1.83.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:bb669918fd88936b15599caff4160a77ab74bdeb25f2231f6e45b61282d6107b", size = 7583253, upload-time = "2026-07-23T15:20:26.313Z" }, + { url = "https://files.pythonhosted.org/packages/a0/97/de1ccb671fb85575bc5192faedf9ecdbdf5b390d2e6584dcf552bcbd370e/grpcio-1.83.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:c19b454d3d3f28db81f2c7c4dbaee96e7f6fd149721733ffe79d6bc530f17404", size = 8605102, upload-time = "2026-07-23T15:20:28.437Z" }, + { url = "https://files.pythonhosted.org/packages/17/0f/0e0ec749a7034ffcbaa050e39779872950ead90c22e7e0116be3f28b2b46/grpcio-1.83.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:61007cd08640abc5c54547ee32505474c482cd733a53cb87551ea81faa6350af", size = 7979826, upload-time = "2026-07-23T15:20:31.182Z" }, + { url = "https://files.pythonhosted.org/packages/83/fa/c3fda157287f64bc65acee6c5aa90c41acf9e0d3a8e69a265eecff6d00a1/grpcio-1.83.0-cp314-cp314-win32.whl", hash = "sha256:32e11c37f5285b0c6fa3042c05fe06903696689749833fc64e67dec71b9bbe33", size = 4471765, upload-time = "2026-07-23T15:20:33.195Z" }, + { url = "https://files.pythonhosted.org/packages/a1/00/b1b26431c9d54eee11724fd6e5585473a2ed47fbc1fb95e5204906a642ce/grpcio-1.83.0-cp314-cp314-win_amd64.whl", hash = "sha256:2bb48cb5e6dd005ca12b89ce4b6ac0b48ff3112c747542ee7986ef611a8ca6d9", size = 5298932, upload-time = "2026-07-23T15:20:35.48Z" }, +] + +[[package]] +name = "grpcio-tools" +version = "1.83.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "grpcio" }, + { name = "protobuf" }, + { name = "setuptools" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c1/b1/50b17b3a2ba8970dbb00b2035a4df218bc6ec88d2d77fc7da4e42e1c7b19/grpcio_tools-1.83.0.tar.gz", hash = "sha256:515907265d14fa9975d0c7723f95a9da01463d7ac607546a03f8741f86a1bb07", size = 6400437, upload-time = "2026-07-23T15:22:18.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b9/f0/d55c59b3af39d2ca975a2bafc5be1a60e0460a1b507bdbcf7bb8a17567db/grpcio_tools-1.83.0-cp311-cp311-linux_armv7l.whl", hash = "sha256:72471a4a46909f1d798836c0a0aa2f568e10f6404585d7b22ac7330dd6a7bc74", size = 2652835, upload-time = "2026-07-23T15:21:06.072Z" }, + { url = "https://files.pythonhosted.org/packages/82/97/59bdf27c5f99848087b3b268c90b3fcc557d400c8d0224cf49c7ac573e60/grpcio_tools-1.83.0-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:7da547dd1e0b1fe3d6d5677e9c1848969ecdbd51a92c342cf82d885c5935de7d", size = 5967887, upload-time = "2026-07-23T15:21:08.19Z" }, + { url = "https://files.pythonhosted.org/packages/f6/c4/117d6688c4cb40240ce48da1ffac005e8da8bf63785634bdbb9143ca367e/grpcio_tools-1.83.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:33350bd94c4913f8eaf6ca79ce69cc673bb46ca5f800e6474b1d6899ed321dad", size = 2704869, upload-time = "2026-07-23T15:21:09.857Z" }, + { url = "https://files.pythonhosted.org/packages/1a/51/e4d1a89f69072bb93b975548c40fe9524219ab5777cce113c1e2183b1f2d/grpcio_tools-1.83.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:0f27da6dc58c910f31dcee4fcbfd05659c10c14b9e132f4f17da48d867e75eb4", size = 3032318, upload-time = "2026-07-23T15:21:11.55Z" }, + { url = "https://files.pythonhosted.org/packages/ae/26/f9a082b79ae7f7dac7050047614000721dcf3d1c6d14bffa881b1f7a9774/grpcio_tools-1.83.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b3a5000e8540efb80f74d9c760f36ed9408294d76edb0fa4b87fd287b0a8a258", size = 2774113, upload-time = "2026-07-23T15:21:13.037Z" }, + { url = "https://files.pythonhosted.org/packages/de/59/b715d431218f0e8382490db7b1d01b3d32392c359c1886eee7e2551edaa8/grpcio_tools-1.83.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:f710032264ed114c9d3b52f4c5cf71d68ccf70f25c4cb5776fe60388374bc01b", size = 3226715, upload-time = "2026-07-23T15:21:14.88Z" }, + { url = "https://files.pythonhosted.org/packages/5f/99/c4e91a2116062f7b42ab99459eeda15ec59a07daad01f4f38753fb1584ac/grpcio_tools-1.83.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:ad00de87334154901e9af50a7f3f95261a0133502c6c0cea1f4e6107245154c3", size = 3799000, upload-time = "2026-07-23T15:21:16.352Z" }, + { url = "https://files.pythonhosted.org/packages/b8/f7/0bb5e45a1f51822c0bcf5a355a06132c43b9dbe5c283edc8d0b63bec7626/grpcio_tools-1.83.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:de2b0c645363f7c4b005f145e06e790870dfb21bb7e162dec6fee2f054de9291", size = 3457774, upload-time = "2026-07-23T15:21:17.953Z" }, + { url = "https://files.pythonhosted.org/packages/e7/51/52aec42a3059bad4263a7a82c715a0fc08220f7533be41f0b9177b05468a/grpcio_tools-1.83.0-cp311-cp311-win32.whl", hash = "sha256:6d1a1c9e62689d04b63b227558b759a55dce8fb81a3934d3b5f95d1ee26a2b45", size = 1022798, upload-time = "2026-07-23T15:21:19.854Z" }, + { url = "https://files.pythonhosted.org/packages/46/22/034d6daa5b2fae4e4c1718111c1b791589c3743d7d8fb07984e0b59c7f2c/grpcio_tools-1.83.0-cp311-cp311-win_amd64.whl", hash = "sha256:a6ac3cc2c2d77f869a96dfaf2b1315852878babddfe2dc49b9fd47afbf502865", size = 1192474, upload-time = "2026-07-23T15:21:21.467Z" }, + { url = "https://files.pythonhosted.org/packages/27/4d/02e4d809d880785a613697e4c0f8134a436ec0142dc3c11989ad1a1c787a/grpcio_tools-1.83.0-cp312-cp312-linux_armv7l.whl", hash = "sha256:bd93bbe18c4424805fd2e39854f75d76f80655175621254dc43cb45ec8e91e85", size = 2653283, upload-time = "2026-07-23T15:21:22.973Z" }, + { url = "https://files.pythonhosted.org/packages/01/c9/23e8423ac54c3858a5cfbca8a954fa292cab7b8a9a1ee9dc3259b906b763/grpcio_tools-1.83.0-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:dc2d370563ee1ee6c1769e49df35ef3f6e75cea8f25acf4ac6e54b335e6f788f", size = 5965938, upload-time = "2026-07-23T15:21:24.686Z" }, + { url = "https://files.pythonhosted.org/packages/bf/a1/6eb17cf322bfbb76a9b9a8a5ca4a6b27f0af84821145969fc464feedaa0e/grpcio_tools-1.83.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8350e236470700b02bc4ba7f27a8796559630170e6527dda41c0344fbe988e56", size = 2705429, upload-time = "2026-07-23T15:21:26.519Z" }, + { url = "https://files.pythonhosted.org/packages/d4/84/f3c7e5e91e5d40ee792f112260bd329db5381de6f83b21387dd6163ebe51/grpcio_tools-1.83.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:b1649f47c4675c1540ad2a77005f4d08392c06202686a0b1bb6b894f96cb75fe", size = 3033412, upload-time = "2026-07-23T15:21:28.183Z" }, + { url = "https://files.pythonhosted.org/packages/5f/ec/acd6c1800925b0d60f8a670b68cf5bc3566aee60e7cb90178b34253bf53a/grpcio_tools-1.83.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7d4edc6ba9fdca70bbf585ff6ab5971b8cd6140b4b316df66fd91d168bc1b617", size = 2774500, upload-time = "2026-07-23T15:21:29.955Z" }, + { url = "https://files.pythonhosted.org/packages/36/e6/c7c6f4e25b7344f1fbc5df69606bfd4b9692d4e32d17781c665d3ed45e70/grpcio_tools-1.83.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:762a9f8a4a4a39bda02feebed94efb8d778e0e5a82d0c8f786dce5ddcb950c7f", size = 3229875, upload-time = "2026-07-23T15:21:31.617Z" }, + { url = "https://files.pythonhosted.org/packages/77/fc/9cbdc4606f378a9c2b569c0b6b57f181f97787006be4131a5820d469b70a/grpcio_tools-1.83.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:c6c469c928a183f1a99ab26e263fe307347ee7023fa623b55bc778846b2f51b9", size = 3803163, upload-time = "2026-07-23T15:21:33.35Z" }, + { url = "https://files.pythonhosted.org/packages/7b/0b/3b754886a02ead1487a967fd11ff13e920218fcf6c8e174f6de0c26dd819/grpcio_tools-1.83.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7980b3ca9dd31c42468c5af8cec97037f83715ea6efbe1b936ecb9c6832ac0f5", size = 3461815, upload-time = "2026-07-23T15:21:34.982Z" }, + { url = "https://files.pythonhosted.org/packages/eb/29/4629c154853f6f677299cde014e615084c15ad1d4fedd6845d2e7d354c7e/grpcio_tools-1.83.0-cp312-cp312-win32.whl", hash = "sha256:fd2ff46917f566b3b63dae191d1b05ef2188fe51e756ef321cbdd707ab29dbfb", size = 1022490, upload-time = "2026-07-23T15:21:36.696Z" }, + { url = "https://files.pythonhosted.org/packages/bb/59/696b9671e32a693c67299724a1f70f81dfb78aca6a3283ea6a65d54e92b8/grpcio_tools-1.83.0-cp312-cp312-win_amd64.whl", hash = "sha256:92d2343806b5c21162a57fbaad24fcd8d935ef530f95a0f706a4e2546fdc0662", size = 1192286, upload-time = "2026-07-23T15:21:38.446Z" }, + { url = "https://files.pythonhosted.org/packages/63/2f/a7a4465b2a5b74b479373bf44d86da5840d7d20871764a39fb300e55e093/grpcio_tools-1.83.0-cp313-cp313-linux_armv7l.whl", hash = "sha256:3277cfbb7cbbd2d72921fbcd7aba6c8ab1c91a9ab27e8045ac0a0f2e0517cec9", size = 2652845, upload-time = "2026-07-23T15:21:40.173Z" }, + { url = "https://files.pythonhosted.org/packages/ab/69/ed8ebaae3bd0ecd2387b0fdb3a696bd4b4d4565d18589ee3ba7c6affcbed/grpcio_tools-1.83.0-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:1aa9617ce9c2bcbfb8f2fa08e6259e1b3cadaab0316e41f71496847af2f0a664", size = 5963575, upload-time = "2026-07-23T15:21:41.943Z" }, + { url = "https://files.pythonhosted.org/packages/13/23/75bddae583077f1374c64e2a87b5924bdbffe162b3855af30c30a0fc8c5c/grpcio_tools-1.83.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ad37c786ea92825534466052f5f22f1f29983b1d00ca71ad43e256715a86bba3", size = 2705094, upload-time = "2026-07-23T15:21:43.675Z" }, + { url = "https://files.pythonhosted.org/packages/f2/ac/7e1b12b1c5afff4f7d578f3c4eafbb1849f8004ddcc236cd3ff95c8be607/grpcio_tools-1.83.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:c82216864d435ecf6f535798d03e9f6b9025a672a2e815715d629aba4ba70349", size = 3033061, upload-time = "2026-07-23T15:21:45.657Z" }, + { url = "https://files.pythonhosted.org/packages/b8/6d/113291a7aad0c47a1e2ba2375595dfc3c2ab648e0ea05ff3d6f0f89055b3/grpcio_tools-1.83.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:04284627655629387b63278591e5efd0aded28d3a08432fe8a8765e4daf2d5b2", size = 2773649, upload-time = "2026-07-23T15:21:47.404Z" }, + { url = "https://files.pythonhosted.org/packages/dd/c2/ce1c6c2475ed5cb0f7c6689bde57e3c105714676227da11b423ff37620a9/grpcio_tools-1.83.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4fe627a5248d8e712f5ec3e019420050e61b10600ed241264aefb379a0f1b338", size = 3229788, upload-time = "2026-07-23T15:21:49.441Z" }, + { url = "https://files.pythonhosted.org/packages/c5/53/823fd52c29630398706de400ce7003b03e17ce91df024fc53cde810d2758/grpcio_tools-1.83.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:1d5a9a9664d2f4bdda000652e7febf9ab4545a6391c8f05babd52ecb27d7e03e", size = 3802531, upload-time = "2026-07-23T15:21:51.37Z" }, + { url = "https://files.pythonhosted.org/packages/30/85/942ee07caf97b75ead416c6ad5f2fb12b16b8bc92fa3d60bbbea4c06e076/grpcio_tools-1.83.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4bf0e421e1ab5f2cd638de44fc903aed3ba4a2fcb19b93c6f528ff0ec63e3a6a", size = 3461032, upload-time = "2026-07-23T15:21:53.343Z" }, + { url = "https://files.pythonhosted.org/packages/d4/0b/54cb428ea18912fa9541509fbc2e10f9807d96e88cf435f86e63c540ac25/grpcio_tools-1.83.0-cp313-cp313-win32.whl", hash = "sha256:7b1bd6db403b38addded54866187eba6f9ab9afadf72bb8d0515ed13f0b16c5c", size = 1022116, upload-time = "2026-07-23T15:21:55.036Z" }, + { url = "https://files.pythonhosted.org/packages/77/80/2369320766091f6daedb924d133037a9f8b84bfb3e4d02d6ccffcd57b0cd/grpcio_tools-1.83.0-cp313-cp313-win_amd64.whl", hash = "sha256:d654c645af7cf608a30644bffb8d1ef6b14e8846482c3d0131d0dda91f6fb590", size = 1191934, upload-time = "2026-07-23T15:21:56.862Z" }, + { url = "https://files.pythonhosted.org/packages/b2/bd/3abb9c200f90110805553ca0e8f7908a0b89485ea99bb271daa37eaafb72/grpcio_tools-1.83.0-cp314-cp314-linux_armv7l.whl", hash = "sha256:1ea047ff4bd2bb32fe5268042cb9c9e7bb054e932b52ad756642622f032ef656", size = 2652841, upload-time = "2026-07-23T15:21:58.726Z" }, + { url = "https://files.pythonhosted.org/packages/a2/46/d5beb04f0ffe552e55eaddd3413786e17a1fa68edb2fd8398969c38bc7e8/grpcio_tools-1.83.0-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:7a8ac9cb3fbf7a5e4fe59f211e77b4fa4d51279c9f480e6ff98037cf56da1ad8", size = 5963493, upload-time = "2026-07-23T15:22:00.547Z" }, + { url = "https://files.pythonhosted.org/packages/40/1b/d8e01ca3281cb59722372c415024a7e70e8a653e70e2075e875394e4f761/grpcio_tools-1.83.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a5fa95fb33a600d2491867a1048f47baa27a830eac01f475043b8ccf63a471eb", size = 2705303, upload-time = "2026-07-23T15:22:02.448Z" }, + { url = "https://files.pythonhosted.org/packages/83/43/75b382d8f116e274ce639ec55a6908dc792627b01d3c47b4f8991701203b/grpcio_tools-1.83.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:0ca67941524662e01adea91571bb79df1ac9b4b2641812ef8636e21945119bee", size = 3033047, upload-time = "2026-07-23T15:22:04.445Z" }, + { url = "https://files.pythonhosted.org/packages/0f/9f/200fb55729b735192fded061f53aa37c88cf9f58933cb108e16f5c1fd967/grpcio_tools-1.83.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e2a5816a5b6b06b42a6989f02944841c2a8b3daaa7f033dac9267f70078028ef", size = 2773830, upload-time = "2026-07-23T15:22:06.519Z" }, + { url = "https://files.pythonhosted.org/packages/d1/bc/e585c4e255599ec45f2e07e0f570352158354285afd65ef30ceda97b445e/grpcio_tools-1.83.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f4a83f002895b11c4a862c366d77165825e0064aff14bf2c7453f59f66599b0e", size = 3229907, upload-time = "2026-07-23T15:22:08.358Z" }, + { url = "https://files.pythonhosted.org/packages/4d/a1/0c098b4ff64d948666b79d0036ce104b8c2209e6f4d1594046044dcfa25d/grpcio_tools-1.83.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:1cfee967ae073bc064862971871229248965422f38556096aec76db19d8a8c79", size = 3802600, upload-time = "2026-07-23T15:22:10.393Z" }, + { url = "https://files.pythonhosted.org/packages/d4/e1/249047aab0da8c6b3b2e0156e6868aa0b598973ddf53f59186c43664ff96/grpcio_tools-1.83.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f7e82ee718ae09f879cb832e4517a56691de24383a2da673be184ad8b18e452f", size = 3461307, upload-time = "2026-07-23T15:22:12.33Z" }, + { url = "https://files.pythonhosted.org/packages/39/aa/11a228c47acecde2e05715bda4480e5a695690e00776c1db391e5a02c1f7/grpcio_tools-1.83.0-cp314-cp314-win32.whl", hash = "sha256:846fd211ebb72f50d39d3874cc0d616c2b9bcb71db51121ca86af29eec013c74", size = 1045047, upload-time = "2026-07-23T15:22:14.23Z" }, + { url = "https://files.pythonhosted.org/packages/30/b1/d1f150b2ab3b4ae9932c05104fe1edbcb7fbf505587ea8db99e49341a05f/grpcio_tools-1.83.0-cp314-cp314-win_amd64.whl", hash = "sha256:b8c9686b0c19f70b63d8d6cfeff5ad3480bdedecd60f14711fe43950f5397253", size = 1224199, upload-time = "2026-07-23T15:22:16.064Z" }, +] + +[[package]] +name = "gunicorn" +version = "26.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6d/b7/a4a3f632f823e432ce6bc65f62961b7980c898c77f075a2f7118cb3846fe/gunicorn-26.0.0.tar.gz", hash = "sha256:ca9346f85e3a4aeeb64d491045c16b9a35647abd37ea15efe53080eb8b090baf", size = 727286, upload-time = "2026-05-05T06:38:25.529Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e6/40/9c2384fc2be4ad25dd4a49decd5ad9ea5a3639814c11bd40ab77cb9f0a14/gunicorn-26.0.0-py3-none-any.whl", hash = "sha256:40233d26a5f0d1872916188c276e21641155111c2853f0c2cd55260aec0d24fc", size = 212009, upload-time = "2026-05-05T06:38:23.007Z" }, +] + +[[package]] +name = "h2" +version = "4.4.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "hpack" }, + { name = "hyperframe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e7/85/7c366e69d84c17bb778fe41419e1fbcce3033d5b7ce29bbffff0a98b859f/h2-4.4.1.tar.gz", hash = "sha256:4e866ffb1a869ae14dd9b5e6beb5c24a13da0495ad72b65925ded182521c1516", size = 2157281, upload-time = "2026-08-03T11:45:09.509Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/22/e85faf23bd72a92d1921e37d674ca56eb298a3c8be31fdecef0ff2b3aaac/h2-4.4.1-py3-none-any.whl", hash = "sha256:0e25f1462b23c9cb82d9eb02e28bc706dac2a68cb457c6a0d74d63c8a2a5d0e6", size = 62636, upload-time = "2026-08-03T11:44:59.164Z" }, +] + +[[package]] +name = "hpack" +version = "4.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/26/5b/fcabf6028144a8723726318b07a32c2f3314acdff6265743cf08a344b18e/hpack-4.2.0.tar.gz", hash = "sha256:0895cfa3b5531fc65fe439c05eb65144f123bf7a394fcaa56aa423548d8e45c0", size = 51300, upload-time = "2026-06-23T18:34:46.667Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/71/b4/4a9fcfb2aef6ba44d9073ecd301443aa00b3dac95de5619f2a7de7ec8a91/hpack-4.2.0-py3-none-any.whl", hash = "sha256:858ac0b02280fa582b5080d68db0899c62a80375e0e5413a74970c5e518b6986", size = 34246, upload-time = "2026-06-23T18:34:45.472Z" }, +] + +[[package]] +name = "hyperframe" +version = "6.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/02/e7/94f8232d4a74cc99514c13a9f995811485a6903d48e5d952771ef6322e30/hyperframe-6.1.0.tar.gz", hash = "sha256:f630908a00854a7adeabd6382b43923a4c4cd4b821fcb527e6ab9e15382a3b08", size = 26566, upload-time = "2025-01-22T21:41:49.302Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/48/30/47d0bf6072f7252e6521f3447ccfa40b421b6824517f82854703d0f5a98b/hyperframe-6.1.0-py3-none-any.whl", hash = "sha256:b03380493a519fce58ea5af42e4a42317bf9bd425596f7a0835ffce80f1a42e5", size = 13007, upload-time = "2025-01-22T21:41:47.295Z" }, +] + +[[package]] +name = "idna" +version = "3.18" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cd/63/9496c57188a2ee585e0f1db071d75089a11e98aa86eb99d9d7618fc1edce/idna-3.18.tar.gz", hash = "sha256:ffb385a7e039654cef1ab9ef32c6fafe283c0c0467bba1d9029738ce4a14a848", size = 196711, upload-time = "2026-06-02T14:34:07.794Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/5e/d4e9f1a599fb8e573b7b87160658329fbf28d19eac2718f51fc3def3aa5a/idna-3.18-py3-none-any.whl", hash = "sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2", size = 65455, upload-time = "2026-06-02T14:34:06.319Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + +[[package]] +name = "jsonschema" +version = "4.26.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "jsonschema-specifications" }, + { name = "referencing" }, + { name = "rpds-py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b3/fc/e067678238fa451312d4c62bf6e6cf5ec56375422aee02f9cb5f909b3047/jsonschema-4.26.0.tar.gz", hash = "sha256:0c26707e2efad8aa1bfc5b7ce170f3fccc2e4918ff85989ba9ffa9facb2be326", size = 366583, upload-time = "2026-01-07T13:41:07.246Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/90/f63fb5873511e014207a475e2bb4e8b2e570d655b00ac19a9a0ca0a385ee/jsonschema-4.26.0-py3-none-any.whl", hash = "sha256:d489f15263b8d200f8387e64b4c3a75f06629559fb73deb8fdfb525f2dab50ce", size = 90630, upload-time = "2026-01-07T13:41:05.306Z" }, +] + +[[package]] +name = "jsonschema-specifications" +version = "2025.9.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "referencing" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/19/74/a633ee74eb36c44aa6d1095e7cc5569bebf04342ee146178e2d36600708b/jsonschema_specifications-2025.9.1.tar.gz", hash = "sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d", size = 32855, upload-time = "2025-09-08T01:34:59.186Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe", size = 18437, upload-time = "2025-09-08T01:34:57.871Z" }, +] + +[[package]] +name = "microdns" +version = "1.0.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "dnslib" }, + { name = "trlib" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/83/a6/d9fd8e82ee23ca4dbd05f607b99151034bde87f1dface0a0b02212e83305/microdns-1.0.3-py3-none-any.whl", hash = "sha256:20bfd41d2db555bd90eec56eb2760884a5ac25b87df6f4309d70ac91cc53fe24", size = 10504, upload-time = "2020-04-28T16:03:54.018Z" }, +] + +[[package]] +name = "microserver" +version = "1.0.8" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "trlib" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/cf/56/61ea5a52250b291e7a96c1b2a03ff970a3531eada5fdff44cd4b07c26c2f/microserver-1.0.8-py3-none-any.whl", hash = "sha256:b229aacefa9790c11819b33784a25bdc0ecf09d3a8110d71cea7580ebfe1a938", size = 18041, upload-time = "2025-01-07T16:43:34.509Z" }, +] + +[[package]] +name = "packaging" +version = "26.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/fa/3944b40b07da9ce895c0e6303a5ab7d53da063554f534556b134a54d6093/packaging-26.3.tar.gz", hash = "sha256:94edc256424af38762eb31306eed28beb9f0efc50a8837492c9d6fd6004aed79", size = 313412, upload-time = "2026-08-04T18:15:28.737Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/63/34/ba1c580383c9eada3711951fef0795c80b829a078d72188184bcab9dd527/packaging-26.3-py3-none-any.whl", hash = "sha256:d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c", size = 129956, upload-time = "2026-08-04T18:15:27.159Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + +[[package]] +name = "prometheus-client" +version = "0.26.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/52/73/f1334c29c2af4cd9dba6c7817e61b611bd0215e2eb5565c6064a4de18802/prometheus_client-0.26.0.tar.gz", hash = "sha256:04a91bcf94e2cf74a44a1a874d651a2e853ed354b6e822f3b7487751465d5c2b", size = 92910, upload-time = "2026-07-24T19:36:41.893Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/eb/a3/b69efbf4143b5b9859b977770bbbabcc2796b702fa69dc40271e45cd5a56/prometheus_client-0.26.0-py3-none-any.whl", hash = "sha256:fa93d06737aa02bacd05794768508bb97d2fbee28cb3bca04eaae92f0ca953d6", size = 64494, upload-time = "2026-07-24T19:36:40.854Z" }, +] + +[[package]] +name = "protobuf" +version = "7.35.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/da/01/9ef0afd7999eb9badb3a768b4aedd78c86d4c65cfaf1958ab276199e76b4/protobuf-7.35.1.tar.gz", hash = "sha256:ce115a26fe0c39a2c29973d914d327e516a6455464489fe3cd1e51a1b354f81a", size = 458717, upload-time = "2026-06-11T21:55:40.257Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/10/03/8aeeb7458d22546bf64b5250ca1daeb5ff757d900e8e4a7476c6f0db843e/protobuf-7.35.1-cp310-abi3-macosx_10_9_universal2.whl", hash = "sha256:24f857477359a85c0c235261b8ba905fd51b2562f4a64ca1df5473f29850cbf6", size = 433226, upload-time = "2026-06-11T21:55:31.719Z" }, + { url = "https://files.pythonhosted.org/packages/37/4b/dfb89eb0e652a1ff073c39a59fb5e3a83cfe9b57a2c83fa6d78270101767/protobuf-7.35.1-cp310-abi3-manylinux2014_aarch64.whl", hash = "sha256:11d6b0ec246892d85215b0a13ca6e0233cf5284b68f0ac02646427f4ff88a799", size = 328847, upload-time = "2026-06-11T21:55:34.035Z" }, + { url = "https://files.pythonhosted.org/packages/0f/58/dc12f2cd484951524af6e3382c785869b9b3fb5e52ee95ae23add53ee8f9/protobuf-7.35.1-cp310-abi3-manylinux2014_s390x.whl", hash = "sha256:b73f9489a4b8b1c9cb1f8ed951c736392592edb24b9d6819f36d2e10b171d5b4", size = 344030, upload-time = "2026-06-11T21:55:34.941Z" }, + { url = "https://files.pythonhosted.org/packages/e4/be/5b3cfe508bfab6761414ff944e3366eb13be4fd71efcd69450f89ba39f43/protobuf-7.35.1-cp310-abi3-manylinux2014_x86_64.whl", hash = "sha256:74758715c53d7158fb76caf4f0cfdacc5329a4b1bb994f865d6cf302d413a1c4", size = 327130, upload-time = "2026-06-11T21:55:35.921Z" }, + { url = "https://files.pythonhosted.org/packages/d8/bc/6d6c7ba8709c85f8f2c390b2b118d6fb08a783676a572271851bf45a7d22/protobuf-7.35.1-cp310-abi3-win32.whl", hash = "sha256:353652e4efd0bca5b5fc2656abf8307ef351f0cf938c9eba09f0e09c20a25c30", size = 428945, upload-time = "2026-06-11T21:55:37.034Z" }, + { url = "https://files.pythonhosted.org/packages/0a/19/8d0cb6f20a1ef7b18f1c8986ad5783f22f84cce39c6ce9a6e645ea55192e/protobuf-7.35.1-cp310-abi3-win_amd64.whl", hash = "sha256:230a75ddfc2de4806e56696ce9640c1cdfdb6543b7cfce98d42a4c0a0e7bdb87", size = 439996, upload-time = "2026-06-11T21:55:38.123Z" }, + { url = "https://files.pythonhosted.org/packages/19/c7/5f7c636ec43e0c545e28d1f1db71990108306f7bdcb89f069ba97e428e7f/protobuf-7.35.1-py3-none-any.whl", hash = "sha256:4bc97768d8fe4ad6743c8a19403e314511ed9f6d13205b687e52421c023ac1b9", size = 171659, upload-time = "2026-06-11T21:55:39.155Z" }, +] + +[[package]] +name = "psutil" +version = "7.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/aa/c6/d1ddf4abb55e93cebc4f2ed8b5d6dbad109ecb8d63748dd2b20ab5e57ebe/psutil-7.2.2.tar.gz", hash = "sha256:0746f5f8d406af344fd547f1c8daa5f5c33dbc293bb8d6a16d80b4bb88f59372", size = 493740, upload-time = "2026-01-28T18:14:54.428Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/51/08/510cbdb69c25a96f4ae523f733cdc963ae654904e8db864c07585ef99875/psutil-7.2.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:2edccc433cbfa046b980b0df0171cd25bcaeb3a68fe9022db0979e7aa74a826b", size = 130595, upload-time = "2026-01-28T18:14:57.293Z" }, + { url = "https://files.pythonhosted.org/packages/d6/f5/97baea3fe7a5a9af7436301f85490905379b1c6f2dd51fe3ecf24b4c5fbf/psutil-7.2.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e78c8603dcd9a04c7364f1a3e670cea95d51ee865e4efb3556a3a63adef958ea", size = 131082, upload-time = "2026-01-28T18:14:59.732Z" }, + { url = "https://files.pythonhosted.org/packages/37/d6/246513fbf9fa174af531f28412297dd05241d97a75911ac8febefa1a53c6/psutil-7.2.2-cp313-cp313t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1a571f2330c966c62aeda00dd24620425d4b0cc86881c89861fbc04549e5dc63", size = 181476, upload-time = "2026-01-28T18:15:01.884Z" }, + { url = "https://files.pythonhosted.org/packages/b8/b5/9182c9af3836cca61696dabe4fd1304e17bc56cb62f17439e1154f225dd3/psutil-7.2.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:917e891983ca3c1887b4ef36447b1e0873e70c933afc831c6b6da078ba474312", size = 184062, upload-time = "2026-01-28T18:15:04.436Z" }, + { url = "https://files.pythonhosted.org/packages/16/ba/0756dca669f5a9300d0cbcbfae9a4c30e446dfc7440ffe43ded5724bfd93/psutil-7.2.2-cp313-cp313t-win_amd64.whl", hash = "sha256:ab486563df44c17f5173621c7b198955bd6b613fb87c71c161f827d3fb149a9b", size = 139893, upload-time = "2026-01-28T18:15:06.378Z" }, + { url = "https://files.pythonhosted.org/packages/1c/61/8fa0e26f33623b49949346de05ec1ddaad02ed8ba64af45f40a147dbfa97/psutil-7.2.2-cp313-cp313t-win_arm64.whl", hash = "sha256:ae0aefdd8796a7737eccea863f80f81e468a1e4cf14d926bd9b6f5f2d5f90ca9", size = 135589, upload-time = "2026-01-28T18:15:08.03Z" }, + { url = "https://files.pythonhosted.org/packages/81/69/ef179ab5ca24f32acc1dac0c247fd6a13b501fd5534dbae0e05a1c48b66d/psutil-7.2.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:eed63d3b4d62449571547b60578c5b2c4bcccc5387148db46e0c2313dad0ee00", size = 130664, upload-time = "2026-01-28T18:15:09.469Z" }, + { url = "https://files.pythonhosted.org/packages/7b/64/665248b557a236d3fa9efc378d60d95ef56dd0a490c2cd37dafc7660d4a9/psutil-7.2.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7b6d09433a10592ce39b13d7be5a54fbac1d1228ed29abc880fb23df7cb694c9", size = 131087, upload-time = "2026-01-28T18:15:11.724Z" }, + { url = "https://files.pythonhosted.org/packages/d5/2e/e6782744700d6759ebce3043dcfa661fb61e2fb752b91cdeae9af12c2178/psutil-7.2.2-cp314-cp314t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fa4ecf83bcdf6e6c8f4449aff98eefb5d0604bf88cb883d7da3d8d2d909546a", size = 182383, upload-time = "2026-01-28T18:15:13.445Z" }, + { url = "https://files.pythonhosted.org/packages/57/49/0a41cefd10cb7505cdc04dab3eacf24c0c2cb158a998b8c7b1d27ee2c1f5/psutil-7.2.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e452c464a02e7dc7822a05d25db4cde564444a67e58539a00f929c51eddda0cf", size = 185210, upload-time = "2026-01-28T18:15:16.002Z" }, + { url = "https://files.pythonhosted.org/packages/dd/2c/ff9bfb544f283ba5f83ba725a3c5fec6d6b10b8f27ac1dc641c473dc390d/psutil-7.2.2-cp314-cp314t-win_amd64.whl", hash = "sha256:c7663d4e37f13e884d13994247449e9f8f574bc4655d509c3b95e9ec9e2b9dc1", size = 141228, upload-time = "2026-01-28T18:15:18.385Z" }, + { url = "https://files.pythonhosted.org/packages/f2/fc/f8d9c31db14fcec13748d373e668bc3bed94d9077dbc17fb0eebc073233c/psutil-7.2.2-cp314-cp314t-win_arm64.whl", hash = "sha256:11fe5a4f613759764e79c65cf11ebdf26e33d6dd34336f8a337aa2996d71c841", size = 136284, upload-time = "2026-01-28T18:15:19.912Z" }, + { url = "https://files.pythonhosted.org/packages/e7/36/5ee6e05c9bd427237b11b3937ad82bb8ad2752d72c6969314590dd0c2f6e/psutil-7.2.2-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:ed0cace939114f62738d808fdcecd4c869222507e266e574799e9c0faa17d486", size = 129090, upload-time = "2026-01-28T18:15:22.168Z" }, + { url = "https://files.pythonhosted.org/packages/80/c4/f5af4c1ca8c1eeb2e92ccca14ce8effdeec651d5ab6053c589b074eda6e1/psutil-7.2.2-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:1a7b04c10f32cc88ab39cbf606e117fd74721c831c98a27dc04578deb0c16979", size = 129859, upload-time = "2026-01-28T18:15:23.795Z" }, + { url = "https://files.pythonhosted.org/packages/b5/70/5d8df3b09e25bce090399cf48e452d25c935ab72dad19406c77f4e828045/psutil-7.2.2-cp36-abi3-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:076a2d2f923fd4821644f5ba89f059523da90dc9014e85f8e45a5774ca5bc6f9", size = 155560, upload-time = "2026-01-28T18:15:25.976Z" }, + { url = "https://files.pythonhosted.org/packages/63/65/37648c0c158dc222aba51c089eb3bdfa238e621674dc42d48706e639204f/psutil-7.2.2-cp36-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b0726cecd84f9474419d67252add4ac0cd9811b04d61123054b9fb6f57df6e9e", size = 156997, upload-time = "2026-01-28T18:15:27.794Z" }, + { url = "https://files.pythonhosted.org/packages/8e/13/125093eadae863ce03c6ffdbae9929430d116a246ef69866dad94da3bfbc/psutil-7.2.2-cp36-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:fd04ef36b4a6d599bbdb225dd1d3f51e00105f6d48a28f006da7f9822f2606d8", size = 148972, upload-time = "2026-01-28T18:15:29.342Z" }, + { url = "https://files.pythonhosted.org/packages/04/78/0acd37ca84ce3ddffaa92ef0f571e073faa6d8ff1f0559ab1272188ea2be/psutil-7.2.2-cp36-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b58fabe35e80b264a4e3bb23e6b96f9e45a3df7fb7eed419ac0e5947c61e47cc", size = 148266, upload-time = "2026-01-28T18:15:31.597Z" }, + { url = "https://files.pythonhosted.org/packages/b4/90/e2159492b5426be0c1fef7acba807a03511f97c5f86b3caeda6ad92351a7/psutil-7.2.2-cp37-abi3-win_amd64.whl", hash = "sha256:eb7e81434c8d223ec4a219b5fc1c47d0417b12be7ea866e24fb5ad6e84b3d988", size = 137737, upload-time = "2026-01-28T18:15:33.849Z" }, + { url = "https://files.pythonhosted.org/packages/8c/c7/7bb2e321574b10df20cbde462a94e2b71d05f9bbda251ef27d104668306a/psutil-7.2.2-cp37-abi3-win_arm64.whl", hash = "sha256:8c233660f575a5a89e6d4cb65d9f938126312bca76d8fe087b947b3a1aaac9ee", size = 134617, upload-time = "2026-01-28T18:15:36.514Z" }, +] + +[[package]] +name = "pyasn1" +version = "0.6.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a4/9a/23310166d960def5897e91fe20e5b724601b02a22e84ba1f94232c0b7f67/pyasn1-0.6.4.tar.gz", hash = "sha256:9c447d8431c947fe4c8febc4ed9e760bc29011a5b01e5c74b67025bd9fb8ce81", size = 151262, upload-time = "2026-07-09T01:12:33.988Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9a/3b/6163796d69c3977d1e4287bea4a6979161cbbdd170ebb430511e8e1999ce/pyasn1-0.6.4-py3-none-any.whl", hash = "sha256:deda9277cfd454080ec40b207fb6df82206a3a2688735233cdcd8d3d565f088b", size = 84410, upload-time = "2026-07-09T01:12:32.92Z" }, +] + +[[package]] +name = "pycparser" +version = "3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", size = 103492, upload-time = "2026-01-21T14:26:51.89Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" }, +] + +[[package]] +name = "pyflakes" +version = "3.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/45/dc/fd034dc20b4b264b3d015808458391acbf9df40b1e54750ef175d39180b1/pyflakes-3.4.0.tar.gz", hash = "sha256:b24f96fafb7d2ab0ec5075b7350b3d2d2218eab42003821c06344973d3ea2f58", size = 64669, upload-time = "2025-06-20T18:45:27.834Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c2/2f/81d580a0fb83baeb066698975cb14a618bdbed7720678566f1b046a95fe8/pyflakes-3.4.0-py2.py3-none-any.whl", hash = "sha256:f742a7dbd0d9cb9ea41e9a24a918996e8170c799fa528688d40dd582c8265f4f", size = 63551, upload-time = "2025-06-20T18:45:26.937Z" }, +] + +[[package]] +name = "pygments" +version = "2.20.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" }, +] + +[[package]] +name = "pylsqpack" +version = "0.3.24" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7c/a0/20b34e654b911a9abb736b242cc0a11912bc79ea3e911f139ea756e39ea2/pylsqpack-0.3.24.tar.gz", hash = "sha256:8ec455f44614228f89e38d40c1b1e37895620e20ec6b21e3b562fa8b79a23890", size = 677187, upload-time = "2026-03-29T15:42:40.136Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/88/71b79d334f67dd595fbed5f3a337e2aa997a96e452bb1b64120bccf5679d/pylsqpack-0.3.24-cp310-abi3-macosx_10_9_x86_64.whl", hash = "sha256:8edf48d0a023cd3629b2c4aaccac9b79a46d566c0f61e7416b5678228433763d", size = 162525, upload-time = "2026-03-29T15:42:25.436Z" }, + { url = "https://files.pythonhosted.org/packages/4e/96/f0a7625075394e93db42bd476abb7240ff1a474acd1ad404158baf68dc6a/pylsqpack-0.3.24-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:e7d956dbc8f7d597b237b9157d0a16bc7c655a1b031239763c18dc8582aff8cc", size = 168643, upload-time = "2026-03-29T15:42:26.744Z" }, + { url = "https://files.pythonhosted.org/packages/42/de/49ec59856ea41468ed879ec143fc429729e37e4860b2119959a2a66fb652/pylsqpack-0.3.24-cp310-abi3-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:b6a8bb42127d5ece8d301a673c8205df25b73b69f8c46b9f0c3034588de1789a", size = 246930, upload-time = "2026-03-29T15:42:28.136Z" }, + { url = "https://files.pythonhosted.org/packages/cd/d3/3e748fa5317782bfe68a7eaf890524aee48281c59f07e9bdfd7774f158db/pylsqpack-0.3.24-cp310-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e3f977d419c60c1d6c2240e6d7a52df820d37eb8c36b4057113bcd7859f53e2c", size = 249234, upload-time = "2026-03-29T15:42:29.583Z" }, + { url = "https://files.pythonhosted.org/packages/22/5b/06f5e354ed882ce036ed65f2a393c98d0f6c71a23fa64b53251ddeb40a7b/pylsqpack-0.3.24-cp310-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6024854eb16d32803d4890fb90a73b9348c74b61c0770680aefaaa75f8456e8c", size = 250274, upload-time = "2026-03-29T15:42:31.03Z" }, + { url = "https://files.pythonhosted.org/packages/61/0e/c95cae2817a5c272b7a3132376165aa16875efcccbbd3e6608f5082770cc/pylsqpack-0.3.24-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:54978a9879471596d84bbad5e67d727014048926bc5bb2dac0eb3701b48c5ac9", size = 246966, upload-time = "2026-03-29T15:42:32.035Z" }, + { url = "https://files.pythonhosted.org/packages/40/fe/d5e84c3b4b2fa716df9e95aeb40d3bfb4de50c21cccccd66e194cfc084ac/pylsqpack-0.3.24-cp310-abi3-musllinux_1_2_i686.whl", hash = "sha256:caf63ddc2e581c764d17432893acce02c5c29ff879d77c2abf1e26aa4eeb831b", size = 246546, upload-time = "2026-03-29T15:42:33.105Z" }, + { url = "https://files.pythonhosted.org/packages/65/f5/88e442ced83c0305f50f45bf521bbce3344ef0c29c3442f010086ff0c124/pylsqpack-0.3.24-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:e3dc5f146fd456b50b227858aed59faa0ff8445aa426e69bb4e50d46c487aab0", size = 248517, upload-time = "2026-03-29T15:42:34.237Z" }, + { url = "https://files.pythonhosted.org/packages/a6/c2/886348974bba20db2a80cf37e97203d7334223b3c1c1babe4159dd12626d/pylsqpack-0.3.24-cp310-abi3-win32.whl", hash = "sha256:8da12be7b35b7c9a8cf73a4c077f72e5022a311f80a401c79904213376f2d767", size = 153483, upload-time = "2026-03-29T15:42:35.214Z" }, + { url = "https://files.pythonhosted.org/packages/0d/22/adbce7adfb41b8f5f222195f7f4f5e58655aa3e83f525bc5f3882b07d6e8/pylsqpack-0.3.24-cp310-abi3-win_amd64.whl", hash = "sha256:c3e2327af25ee616ce4483a8748f0957cf017cbca82d58ed15efea68f70f94ff", size = 156145, upload-time = "2026-03-29T15:42:36.902Z" }, + { url = "https://files.pythonhosted.org/packages/a5/2e/6fb6d797ce88741a0e18984bbab69160abc0971a41f4478cab6c8255a8dc/pylsqpack-0.3.24-cp310-abi3-win_arm64.whl", hash = "sha256:23b4d8af48836893beac356c10ca268161953de5bf9ed691526a93f5c82433e9", size = 153424, upload-time = "2026-03-29T15:42:38.73Z" }, +] + +[[package]] +name = "pyopenssl" +version = "26.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3f/e8/7325d258199b159eb2c03fe32107533e2832e70e63f4fb88a6aa00023201/pyopenssl-26.4.0.tar.gz", hash = "sha256:28dfcce0162b9211413e26dfbfdf1d24317fbeba18fc93c12400a1856b2a0bc7", size = 182046, upload-time = "2026-08-01T19:50:50.512Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/51/ad/2cf6d3fa2fae5c79e1ed9960c0d42badd0f94d81dd12b50604cdc839e648/pyopenssl-26.4.0-py3-none-any.whl", hash = "sha256:f0eb0cb2d581d3ad2b9c489468485e7f2ab6727d08401bcf9d824c3caddf3c1c", size = 56026, upload-time = "2026-08-01T19:50:48.94Z" }, +] + +[[package]] +name = "pytest" +version = "9.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e4/47/b9efed96c114afcfa3c9d3fe98a76a1d14c74a9e266d397cf6eb64be5e01/pytest-9.1.1.tar.gz", hash = "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313", size = 1636369, upload-time = "2026-06-19T10:58:32.857Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c", size = 386536, upload-time = "2026-06-19T10:58:31.347Z" }, +] + +[[package]] +name = "pytest-xdist" +version = "3.8.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "execnet" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/78/b4/439b179d1ff526791eb921115fca8e44e596a13efeda518b9d845a619450/pytest_xdist-3.8.0.tar.gz", hash = "sha256:7e578125ec9bc6050861aa93f2d59f1d8d085595d6551c2c90b6f4fad8d3a9f1", size = 88069, upload-time = "2025-07-01T13:30:59.346Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ca/31/d4e37e9e550c2b92a9cbc2e4d0b7420a27224968580b5a447f420847c975/pytest_xdist-3.8.0-py3-none-any.whl", hash = "sha256:202ca578cfeb7370784a8c33d6d05bc6e13b4f25b5053c30a152269fd10f0b88", size = 46396, upload-time = "2025-07-01T13:30:56.632Z" }, +] + +[[package]] +name = "python-jose" +version = "3.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "ecdsa" }, + { name = "pyasn1" }, + { name = "rsa" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c6/77/3a1c9039db7124eb039772b935f2244fbb73fc8ee65b9acf2375da1c07bf/python_jose-3.5.0.tar.gz", hash = "sha256:fb4eaa44dbeb1c26dcc69e4bd7ec54a1cb8dd64d3b4d81ef08d90ff453f2b01b", size = 92726, upload-time = "2025-05-28T17:31:54.288Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d9/c3/0bd11992072e6a1c513b16500a5d07f91a24017c5909b02c72c62d7ad024/python_jose-3.5.0-py2.py3-none-any.whl", hash = "sha256:abd1202f23d34dfad2c3d28cb8617b90acf34132c7afd60abd0b0b7d3cb55771", size = 34624, upload-time = "2025-05-28T17:31:52.802Z" }, +] + +[[package]] +name = "pyyaml" +version = "6.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", size = 185826, upload-time = "2025-09-25T21:31:58.655Z" }, + { url = "https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", size = 175577, upload-time = "2025-09-25T21:32:00.088Z" }, + { url = "https://files.pythonhosted.org/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", size = 775556, upload-time = "2025-09-25T21:32:01.31Z" }, + { url = "https://files.pythonhosted.org/packages/10/cb/16c3f2cf3266edd25aaa00d6c4350381c8b012ed6f5276675b9eba8d9ff4/pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00", size = 882114, upload-time = "2025-09-25T21:32:03.376Z" }, + { url = "https://files.pythonhosted.org/packages/71/60/917329f640924b18ff085ab889a11c763e0b573da888e8404ff486657602/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d", size = 806638, upload-time = "2025-09-25T21:32:04.553Z" }, + { url = "https://files.pythonhosted.org/packages/dd/6f/529b0f316a9fd167281a6c3826b5583e6192dba792dd55e3203d3f8e655a/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a", size = 767463, upload-time = "2025-09-25T21:32:06.152Z" }, + { url = "https://files.pythonhosted.org/packages/f2/6a/b627b4e0c1dd03718543519ffb2f1deea4a1e6d42fbab8021936a4d22589/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4", size = 794986, upload-time = "2025-09-25T21:32:07.367Z" }, + { url = "https://files.pythonhosted.org/packages/45/91/47a6e1c42d9ee337c4839208f30d9f09caa9f720ec7582917b264defc875/pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b", size = 142543, upload-time = "2025-09-25T21:32:08.95Z" }, + { url = "https://files.pythonhosted.org/packages/da/e3/ea007450a105ae919a72393cb06f122f288ef60bba2dc64b26e2646fa315/pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf", size = 158763, upload-time = "2025-09-25T21:32:09.96Z" }, + { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" }, + { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" }, + { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" }, + { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" }, + { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" }, + { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" }, + { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" }, + { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" }, + { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" }, + { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" }, + { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" }, + { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" }, + { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" }, + { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" }, + { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" }, + { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" }, + { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" }, + { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" }, + { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" }, + { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" }, + { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" }, + { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" }, + { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" }, + { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" }, + { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" }, + { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" }, + { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" }, + { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" }, + { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" }, + { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" }, + { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" }, + { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" }, + { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" }, + { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, +] + +[[package]] +name = "referencing" +version = "0.37.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "rpds-py" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/22/f5/df4e9027acead3ecc63e50fe1e36aca1523e1719559c499951bb4b53188f/referencing-0.37.0.tar.gz", hash = "sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8", size = 78036, upload-time = "2025-10-13T15:30:48.871Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl", hash = "sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231", size = 26766, upload-time = "2025-10-13T15:30:47.625Z" }, +] + +[[package]] +name = "requests" +version = "2.34.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ac/c3/e2a2b89f2d3e2179abd6d00ebd70bff6273f37fb3e0cc209f48b39d00cbf/requests-2.34.2.tar.gz", hash = "sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed", size = 142856, upload-time = "2026-05-14T19:25:27.735Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl", hash = "sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0", size = 73075, upload-time = "2026-05-14T19:25:26.443Z" }, +] + +[[package]] +name = "rpds-py" +version = "2026.6.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/aa/2a/9618a122aeb2a169a28b03889a2995fe297588964333d4a7d67bdf46e147/rpds_py-2026.6.3.tar.gz", hash = "sha256:1cebd1337c242e4ec2293e541f712b2da849b29f48f0c293684b71c0632625d4", size = 64051, upload-time = "2026-06-30T07:17:53.009Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/1f/a2dca5ffdbf1d475ffc4e80e4d5d720ff3a00f691795910116960ee12511/rpds_py-2026.6.3-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:7b689145a1485c335569bd056464f3243a29af7ed3871c7be31ad624ba239bc7", size = 342174, upload-time = "2026-06-30T07:14:54.821Z" }, + { url = "https://files.pythonhosted.org/packages/4d/dc/323d08583c0832911768663d1944f0107fcd4088704858d84b5e06d105a0/rpds_py-2026.6.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:db08f45aecde626498fb3df07bcf6d2ec040af42e859a4f5040d79c200342911", size = 345513, upload-time = "2026-06-30T07:14:56.515Z" }, + { url = "https://files.pythonhosted.org/packages/0b/2a/e31989834d18d2f26ec1d2774c5b1eb3331df4ea8ada525175294c94b48a/rpds_py-2026.6.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:acc992ab27b15f852c76755eb2ab7dce86585ddadba6fa5946e58556088845b4", size = 373783, upload-time = "2026-06-30T07:14:57.736Z" }, + { url = "https://files.pythonhosted.org/packages/87/fe/e80107ee3639585c9941c17d6a42cd65325022f656c023191fce78c324c8/rpds_py-2026.6.3-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7f88d653e7b3b779d71ae7454e20dcc9b6bae903f33c269db9f2be41bda3f261", size = 378316, upload-time = "2026-06-30T07:14:59.077Z" }, + { url = "https://files.pythonhosted.org/packages/22/6f/81e3adf81acfb6fa694de2a6e4e7d8863121e3e0799e0a7725e6cf5679c4/rpds_py-2026.6.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e52655eaf81e32593abedaa4bfe33170c8cfedf3365ed9be6e11e07f148f0278", size = 499423, upload-time = "2026-06-30T07:15:00.488Z" }, + { url = "https://files.pythonhosted.org/packages/2d/9a/41263969df0ce3d9af2a96d5005a288200af1989aed3354bfceb5fc0b21f/rpds_py-2026.6.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dfcc8b909769d19db55c7cc9541eb64b9b774b1057ffffb4f1048070475bb9f9", size = 386077, upload-time = "2026-06-30T07:15:01.911Z" }, + { url = "https://files.pythonhosted.org/packages/5e/19/7e98f468bd50346faff5b10e5297374b443bfdddacc8e9fbc65984539597/rpds_py-2026.6.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c1255b302953c86a486b81d330d5ee1d5bd937691ce271b6be0ef0e299eaab7", size = 371315, upload-time = "2026-06-30T07:15:03.317Z" }, + { url = "https://files.pythonhosted.org/packages/99/3c/2b973b4d371906a134b03decfea7f5d9835a2c6d263454392e15b64b5b18/rpds_py-2026.6.3-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:8d2294a31386bfa251d8c8a39472beee17db67d4f1a6eabea665d35c9a4461c3", size = 383502, upload-time = "2026-06-30T07:15:04.627Z" }, + { url = "https://files.pythonhosted.org/packages/98/2a/12e2799500af0a307bca76b63361c51f9fe479223561489c29eea1f2ee41/rpds_py-2026.6.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f8f23ead891a3b762f35ab3b04623da7056545b48aa60d59957e6789914545da", size = 402673, upload-time = "2026-06-30T07:15:05.856Z" }, + { url = "https://files.pythonhosted.org/packages/2d/e3/21e5872d165fe08be4f229e3d5ee9d90019c0bf0e5538de60dbd54009450/rpds_py-2026.6.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:421aba32367055614287a4292b6a17f1939c9452299f7a0209c117e990b646d4", size = 549964, upload-time = "2026-06-30T07:15:07.159Z" }, + { url = "https://files.pythonhosted.org/packages/1a/d0/5ee0fe36844297de8123bee27bc12078c1a7416ad9f1b8a8ca18d6b0c0ac/rpds_py-2026.6.3-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:1e5822dfc2f0d4ab7e745eaa6d85945069329beeccef965af3f3bb26058fcab6", size = 615446, upload-time = "2026-06-30T07:15:08.531Z" }, + { url = "https://files.pythonhosted.org/packages/b1/80/1ea5873cb683f2fbe5f21b23ea1f6d179ead19f3c5b249b7eb5dca568ef2/rpds_py-2026.6.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:83e35b57523816c8613fd0776b40cd8bb9f596b37ddd2692eb4a6bb5ab2f8c93", size = 576975, upload-time = "2026-06-30T07:15:09.97Z" }, + { url = "https://files.pythonhosted.org/packages/c9/e1/90ef639217a5ddb15b7f4f61b1c33911fd044ad03c311bafdd2bcab85582/rpds_py-2026.6.3-cp311-cp311-win32.whl", hash = "sha256:de3eceba0b683bcbb1ab93da016d0270df1f9ae7be716b40214c5dafac6ea45a", size = 204453, upload-time = "2026-06-30T07:15:11.324Z" }, + { url = "https://files.pythonhosted.org/packages/f2/b7/b7a1695d7af36f521fb11e80d6d3adbd744f73b921859bd3c2a2c0dc706f/rpds_py-2026.6.3-cp311-cp311-win_amd64.whl", hash = "sha256:2c54a076ca4d370980ab57bc0e31df57bbe8d41340436a90ef8b1219a3cbb127", size = 223219, upload-time = "2026-06-30T07:15:12.476Z" }, + { url = "https://files.pythonhosted.org/packages/d7/a2/145afacf796e4506062825941176ad9445c2dcf2b3b6a1f13d3030a15e19/rpds_py-2026.6.3-cp311-cp311-win_arm64.whl", hash = "sha256:168c733a7112e071bb7a66460e667edfcff06c017a3c523f7a8a8e08d0140804", size = 219137, upload-time = "2026-06-30T07:15:13.631Z" }, + { url = "https://files.pythonhosted.org/packages/5c/be/2e8974163072e7bab7df1a5acd54c4498e75e35d6d18b864d3a9d5dadc92/rpds_py-2026.6.3-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a0811d33247c3d6128a3001d763f2aa056bb3425204335400ac54f89eec3a0d0", size = 343691, upload-time = "2026-06-30T07:15:14.96Z" }, + { url = "https://files.pythonhosted.org/packages/a4/73/319dfa745dd668efe89309141ded489126461fcecd2b8f3a3cda185129b6/rpds_py-2026.6.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:538949e262e46caa31ac01bdb3c1e8f642622922cacbabbae6a8445d9dc33eaf", size = 338542, upload-time = "2026-06-30T07:15:16.267Z" }, + { url = "https://files.pythonhosted.org/packages/21/63/4239893be1c4d09b709b1a8f6be4188f0870084ff547f46606b8a75f1b03/rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:55927d532399c2c646100ff7feb48eaa940ad70f42cd68e1328f3ded9f81ca24", size = 368180, upload-time = "2026-06-30T07:15:17.62Z" }, + { url = "https://files.pythonhosted.org/packages/1c/ca/9c5de382225234ceb37b1844ebdb140db12b2a278bb9efe2fcd19f6c82ce/rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f56f1695bc5c0871cbc33dc0130fcf503aab0c57dcc5a6700a4f49eba4f2652e", size = 375067, upload-time = "2026-06-30T07:15:18.952Z" }, + { url = "https://files.pythonhosted.org/packages/87/dc/863f69d1bf04ade34b7fe0d59b9fdf6f0135fe2d7cbca74f1d665589559d/rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:270b293dae9058fc9fcedab50f13cebf46fb8ed1d1d54e0521a9da5d6b211975", size = 490509, upload-time = "2026-06-30T07:15:20.434Z" }, + { url = "https://files.pythonhosted.org/packages/ce/ef/eac16a12048b45ec7c7fa94f2be3438a5f26bf9cc8580b18a1cfd609b7f6/rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:127565fead0a10943b282957bd5447804ff3160ad79f2ad2635e6d249e380680", size = 382754, upload-time = "2026-06-30T07:15:21.831Z" }, + { url = "https://files.pythonhosted.org/packages/04/8f/d2f3f532616be4d06c316ef119683e832bd3d41e112bf3a88f4151c95b17/rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ecabd69db66de867690f9797f2f8fa27ba501bbc24540cbdbdc649cd15888ba6", size = 366189, upload-time = "2026-06-30T07:15:23.371Z" }, + { url = "https://files.pythonhosted.org/packages/e3/29/41a7b0e98a4b44cd676ab7598419623373eb43b20be68c084935c1a8cf88/rpds_py-2026.6.3-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:58eadac9cd119677b60e1cf8ac4052f35949d71b8a9e5556efccbe82533cf22a", size = 377750, upload-time = "2026-06-30T07:15:24.659Z" }, + { url = "https://files.pythonhosted.org/packages/2e/05/ecda0bec46f9a1565090bcdc941d023f6a25aff85fda28f89f8d19878152/rpds_py-2026.6.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7491ee23305ac3eb59e492b6945881f5cd77a6f731061a3f25b77fd40f9e99a4", size = 395576, upload-time = "2026-06-30T07:15:25.987Z" }, + { url = "https://files.pythonhosted.org/packages/68/a8/6ed52f03ee6cb854ce78785cc9a9a672eb880e83fd7224d471f667d151f1/rpds_py-2026.6.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2c99f7e8ccb3dd6e3e4bfeac657a7b208c9bac8075f4b078c02d7404c34107fa", size = 543807, upload-time = "2026-06-30T07:15:27.356Z" }, + { url = "https://files.pythonhosted.org/packages/8f/d6/156c0d3eea27ba09b92562ba2364ba124c0a061b199e17eac637cd25a5e2/rpds_py-2026.6.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:62698275682bf121181861295c9181e789030a2d516071f5b8f3c23c170cd0fc", size = 611187, upload-time = "2026-06-30T07:15:28.931Z" }, + { url = "https://files.pythonhosted.org/packages/f1/31/774212ed989c62f7f310220089f9b0a3fb8f40f5443d1727abd5d9f52bc9/rpds_py-2026.6.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a214c993455f99a89aaeadc9b21241900037adc9d97203e374d75513c5911822", size = 573030, upload-time = "2026-06-30T07:15:30.553Z" }, + { url = "https://files.pythonhosted.org/packages/c9/50/22f73127a41f1ce4f87fe39aadfb9a126345801c274aa93ae88456249327/rpds_py-2026.6.3-cp312-cp312-win32.whl", hash = "sha256:501f9f04a588d6a09179368c57071301445191767c64e4b52a6aa9871f1ef5ed", size = 202185, upload-time = "2026-06-30T07:15:32.027Z" }, + { url = "https://files.pythonhosted.org/packages/04/3a/f0ee4d4dde9d3b69dedf1b5f74e7a40017046d55052d173e418c6a94f960/rpds_py-2026.6.3-cp312-cp312-win_amd64.whl", hash = "sha256:2c958bf94822e9290a40aaf2a822d4bc5c88099093e3948ad6c571eca9272e5f", size = 220394, upload-time = "2026-06-30T07:15:33.359Z" }, + { url = "https://files.pythonhosted.org/packages/f3/83/3382fe37f809b59f02aac04dbc4e765b480b46ee0227ed516e3bdc4d3dfc/rpds_py-2026.6.3-cp312-cp312-win_arm64.whl", hash = "sha256:22bffe6042b9bcb0822bcd1955ec00e245daf17b4344e4ed8e9551b976b63e96", size = 215753, upload-time = "2026-06-30T07:15:34.778Z" }, + { url = "https://files.pythonhosted.org/packages/a4/9e/b818ee580026ec578138e961027a68820c40afeb1ec8f6819b54fb99e196/rpds_py-2026.6.3-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:3cfe765c1da0072636ca06628261e0ea05688e160d5c8a03e0217c3854037223", size = 343012, upload-time = "2026-06-30T07:15:36.005Z" }, + { url = "https://files.pythonhosted.org/packages/f3/6b/686d9dc4359a8f163cfbbf89ee0b4e586431de22fe8248edb63a8cf50d49/rpds_py-2026.6.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f4d78253f6996be4901669ad25319f842f740eccf4d58e3c7f3dd39e6dde1d8f", size = 338203, upload-time = "2026-06-30T07:15:37.462Z" }, + { url = "https://files.pythonhosted.org/packages/9e/9b/069aa329940f8207615e091f5eedbbd40e1e15eac68a0790fd05ccdf796c/rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:54f45a148e28767bf343d33a684693c70e451c6f4c0e9904709a723fafbdfc1f", size = 367984, upload-time = "2026-06-30T07:15:39.008Z" }, + { url = "https://files.pythonhosted.org/packages/14/db/34c203e4becff3703e4d3bc121842c00b8689197f398161203a880052f4e/rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:842e7b070435622248c7a2c44ae53fa1440e073cc3023bc919fed570884097a7", size = 374815, upload-time = "2026-06-30T07:15:40.253Z" }, + { url = "https://files.pythonhosted.org/packages/ee/7d/8071067d2cc453d916ad836e828c943f575e8a44612537759002a1e07381/rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8020133a74bd81b4572dd8e4be028a6b1ebcd70e6726edc3918008c08bee6ee6", size = 490545, upload-time = "2026-06-30T07:15:41.729Z" }, + { url = "https://files.pythonhosted.org/packages/a3/42/da06c5aa8f0484ff07f270787434204d9f4535e2f8c3b51ed402267e63c3/rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cdc7e35386f3847df728fbcb5e887e2d79c19e2fa1eba9e51b6621d23e3243af", size = 382828, upload-time = "2026-06-30T07:15:43.327Z" }, + { url = "https://files.pythonhosted.org/packages/57/d7/fe978efc2ae50abe48eb7464668ea99f53c010c60aeebb7b35ad27f23661/rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:acac386b453c2516111b50985d60ce46e7fadb5ea71ae7b25f4c946935bf27cf", size = 365678, upload-time = "2026-06-30T07:15:44.992Z" }, + { url = "https://files.pythonhosted.org/packages/69/9d/1d8922e1990b2a6eb532b6ff53d3e73d2b3bbffc84116c75826bee73dfc6/rpds_py-2026.6.3-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:425560c6fa0415f27261727bb20bd097568485e5eb0c121f1949417d1c516885", size = 377811, upload-time = "2026-06-30T07:15:46.523Z" }, + { url = "https://files.pythonhosted.org/packages/b1/3d/198dceafb4fb034a6a47347e1b0735d34e0bd4a50be4e898d408ee66cb14/rpds_py-2026.6.3-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a550fb4950a06dde3beb4721f5ad4b25bf4513784665b0a8522c792e2bd822a4", size = 395382, upload-time = "2026-06-30T07:15:47.955Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f1/13968e49655d40b6b19d8b9140296bbc6f1d86b3f0f6c346cf9f1adddf4b/rpds_py-2026.6.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4f4bca01b63096f606e095734dd56e74e175f94cfbf24ff3d63281cec61f7bb7", size = 543832, upload-time = "2026-06-30T07:15:49.33Z" }, + { url = "https://files.pythonhosted.org/packages/ac/ab/289bcb1b90bd3e40a2900c561fa0e2087345ecbb094f0b870f2345142b7c/rpds_py-2026.6.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ccffae9a092a00deb7efd545fe5e2c33c33b88e7c054337e9a74c179347d0b7d", size = 611011, upload-time = "2026-06-30T07:15:50.847Z" }, + { url = "https://files.pythonhosted.org/packages/1e/16/5043105e679436ccfbc8e5e0dd2d663ed18a8b8113515fd06a5e5d77c83e/rpds_py-2026.6.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1cf01971c4f2c5553b772a542e4aaf191789cd331bc2cd4ff0e6e65ba49e1e97", size = 572431, upload-time = "2026-06-30T07:15:52.394Z" }, + { url = "https://files.pythonhosted.org/packages/85/ed/adab103321c0a6565d5ae1c2998349bc3ee175b82ccc5ae8fc04cc413075/rpds_py-2026.6.3-cp313-cp313-win32.whl", hash = "sha256:8c3d1e9c15b9d51ca0391e13da1a25a0a4df3c58a37c9dc368e0736cf7f69df0", size = 201710, upload-time = "2026-06-30T07:15:53.894Z" }, + { url = "https://files.pythonhosted.org/packages/7b/ed/a03b09668e74e5dabbf2e211f6468e1820c0552f7b0500082da31841bf7b/rpds_py-2026.6.3-cp313-cp313-win_amd64.whl", hash = "sha256:9250a9a0a6fd4648b3f868da8d91a4c52b5811a62df58e753d50ae4454a36f80", size = 219454, upload-time = "2026-06-30T07:15:55.25Z" }, + { url = "https://files.pythonhosted.org/packages/27/17/b8642c12930b71bc2b25831f6708ccf0f75abcd11883932ec9ce54ba3a78/rpds_py-2026.6.3-cp313-cp313-win_arm64.whl", hash = "sha256:900a67df3fd1660b035a4761c4ce73c382ea6b35f90f9863c36c6fd8bf8b09bb", size = 215063, upload-time = "2026-06-30T07:15:56.573Z" }, + { url = "https://files.pythonhosted.org/packages/b6/36/7fbe9dcdaf857fb3f63c2a2284b62492d95f5e8334e947e5fb6e7f68c9be/rpds_py-2026.6.3-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:931908d9fc855d8f74783377822be318edb6dcb19e47169dc038f9a1bf60b06e", size = 344510, upload-time = "2026-06-30T07:15:57.921Z" }, + { url = "https://files.pythonhosted.org/packages/ba/54/f785cc3d3f60839ca57a5af4927a9f347b07b2799c373fc20f7949f87c7e/rpds_py-2026.6.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:d7469697dce35be237db177d42e2a2ee26e6dcc5fc052078a6fefabd288c6edd", size = 339495, upload-time = "2026-06-30T07:15:59.238Z" }, + { url = "https://files.pythonhosted.org/packages/63/ef/d4cdaf309e6b095b43597103cf8c0b951d6cca2acce68c474f75ec12e0c7/rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bcfbcf66006befb9fd2aeaa9e01feaf881b4dc330a02ba07d2322b1c11be7b5d", size = 369454, upload-time = "2026-06-30T07:16:01.021Z" }, + { url = "https://files.pythonhosted.org/packages/96/4a/9559a68b7ee15db09d7981212e8c2e219d2a1d6d4faa0391d813c3496a36/rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:847927daf4cffbd4e90e42bc890069897101edd015f956cb8721b3473372edda", size = 374583, upload-time = "2026-06-30T07:16:02.287Z" }, + { url = "https://files.pythonhosted.org/packages/ef/75/8964aa7d2c6e8ac43eba8eb6e6b0fdda1f46d39f2fc3e6aa9f2cb17f485d/rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aca6c1ef08a82bfe327cc156da694660f599923e2e6665b6d81c9c2d0ac9ffc8", size = 492919, upload-time = "2026-06-30T07:16:03.723Z" }, + { url = "https://files.pythonhosted.org/packages/8f/97/6908094ac804115e65aedfd90f1b5fee4eebebd3f6c4cfc5419939267565/rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ae50181a047c871561212bb97f7932a2d45fb53e947bd9b57ebad85b529cbc53", size = 383725, upload-time = "2026-06-30T07:16:05.305Z" }, + { url = "https://files.pythonhosted.org/packages/d1/9c/0d1fdc2e7aba23e290d603bc494e97bd205bae262ce33c6b32a69768ed5e/rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dc319e5a1de4b6913aac94bf6a2f9e847371e0a140a43dd4991db1a09bc2d504", size = 367255, upload-time = "2026-06-30T07:16:07.086Z" }, + { url = "https://files.pythonhosted.org/packages/c4/fe/f0209ca4a9ed074bc8acb44dfd0e81c3122e94c9689f5645b7973a866719/rpds_py-2026.6.3-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:e4316bf32babbed84e691e352faf967ce2f0f024174a8643c37c94a1080374fc", size = 379060, upload-time = "2026-06-30T07:16:08.525Z" }, + { url = "https://files.pythonhosted.org/packages/c6/8d/f1cc54c616b9d8897de8738aac148d20afca93f68187475fe194d09a71b9/rpds_py-2026.6.3-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8c6e5a2f750cc71c3e3b11d71661f21d6f9bc6cebc6564b1466417a1ec03ec77", size = 395960, upload-time = "2026-06-30T07:16:09.989Z" }, + { url = "https://files.pythonhosted.org/packages/fb/04/aafff00f73aeca2945f734f1d483c64ab8f472d0864ab02377fd8e89c3b2/rpds_py-2026.6.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4470ce197d4090875cf6affbf1f853338387428df97c4fb7b7106317b8214698", size = 545356, upload-time = "2026-06-30T07:16:11.816Z" }, + { url = "https://files.pythonhosted.org/packages/fd/cc/e229663b9e4ddac5a4acbe9085dd80a71af2a5d356b8b39d6bff233f24b0/rpds_py-2026.6.3-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ea964164cc9afa72d4d9b23cc28dafae93693c0a53e0b42acbff15b22c3f9ddd", size = 612319, upload-time = "2026-06-30T07:16:13.586Z" }, + { url = "https://files.pythonhosted.org/packages/e3/7a/8a0e6d3e6cd066af108b71b43122c3fe158dd9eb86acac626593a2582eb1/rpds_py-2026.6.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:639c8929aa0afe81be836b04de888460d6bed38b9c54cfc18da8f6bfabf5af5d", size = 573508, upload-time = "2026-06-30T07:16:15.23Z" }, + { url = "https://files.pythonhosted.org/packages/87/03/2a69ab618a789cf6cf85c86bb844c62d090e700ab1a2aa676b3741b6c516/rpds_py-2026.6.3-cp314-cp314-win32.whl", hash = "sha256:882076c00c0a608b131187055ddc5ae29f2e7eaf870d6168980420d58528a5c8", size = 202504, upload-time = "2026-06-30T07:16:16.893Z" }, + { url = "https://files.pythonhosted.org/packages/85/62/a3892ba945f4e24c78f352e5de3c7620d8479f73f211406a97263d13c7d2/rpds_py-2026.6.3-cp314-cp314-win_amd64.whl", hash = "sha256:0be972be84cfcaf46c8c6edf690ca0f154ac17babf1f6a955a51579b34ad2dc5", size = 220380, upload-time = "2026-06-30T07:16:18.108Z" }, + { url = "https://files.pythonhosted.org/packages/3d/e7/c2bd44dc831931815ad11ebb5f430b5a0a4d3caa9de837107876c30c3432/rpds_py-2026.6.3-cp314-cp314-win_arm64.whl", hash = "sha256:2a9c6f195058cb45335e8cc3802745c603d716eb96bc9625950c1aac71c0c703", size = 215976, upload-time = "2026-06-30T07:16:19.654Z" }, + { url = "https://files.pythonhosted.org/packages/79/9c/fff7b74bce9a091ec9a012a03f9ff5f69364eaf9451060dfc4486da2ffdd/rpds_py-2026.6.3-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:f90938e92afda60266da758ee7d363447f7f0138c9559f9e1811629580582d90", size = 346840, upload-time = "2026-06-30T07:16:21.268Z" }, + { url = "https://files.pythonhosted.org/packages/e9/44/77bcb1168b33704908295533d27f10eb811e9e3e193e8993dc99572211d3/rpds_py-2026.6.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ec829541c45bca16e61c7ae50c20501f213605beb75d1aba91a6ee37fbbb56a4", size = 340282, upload-time = "2026-06-30T07:16:22.875Z" }, + { url = "https://files.pythonhosted.org/packages/87/3c/7a9081c7c9e645b39efe19e4ffbeccd80add246327cd9b888aecffd72317/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:afd70d95892096cdb26f15a00c45907b17817577aa8d1c76b2dcc2788391f9e9", size = 370403, upload-time = "2026-06-30T07:16:24.415Z" }, + { url = "https://files.pythonhosted.org/packages/f7/69/af47021eb7dad6ff3396cb001c08f0f3c4d06c20253f75be6421a59fe6b7/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:29dfa0533a5d4c94d4dfa1b694fcb56c9c63aad8330ffdd816fd225d0a7a162f", size = 376055, upload-time = "2026-06-30T07:16:26.111Z" }, + { url = "https://files.pythonhosted.org/packages/81/fc/a3bcf517084396a6dd258c592567a3c011ba4557f2fde23dceaf26e74f2e/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:af05d726809bff6b141be124d4c7ce998f9c9c7f30edb1f46c07aa103d540b41", size = 494419, upload-time = "2026-06-30T07:16:27.596Z" }, + { url = "https://files.pythonhosted.org/packages/c9/eb/13d529d1788135425c7bf207f8463458ca5d92e43f3f701365b83e9dffc1/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9826217f048f620d9a712672818bf231442c1b35d96b227a07eabd11b4bb6945", size = 384848, upload-time = "2026-06-30T07:16:29.183Z" }, + { url = "https://files.pythonhosted.org/packages/8e/f4/b7ac49f30013aba8f7b9566b1dd07e81de95e708c1374b7bacc5b9bc5c9c/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:536bceea4fa4acf7e1c61da2b5786304367c816c8895be71b8f537c480b0ea1f", size = 371369, upload-time = "2026-06-30T07:16:30.912Z" }, + { url = "https://files.pythonhosted.org/packages/31/86/6260bafa622f788b07ddec0e52d810305c8b9b0b8c27f58a2ab04bf62b4f/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:bc0011654b91cc4fb2ae701bec0a0ba1e552c0714247fa7af6c59e0ccfa3a4e1", size = 379673, upload-time = "2026-06-30T07:16:32.486Z" }, + { url = "https://files.pythonhosted.org/packages/19/c3/03f1ee79a047b48daeca157c89a18509cde22b6b951d642b9b0af1be660a/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:539d75de9e0d536c84ff18dfeb805398e58227001ce09231a26a08b9aed1ee0e", size = 397500, upload-time = "2026-06-30T07:16:34.471Z" }, + { url = "https://files.pythonhosted.org/packages/f0/95/8ed0cd8c377dca12aea498f119fe639fc474d1461545c39d2b5872eb1c0f/rpds_py-2026.6.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:166cf54d9f44fc6ceb53c7860258dde44a81406646de79f8ed3234fca3b6e538", size = 545978, upload-time = "2026-06-30T07:16:36.45Z" }, + { url = "https://files.pythonhosted.org/packages/d3/f2/0eb57f0eaa83f8fc152a7e03de968ab77e1f00732bebc892b190c6eebde7/rpds_py-2026.6.3-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:d34c20167764fbcf927194d532dd7e0c56772f0a5f943fa5ef9e9afbba8fb9db", size = 613350, upload-time = "2026-06-30T07:16:38.213Z" }, + { url = "https://files.pythonhosted.org/packages/5b/de/e0674bdbc3ef7634989b3f854c3f34bc1f587d36e5bfdc5c378d57034619/rpds_py-2026.6.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ea7bb13b7c9a29791f87a0387ba7d3ad3a6d783d827e4d3f27b40a0ff44495e2", size = 576486, upload-time = "2026-06-30T07:16:39.797Z" }, + { url = "https://files.pythonhosted.org/packages/f2/f6/21101359743cd136ada781e8210a85769578422ba460672eea0e29739200/rpds_py-2026.6.3-cp314-cp314t-win32.whl", hash = "sha256:6de4744d05bd1aa1be4ed7ea1189e3979196808008113bbbf899a460966b925e", size = 201068, upload-time = "2026-06-30T07:16:41.316Z" }, + { url = "https://files.pythonhosted.org/packages/a6/b2/9574d4d44f7760c2aa32d92a0a4f41698e33f5b204a0bf5c9758f52c79d5/rpds_py-2026.6.3-cp314-cp314t-win_amd64.whl", hash = "sha256:c7b9a2f8f4d8e90af72571d3d495deebdd7e3c75451f5b41719aee166e940fc2", size = 220600, upload-time = "2026-06-30T07:16:43.091Z" }, + { url = "https://files.pythonhosted.org/packages/08/ae/f23a2697e6ee6340a578b0f136be6483657bef0c6f9497b752bb5c0964bb/rpds_py-2026.6.3-cp315-cp315-macosx_10_12_x86_64.whl", hash = "sha256:e059c5dde6452b44424bd1834557556c226b57781dee1227af23518459722b13", size = 344726, upload-time = "2026-06-30T07:16:44.5Z" }, + { url = "https://files.pythonhosted.org/packages/c3/63/e7b3a1a5358dd32c930a1062d8e15b67fd6e8922e81df9e91706d66ee5c8/rpds_py-2026.6.3-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:2f7c26fbc5acd2522b95d4177fe4710ffd8e9b20529e703ffbf8db4d93903f05", size = 339587, upload-time = "2026-06-30T07:16:46.255Z" }, + { url = "https://files.pythonhosted.org/packages/ec/64/10a85681916ca55fffb91b0a211f84e34297c109243484dd6394660a8a7c/rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a3086b538543802f84c843911242db20447de00d8752dd0efc936dbcf02218ba", size = 369585, upload-time = "2026-06-30T07:16:48.101Z" }, + { url = "https://files.pythonhosted.org/packages/76/c2/baf95c7c38823e12ba34407c5f5767a89e5cf2233895e56f608167ae9493/rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8f2e5c5ee828d42cb11760761c0af6507927bec42d0ad5458f97c9203b054617", size = 375479, upload-time = "2026-06-30T07:16:49.93Z" }, + { url = "https://files.pythonhosted.org/packages/6a/94/0aad06c72d65101e11d33528d438cda99a39ce0da99466e156158f2541d3/rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ed0c1e5d10cdc7135537988c74a0188da68e2f3c30813ba3744ab1e42e0480f9", size = 492418, upload-time = "2026-06-30T07:16:51.641Z" }, + { url = "https://files.pythonhosted.org/packages/b5/17/de3f5a479a1f056535d7489819639d8cd591ea6281d700390b43b1abd745/rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8c2642a7603ec0b16ed77da4555db3b4b472341904873788327c0b0d7b95f1bb", size = 384123, upload-time = "2026-06-30T07:16:53.622Z" }, + { url = "https://files.pythonhosted.org/packages/46/7d/bf09bd1b145bb2671c03e1e6d1ab8651858d90d8c7dfeadd85a37a934fd8/rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8e4320744c1ffdd95a603def63344bfab2d33edeab301c5007e7de9f9f5b3885", size = 367351, upload-time = "2026-06-30T07:16:55.241Z" }, + { url = "https://files.pythonhosted.org/packages/a3/ea/1bb734f314b8be319149ddee80b18bd41372bdcfbdf88d28131c0cd37719/rpds_py-2026.6.3-cp315-cp315-manylinux_2_31_riscv64.whl", hash = "sha256:a9f4645593036b81bbdb36b9c8e0ea0d1c3fee968c4d59db0344c14087ef143a", size = 378827, upload-time = "2026-06-30T07:16:56.841Z" }, + { url = "https://files.pythonhosted.org/packages/4b/93/d9611e5b25e26df9a3649813ed66193ace9347a7c7fc4ab7cf70e94851c0/rpds_py-2026.6.3-cp315-cp315-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e55d236be29255554da47abe5c577637db7c24a02b8b46f0ca9524c855801868", size = 395966, upload-time = "2026-06-30T07:16:58.557Z" }, + { url = "https://files.pythonhosted.org/packages/c3/cb/99d77e16e5534ae1d90629bbe419ba6ee170833a6a85e3aa1cc41726fbbc/rpds_py-2026.6.3-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:24e9c5386e16669b674a69c156c8eeefcb578f3b3397b713b08e6d60f3c7b187", size = 545680, upload-time = "2026-06-30T07:17:00.164Z" }, + { url = "https://files.pythonhosted.org/packages/59/15/11a29755f790cef7a2f755e8e14f4f0c33f39489e1893a632a2eee59672b/rpds_py-2026.6.3-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:c60924535c75f1566b6eb75b5c31a48a43fef04fa2d0d201acbad8a9969c6107", size = 611853, upload-time = "2026-06-30T07:17:01.962Z" }, + { url = "https://files.pythonhosted.org/packages/68/86/0c27547e21644da938fb530f7e1a8148dd24d02db07e7a5f2567a17ce710/rpds_py-2026.6.3-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:38a2fea2787428f811719ceb9114cb78964a3138838320c29ac39526c79c16ba", size = 573715, upload-time = "2026-06-30T07:17:03.693Z" }, + { url = "https://files.pythonhosted.org/packages/29/71/4d8fcf700931815594bce892255bbd973b94efaf0fc1932b0590df18d886/rpds_py-2026.6.3-cp315-cp315-win32.whl", hash = "sha256:d483fe17f01ad64b7bf7cc38fcefff1ca9fb83f8c2b2542b68f97ffe0611b369", size = 202864, upload-time = "2026-06-30T07:17:05.746Z" }, + { url = "https://files.pythonhosted.org/packages/eb/62/b577562de0edbb55b2be85ce5fd09c33e386b9b13eee09833af4240fd5c4/rpds_py-2026.6.3-cp315-cp315-win_amd64.whl", hash = "sha256:67e3a721ffc5d8d2210d3671872298c4a84e4b8035cfe42ffd7cde35d772b146", size = 220430, upload-time = "2026-06-30T07:17:07.471Z" }, + { url = "https://files.pythonhosted.org/packages/c8/95/d6d0b2509825141eef60669a5739eec88dbc6a48053d6c92993a5704defe/rpds_py-2026.6.3-cp315-cp315-win_arm64.whl", hash = "sha256:6e84adbcf4bf841aed8116a8264b9f50b4cb3e7bd89b516122e616ac56ca269e", size = 215877, upload-time = "2026-06-30T07:17:09.008Z" }, + { url = "https://files.pythonhosted.org/packages/b7/bf/f3ea278f0afd615c1d0f19cb69043a41526e2bb600c2b536eb192218eb27/rpds_py-2026.6.3-cp315-cp315t-macosx_10_12_x86_64.whl", hash = "sha256:ae6dd8f10bd17aad820876d24caec9efdafd80a318d16c0a48edb5e136902c6b", size = 346933, upload-time = "2026-06-30T07:17:10.762Z" }, + { url = "https://files.pythonhosted.org/packages/9d/29/9907bdf1c5346763cf10b7f6852aad86652168c259def904cbe0082c5864/rpds_py-2026.6.3-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:bdbd97738551fca3917c1bd7188bec1920bb520104f28e7e1007f9ceb17b7690", size = 340274, upload-time = "2026-06-30T07:17:12.266Z" }, + { url = "https://files.pythonhosted.org/packages/6f/2c/8e03767b5778ef25cebf74a7a91a2c3806f8eced4c92cb7406bbe060756d/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b95977e7211527ab0ba576e286d023389fbeeb32a6b7b771665d333c60e5342", size = 370763, upload-time = "2026-06-30T07:17:14.107Z" }, + { url = "https://files.pythonhosted.org/packages/2e/e1/df2a7e1ba2efd796af26194250b8d42c821b46592311595162af9ef0528d/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d15fde0e6fb0d88a60d221204873743e5d9f0b7d29165e62cd86d0413ad74ba6", size = 376467, upload-time = "2026-06-30T07:17:15.76Z" }, + { url = "https://files.pythonhosted.org/packages/6b/de/8a0814d1946af29cb068fb259aa8622f856df1d0bab58429448726b537f5/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a136d453475ac0fcbda502ef1e6504bd28d6d904700915d278deeab0d00fe140", size = 496689, upload-time = "2026-06-30T07:17:17.308Z" }, + { url = "https://files.pythonhosted.org/packages/df/f3/f19e0c852ba13694f5a79f3b719331051573cb5693feacf8a88ffffc3a71/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f826877d462181e5eb1c26a0026b8d0cab05d99844ecb6d8bf3627a2ca0c0442", size = 385340, upload-time = "2026-06-30T07:17:18.928Z" }, + { url = "https://files.pythonhosted.org/packages/e2/ae/7ec3a9d2d4351f99e37bcb06b6b6f954512646bfdbf9742e1de727865daf/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:79486287de1730dbaff3dbd124d0ca4d2ef7f9d29bf2544f1f93c09b5bcbbd12", size = 372179, upload-time = "2026-06-30T07:17:20.539Z" }, + { url = "https://files.pythonhosted.org/packages/d3/ac/9cee911dff2aaa9a5a8354f6610bf2e6a616de9197c5fff4f54f82585f1e/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_31_riscv64.whl", hash = "sha256:808345f53cb952433ca2816f1604ff3515608a81784954f38d4452acfe8e61d5", size = 379993, upload-time = "2026-06-30T07:17:22.212Z" }, + { url = "https://files.pythonhosted.org/packages/83/6b/7c2a07ba88d1e9a936612f7a5d067467ed03d971d5a06f7d309dff044a7e/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1967debc37f64f2c4dc90a7f563aec558b471966e12adcac4e1c4240496b6ebf", size = 398909, upload-time = "2026-06-30T07:17:23.66Z" }, + { url = "https://files.pythonhosted.org/packages/97/0b/776ffcb66783637b0031f6d58d6fb55913c8b5abf00aeecd46bf933fb477/rpds_py-2026.6.3-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:f0840b5b17057f7fd918b76183a4b5a0635f43e14eb2ce60dce1d4ee4707ea00", size = 546584, upload-time = "2026-06-30T07:17:25.264Z" }, + { url = "https://files.pythonhosted.org/packages/55/33/ba3bc04d7092bd553c9b2b195624992d2cc4f3de1f380b7b93cbee67bd79/rpds_py-2026.6.3-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:faa679d19a6696fd54259ad321251ad77a13e70e03dd834daa762a44fb6196ef", size = 614357, upload-time = "2026-06-30T07:17:26.888Z" }, + { url = "https://files.pythonhosted.org/packages/8b/71/14edf065f04630b1a8472f7653cad03f6c478bcf95ea0e6aed55451e33ea/rpds_py-2026.6.3-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:23a439f31ccbeff1574e24889128821d1f7917470e830cf6544dced1c662262a", size = 576533, upload-time = "2026-06-30T07:17:28.546Z" }, + { url = "https://files.pythonhosted.org/packages/ba/76/65002b08596c389105720a8c0d22298b8dc25a4baf89b2ce431343c8b1de/rpds_py-2026.6.3-cp315-cp315t-win32.whl", hash = "sha256:913ca42ccad3f8cc6e292b587ae8ae49c8c823e5dce51a736252fc7c7cdfa577", size = 201204, upload-time = "2026-06-30T07:17:30.193Z" }, + { url = "https://files.pythonhosted.org/packages/8c/97/d855d6b3c322d1f27e26f5241c42016b56cf01377ea8ed348285f54652f0/rpds_py-2026.6.3-cp315-cp315t-win_amd64.whl", hash = "sha256:ae3d4fe8c0b9213624fdce7279d70e3b148b682ca20719ebd193a23ebfa47324", size = 220719, upload-time = "2026-06-30T07:17:31.788Z" }, + { url = "https://files.pythonhosted.org/packages/b4/9c/f0d19ac587fd0e4ab6b72cda355e9c5a6166b01ef7e064e437aef8eb9fef/rpds_py-2026.6.3-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:4cf2d36a2357e4d07bb5a4f98801265327b48256867816cfd2ceb001e9754a8f", size = 349791, upload-time = "2026-06-30T07:17:33.315Z" }, + { url = "https://files.pythonhosted.org/packages/38/c7/1d49d204c9fd2ee6c537601dc4c1ba921e03363ca576bfab94a00254ac9a/rpds_py-2026.6.3-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:30c6dc199b24a5e3e81d50da0f00858c5bbdb2617a750395687f4339c5818171", size = 352842, upload-time = "2026-06-30T07:17:34.897Z" }, + { url = "https://files.pythonhosted.org/packages/ac/e5/c0b5dc93cd0d4c06ce1f438907649514e2ea077bcd911e3154a51e96c38e/rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9891e594296ab9dada6551c8e7b387b2721f27a67eecd528412e8906247a7b90", size = 382094, upload-time = "2026-06-30T07:17:36.514Z" }, + { url = "https://files.pythonhosted.org/packages/0d/54/ec0e907b4ca8d541112db352409bd15f871c9b243e0c92c9b5a46ae96f01/rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b5c2dc92304aa48a4a60443b548bb12f12e119d4b72f314015e67b9e1be97fca", size = 388662, upload-time = "2026-06-30T07:17:38.235Z" }, + { url = "https://files.pythonhosted.org/packages/d3/f4/921c22a4fd0f1c1ac13a3996ffbf0aa67951e2c8ad0d1d9574938a2932e8/rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:127e08c0642d880cf32ca47ec2a4a77b901f7e2dd1ad9762adb13955d72ffcc9", size = 504896, upload-time = "2026-06-30T07:17:39.689Z" }, + { url = "https://files.pythonhosted.org/packages/0b/1b/a114b972cefa1ab1cdb3c7bb177cd3844a12826c507c722d3a73516dbbaf/rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8bb68f03f395eb793220b45c097bd4d8c32944393da0fad8b999efac0868fc8c", size = 391545, upload-time = "2026-06-30T07:17:41.336Z" }, + { url = "https://files.pythonhosted.org/packages/4e/98/af9b3db77d47fcbe6c8c1f36e2c2147ec70292819e99c325f871584a1c11/rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a3450b693fde92133e9f51060568a4c31fcca76d5e53bbd611e689ca446517e9", size = 380059, upload-time = "2026-06-30T07:17:42.857Z" }, + { url = "https://files.pythonhosted.org/packages/c9/ba/0efd8668b97c1d26a61566386c636a7a7a09829e474fdf807caa15a2c844/rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:5e8d07bddee435a2ff6f1920e18feff28d0bc4533e42f4bf6927fbd073312c41", size = 393235, upload-time = "2026-06-30T07:17:44.637Z" }, + { url = "https://files.pythonhosted.org/packages/62/90/8c139ee9690f73b0829f32647de6f40d826f8f443af6fa72644f96351aac/rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3a83ae6c67b7676b9878378547ca8e93ed77a580037bcbcd1d32f739e1e6089c", size = 413008, upload-time = "2026-06-30T07:17:46.225Z" }, + { url = "https://files.pythonhosted.org/packages/9c/97/0043896fdd7828ce09a1d9a8b06433714d0960fc4ff3fc4aa72b666b764e/rpds_py-2026.6.3-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:2bfd04c19ddbd6640de0b51894d764bd2758854d5b75bd102d2ef10cb9c293a9", size = 558118, upload-time = "2026-06-30T07:17:47.759Z" }, + { url = "https://files.pythonhosted.org/packages/f6/40/02355f0e134f783a8f9814c4680a1bd311d37671577a5964ea838573ff37/rpds_py-2026.6.3-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:ca6546b66be9dc4738b1b043d5ebd5488c66c578c5ff0fd0e8065313fe3afb76", size = 623138, upload-time = "2026-06-30T07:17:49.355Z" }, + { url = "https://files.pythonhosted.org/packages/10/85/48f0abdcef5cce4e034c7a5b0ceeceba0b01bf0d942824f4bb720afe2dec/rpds_py-2026.6.3-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:8e65860d238379ed982fd9ba690579b5e95af2f4840f99c772816dbe573cb826", size = 586486, upload-time = "2026-06-30T07:17:51.141Z" }, +] + +[[package]] +name = "rsa" +version = "4.9.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyasn1" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/da/8a/22b7beea3ee0d44b1916c0c1cb0ee3af23b700b6da9f04991899d0c555d4/rsa-4.9.1.tar.gz", hash = "sha256:e7bdbfdb5497da4c07dfd35530e1a902659db6ff241e39d9953cad06ebd0ae75", size = 29034, upload-time = "2025-04-16T09:51:18.218Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/64/8d/0133e4eb4beed9e425d9a98ed6e081a55d195481b7632472be1af08d2f6b/rsa-4.9.1-py3-none-any.whl", hash = "sha256:68635866661c6836b8d39430f97a996acbd61bfa49406748ea243539fe239762", size = 34696, upload-time = "2025-04-16T09:51:17.142Z" }, +] + +[[package]] +name = "service-identity" +version = "26.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "cryptography" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/61/87/ad52e2c582c0f0e7f0a1b86950494c38d67422dc0f5ed9044a5fb9569a49/service_identity-26.1.0.tar.gz", hash = "sha256:6358c52882c96e66ac4a55eb3a72c7dd4a70763f8cc6fa4e70abde2656f4bf3b", size = 42898, upload-time = "2026-05-30T12:04:55.184Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/93/eb/2433e1af4ff903499144de4846569fb3300b816179ae99a03c2f011b666a/service_identity-26.1.0-py3-none-any.whl", hash = "sha256:68c32dadbb69135fb951077677e07cd7f6031020f3a8c8f47a28cda8a0742118", size = 11370, upload-time = "2026-05-30T12:04:53.911Z" }, +] + +[[package]] +name = "setuptools" +version = "84.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6d/44/f5da03a8ef95d369145c5bb53050e7877c9f3d312e128605fd9504829143/setuptools-84.0.0.tar.gz", hash = "sha256:f4695c21257f0d9b537ec2692c941d02ee143b7cc1276941349a546573b2ef73", size = 1168449, upload-time = "2026-08-08T18:27:58.365Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/95/9c/c510029fc6ef33a6275cd2c5d3cecd6613dfd6aa401d57c54f1c18852ccf/setuptools-84.0.0-py3-none-any.whl", hash = "sha256:51a52592b3b99e102b609654876bd65f19f999935166d1352678931132b0c670", size = 818216, upload-time = "2026-08-08T18:27:56.719Z" }, +] + +[[package]] +name = "six" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, +] + +[[package]] +name = "traffic-replay" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "microdns" }, + { name = "microserver" }, + { name = "trlib" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/50/b3/e3968cf11a5c3358546dbc598869e9b83e5e15bbe04752e80369c61278a7/traffic_replay-1.0.0.tar.gz", hash = "sha256:596f5a2d706c2c065e41fdb32ea2a760d61ae0a3b0a51d3254a7864ca25f3466", size = 11783, upload-time = "2018-12-03T23:05:15.985Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8e/d1/0511a1e61b0d1a0baf2c2a40baf3f870474a3aac7c0e5b3f67953952e791/traffic_replay-1.0.0-py3-none-any.whl", hash = "sha256:1281ff25d1d02c0c634990d15c9835bf396dc07e1ce2c51b894376096e2981bb", size = 25638, upload-time = "2018-12-03T23:05:14.138Z" }, +] + +[[package]] +name = "trlib" +version = "1.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c9/fc/9814b4091a8c34029b4bceb93f8810f3a00a55d4fcb12a46e64b783e9798/trlib-1.0.1.tar.gz", hash = "sha256:f5874d226194507f85ce17c0358cb185e8722e3443db3a09ab1bc5244825f231", size = 9385, upload-time = "2019-01-04T19:28:32.768Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/13/2f/db97c51a5c0a434af25f719835a04fc0be7a5df614246d8ef94e717daf7f/trlib-1.0.1-py3-none-any.whl", hash = "sha256:d00dad7d6f27a3ef4cb450ee93d846996b94bf09371eed4ab0db9d2f6bc5bddb", size = 22230, upload-time = "2019-01-04T19:28:31.474Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/cc/6253133b5bb138fc3306cebfbda2c520f545d36b5be2c7255cc528bb45d6/typing_extensions-4.16.0.tar.gz", hash = "sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5", size = 113555, upload-time = "2026-07-02T08:40:05.92Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8", size = 45571, upload-time = "2026-07-02T08:40:04.659Z" }, +] + +[[package]] +name = "urllib3" +version = "2.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/53/0c/06f8b233b8fd13b9e5ee11424ef85419ba0d8ba0b3138bf360be2ff56953/urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c", size = 433602, upload-time = "2026-05-07T16:13:18.596Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897", size = 131087, upload-time = "2026-05-07T16:13:17.151Z" }, +] From 9b3e7fd31b19116490ccc57f7b02451ffc013454 Mon Sep 17 00:00:00 2001 From: bneradt Date: Fri, 14 Aug 2026 14:40:00 -0500 Subject: [PATCH 3/3] Improve procedural Uranium test support Procedural Uranium tests need reusable process ownership and curl targeting that works for multiple ATS instances and Unix sockets. The test tooling layout and command line also made framework code difficult to distinguish from the ATS test inventory. This patch adds fixture-owned ATS factories and ATS-aware curl requests, including UDS support, and converts representative basic coverage to the procedural API. It moves the harness and its unit tests under tests/tools/uranium, updates imports, and lets pytest own selection, parallelism, and collection options while the wrapper handles Docker execution. --- tests/pytest_tests/test_services.py | 109 ---------- .../uranium}/config.py | 0 .../uranium}/min_cfg/ip_allow.yaml | 0 .../uranium}/min_cfg/readme.txt | 0 .../uranium}/min_cfg/storage.yaml | 0 .../uranium}/plugin.py | 46 ++--- .../uranium}/process.py | 0 .../uranium}/replay.py | 2 +- .../uranium}/runner.py | 74 +++---- .../uranium}/runtime.py | 0 .../uranium}/scenario.py | 2 +- .../uranium}/services.py | 62 +++++- .../uranium/tests}/test_config.py | 8 +- .../uranium/tests}/test_process.py | 4 +- .../uranium/tests}/test_runner.py | 69 ++++--- tests/tools/uranium/tests/test_services.py | 186 ++++++++++++++++++ .../uranium_tests/ats_probe/test_ats_probe.py | 2 +- .../ats_probe/test_cache_dir_probe.py | 2 +- .../bad_http_fmt/test_bad_http_fmt.py | 2 +- tests/uranium_tests/basic/test_basic.py | 4 +- tests/uranium_tests/basic/test_config.py | 6 +- tests/uranium_tests/basic/test_copy_config.py | 2 +- .../uranium_tests/basic/test_copy_config2.py | 53 +---- tests/uranium_tests/basic/test_deny0.py | 2 +- tests/uranium_tests/bigobj/test_bigobj.py | 2 +- .../body_factory/test_http204_response.py | 2 +- .../test_http204_response_plugin.py | 2 +- .../body_factory/test_http304_response.py | 2 +- .../body_factory/test_http_head_no_origin.py | 2 +- .../body_factory/test_http_with_origin.py | 2 +- .../cache/test_background_fill.py | 2 +- .../cache/test_cache_config_reload.py | 2 +- .../cache/test_cache_corrupt_recursive_uaf.py | 2 +- .../cache/test_cache_generation_clear.py | 2 +- .../cache/test_cache_generation_disjoint.py | 2 +- .../cache/test_cache_shm_bad_disk_dropped.py | 2 +- .../cache/test_cache_shm_concurrent_attach.py | 2 +- .../test_cache_shm_control_size_mismatch.py | 2 +- .../cache/test_cache_shm_dir_invalid.py | 2 +- .../cache/test_cache_shm_fast_restart.py | 2 +- .../cache/test_cache_shm_purge_on_disable.py | 2 +- .../cache/test_cache_shm_schema_mismatch.py | 2 +- .../cache/test_cache_shm_storage_mismatch.py | 2 +- .../cache/test_cache_shm_unclean_shutdown.py | 2 +- .../cache/test_cache_write_lock_contention.py | 2 +- .../cache/test_disjoint_wait_for_cache.py | 2 +- .../cache/test_host_down_range_recursion.py | 2 +- .../cache/test_negative_caching.py | 2 +- .../cache/test_proxy_serve_stale_dns_fail.py | 2 +- .../cache/test_storage_metrics.py | 2 +- .../test_bad_chunked_encoding.py | 2 +- .../test_chunk_extension_quoted_string.py | 2 +- .../test_chunk_trailer_bare_lf.py | 2 +- .../chunked_encoding/test_chunked_encoding.py | 2 +- .../test_chunked_encoding_disabled.py | 2 +- .../test_chunked_encoding_h2.py | 2 +- .../test_per_client_connection_max.py | 2 +- .../test_verify_global_plugin.py | 2 +- .../test_verify_remap_plugin.py | 2 +- .../test_config_destroy_thread.py | 2 +- tests/uranium_tests/connect/test_connect.py | 2 +- .../connect/test_connect_handshake.py | 2 +- .../connect/test_connect_parent_error_body.py | 2 +- .../test_h2_malformed_request_logging.py | 2 +- .../test_connect_down_policy.py | 2 +- .../test_schedule_every_on_entire_pool.py | 2 +- .../test_schedule_every_on_pool.py | 2 +- .../test_schedule_every_on_thread.py | 2 +- .../test_schedule_on_entire_pool.py | 2 +- .../cont_schedule/test_schedule_on_pool.py | 2 +- .../cont_schedule/test_schedule_on_thread.py | 2 +- .../cont_schedule/test_thread_affinity.py | 2 +- .../continuations/test_double.py | 2 +- .../continuations/test_double_h2.py | 2 +- .../continuations/test_openclose.py | 2 +- .../continuations/test_openclose_h2.py | 2 +- .../continuations/test_session_id.py | 2 +- tests/uranium_tests/cripts/test_cripts.py | 2 +- .../dns/test_dns_down_nameserver.py | 2 +- tests/uranium_tests/dns/test_dns_host_down.py | 2 +- .../dns/test_dns_reverse_lookup.py | 2 +- tests/uranium_tests/dns/test_dns_ttl.py | 2 +- tests/uranium_tests/dns/test_splitdns.py | 2 +- .../uranium_tests/dns/test_splitdns_reload.py | 2 +- .../early_hints/test_early_hints.py | 2 +- .../forward_proxy/test_forward_proxy.py | 2 +- tests/uranium_tests/h2/grpc/test_grpc.py | 2 +- .../h2/test_h2_origin_cert_reverify.py | 2 +- .../h2/test_h2_origin_trailers_h1.py | 2 +- tests/uranium_tests/h2/test_h2disable.py | 2 +- .../h2/test_h2disable_no_accept_threads.py | 2 +- tests/uranium_tests/h2/test_h2enable.py | 2 +- .../h2/test_h2enable_no_accept_threads.py | 2 +- .../uranium_tests/h2/test_h2get_with_body.py | 2 +- tests/uranium_tests/h2/test_h2origin.py | 2 +- .../h2/test_h2origin_single_thread.py | 2 +- tests/uranium_tests/h2/test_h2spec.py | 2 +- tests/uranium_tests/h2/test_http2.py | 2 +- .../h2/test_http2_close_connection.py | 2 +- .../h2/test_http2_concurrent_streams.py | 2 +- .../h2/test_http2_crlf_header_validation.py | 2 +- .../h2/test_http2_empty_data_frame.py | 2 +- .../h2/test_http2_flow_control.py | 2 +- .../h2/test_http2_max_active_streams.py | 2 +- tests/uranium_tests/h2/test_http2_priority.py | 2 +- .../uranium_tests/h2/test_http2_rst_stream.py | 2 +- .../h2/test_http2_txn_start_read_gate.py | 2 +- .../h2/test_http2_write_threshold.py | 2 +- tests/uranium_tests/h2/test_httpbin.py | 2 +- tests/uranium_tests/h2/test_nghttp.py | 2 +- tests/uranium_tests/h3/test_h3_curl.py | 2 +- tests/uranium_tests/h3/test_h3_go_client.py | 2 +- .../uranium_tests/h3/test_h3_python_client.py | 2 +- tests/uranium_tests/h3/test_h3_range_cache.py | 2 +- .../h3/test_h3_session_ticket.py | 2 +- tests/uranium_tests/h3/test_h3_sni_check.py | 2 +- .../headers/test_cache_and_req_body.py | 2 +- .../headers/test_field_name_space.py | 2 +- tests/uranium_tests/headers/test_forwarded.py | 2 +- .../test_general_connection_failure_502.py | 2 +- .../headers/test_good_request_after_bad.py | 2 +- tests/uranium_tests/headers/test_http408.py | 2 +- .../headers/test_normalize_ae.py | 2 +- .../headers/test_oversized_field_h2.py | 2 +- tests/uranium_tests/headers/test_syntax.py | 2 +- tests/uranium_tests/headers/test_via.py | 2 +- .../ip_allow/test_connect_destination_acl.py | 2 +- tests/uranium_tests/ip_allow/test_ip_allow.py | 2 +- .../test_ip_allow_reload_triggered.py | 2 +- .../ip_allow/test_ip_category.py | 2 +- .../jsonrpc/test_basic_plugin_handler.py | 2 +- .../jsonrpc/test_config_reload_dedup.py | 2 +- .../jsonrpc/test_config_reload_failures.py | 2 +- .../jsonrpc/test_config_reload_full_smoke.py | 2 +- .../test_config_reload_reserve_subtask.py | 2 +- .../jsonrpc/test_config_reload_rpc.py | 2 +- .../jsonrpc/test_config_reload_tracking.py | 2 +- .../jsonrpc/test_jsonrpc_api_schema.py | 2 +- tests/uranium_tests/lib/ports.py | 2 +- .../uranium_tests/logging/test_all_headers.py | 2 +- .../logging/test_binary_log_v3.py | 2 +- .../uranium_tests/logging/test_custom_log.py | 2 +- tests/uranium_tests/logging/test_log_field.py | 2 +- .../logging/test_log_field_json.py | 2 +- .../logging/test_log_filenames.py | 2 +- .../uranium_tests/logging/test_log_filter.py | 2 +- .../logging/test_log_milestone_fields.py | 2 +- .../uranium_tests/logging/test_log_mstsms.py | 2 +- tests/uranium_tests/logging/test_log_pipe.py | 2 +- .../logging/test_log_plugin_init.py | 2 +- .../logging/test_log_retention.py | 2 +- .../logging/test_new_log_flds.py | 2 +- tests/uranium_tests/logging/test_pqsi_pqsp.py | 2 +- .../uranium_tests/logging/test_qlog_quiche.py | 2 +- tests/uranium_tests/logging/test_sigusr2.py | 2 +- .../test_parent_config_dest_ip.py | 2 +- .../strategies_ch/test_strategies_ch.py | 2 +- .../strategies_ch2/test_strategies_ch2.py | 2 +- .../strategies_ch5/test_strategies_ch5.py | 2 +- .../strategies_stale/test_strategies_stale.py | 2 +- .../test_zzz_strategies_peer.py | 2 +- .../test_zzz_strategies_peer2.py | 2 +- .../null_transform/test_null_transform.py | 2 +- .../test_per_server_connection_max.py | 2 +- .../test_per_server_metric_enabled.py | 2 +- .../test_parent_config_reload.py | 2 +- .../test_consistent_hash_ring_walk.py | 2 +- .../parent_proxy/test_parent_retry.py | 2 +- .../test_delete_maxforwards_body_desync.py | 2 +- .../pipeline/test_delete_maxfwd_noop_drain.py | 2 +- .../pipeline/test_noop_keepalive.py | 2 +- tests/uranium_tests/pipeline/test_pipeline.py | 2 +- .../pluginTest/TSVConnFd/test_TSVConnFd.py | 2 +- .../access_control/test_access_control.py | 2 +- .../block_errors/test_block_errors.py | 2 +- .../pluginTest/cache_fill/test_cache_fill.py | 2 +- .../cache_promote/test_cache_promote.py | 2 +- .../test_cache_range_requests.py | 2 +- ...range_requests_cache_complete_responses.py | 2 +- .../test_cache_range_requests_cachekey.py | 2 +- ...st_cache_range_requests_cachekey_global.py | 2 +- .../test_cache_range_requests_ident.py | 2 +- .../cert_update/test_cert_update.py | 2 +- .../pluginTest/certifier/test_certifier.py | 2 +- .../test_client_context_dump.py | 2 +- .../combo_handler/test_combo_handler.py | 2 +- .../pluginTest/compress/test_compress.py | 2 +- .../test_compress_cache_untransformed.py | 2 +- .../test_compress_content_type_params.py | 2 +- .../compress/test_compress_range.py | 2 +- .../pluginTest/conf_remap/test_conf_remap.py | 2 +- .../cookie_remap/test_bucketcookie.py | 2 +- .../cookie_remap/test_collapseslashes.py | 2 +- .../pluginTest/cookie_remap/test_connector.py | 2 +- .../test_disable_pristine_host_hdr.py | 2 +- .../cookie_remap/test_existscookie.py | 2 +- .../cookie_remap/test_matchcookie.py | 2 +- .../pluginTest/cookie_remap/test_matchuri.py | 2 +- .../cookie_remap/test_notexistscookie.py | 2 +- .../cookie_remap/test_pcollapseslashes.py | 2 +- .../cookie_remap/test_psubstitute.py | 2 +- .../cookie_remap/test_regexcookie.py | 2 +- .../pluginTest/cookie_remap/test_setstatus.py | 2 +- .../pluginTest/cookie_remap/test_subcookie.py | 2 +- .../cookie_remap/test_substitute.py | 2 +- .../pluginTest/cppapi/test_cppapi.py | 2 +- .../pluginTest/crash_test/test_crash_test.py | 2 +- .../pluginTest/escalate/test_escalate.py | 2 +- .../uranium_tests/pluginTest/esi/test_esi.py | 2 +- .../pluginTest/esi/test_esi_304.py | 2 +- .../esi/test_esi_nested_html_comment.py | 2 +- .../pluginTest/esi/test_esi_nested_include.py | 2 +- .../esi/test_esi_request_size_cap.py | 2 +- .../pluginTest/esi/test_esi_ssrf_validate.py | 2 +- .../test_header_rewrite_bad_run_plugin.py | 2 +- .../test_header_rewrite_set_body_from.py | 2 +- .../test_header_rewrite_url_glob.py | 2 +- .../healthchecks/test_healthchecks.py | 2 +- .../ja3_fingerprint/test_ja3_fingerprint.py | 2 +- .../ja4_fingerprint/test_ja4_fingerprint.py | 2 +- .../jax_fingerprint/test_jax_fingerprint.py | 2 +- .../pluginTest/lua/test_lua_client_hook.py | 2 +- .../pluginTest/lua/test_lua_debug_tags.py | 2 +- .../lua/test_lua_fetch_ipv6_cliaddr.py | 2 +- .../lua/test_lua_global_shutdown.py | 2 +- .../pluginTest/lua/test_lua_header_table.py | 2 +- .../pluginTest/lua/test_lua_proxy_protocol.py | 2 +- .../lua/test_lua_remap_after_hook.py | 2 +- .../lua/test_lua_set_error_response.py | 2 +- .../pluginTest/lua/test_lua_states_stats.py | 2 +- .../pluginTest/lua/test_lua_verified_addr.py | 2 +- .../pluginTest/lua/test_lua_watermark.py | 2 +- .../money_trace/test_money_trace.py | 2 +- .../money_trace/test_money_trace_global.py | 2 +- .../multiplexer/test_multiplexer.py | 2 +- .../test_origin_server_auth.py | 2 +- .../test_s3_url_encoding.py | 2 +- .../packet_mark/test_packet_mark.py | 2 +- .../test_per_plugin_metrics.py | 2 +- .../plugin_yaml/test_plugin_yaml.py | 2 +- .../polite_hook_wait/test_polite_hook_wait.py | 2 +- .../test_prefetch_bad_count_refused.py | 2 +- .../test_prefetch_bad_pattern_refused.py | 2 +- .../prefetch/test_prefetch_bignum.py | 2 +- .../pluginTest/prefetch/test_prefetch_cmcd.py | 2 +- .../test_prefetch_empty_replacement.py | 2 +- .../prefetch/test_prefetch_optional_group.py | 2 +- .../prefetch/test_prefetch_overflow.py | 2 +- .../prefetch/test_prefetch_simple.py | 2 +- .../pluginTest/rate_limit/test_rate_limit.py | 2 +- .../rate_limit/test_rate_limit_iprep.py | 2 +- .../rate_limit/test_rate_limit_sni.py | 2 +- .../rate_limit/test_rate_limit_sni_expiry.py | 2 +- .../rate_limit/test_rate_limit_sni_queue.py | 2 +- .../rate_limit/test_rate_limit_sni_reject.py | 2 +- .../redirect_limit/test_redirect_limit.py | 2 +- .../regex_remap/test_regex_remap.py | 2 +- .../regex_revalidate/test_regex_revalidate.py | 2 +- .../test_regex_revalidate_miss.py | 2 +- .../test_regex_revalidate_state.py | 2 +- .../remap_stats/test_remap_stats.py | 2 +- .../remap_stats/test_remap_stats_post.py | 2 +- .../test_server_push_preload.py | 2 +- .../pluginTest/slice/test_slice.py | 2 +- .../slice/test_slice_conditional.py | 2 +- .../slice/test_slice_content_shrink.py | 2 +- .../pluginTest/slice/test_slice_crr_ident.py | 2 +- .../pluginTest/slice/test_slice_error.py | 2 +- .../pluginTest/slice/test_slice_ident.py | 2 +- .../pluginTest/slice/test_slice_prefetch.py | 2 +- .../pluginTest/slice/test_slice_purge.py | 2 +- .../pluginTest/slice/test_slice_purge_gaps.py | 2 +- .../pluginTest/slice/test_slice_regex.py | 2 +- .../pluginTest/slice/test_slice_rm_range.py | 2 +- .../slice/test_slice_selfhealing.py | 2 +- .../slice/test_slice_stale_generation.py | 2 +- .../pluginTest/sslheaders/test_sslheaders.py | 2 +- .../stale_response/test_stale_response.py | 2 +- .../pluginTest/statichit/test_statichit.py | 2 +- .../stats_over_http/test_stats_over_http.py | 2 +- .../pluginTest/stek_share/test_stek_share.py | 2 +- .../strategies/test_strategies_plugins.py | 2 +- .../pluginTest/test_hooks/test_hook_add.py | 2 +- .../test_hooks/test_ssn_start_delay_hook.py | 2 +- .../pluginTest/test_hooks/test_test_hooks.py | 2 +- .../traffic_dump/test_traffic_dump.py | 2 +- .../traffic_dump/test_traffic_dump_http3.py | 2 +- .../test_traffic_dump_ip_filter.py | 2 +- .../test_traffic_dump_response_body.py | 2 +- .../test_traffic_dump_sni_filter.py | 2 +- .../transform/test_transaction_data_sink.py | 2 +- .../tsapi/test_test_TSHttpSsnInfo.py | 2 +- .../tsapi/test_test_TSHttpTxnServerAddrSet.py | 2 +- .../test_test_TSHttpTxnServerAddrSet_retry.py | 2 +- .../tsapi/test_test_TSHttpTxnVerifiedAddr.py | 2 +- .../tsapi/test_test_TSVConnPPInfo.py | 2 +- .../pluginTest/tsapi/test_tsapi.py | 2 +- .../txn_box/basic/test_txn_box_basic.py | 2 +- .../txn_box/basic/test_txn_box_cmp.py | 2 +- .../txn_box/basic/test_txn_box_ip_addr.py | 2 +- .../txn_box/basic/test_txn_box_mod.py | 2 +- .../txn_box/basic/test_txn_box_multi_cfg.py | 2 +- .../txn_box/basic/test_txn_box_redirect.py | 2 +- .../txn_box/basic/test_txn_box_reply.py | 2 +- .../txn_box/basic/test_txn_box_rxp.py | 2 +- .../txn_box/basic/test_txn_box_stat.py | 2 +- .../txn_box/basic/test_txn_box_tls.py | 2 +- .../txn_box/basic/test_txn_box_tls_cert.py | 2 +- .../txn_box/basic/test_txn_box_tuple.py | 2 +- .../txn_box/basic/test_txn_box_txn_debug.py | 2 +- .../txn_box/basic/test_txn_box_txn_error.py | 2 +- .../txn_box/basic/test_txn_box_txn_open_1.py | 2 +- .../txn_box/basic/test_txn_box_txn_open_2.py | 2 +- .../txn_box/basic/test_txn_box_txn_open_3.py | 2 +- .../txn_box/basic/test_txn_box_with.py | 2 +- .../ct_header/test_txn_box_ct_header.py | 2 +- .../example/test_txn_box_accept_encoding.py | 2 +- .../txn_box/prod/test_txn_box_cors_origin.py | 2 +- .../prod/test_txn_box_cors_referrer.py | 2 +- .../txn_box/prod/test_txn_box_ip_acl.py | 2 +- .../txn_box/prod/test_txn_box_mTLS.py | 2 +- .../txn_box/prod/test_txn_box_query.py | 2 +- .../prod/test_txn_box_query_delete_filter.py | 2 +- .../prod/test_txn_box_query_delete_regex.py | 2 +- .../txn_box/prod/test_txn_box_stanley.py | 2 +- .../txn_box/prod/test_txn_box_vznith_1.py | 2 +- .../txn_box/prod/test_txn_box_yts_3489.py | 2 +- .../txn_box/ramp/test_txn_box_multi_ramp_1.py | 2 +- .../txn_box/ramp/test_txn_box_multi_ramp_2.py | 2 +- .../txn_box/ramp/test_txn_box_multi_ramp_3.py | 2 +- .../txn_box/ramp/test_txn_box_ramp.py | 2 +- .../txn_box/remap/test_txn_box_remap_base.py | 2 +- .../txn_box/smoke/test_txn_box_smoke.py | 2 +- .../txn_box/smoke/test_txn_box_smoke_2.py | 2 +- .../static_file/test_txn_box_static_file.py | 2 +- .../uri_signing/test_uri_signing.py | 2 +- .../pluginTest/url_sig/test_url_sig.py | 2 +- .../test_webp_transform_cache_refused.py | 2 +- .../test_webp_transform_chunked_cap.py | 2 +- .../test_webp_transform_decode_limit.py | 2 +- .../test_webp_transform_max_buffer_size.py | 2 +- .../test_webp_transform_size_cap.py | 2 +- .../xdebug/x_cache_info/test_x_cache_info.py | 2 +- .../x_effective_url/test_x_effective_url.py | 2 +- .../pluginTest/xdebug/x_probe/test_x_probe.py | 2 +- .../test_x_probe_full_json.py | 2 +- .../test_x_probe_full_json_slow_origin.py | 2 +- .../pluginTest/xdebug/x_remap/test_x_remap.py | 2 +- tests/uranium_tests/post/test_expect_tests.py | 2 +- .../uranium_tests/post/test_post_continue.py | 2 +- .../post/test_post_early_return.py | 2 +- .../test_simple_post_valid_buffer_check.py | 2 +- .../post_slow_server/test_post_slow_server.py | 2 +- .../test_post_slow_server_max_requests_in.py | 2 +- .../proxy_protocol/test_proxy_protocol.py | 2 +- .../uranium_tests/qmux/test_qmux_go_client.py | 2 +- .../records/test_records_config_to_yaml.py | 2 +- .../test_records_runroot_precedence.py | 2 +- .../records/test_records_yaml.py | 2 +- .../records/test_traffic_ctl_cold_config.py | 2 +- .../records/test_ts_max_records_param.py | 2 +- .../redirect/test_number_of_redirects.py | 2 +- tests/uranium_tests/redirect/test_redirect.py | 2 +- .../redirect/test_redirect_actions.py | 2 +- .../redirect/test_redirect_post.py | 2 +- .../redirect/test_redirect_stale.py | 2 +- .../test_redirect_to_same_origin_on_cache.py | 2 +- .../remap/test_basic_conf_remap_yaml.py | 2 +- .../remap/test_conf_remap_float.py | 2 +- .../remap/test_map_with_recv_port.py | 2 +- tests/uranium_tests/remap/test_regex_map.py | 2 +- tests/uranium_tests/remap/test_remap_acl.py | 2 +- tests/uranium_tests/remap/test_remap_http.py | 2 +- tests/uranium_tests/remap/test_remap_https.py | 2 +- .../remap/test_remap_ip_resolve.py | 2 +- .../remap/test_remap_load_empty_failure.py | 2 +- .../remap/test_remap_load_empty_success.py | 2 +- .../remap/test_remap_load_missing_failure.py | 2 +- .../remap/test_remap_load_missing_success.py | 2 +- .../uranium_tests/remap/test_remap_reload.py | 2 +- tests/uranium_tests/remap/test_remap_ws.py | 2 +- .../test_basic_conf_remap_yaml_yaml.py | 2 +- .../remap_yaml/test_conf_remap_float_yaml.py | 2 +- .../test_map_with_recv_port_yaml.py | 2 +- .../remap_yaml/test_regex_map_yaml.py | 2 +- .../remap_yaml/test_remap_acl_yaml.py | 2 +- .../remap_yaml/test_remap_http_yaml.py | 2 +- .../remap_yaml/test_remap_https_yaml.py | 2 +- .../remap_yaml/test_remap_ip_resolve_yaml.py | 2 +- .../test_remap_load_empty_failure_yaml.py | 2 +- .../test_remap_load_empty_success_yaml.py | 2 +- .../test_remap_load_missing_failure_yaml.py | 2 +- .../test_remap_load_missing_success_yaml.py | 2 +- .../remap_yaml/test_remap_reload_yaml.py | 2 +- .../remap_yaml/test_remap_ws_yaml.py | 2 +- .../runroot/test_runroot_error.py | 2 +- .../runroot/test_runroot_init.py | 2 +- .../runroot/test_runroot_remove.py | 2 +- .../uranium_tests/runroot/test_runroot_use.py | 2 +- .../runroot/test_runroot_verify.py | 2 +- .../session_sharing/test_session_match.py | 2 +- .../test_crashlog_no_false_positive.py | 2 +- .../uranium_tests/shutdown/test_emergency.py | 2 +- tests/uranium_tests/shutdown/test_fatal.py | 2 +- .../slow_post/test_quick_server.py | 2 +- .../slow_post/test_server_abort.py | 2 +- .../uranium_tests/slow_post/test_slow_post.py | 2 +- .../statistics/test_metric_response_000.py | 2 +- .../thread_config/test_thread_config.py | 2 +- .../timeout/test_accept_timeout.py | 2 +- .../timeout/test_active_timeout.py | 2 +- .../timeout/test_conn_timeout.py | 2 +- .../test_default_inactivity_timeout.py | 2 +- .../test_http2_incomplete_header_timeout.py | 2 +- .../timeout/test_http2_no_activity_timeout.py | 2 +- .../timeout/test_inactive_client_timeout.py | 2 +- .../timeout/test_inactive_timeout.py | 2 +- .../timeout/test_quic_no_activity_timeout.py | 2 +- .../timeout/test_quic_poll_timeout.py | 2 +- .../timeout/test_tls_conn_timeout.py | 2 +- .../timeout/test_tunnel_active_timeout.py | 2 +- tests/uranium_tests/tls/test_allow_plain.py | 2 +- .../tls/test_exit_on_cert_load_fail.py | 2 +- .../tls/test_proxy_protocol_addressless.py | 2 +- .../uranium_tests/tls/test_ssl_key_dialog.py | 2 +- .../tls/test_ssl_multicert_loader.py | 2 +- tests/uranium_tests/tls/test_tls.py | 2 +- .../uranium_tests/tls/test_tls_0rtt_server.py | 2 +- .../tls/test_tls_accept_timeout_crash.py | 2 +- .../tls/test_tls_async_handshake.py | 2 +- tests/uranium_tests/tls/test_tls_bad_alpn.py | 2 +- tests/uranium_tests/tls/test_tls_cert_comp.py | 2 +- .../tls/test_tls_cert_comp_cache.py | 2 +- .../tls/test_tls_check_cert_select_plugin.py | 2 +- .../tls/test_tls_check_cert_selection.py | 2 +- .../test_tls_check_cert_selection_reload.py | 2 +- .../tls/test_tls_check_dual_cert_selection.py | 2 +- .../test_tls_check_dual_cert_selection2.py | 2 +- ...st_tls_check_dual_cert_selection_plugin.py | 2 +- .../tls/test_tls_client_alpn_configuration.py | 2 +- .../uranium_tests/tls/test_tls_client_cert.py | 2 +- .../tls/test_tls_client_cert2.py | 2 +- .../tls/test_tls_client_cert2_plugin.py | 2 +- .../tls/test_tls_client_cert_override.py | 2 +- .../test_tls_client_cert_override_plugin.py | 2 +- .../tls/test_tls_client_cert_plugin.py | 2 +- .../tls/test_tls_client_verify.py | 2 +- .../tls/test_tls_client_verify2.py | 2 +- .../tls/test_tls_client_verify3.py | 2 +- .../tls/test_tls_client_versions.py | 2 +- .../tls/test_tls_client_versions_minmax.py | 2 +- .../tls/test_tls_engine_abort.py | 2 +- .../tls/test_tls_flow_control.py | 2 +- .../tls/test_tls_forward_nonhttp.py | 2 +- .../tls/test_tls_h2_cipher_suite.py | 2 +- .../tls/test_tls_hooks_client_verify.py | 2 +- .../tls/test_tls_hooks_verify.py | 2 +- tests/uranium_tests/tls/test_tls_keepalive.py | 2 +- tests/uranium_tests/tls/test_tls_ocsp.py | 2 +- .../tls/test_tls_origin_open_failed.py | 2 +- .../tls/test_tls_origin_post_abort.py | 2 +- .../tls/test_tls_origin_session_reuse.py | 2 +- .../tls/test_tls_partial_blind_tunnel.py | 2 +- .../uranium_tests/tls/test_tls_record_size.py | 2 +- .../tls/test_tls_reload_under_load.py | 2 +- .../tls/test_tls_renegotiation.py | 2 +- .../tls/test_tls_renegotiation_allowed.py | 2 +- .../tls/test_tls_secret_update_default.py | 2 +- .../tls/test_tls_session_key_logging.py | 2 +- .../tls/test_tls_session_reuse.py | 2 +- .../uranium_tests/tls/test_tls_sni_groups.py | 2 +- .../tls/test_tls_sni_host_policy.py | 2 +- .../tls/test_tls_sni_ip_allow.py | 2 +- .../tls/test_tls_sni_parent_failover.py | 2 +- .../uranium_tests/tls/test_tls_sni_ticket.py | 2 +- .../tls/test_tls_sni_with_port.py | 2 +- .../tls/test_tls_sni_yaml_reload.py | 2 +- tests/uranium_tests/tls/test_tls_ticket.py | 2 +- tests/uranium_tests/tls/test_tls_tunnel.py | 2 +- .../tls/test_tls_tunnel_forward.py | 2 +- tests/uranium_tests/tls/test_tls_verify.py | 2 +- tests/uranium_tests/tls/test_tls_verify2.py | 2 +- tests/uranium_tests/tls/test_tls_verify3.py | 2 +- tests/uranium_tests/tls/test_tls_verify4.py | 2 +- .../uranium_tests/tls/test_tls_verify_base.py | 2 +- .../tls/test_tls_verify_ca_override.py | 2 +- .../tls/test_tls_verify_not_pristine.py | 2 +- .../tls/test_tls_verify_override.py | 2 +- .../tls/test_tls_verify_override_base.py | 2 +- .../tls/test_tls_verify_override_sni.py | 2 +- .../uranium_tests/tls_hooks/test_tls_hooks.py | 2 +- .../tls_hooks/test_tls_hooks10.py | 2 +- .../tls_hooks/test_tls_hooks11.py | 2 +- .../tls_hooks/test_tls_hooks12.py | 2 +- .../tls_hooks/test_tls_hooks13.py | 2 +- .../tls_hooks/test_tls_hooks14.py | 2 +- .../tls_hooks/test_tls_hooks15.py | 2 +- .../tls_hooks/test_tls_hooks16.py | 2 +- .../tls_hooks/test_tls_hooks17.py | 2 +- .../tls_hooks/test_tls_hooks18.py | 2 +- .../tls_hooks/test_tls_hooks2.py | 2 +- .../tls_hooks/test_tls_hooks3.py | 2 +- .../tls_hooks/test_tls_hooks4.py | 2 +- .../tls_hooks/test_tls_hooks6.py | 2 +- .../tls_hooks/test_tls_hooks7.py | 2 +- .../tls_hooks/test_tls_hooks8.py | 2 +- .../tls_hooks/test_tls_hooks9.py | 2 +- .../test_tls_hooks_close_while_parked.py | 2 +- .../test_convert_plugin_config.py | 2 +- .../test_convert_ssl_multicert.py | 2 +- .../convert_storage/test_convert_storage.py | 2 +- .../traffic_ctl/remap_inc/test_remap_inc.py | 2 +- .../test_show_ssl_multicert.py | 2 +- .../test_traffic_ctl_config_output.py | 2 +- .../test_traffic_ctl_config_reload.py | 2 +- .../test_traffic_ctl_server_debug.py | 2 +- .../test_traffic_ctl_server_output.py | 2 +- .../test_traffic_ctl_set_read_only.py | 2 +- .../tunnel/test_tunnel_transform.py | 2 +- tests/uranium_tests/tunnel/test_txn_type.py | 2 +- .../uranium_tests/uds/test_uds_socket_perm.py | 2 +- tests/uranium_tests/url/test_uri.py | 2 +- tests/urtest.sh | 2 +- tests/urtest.sh.in | 2 +- 524 files changed, 855 insertions(+), 780 deletions(-) delete mode 100644 tests/pytest_tests/test_services.py rename tests/{uranium_testkit => tools/uranium}/config.py (100%) rename tests/{uranium_testkit => tools/uranium}/min_cfg/ip_allow.yaml (100%) rename tests/{uranium_testkit => tools/uranium}/min_cfg/readme.txt (100%) rename tests/{uranium_testkit => tools/uranium}/min_cfg/storage.yaml (100%) rename tests/{uranium_testkit => tools/uranium}/plugin.py (90%) rename tests/{uranium_testkit => tools/uranium}/process.py (100%) rename tests/{uranium_testkit => tools/uranium}/replay.py (99%) rename tests/{uranium_testkit => tools/uranium}/runner.py (88%) rename tests/{uranium_testkit => tools/uranium}/runtime.py (100%) rename tests/{uranium_testkit => tools/uranium}/scenario.py (99%) rename tests/{uranium_testkit => tools/uranium}/services.py (63%) rename tests/{pytest_tests => tools/uranium/tests}/test_config.py (92%) rename tests/{pytest_tests => tools/uranium/tests}/test_process.py (95%) rename tests/{pytest_tests => tools/uranium/tests}/test_runner.py (72%) create mode 100644 tests/tools/uranium/tests/test_services.py diff --git a/tests/pytest_tests/test_services.py b/tests/pytest_tests/test_services.py deleted file mode 100644 index fabb50c49db..00000000000 --- a/tests/pytest_tests/test_services.py +++ /dev/null @@ -1,109 +0,0 @@ -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License 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. -"""Unit tests for procedural Uranium service objects.""" - -from pathlib import Path -import subprocess -from types import SimpleNamespace -from typing import Any - -import pytest - -from uranium_testkit.services import ATS, Curl - - -class FakeProcess: - """Record ATS lifecycle calls without starting a process.""" - - def __init__(self) -> None: - self.Variables = type("Variables", (), {"port": 12345})() - self.Disk = SimpleNamespace(records_config=FakeRecords()) - self.was_started = False - self.was_stopped = False - self.was_validated = False - - def start(self) -> None: - self.was_started = True - - def stop(self) -> None: - self.was_stopped = True - - def validate(self) -> None: - self.was_validated = True - - def is_running(self) -> bool: - return self.was_started and not self.was_stopped - - -class FakeRecords: - """Record staged records.yaml updates.""" - - def __init__(self) -> None: - self.values: dict[str, Any] = {} - - def update(self, values: dict[str, Any]) -> None: - self.values.update(values) - - -class FakeScenario: - """Return a single fake ATS process.""" - - def __init__(self) -> None: - self.process = FakeProcess() - - def MakeATSProcess(self, name: str) -> FakeProcess: # noqa: N802 - assert name == "ats" - return self.process - - -def test_ats_owns_process_lifecycle() -> None: - """Start, stop, and validate the fixture-owned ATS process.""" - - scenario = FakeScenario() - ats = ATS(scenario) # type: ignore[arg-type] - - assert ats.http_port == 12345 - assert not ats.is_running - - ats.records.update({"proxy.config.http.server_ports": "12345"}) - assert scenario.process.Disk.records_config.values == {"proxy.config.http.server_ports": "12345"} - - ats.start() - assert ats.is_running - - ats.close() - assert scenario.process.was_stopped - assert scenario.process.was_validated - - -def test_curl_returns_a_plain_command_result(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None: - """Expose curl status and output for ordinary pytest assertions.""" - - observed: dict[str, Any] = {} - - def run(command: tuple[str, ...], **kwargs: Any) -> subprocess.CompletedProcess[str]: - observed["command"] = command - observed.update(kwargs) - return subprocess.CompletedProcess(command, 7, "response", "diagnostic") - - monkeypatch.setattr("uranium_testkit.services.subprocess.run", run) - - result = Curl(tmp_path).get("http://127.0.0.1:12345", headers={"X-Test": "value"}) - - assert observed["command"] == ("curl", "--header", "X-Test: value", "http://127.0.0.1:12345") - assert observed["cwd"] == tmp_path - assert result.returncode == 7 - assert result.output == "responsediagnostic" diff --git a/tests/uranium_testkit/config.py b/tests/tools/uranium/config.py similarity index 100% rename from tests/uranium_testkit/config.py rename to tests/tools/uranium/config.py diff --git a/tests/uranium_testkit/min_cfg/ip_allow.yaml b/tests/tools/uranium/min_cfg/ip_allow.yaml similarity index 100% rename from tests/uranium_testkit/min_cfg/ip_allow.yaml rename to tests/tools/uranium/min_cfg/ip_allow.yaml diff --git a/tests/uranium_testkit/min_cfg/readme.txt b/tests/tools/uranium/min_cfg/readme.txt similarity index 100% rename from tests/uranium_testkit/min_cfg/readme.txt rename to tests/tools/uranium/min_cfg/readme.txt diff --git a/tests/uranium_testkit/min_cfg/storage.yaml b/tests/tools/uranium/min_cfg/storage.yaml similarity index 100% rename from tests/uranium_testkit/min_cfg/storage.yaml rename to tests/tools/uranium/min_cfg/storage.yaml diff --git a/tests/uranium_testkit/plugin.py b/tests/tools/uranium/plugin.py similarity index 90% rename from tests/uranium_testkit/plugin.py rename to tests/tools/uranium/plugin.py index aedb6de7934..7ff6ecc533c 100644 --- a/tests/uranium_testkit/plugin.py +++ b/tests/tools/uranium/plugin.py @@ -20,7 +20,6 @@ from collections.abc import Callable, Iterator from pathlib import Path from typing import Any -import fnmatch import os import pytest @@ -30,7 +29,7 @@ from .replay import ReplaySkip, ReplayTest from .runtime import TestRuntime from .scenario import UraniumTest -from .services import ATS, Curl +from .services import ATS, ATSFactory, Curl def pytest_addoption(parser: pytest.Parser) -> None: @@ -41,7 +40,6 @@ def pytest_addoption(parser: pytest.Parser) -> None: group.addoption("--proxy-verifier-bin", help="Directory containing verifier-client and verifier-server") group.addoption("--build-root", help="ATS build directory containing test plugins") group.addoption("--sandbox", help="Directory for isolated test process trees") - group.addoption("--urtest-filter", action="append", default=[], help="Glob selecting Uranium test names") group.addoption("--urtest-shard-index", type=int, help="Zero-based CI shard to collect") group.addoption("--urtest-shard-count", type=int, help="Total number of CI shards") group.addoption("--curl-uds", action="store_true", help="Run supported Uranium tests with curl Unix sockets") @@ -66,7 +64,7 @@ def pytest_collect_file(file_path: Path, parent: pytest.Collector) -> pytest.Fil def pytest_collection_modifyitems(config: pytest.Config, items: list[pytest.Item]) -> None: - """Apply stable name filtering and zero-based CI sharding.""" + """Apply serial scheduling, UDS exclusions, and zero-based CI sharding.""" for item in items: if item.path.name.startswith("test_") and "uranium_tests" in item.path.parts: @@ -77,11 +75,6 @@ def pytest_collection_modifyitems(config: pytest.Config, items: list[pytest.Item selected = items deselected: list[pytest.Item] = [] - patterns = config.getoption("urtest_filter") - if patterns: - selected, rejected = _partition_items(selected, lambda item: _matches_filter(item, patterns)) - deselected.extend(rejected) - if config.getoption("curl_uds"): unsupported = {"h2", "tls", "tls_hooks"} selected, rejected = _partition_items( @@ -169,7 +162,7 @@ def get_runtime(config: pytest.Config) -> TestRuntime: if cached is not None: return cached - repository_root = Path(__file__).resolve().parents[2] + repository_root = Path(__file__).resolve().parents[3] values = { "ats_bin": config.getoption("ats_bin") or os.environ.get("ATS_BIN"), "verifier_bin": config.getoption("proxy_verifier_bin") or os.environ.get("PROXY_VERIFIER_BIN"), @@ -236,21 +229,31 @@ def urtest(uranium_scenario: UraniumTest) -> UraniumTest: @pytest.fixture -def ats(uranium_scenario: UraniumTest) -> Iterator[ATS]: - """Provide one configured Traffic Server with fixture-owned cleanup.""" +def ats_factory(uranium_scenario: UraniumTest) -> Iterator[ATSFactory]: + """Create Traffic Server instances with fixture-owned cleanup.""" - service = ATS(uranium_scenario) + factory = ATSFactory(uranium_scenario) try: - yield service + yield factory finally: - service.close() + factory.close() + + +@pytest.fixture +def ats(ats_factory: ATSFactory) -> ATS: + """Provide one configured Traffic Server with fixture-owned cleanup.""" + + return ats_factory.create("ats") @pytest.fixture def curl(uranium_scenario: UraniumTest) -> Curl: """Provide a curl client rooted in this test's sandbox.""" - return Curl(Path(uranium_scenario.RunDirectory)) + return Curl( + Path(uranium_scenario.RunDirectory), + use_uds=bool(uranium_scenario.Variables.get("CurlUds", False)), + ) def _partition_items(items: list[pytest.Item], predicate: Callable[[pytest.Item], @@ -264,15 +267,6 @@ def _partition_items(items: list[pytest.Item], predicate: Callable[[pytest.Item] return selected, rejected -def _matches_filter(item: pytest.Item, patterns: list[str]) -> bool: - """Match procedural basenames, direct replay names, and pytest node IDs.""" - - names = {item.name, item.path.name, item.nodeid} - names.update({name.removesuffix(suffix) for name in names for suffix in (".py", ".test.yaml", ".test.yml")}) - names.update({name.removeprefix("test_") for name in names}) - return any(fnmatch.fnmatch(name, pattern) for name in names for pattern in patterns) - - def _is_below_uranium_directory(item: pytest.Item, directories: set[str]) -> bool: """Return whether an item is in a curl-UDS-incompatible directory.""" @@ -287,7 +281,7 @@ def _is_below_uranium_directory(item: pytest.Item, directories: set[str]) -> boo def _is_serial_test(test_path: Path) -> bool: """Read the existing list of tests that require exclusive execution.""" - tests_root = Path(__file__).resolve().parents[1] + tests_root = Path(__file__).resolve().parents[2] try: relative = test_path.resolve().relative_to(tests_root / "uranium_tests").as_posix() except ValueError: diff --git a/tests/uranium_testkit/process.py b/tests/tools/uranium/process.py similarity index 100% rename from tests/uranium_testkit/process.py rename to tests/tools/uranium/process.py diff --git a/tests/uranium_testkit/replay.py b/tests/tools/uranium/replay.py similarity index 99% rename from tests/uranium_testkit/replay.py rename to tests/tools/uranium/replay.py index 7f8f81d2bf7..b47810f40af 100644 --- a/tests/uranium_testkit/replay.py +++ b/tests/tools/uranium/replay.py @@ -317,7 +317,7 @@ def _prepare_ats_tree(self, root: Path) -> dict[str, Path]: body_factory = Path(self.runtime.layout["SYSCONFDIR"]) / "body_factory" if body_factory.is_dir(): shutil.copytree(body_factory, paths["body_factory"], dirs_exist_ok=True) - min_config = self.runtime.repository_root / "tests" / "uranium_testkit" / "min_cfg" + min_config = self.runtime.repository_root / "tests" / "tools" / "uranium" / "min_cfg" for source in min_config.iterdir(): if source.is_file() and source.name != "readme.txt": shutil.copy2(source, paths["config"] / source.name) diff --git a/tests/uranium_testkit/runner.py b/tests/tools/uranium/runner.py similarity index 88% rename from tests/uranium_testkit/runner.py rename to tests/tools/uranium/runner.py index 7c40566630d..db2c639f08f 100644 --- a/tests/uranium_testkit/runner.py +++ b/tests/tools/uranium/runner.py @@ -132,6 +132,8 @@ def run_configured(config: ConfiguredBuild, arguments: Sequence[str]) -> int: return launch_docker(config.source_root, test_arguments) pytest_arguments = translate_arguments(test_arguments, os.environ) + if any(argument in ("-h", "--help") for argument in test_arguments): + print(runner_help(), flush=True) if config.curl_uds and "--curl-uds" not in pytest_arguments: pytest_arguments.append("--curl-uds") command = [ @@ -142,8 +144,8 @@ def run_configured(config: ConfiguredBuild, arguments: Sequence[str]) -> int: "pytest", "--import-mode=importlib", "-p", - "uranium_testkit.plugin", - str(config.source_root / "tests" / "pytest_tests"), + "tools.uranium.plugin", + str(config.source_root / "tests" / "tools" / "uranium" / "tests"), str(config.source_root / "tests" / "uranium_tests"), f"--ats-bin={config.install_prefix / 'bin'}", f"--proxy-verifier-bin={config.verifier_bin}", @@ -173,6 +175,24 @@ def run_configured(config: ConfiguredBuild, arguments: Sequence[str]) -> int: return result.returncode +def runner_help() -> str: + """Describe wrapper-owned options before pytest renders its own help.""" + + return f"""usage: urtest.sh [urtest.sh options] [pytest options] + +urtest.sh options (consumed before pytest; these spellings take precedence): + -h, --help Show this section followed by pytest's help. + --run-in-docker Run in {DEFAULT_IMAGE}. + --no-run-in-docker Run in the current environment. + +Docker is the default unless urtest.sh is already running inside the official +Fedora 44 test container. Any argument not consumed above is passed to pytest. +Common pytest options include -k for selection, -n for parallel workers, and +--collect-only/--co for listing tests; they are documented below. + +pytest options (passed through after urtest.sh processing):""" + + def choose_docker_mode(arguments: Sequence[str]) -> tuple[bool, list[str]]: """Apply explicit Docker flags, then the official-container default.""" @@ -279,41 +299,9 @@ def launch_docker(source_root: Path, arguments: Sequence[str]) -> int: def translate_arguments(arguments: Sequence[str], environment: Mapping[str, str]) -> list[str]: - """Translate the established ``urtest.sh`` CLI into pytest options.""" - - translated = [] - filters = [] - index = 0 - while index < len(arguments): - argument = arguments[index] - if argument in ("-j", "--jobs"): - value, index = _required_value(arguments, index, argument) - translated.extend(["-n", value, "--dist", "loadgroup"]) - elif argument.startswith("-j") and argument != "-j": - translated.extend(["-n", argument[2:], "--dist", "loadgroup"]) - elif argument.startswith("--jobs="): - translated.extend(["-n", argument.split("=", 1)[1], "--dist", "loadgroup"]) - elif argument in ("-f", "--filter", "--filters"): - index += 1 - start = index - while index < len(arguments) and not arguments[index].startswith("-"): - filters.append(arguments[index]) - index += 1 - if start == index: - raise RunnerError(f"{argument} requires at least one test name") - continue - elif argument.startswith(("--filter=", "--filters=")): - filters.append(argument.split("=", 1)[1]) - elif argument in ("-C", "--clean"): - value, index = _required_value(arguments, index, argument) - translated.extend(["--legacy-clean", value]) - elif argument.startswith("--clean="): - translated.extend(["--legacy-clean", argument.split("=", 1)[1]]) - elif argument == "--list": - translated.extend(["--collect-only", "-q"]) - else: - translated.append(argument) - index += 1 + """Add CI sharding options without reinterpreting pytest arguments.""" + + translated = list(arguments) shard_count = _nonnegative_integer(environment.get("SHARDCNT")) shard_index = _nonnegative_integer(environment.get("SHARD")) @@ -321,9 +309,6 @@ def translate_arguments(arguments: Sequence[str], environment: Mapping[str, str] if shard_index is None or shard_index >= shard_count: raise RunnerError("SHARD and SHARDCNT must satisfy 0 <= SHARD < SHARDCNT") translated.extend(["--urtest-shard-index", str(shard_index), "--urtest-shard-count", str(shard_count)]) - else: - for pattern in filters: - translated.extend(["--urtest-filter", pattern]) return translated @@ -359,15 +344,6 @@ def _parse_configured_build(arguments: list[str]) -> tuple[ConfiguredBuild, list ), test_arguments -def _required_value(arguments: Sequence[str], index: int, option: str) -> tuple[str, int]: - """Return the following CLI value and its index.""" - - value_index = index + 1 - if value_index >= len(arguments): - raise RunnerError(f"{option} requires a value") - return arguments[value_index], value_index - - def _nonnegative_integer(value: str | None) -> int | None: """Parse a nonnegative environment integer.""" diff --git a/tests/uranium_testkit/runtime.py b/tests/tools/uranium/runtime.py similarity index 100% rename from tests/uranium_testkit/runtime.py rename to tests/tools/uranium/runtime.py diff --git a/tests/uranium_testkit/scenario.py b/tests/tools/uranium/scenario.py similarity index 99% rename from tests/uranium_testkit/scenario.py rename to tests/tools/uranium/scenario.py index 1887c76ac02..4625d5a0694 100644 --- a/tests/uranium_testkit/scenario.py +++ b/tests/tools/uranium/scenario.py @@ -1230,7 +1230,7 @@ def _make_ats_process( installed_body_factory = Path(self.runtime.layout["SYSCONFDIR"]) / "body_factory" if installed_body_factory.is_dir(): shutil.copytree(installed_body_factory, body_factory, dirs_exist_ok=True) - min_config = self.runtime.repository_root / "tests" / "uranium_testkit" / "min_cfg" + min_config = self.runtime.repository_root / "tests" / "tools" / "uranium" / "min_cfg" if min_config.is_dir(): for source in min_config.iterdir(): if source.is_file() and source.name != "readme.txt": diff --git a/tests/uranium_testkit/services.py b/tests/tools/uranium/services.py similarity index 63% rename from tests/uranium_testkit/services.py rename to tests/tools/uranium/services.py index 059a1321369..6f563b4e0c6 100644 --- a/tests/uranium_testkit/services.py +++ b/tests/tools/uranium/services.py @@ -21,6 +21,7 @@ from dataclasses import dataclass from pathlib import Path import subprocess +from typing import Any from .scenario import FileNode, Process, UraniumTest @@ -56,8 +57,8 @@ def update(self, values: Mapping[str, object]) -> None: class ATS: """A fixture-owned Traffic Server process.""" - def __init__(self, scenario: UraniumTest, name: str = "ats") -> None: - self._process: Process = scenario.MakeATSProcess(name) + def __init__(self, scenario: UraniumTest, name: str = "ats", **process_options: Any) -> None: + self._process: Process = scenario.MakeATSProcess(name, **process_options) self._was_started = False self.records = RecordsConfig(self._process.Disk.records_config) @@ -67,6 +68,12 @@ def http_port(self) -> int: return int(self._process.Variables.port) + @property + def uds_path(self) -> str: + """Return the Unix-domain socket path selected for this process.""" + + return str(self._process.Variables.uds_path) + @property def is_running(self) -> bool: """Return whether Traffic Server is still running.""" @@ -87,26 +94,67 @@ def close(self) -> None: self._process.validate() +class ATSFactory: + """Create Traffic Server instances owned by one pytest scenario.""" + + def __init__(self, scenario: UraniumTest) -> None: + self._scenario = scenario + self._services: list[ATS] = [] + self._names: set[str] = set() + + def create(self, name: str | None = None, **process_options: Any) -> ATS: + """Create a named Traffic Server with fixture-owned cleanup.""" + + process_name = name or f"ats{len(self._services) + 1}" + if process_name in self._names: + raise ValueError(f"Traffic Server process {process_name!r} already exists") + service = ATS(self._scenario, process_name, **process_options) + + self._services.append(service) + self._names.add(process_name) + return service + + def close(self) -> None: + """Stop and validate every created instance in reverse order.""" + + failures: list[Exception] = [] + for service in reversed(self._services): + try: + service.close() + except Exception as error: + failures.append(error) + self._services.clear() + self._names.clear() + if failures: + raise ExceptionGroup("Traffic Server cleanup failed", failures) + + class Curl: """Run curl requests and return ordinary Python result objects.""" - def __init__(self, working_directory: Path) -> None: + def __init__(self, working_directory: Path, *, use_uds: bool = False) -> None: self._working_directory = working_directory + self._use_uds = use_uds def get( self, - url: str, + ats: ATS, + path: str = "/", *, headers: Mapping[str, str] | None = None, options: Sequence[str] = (), timeout: float = 30, ) -> CommandResult: - """Issue a GET request to @a url.""" + """Issue a GET request to @a path through @a ats.""" - arguments = [*options] + request_path = path if path.startswith("/") else f"/{path}" + arguments = [] + if self._use_uds: + arguments.extend(["--unix-socket", ats.uds_path]) + arguments.extend(options) for name, value in (headers or {}).items(): arguments.extend(["--header", f"{name}: {value}"]) - arguments.append(url) + arguments.append(f"http://127.0.0.1:{ats.http_port}{request_path}") return self.run(*arguments, timeout=timeout) def run(self, *arguments: str, timeout: float = 30) -> CommandResult: diff --git a/tests/pytest_tests/test_config.py b/tests/tools/uranium/tests/test_config.py similarity index 92% rename from tests/pytest_tests/test_config.py rename to tests/tools/uranium/tests/test_config.py index 2de281a142b..d67efbcd183 100644 --- a/tests/pytest_tests/test_config.py +++ b/tests/tools/uranium/tests/test_config.py @@ -20,13 +20,13 @@ import pytest import yaml -from uranium_testkit.config import ReplayConfigError, ReplaySpec, merge_flat_records, replace_server_ports +from tools.uranium.config import ReplayConfigError, ReplaySpec, merge_flat_records, replace_server_ports def test_all_migrated_replays_are_valid() -> None: """Verify every directly collected replay has valid test metadata.""" - uranium_tests = Path(__file__).parents[1] / "uranium_tests" + uranium_tests = Path(__file__).parents[3] / "uranium_tests" replay_files = list(uranium_tests.rglob("*.test.yaml")) assert len(replay_files) >= 96 for path in replay_files: @@ -36,7 +36,7 @@ def test_all_migrated_replays_are_valid() -> None: def test_no_uranium_test_registers_an_uranium_replay() -> None: """Keep replay ownership out of legacy Python wrappers.""" - uranium_tests = Path(__file__).parents[1] / "uranium_tests" + uranium_tests = Path(__file__).parents[3] / "uranium_tests" registrations = [path for path in uranium_tests.rglob("test_*.py") if "Test.ATSReplayTest(" in path.read_text()] assert registrations == [] @@ -44,7 +44,7 @@ def test_no_uranium_test_registers_an_uranium_replay() -> None: def test_all_bespoke_tests_are_available_to_pytest() -> None: """Keep the compatibility inventory explicit while definitions migrate.""" - uranium_tests = Path(__file__).parents[1] / "uranium_tests" + uranium_tests = Path(__file__).parents[3] / "uranium_tests" assert len(list(uranium_tests.rglob("test_*.py"))) == 505 diff --git a/tests/pytest_tests/test_process.py b/tests/tools/uranium/tests/test_process.py similarity index 95% rename from tests/pytest_tests/test_process.py rename to tests/tools/uranium/tests/test_process.py index 1492723c78c..3e475f98977 100644 --- a/tests/pytest_tests/test_process.py +++ b/tests/tools/uranium/tests/test_process.py @@ -20,8 +20,8 @@ import pytest -from uranium_testkit.process import ManagedProcess, ProcessError -from uranium_testkit.replay import ReplayTest +from tools.uranium.process import ManagedProcess, ProcessError +from tools.uranium.replay import ReplayTest def test_managed_process_captures_output(tmp_path: Path) -> None: diff --git a/tests/pytest_tests/test_runner.py b/tests/tools/uranium/tests/test_runner.py similarity index 72% rename from tests/pytest_tests/test_runner.py rename to tests/tools/uranium/tests/test_runner.py index cad42e3f88f..5b357509647 100644 --- a/tests/pytest_tests/test_runner.py +++ b/tests/tools/uranium/tests/test_runner.py @@ -21,7 +21,7 @@ import pytest -from uranium_testkit.runner import ( +from tools.uranium.runner import ( RunnerError, _cmake_cache_value, _copy_sandbox_artifacts, @@ -29,6 +29,7 @@ choose_docker_mode, is_official_test_container, launch_docker, + runner_help, translate_arguments, ) @@ -62,35 +63,47 @@ def test_official_container_requires_fedora_44_and_container_marker(tmp_path: Pa assert not is_official_test_container(tmp_path) -def test_urtest_arguments_translate_to_pytest() -> None: - """Preserve the common established command-line spellings.""" +def test_pytest_arguments_pass_through_unchanged() -> None: + """Leave selection, parallelism, collection, and sandbox options to pytest.""" - translated = translate_arguments( - ["-j2", "-v", "--sandbox", "/tmp/sb", "--clean=none", "-f", "cache-*", "tls-basic"], - {}, - ) + arguments = ["-n", "2", "-v", "--sandbox", "/tmp/sb", "--collect-only", "-k", "cache or tls"] + + assert translate_arguments(arguments, {}) == arguments + + +def test_runner_help_separates_wrapper_and_pytest_options() -> None: + """Explain the wrapper's priority before displaying downstream help.""" + + help_text = runner_help() + + assert "urtest.sh options (consumed before pytest; these spellings take precedence):" in help_text + assert "-k for selection, -n for parallel workers" in help_text + assert "--collect-only/--co for listing tests" in help_text + assert "-j N" not in help_text + assert "--clean" not in help_text + assert "--list" not in help_text + assert "Docker is the default" in help_text + assert help_text.endswith("pytest options (passed through after urtest.sh processing):") + + +def test_ci_sharding_preserves_pytest_selection() -> None: + """Combine native pytest selection with stable CI sharding.""" + + translated = translate_arguments(["-k", "legacy-one or legacy-two"], {"SHARD": "3", "SHARDCNT": "12"}) assert translated == [ - "-n", - "2", - "--dist", - "loadgroup", - "-v", - "--sandbox", - "/tmp/sb", - "--legacy-clean", - "none", - "--urtest-filter", - "cache-*", - "--urtest-filter", - "tls-basic", + "-k", + "legacy-one or legacy-two", + "--urtest-shard-index", + "3", + "--urtest-shard-count", + "12", ] -def test_ci_sharding_replaces_old_python_only_filter_list() -> None: - """Let pytest shard its full inventory when old Jenkins passes Python names.""" +def test_pytest_short_options_are_not_reinterpreted() -> None: + """Leave pytest and plugin short options under pytest's control.""" - translated = translate_arguments(["-f", "legacy-one", "legacy-two"], {"SHARD": "3", "SHARDCNT": "12"}) - assert translated == ["--urtest-shard-index", "3", "--urtest-shard-count", "12"] + assert translate_arguments(["-f", "-k", "basic"], {}) == ["-f", "-k", "basic"] def test_invalid_ci_shard_fails() -> None: @@ -113,16 +126,16 @@ def record_run(command: list[str], *, check: bool) -> subprocess.CompletedProces def find_executable(executable: str) -> str: return f"/usr/bin/{executable}" - monkeypatch.setattr("uranium_testkit.runner.shutil.which", find_executable) - monkeypatch.setattr("uranium_testkit.runner.subprocess.run", record_run) + monkeypatch.setattr("tools.uranium.runner.shutil.which", find_executable) + monkeypatch.setattr("tools.uranium.runner.subprocess.run", record_run) monkeypatch.setenv("SHARD", "3") monkeypatch.setenv("SHARDCNT", "12") - assert launch_docker(tmp_path, ["-j2", "-q"]) == 0 + assert launch_docker(tmp_path, ["-n2", "-q"]) == 0 command = commands[0] assert command[command.index("SHARD=3") - 1] == "--env" assert command[command.index("SHARDCNT=12") - 1] == "--env" - assert command[-4:] == [str(tmp_path / "tests" / "urtest.sh"), "--no-run-in-docker", "-j2", "-q"] + assert command[-4:] == [str(tmp_path / "tests" / "urtest.sh"), "--no-run-in-docker", "-n2", "-q"] def test_source_runner_keeps_a_short_stable_sandbox(tmp_path: Path) -> None: diff --git a/tests/tools/uranium/tests/test_services.py b/tests/tools/uranium/tests/test_services.py new file mode 100644 index 00000000000..f8399727522 --- /dev/null +++ b/tests/tools/uranium/tests/test_services.py @@ -0,0 +1,186 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. +"""Unit tests for procedural Uranium service objects.""" + +from pathlib import Path +import subprocess +from types import SimpleNamespace +from typing import Any + +import pytest + +from tools.uranium.services import ATS, ATSFactory, Curl + + +class FakeProcess: + """Record ATS lifecycle calls without starting a process.""" + + def __init__(self, port: int) -> None: + self.Variables = type("Variables", (), {"port": port, "uds_path": f"/tmp/ats-{port}.sock"})() + self.Disk = SimpleNamespace(records_config=FakeRecords()) + self.was_started = False + self.was_stopped = False + self.was_validated = False + self.validation_error: Exception | None = None + + def start(self) -> None: + self.was_started = True + + def stop(self) -> None: + self.was_stopped = True + + def validate(self) -> None: + self.was_validated = True + if self.validation_error is not None: + raise self.validation_error + + def is_running(self) -> bool: + return self.was_started and not self.was_stopped + + +class FakeRecords: + """Record staged records.yaml updates.""" + + def __init__(self) -> None: + self.values: dict[str, Any] = {} + + def update(self, values: dict[str, Any]) -> None: + self.values.update(values) + + +class FakeScenario: + """Create fake ATS processes with distinct ports.""" + + def __init__(self) -> None: + self.processes: dict[str, FakeProcess] = {} + self.process_options: dict[str, dict[str, Any]] = {} + + def MakeATSProcess(self, name: str, **options: Any) -> FakeProcess: # noqa: N802 + process = FakeProcess(12345 + len(self.processes)) + + self.processes[name] = process + self.process_options[name] = options + return process + + +def test_ats_owns_process_lifecycle() -> None: + """Start, stop, and validate the fixture-owned ATS process.""" + + scenario = FakeScenario() + ats = ATS(scenario) # type: ignore[arg-type] + + assert ats.http_port == 12345 + assert ats.uds_path == "/tmp/ats-12345.sock" + assert not ats.is_running + + ats.records.update({"proxy.config.http.server_ports": "12345"}) + assert scenario.processes["ats"].Disk.records_config.values == {"proxy.config.http.server_ports": "12345"} + + ats.start() + assert ats.is_running + + ats.close() + assert scenario.processes["ats"].was_stopped + assert scenario.processes["ats"].was_validated + + +def test_ats_factory_owns_multiple_process_lifecycles() -> None: + """Create, stop, and validate multiple independent Traffic Servers.""" + + scenario = FakeScenario() + factory = ATSFactory(scenario) # type: ignore[arg-type] + + first = factory.create("first") + second = factory.create("second", enable_cache=False) + + assert first.http_port == 12345 + assert second.http_port == 12346 + assert scenario.process_options["second"] == {"enable_cache": False} + + first.start() + second.start() + factory.close() + + assert all(process.was_stopped for process in scenario.processes.values()) + assert all(process.was_validated for process in scenario.processes.values()) + + +def test_ats_factory_rejects_duplicate_names() -> None: + """Keep each public service handle bound to a distinct process.""" + + factory = ATSFactory(FakeScenario()) # type: ignore[arg-type] + + factory.create("ats") + + with pytest.raises(ValueError, match="already exists"): + factory.create("ats") + + +def test_ats_factory_closes_all_processes_after_validation_failure() -> None: + """Do not leak one Traffic Server when another fails during teardown.""" + + scenario = FakeScenario() + factory = ATSFactory(scenario) # type: ignore[arg-type] + first = factory.create("first") + second = factory.create("second") + first.start() + second.start() + scenario.processes["second"].validation_error = RuntimeError("validation failed") + + with pytest.raises(ExceptionGroup, match="cleanup failed"): + factory.close() + + assert all(process.was_stopped for process in scenario.processes.values()) + assert all(process.was_validated for process in scenario.processes.values()) + + +@pytest.mark.parametrize( + ("use_uds", "transport_arguments"), + [ + (False, ()), + (True, ("--unix-socket", "/tmp/ats-12345.sock")), + ], +) +def test_curl_targets_ats_transport( + monkeypatch: pytest.MonkeyPatch, + tmp_path: Path, + use_uds: bool, + transport_arguments: tuple[str, ...], +) -> None: + """Expose curl status and output for ordinary pytest assertions.""" + + observed: dict[str, Any] = {} + + def run(command: tuple[str, ...], **kwargs: Any) -> subprocess.CompletedProcess[str]: + observed["command"] = command + observed.update(kwargs) + return subprocess.CompletedProcess(command, 7, "response", "diagnostic") + + monkeypatch.setattr("tools.uranium.services.subprocess.run", run) + ats = ATS(FakeScenario()) # type: ignore[arg-type] + + result = Curl(tmp_path, use_uds=use_uds).get(ats, "status", headers={"X-Test": "value"}) + + assert observed["command"] == ( + "curl", + *transport_arguments, + "--header", + "X-Test: value", + "http://127.0.0.1:12345/status", + ) + assert observed["cwd"] == tmp_path + assert result.returncode == 7 + assert result.output == "responsediagnostic" diff --git a/tests/uranium_tests/ats_probe/test_ats_probe.py b/tests/uranium_tests/ats_probe/test_ats_probe.py index 8d35ffedbdb..6df3f7ef236 100644 --- a/tests/uranium_tests/ats_probe/test_ats_probe.py +++ b/tests/uranium_tests/ats_probe/test_ats_probe.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_ats_probe(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/ats_probe/test_cache_dir_probe.py b/tests/uranium_tests/ats_probe/test_cache_dir_probe.py index c2ba2c0891f..72d042c7d6b 100644 --- a/tests/uranium_tests/ats_probe/test_cache_dir_probe.py +++ b/tests/uranium_tests/ats_probe/test_cache_dir_probe.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_cache_dir_probe(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/bad_http_fmt/test_bad_http_fmt.py b/tests/uranium_tests/bad_http_fmt/test_bad_http_fmt.py index 100df0d84da..e5fe0cfd7f0 100644 --- a/tests/uranium_tests/bad_http_fmt/test_bad_http_fmt.py +++ b/tests/uranium_tests/bad_http_fmt/test_bad_http_fmt.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_bad_http_fmt(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/basic/test_basic.py b/tests/uranium_tests/basic/test_basic.py index e37dcd69cd3..5dcb0e8aae4 100644 --- a/tests/uranium_tests/basic/test_basic.py +++ b/tests/uranium_tests/basic/test_basic.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.services import ATS, Curl +from tools.uranium.services import ATS, Curl def test_traffic_server_starts(ats: ATS, curl: Curl) -> None: @@ -22,6 +22,6 @@ def test_traffic_server_starts(ats: ATS, curl: Curl) -> None: ats.start() - result = curl.get(f"http://127.0.0.1:{ats.http_port}") + result = curl.get(ats) assert result.returncode == 0, result.output diff --git a/tests/uranium_tests/basic/test_config.py b/tests/uranium_tests/basic/test_config.py index 38a41588915..16c3c861e22 100644 --- a/tests/uranium_tests/basic/test_config.py +++ b/tests/uranium_tests/basic/test_config.py @@ -14,15 +14,15 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.services import ATS, Curl +from tools.uranium.services import ATS, Curl def test_traffic_server_starts_with_custom_listener(ats: ATS, curl: Curl) -> None: """Verify that records.yaml can replace the default listener configuration.""" - ats.records.update({"proxy.config.http.server_ports": str(ats.http_port)}) + ats.records.update({"proxy.config.http.server_ports": f"{ats.http_port} {ats.uds_path}"}) ats.start() - result = curl.get(f"http://127.0.0.1:{ats.http_port}") + result = curl.get(ats) assert result.returncode == 0, result.output diff --git a/tests/uranium_tests/basic/test_copy_config.py b/tests/uranium_tests/basic/test_copy_config.py index f53a1354743..d71c6d2c4f9 100644 --- a/tests/uranium_tests/basic/test_copy_config.py +++ b/tests/uranium_tests/basic/test_copy_config.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_copy_config(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/basic/test_copy_config2.py b/tests/uranium_tests/basic/test_copy_config2.py index e1a4b9bd280..464d5c577c4 100644 --- a/tests/uranium_tests/basic/test_copy_config2.py +++ b/tests/uranium_tests/basic/test_copy_config2.py @@ -14,51 +14,18 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.services import ATSFactory, Curl -def test_copy_config2(urtest: UraniumTest) -> None: - ''' - ''' - # Licensed to the Apache Software Foundation (ASF) under one - # or more contributor license agreements. See the NOTICE file - # distributed with this work for additional information - # regarding copyright ownership. The ASF licenses this file - # to you under the Apache License, Version 2.0 (the - # "License"); you may not use this file except in compliance - # with the License. You may obtain a copy of the License at - # - # http://www.apache.org/licenses/LICENSE-2.0 - # - # Unless required by applicable law or agreed to in writing, software - # distributed under the License 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. +def test_multiple_traffic_servers_run_concurrently(ats_factory: ATSFactory, curl: Curl) -> None: + """Verify that two independently configured Traffic Servers can run together.""" - urtest.Summary = "Test start up of Traffic server with generated ports of more than one servers at the same time" + servers = [ats_factory.create("ts1"), ats_factory.create("ts2")] + for server in servers: + server.start() - # set up some ATS processes - ts1 = urtest.MakeATSProcess("ts1") - ts2 = urtest.MakeATSProcess("ts2") + for server in servers: + result = curl.get(server) - # setup a testrun - t = urtest.AddTestRun("Talk to ts1") - t.StillRunningAfter = ts1 - t.StillRunningAfter += ts2 - p = t.Processes.Default - t.MakeCurlCommand("127.0.0.1:{0}".format(ts1.Variables.port), ts=ts1) - p.ReturnCode = 0 - p.StartBefore(urtest.Processes.ts1) - p.StartBefore(urtest.Processes.ts2) - - # setup a testrun - t = urtest.AddTestRun("Talk to ts2") - t.StillRunningBefore = ts1 - t.StillRunningBefore += ts2 - t.StillRunningAfter = ts1 - t.StillRunningAfter += ts2 - p = t.Processes.Default - t.MakeCurlCommand("127.0.0.1:{0}".format(ts2.Variables.port), ts=ts2) - p.ReturnCode = 0 - urtest.execute() + assert result.returncode == 0, result.output + assert all(instance.is_running for instance in servers) diff --git a/tests/uranium_tests/basic/test_deny0.py b/tests/uranium_tests/basic/test_deny0.py index dc0be1dc945..c146898d918 100644 --- a/tests/uranium_tests/basic/test_deny0.py +++ b/tests/uranium_tests/basic/test_deny0.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_deny0(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/bigobj/test_bigobj.py b/tests/uranium_tests/bigobj/test_bigobj.py index f9228753d5d..155a07b4279 100644 --- a/tests/uranium_tests/bigobj/test_bigobj.py +++ b/tests/uranium_tests/bigobj/test_bigobj.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_bigobj(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/body_factory/test_http204_response.py b/tests/uranium_tests/body_factory/test_http204_response.py index 1e715d5d412..c762ab5374b 100644 --- a/tests/uranium_tests/body_factory/test_http204_response.py +++ b/tests/uranium_tests/body_factory/test_http204_response.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_http204_response(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/body_factory/test_http204_response_plugin.py b/tests/uranium_tests/body_factory/test_http204_response_plugin.py index 35dd52b02da..3e449d08d4e 100644 --- a/tests/uranium_tests/body_factory/test_http204_response_plugin.py +++ b/tests/uranium_tests/body_factory/test_http204_response_plugin.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_http204_response_plugin(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/body_factory/test_http304_response.py b/tests/uranium_tests/body_factory/test_http304_response.py index 03de8daa1cf..86070b6e9cd 100644 --- a/tests/uranium_tests/body_factory/test_http304_response.py +++ b/tests/uranium_tests/body_factory/test_http304_response.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_http304_response(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/body_factory/test_http_head_no_origin.py b/tests/uranium_tests/body_factory/test_http_head_no_origin.py index a529be7bb1c..3bd0b9fc679 100644 --- a/tests/uranium_tests/body_factory/test_http_head_no_origin.py +++ b/tests/uranium_tests/body_factory/test_http_head_no_origin.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_http_head_no_origin(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/body_factory/test_http_with_origin.py b/tests/uranium_tests/body_factory/test_http_with_origin.py index 4dca582ba88..9bdf1bbcbcd 100644 --- a/tests/uranium_tests/body_factory/test_http_with_origin.py +++ b/tests/uranium_tests/body_factory/test_http_with_origin.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_http_with_origin(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/cache/test_background_fill.py b/tests/uranium_tests/cache/test_background_fill.py index 248e5c16640..399a22badd5 100644 --- a/tests/uranium_tests/cache/test_background_fill.py +++ b/tests/uranium_tests/cache/test_background_fill.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_background_fill(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/cache/test_cache_config_reload.py b/tests/uranium_tests/cache/test_cache_config_reload.py index d8cdafaf072..430e4c176b5 100644 --- a/tests/uranium_tests/cache/test_cache_config_reload.py +++ b/tests/uranium_tests/cache/test_cache_config_reload.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_cache_config_reload(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/cache/test_cache_corrupt_recursive_uaf.py b/tests/uranium_tests/cache/test_cache_corrupt_recursive_uaf.py index 2517dbc70e6..a9465082e83 100644 --- a/tests/uranium_tests/cache/test_cache_corrupt_recursive_uaf.py +++ b/tests/uranium_tests/cache/test_cache_corrupt_recursive_uaf.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_cache_corrupt_recursive_uaf(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/cache/test_cache_generation_clear.py b/tests/uranium_tests/cache/test_cache_generation_clear.py index 4b90efdc42a..3e3031837fb 100644 --- a/tests/uranium_tests/cache/test_cache_generation_clear.py +++ b/tests/uranium_tests/cache/test_cache_generation_clear.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_cache_generation_clear(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/cache/test_cache_generation_disjoint.py b/tests/uranium_tests/cache/test_cache_generation_disjoint.py index c975fff71c7..3bfccfa926d 100644 --- a/tests/uranium_tests/cache/test_cache_generation_disjoint.py +++ b/tests/uranium_tests/cache/test_cache_generation_disjoint.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_cache_generation_disjoint(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/cache/test_cache_shm_bad_disk_dropped.py b/tests/uranium_tests/cache/test_cache_shm_bad_disk_dropped.py index 2072bfb68c2..4b4c5ed2dd4 100644 --- a/tests/uranium_tests/cache/test_cache_shm_bad_disk_dropped.py +++ b/tests/uranium_tests/cache/test_cache_shm_bad_disk_dropped.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_cache_shm_bad_disk_dropped(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/cache/test_cache_shm_concurrent_attach.py b/tests/uranium_tests/cache/test_cache_shm_concurrent_attach.py index e2ac81f9757..6ebcbe909bb 100644 --- a/tests/uranium_tests/cache/test_cache_shm_concurrent_attach.py +++ b/tests/uranium_tests/cache/test_cache_shm_concurrent_attach.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_cache_shm_concurrent_attach(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/cache/test_cache_shm_control_size_mismatch.py b/tests/uranium_tests/cache/test_cache_shm_control_size_mismatch.py index f0b92b6002a..d9e8bba3a29 100644 --- a/tests/uranium_tests/cache/test_cache_shm_control_size_mismatch.py +++ b/tests/uranium_tests/cache/test_cache_shm_control_size_mismatch.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_cache_shm_control_size_mismatch(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/cache/test_cache_shm_dir_invalid.py b/tests/uranium_tests/cache/test_cache_shm_dir_invalid.py index f76158781f0..6cb22760b82 100644 --- a/tests/uranium_tests/cache/test_cache_shm_dir_invalid.py +++ b/tests/uranium_tests/cache/test_cache_shm_dir_invalid.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_cache_shm_dir_invalid(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/cache/test_cache_shm_fast_restart.py b/tests/uranium_tests/cache/test_cache_shm_fast_restart.py index 27704a7d93a..f6874291449 100644 --- a/tests/uranium_tests/cache/test_cache_shm_fast_restart.py +++ b/tests/uranium_tests/cache/test_cache_shm_fast_restart.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_cache_shm_fast_restart(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/cache/test_cache_shm_purge_on_disable.py b/tests/uranium_tests/cache/test_cache_shm_purge_on_disable.py index 7cfb81bdef1..8582bd2c32c 100644 --- a/tests/uranium_tests/cache/test_cache_shm_purge_on_disable.py +++ b/tests/uranium_tests/cache/test_cache_shm_purge_on_disable.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_cache_shm_purge_on_disable(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/cache/test_cache_shm_schema_mismatch.py b/tests/uranium_tests/cache/test_cache_shm_schema_mismatch.py index 30d96bf8f8d..2154d850ad0 100644 --- a/tests/uranium_tests/cache/test_cache_shm_schema_mismatch.py +++ b/tests/uranium_tests/cache/test_cache_shm_schema_mismatch.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_cache_shm_schema_mismatch(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/cache/test_cache_shm_storage_mismatch.py b/tests/uranium_tests/cache/test_cache_shm_storage_mismatch.py index 5765496f5e6..d16bb71b07d 100644 --- a/tests/uranium_tests/cache/test_cache_shm_storage_mismatch.py +++ b/tests/uranium_tests/cache/test_cache_shm_storage_mismatch.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_cache_shm_storage_mismatch(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/cache/test_cache_shm_unclean_shutdown.py b/tests/uranium_tests/cache/test_cache_shm_unclean_shutdown.py index db877be3379..03e88134fb5 100644 --- a/tests/uranium_tests/cache/test_cache_shm_unclean_shutdown.py +++ b/tests/uranium_tests/cache/test_cache_shm_unclean_shutdown.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_cache_shm_unclean_shutdown(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/cache/test_cache_write_lock_contention.py b/tests/uranium_tests/cache/test_cache_write_lock_contention.py index eacc382499c..51ba00379ff 100644 --- a/tests/uranium_tests/cache/test_cache_write_lock_contention.py +++ b/tests/uranium_tests/cache/test_cache_write_lock_contention.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_cache_write_lock_contention(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/cache/test_disjoint_wait_for_cache.py b/tests/uranium_tests/cache/test_disjoint_wait_for_cache.py index 8eade32e3ca..acfaf63bc42 100644 --- a/tests/uranium_tests/cache/test_disjoint_wait_for_cache.py +++ b/tests/uranium_tests/cache/test_disjoint_wait_for_cache.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_disjoint_wait_for_cache(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/cache/test_host_down_range_recursion.py b/tests/uranium_tests/cache/test_host_down_range_recursion.py index 72095dcce5d..dc1b596ff23 100644 --- a/tests/uranium_tests/cache/test_host_down_range_recursion.py +++ b/tests/uranium_tests/cache/test_host_down_range_recursion.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_host_down_range_recursion(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/cache/test_negative_caching.py b/tests/uranium_tests/cache/test_negative_caching.py index 77b49859d81..9af6d72bc90 100644 --- a/tests/uranium_tests/cache/test_negative_caching.py +++ b/tests/uranium_tests/cache/test_negative_caching.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_negative_caching(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/cache/test_proxy_serve_stale_dns_fail.py b/tests/uranium_tests/cache/test_proxy_serve_stale_dns_fail.py index 1245857e9a3..ac7735830fc 100644 --- a/tests/uranium_tests/cache/test_proxy_serve_stale_dns_fail.py +++ b/tests/uranium_tests/cache/test_proxy_serve_stale_dns_fail.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_proxy_serve_stale_dns_fail(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/cache/test_storage_metrics.py b/tests/uranium_tests/cache/test_storage_metrics.py index 69bab85541a..2a75dfc00ae 100644 --- a/tests/uranium_tests/cache/test_storage_metrics.py +++ b/tests/uranium_tests/cache/test_storage_metrics.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_storage_metrics(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/chunked_encoding/test_bad_chunked_encoding.py b/tests/uranium_tests/chunked_encoding/test_bad_chunked_encoding.py index 3bb39e3adf6..da4506e91d5 100644 --- a/tests/uranium_tests/chunked_encoding/test_bad_chunked_encoding.py +++ b/tests/uranium_tests/chunked_encoding/test_bad_chunked_encoding.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_bad_chunked_encoding(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/chunked_encoding/test_chunk_extension_quoted_string.py b/tests/uranium_tests/chunked_encoding/test_chunk_extension_quoted_string.py index a03fb6dd505..fd9397275e3 100644 --- a/tests/uranium_tests/chunked_encoding/test_chunk_extension_quoted_string.py +++ b/tests/uranium_tests/chunked_encoding/test_chunk_extension_quoted_string.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_chunk_extension_quoted_string(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/chunked_encoding/test_chunk_trailer_bare_lf.py b/tests/uranium_tests/chunked_encoding/test_chunk_trailer_bare_lf.py index bc89fe4d744..63945a9b9d7 100644 --- a/tests/uranium_tests/chunked_encoding/test_chunk_trailer_bare_lf.py +++ b/tests/uranium_tests/chunked_encoding/test_chunk_trailer_bare_lf.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_chunk_trailer_bare_lf(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/chunked_encoding/test_chunked_encoding.py b/tests/uranium_tests/chunked_encoding/test_chunked_encoding.py index 0d7e99cf807..39cbda258b8 100644 --- a/tests/uranium_tests/chunked_encoding/test_chunked_encoding.py +++ b/tests/uranium_tests/chunked_encoding/test_chunked_encoding.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_chunked_encoding(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/chunked_encoding/test_chunked_encoding_disabled.py b/tests/uranium_tests/chunked_encoding/test_chunked_encoding_disabled.py index df66e3f4878..abd800b9167 100644 --- a/tests/uranium_tests/chunked_encoding/test_chunked_encoding_disabled.py +++ b/tests/uranium_tests/chunked_encoding/test_chunked_encoding_disabled.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_chunked_encoding_disabled(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/chunked_encoding/test_chunked_encoding_h2.py b/tests/uranium_tests/chunked_encoding/test_chunked_encoding_h2.py index c9c88b7e3b7..84d56ea873a 100644 --- a/tests/uranium_tests/chunked_encoding/test_chunked_encoding_h2.py +++ b/tests/uranium_tests/chunked_encoding/test_chunked_encoding_h2.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_chunked_encoding_h2(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/client_connection/test_per_client_connection_max.py b/tests/uranium_tests/client_connection/test_per_client_connection_max.py index cac8f00c315..214795ac4ec 100644 --- a/tests/uranium_tests/client_connection/test_per_client_connection_max.py +++ b/tests/uranium_tests/client_connection/test_per_client_connection_max.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_per_client_connection_max(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/command_argument/test_verify_global_plugin.py b/tests/uranium_tests/command_argument/test_verify_global_plugin.py index cc5208d8717..61244df8c33 100644 --- a/tests/uranium_tests/command_argument/test_verify_global_plugin.py +++ b/tests/uranium_tests/command_argument/test_verify_global_plugin.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_verify_global_plugin(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/command_argument/test_verify_remap_plugin.py b/tests/uranium_tests/command_argument/test_verify_remap_plugin.py index 909ae114d71..78da4c88c6e 100644 --- a/tests/uranium_tests/command_argument/test_verify_remap_plugin.py +++ b/tests/uranium_tests/command_argument/test_verify_remap_plugin.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_verify_remap_plugin(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/config_processor/test_config_destroy_thread.py b/tests/uranium_tests/config_processor/test_config_destroy_thread.py index 2458cddccf4..bdcf59f6323 100644 --- a/tests/uranium_tests/config_processor/test_config_destroy_thread.py +++ b/tests/uranium_tests/config_processor/test_config_destroy_thread.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_config_destroy_thread(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/connect/test_connect.py b/tests/uranium_tests/connect/test_connect.py index ae1f268badb..02b2a04839d 100644 --- a/tests/uranium_tests/connect/test_connect.py +++ b/tests/uranium_tests/connect/test_connect.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_connect(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/connect/test_connect_handshake.py b/tests/uranium_tests/connect/test_connect_handshake.py index 26cdf3f3761..21bcb7c6f5e 100644 --- a/tests/uranium_tests/connect/test_connect_handshake.py +++ b/tests/uranium_tests/connect/test_connect_handshake.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_connect_handshake(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/connect/test_connect_parent_error_body.py b/tests/uranium_tests/connect/test_connect_parent_error_body.py index 82654fe7a7e..ee53772cce9 100644 --- a/tests/uranium_tests/connect/test_connect_parent_error_body.py +++ b/tests/uranium_tests/connect/test_connect_parent_error_body.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_connect_parent_error_body(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/connect/test_h2_malformed_request_logging.py b/tests/uranium_tests/connect/test_h2_malformed_request_logging.py index ce88b15563c..930123f84bf 100644 --- a/tests/uranium_tests/connect/test_h2_malformed_request_logging.py +++ b/tests/uranium_tests/connect/test_h2_malformed_request_logging.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_h2_malformed_request_logging(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/connect_down_policy/test_connect_down_policy.py b/tests/uranium_tests/connect_down_policy/test_connect_down_policy.py index e1da03698c5..e9bad4d4b56 100644 --- a/tests/uranium_tests/connect_down_policy/test_connect_down_policy.py +++ b/tests/uranium_tests/connect_down_policy/test_connect_down_policy.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_connect_down_policy(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/cont_schedule/test_schedule_every_on_entire_pool.py b/tests/uranium_tests/cont_schedule/test_schedule_every_on_entire_pool.py index 15e247bc872..d56a0f0f6af 100644 --- a/tests/uranium_tests/cont_schedule/test_schedule_every_on_entire_pool.py +++ b/tests/uranium_tests/cont_schedule/test_schedule_every_on_entire_pool.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_schedule_every_on_entire_pool(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/cont_schedule/test_schedule_every_on_pool.py b/tests/uranium_tests/cont_schedule/test_schedule_every_on_pool.py index 2fc114f32bb..649882f51bd 100644 --- a/tests/uranium_tests/cont_schedule/test_schedule_every_on_pool.py +++ b/tests/uranium_tests/cont_schedule/test_schedule_every_on_pool.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_schedule_every_on_pool(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/cont_schedule/test_schedule_every_on_thread.py b/tests/uranium_tests/cont_schedule/test_schedule_every_on_thread.py index 4b893b673ca..c93f1643b01 100644 --- a/tests/uranium_tests/cont_schedule/test_schedule_every_on_thread.py +++ b/tests/uranium_tests/cont_schedule/test_schedule_every_on_thread.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_schedule_every_on_thread(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/cont_schedule/test_schedule_on_entire_pool.py b/tests/uranium_tests/cont_schedule/test_schedule_on_entire_pool.py index 29d2a7dc99f..fc37b6298d7 100644 --- a/tests/uranium_tests/cont_schedule/test_schedule_on_entire_pool.py +++ b/tests/uranium_tests/cont_schedule/test_schedule_on_entire_pool.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_schedule_on_entire_pool(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/cont_schedule/test_schedule_on_pool.py b/tests/uranium_tests/cont_schedule/test_schedule_on_pool.py index 31d7a8ebb5e..28d5b958d50 100644 --- a/tests/uranium_tests/cont_schedule/test_schedule_on_pool.py +++ b/tests/uranium_tests/cont_schedule/test_schedule_on_pool.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_schedule_on_pool(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/cont_schedule/test_schedule_on_thread.py b/tests/uranium_tests/cont_schedule/test_schedule_on_thread.py index 587d5234fbd..b7a6fb69ddf 100644 --- a/tests/uranium_tests/cont_schedule/test_schedule_on_thread.py +++ b/tests/uranium_tests/cont_schedule/test_schedule_on_thread.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_schedule_on_thread(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/cont_schedule/test_thread_affinity.py b/tests/uranium_tests/cont_schedule/test_thread_affinity.py index f794ecea322..fcc58b7afdf 100644 --- a/tests/uranium_tests/cont_schedule/test_thread_affinity.py +++ b/tests/uranium_tests/cont_schedule/test_thread_affinity.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_thread_affinity(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/continuations/test_double.py b/tests/uranium_tests/continuations/test_double.py index 38171891824..75dde895d74 100644 --- a/tests/uranium_tests/continuations/test_double.py +++ b/tests/uranium_tests/continuations/test_double.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_double(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/continuations/test_double_h2.py b/tests/uranium_tests/continuations/test_double_h2.py index 6855d031df2..0bf3e7d36dc 100644 --- a/tests/uranium_tests/continuations/test_double_h2.py +++ b/tests/uranium_tests/continuations/test_double_h2.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_double_h2(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/continuations/test_openclose.py b/tests/uranium_tests/continuations/test_openclose.py index 56587bbf86f..599b88bf3f8 100644 --- a/tests/uranium_tests/continuations/test_openclose.py +++ b/tests/uranium_tests/continuations/test_openclose.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_openclose(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/continuations/test_openclose_h2.py b/tests/uranium_tests/continuations/test_openclose_h2.py index ed232774436..3e5f9a054f7 100644 --- a/tests/uranium_tests/continuations/test_openclose_h2.py +++ b/tests/uranium_tests/continuations/test_openclose_h2.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_openclose_h2(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/continuations/test_session_id.py b/tests/uranium_tests/continuations/test_session_id.py index 8c36c8458dd..81c77ec1746 100644 --- a/tests/uranium_tests/continuations/test_session_id.py +++ b/tests/uranium_tests/continuations/test_session_id.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_session_id(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/cripts/test_cripts.py b/tests/uranium_tests/cripts/test_cripts.py index 45d32d24b2c..db1e1a6cd37 100644 --- a/tests/uranium_tests/cripts/test_cripts.py +++ b/tests/uranium_tests/cripts/test_cripts.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_cripts(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/dns/test_dns_down_nameserver.py b/tests/uranium_tests/dns/test_dns_down_nameserver.py index 739c5001880..e0ea98ad0a2 100644 --- a/tests/uranium_tests/dns/test_dns_down_nameserver.py +++ b/tests/uranium_tests/dns/test_dns_down_nameserver.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_dns_down_nameserver(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/dns/test_dns_host_down.py b/tests/uranium_tests/dns/test_dns_host_down.py index ee293f41016..963a4341535 100644 --- a/tests/uranium_tests/dns/test_dns_host_down.py +++ b/tests/uranium_tests/dns/test_dns_host_down.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_dns_host_down(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/dns/test_dns_reverse_lookup.py b/tests/uranium_tests/dns/test_dns_reverse_lookup.py index 6d5e08ea693..9e3710e3c65 100644 --- a/tests/uranium_tests/dns/test_dns_reverse_lookup.py +++ b/tests/uranium_tests/dns/test_dns_reverse_lookup.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_dns_reverse_lookup(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/dns/test_dns_ttl.py b/tests/uranium_tests/dns/test_dns_ttl.py index c1545860d88..a077b449c5f 100644 --- a/tests/uranium_tests/dns/test_dns_ttl.py +++ b/tests/uranium_tests/dns/test_dns_ttl.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_dns_ttl(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/dns/test_splitdns.py b/tests/uranium_tests/dns/test_splitdns.py index 981de9c8e0c..b5b346f8a54 100644 --- a/tests/uranium_tests/dns/test_splitdns.py +++ b/tests/uranium_tests/dns/test_splitdns.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_splitdns(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/dns/test_splitdns_reload.py b/tests/uranium_tests/dns/test_splitdns_reload.py index 81b1ee33d80..d7ac25405aa 100644 --- a/tests/uranium_tests/dns/test_splitdns_reload.py +++ b/tests/uranium_tests/dns/test_splitdns_reload.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_splitdns_reload(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/early_hints/test_early_hints.py b/tests/uranium_tests/early_hints/test_early_hints.py index 2e69e50bb84..bb2bcbbd3d8 100644 --- a/tests/uranium_tests/early_hints/test_early_hints.py +++ b/tests/uranium_tests/early_hints/test_early_hints.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_early_hints(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/forward_proxy/test_forward_proxy.py b/tests/uranium_tests/forward_proxy/test_forward_proxy.py index 6dba61d5ab0..eb92c16a866 100644 --- a/tests/uranium_tests/forward_proxy/test_forward_proxy.py +++ b/tests/uranium_tests/forward_proxy/test_forward_proxy.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_forward_proxy(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/h2/grpc/test_grpc.py b/tests/uranium_tests/h2/grpc/test_grpc.py index 34811c236d4..86943f4d647 100644 --- a/tests/uranium_tests/h2/grpc/test_grpc.py +++ b/tests/uranium_tests/h2/grpc/test_grpc.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_grpc(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/h2/test_h2_origin_cert_reverify.py b/tests/uranium_tests/h2/test_h2_origin_cert_reverify.py index d739165d6e7..b6809ef825d 100644 --- a/tests/uranium_tests/h2/test_h2_origin_cert_reverify.py +++ b/tests/uranium_tests/h2/test_h2_origin_cert_reverify.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_h2_origin_cert_reverify(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/h2/test_h2_origin_trailers_h1.py b/tests/uranium_tests/h2/test_h2_origin_trailers_h1.py index 8019d7d6ca6..11f59975e2a 100644 --- a/tests/uranium_tests/h2/test_h2_origin_trailers_h1.py +++ b/tests/uranium_tests/h2/test_h2_origin_trailers_h1.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_h2_origin_trailers_h1(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/h2/test_h2disable.py b/tests/uranium_tests/h2/test_h2disable.py index 22ed01ab028..d044be8ea9f 100644 --- a/tests/uranium_tests/h2/test_h2disable.py +++ b/tests/uranium_tests/h2/test_h2disable.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_h2disable(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/h2/test_h2disable_no_accept_threads.py b/tests/uranium_tests/h2/test_h2disable_no_accept_threads.py index d7660fc530f..df69b2a9ef6 100644 --- a/tests/uranium_tests/h2/test_h2disable_no_accept_threads.py +++ b/tests/uranium_tests/h2/test_h2disable_no_accept_threads.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_h2disable_no_accept_threads(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/h2/test_h2enable.py b/tests/uranium_tests/h2/test_h2enable.py index 9321c8d7d0b..832a2673e13 100644 --- a/tests/uranium_tests/h2/test_h2enable.py +++ b/tests/uranium_tests/h2/test_h2enable.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_h2enable(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/h2/test_h2enable_no_accept_threads.py b/tests/uranium_tests/h2/test_h2enable_no_accept_threads.py index f100420cfab..2adbde86851 100644 --- a/tests/uranium_tests/h2/test_h2enable_no_accept_threads.py +++ b/tests/uranium_tests/h2/test_h2enable_no_accept_threads.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_h2enable_no_accept_threads(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/h2/test_h2get_with_body.py b/tests/uranium_tests/h2/test_h2get_with_body.py index 614a1540483..2821f3e1b1e 100644 --- a/tests/uranium_tests/h2/test_h2get_with_body.py +++ b/tests/uranium_tests/h2/test_h2get_with_body.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_h2get_with_body(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/h2/test_h2origin.py b/tests/uranium_tests/h2/test_h2origin.py index 27c15c7de8a..3126e4da299 100644 --- a/tests/uranium_tests/h2/test_h2origin.py +++ b/tests/uranium_tests/h2/test_h2origin.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_h2origin(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/h2/test_h2origin_single_thread.py b/tests/uranium_tests/h2/test_h2origin_single_thread.py index 2b277ae149e..1111d092dca 100644 --- a/tests/uranium_tests/h2/test_h2origin_single_thread.py +++ b/tests/uranium_tests/h2/test_h2origin_single_thread.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_h2origin_single_thread(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/h2/test_h2spec.py b/tests/uranium_tests/h2/test_h2spec.py index ac8f96c983e..981b614bc57 100644 --- a/tests/uranium_tests/h2/test_h2spec.py +++ b/tests/uranium_tests/h2/test_h2spec.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_h2spec(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/h2/test_http2.py b/tests/uranium_tests/h2/test_http2.py index ad1c1c86a83..2b5f32c8ab3 100644 --- a/tests/uranium_tests/h2/test_http2.py +++ b/tests/uranium_tests/h2/test_http2.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_http2(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/h2/test_http2_close_connection.py b/tests/uranium_tests/h2/test_http2_close_connection.py index 9299b2a3795..d8bc8ba4744 100644 --- a/tests/uranium_tests/h2/test_http2_close_connection.py +++ b/tests/uranium_tests/h2/test_http2_close_connection.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_http2_close_connection(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/h2/test_http2_concurrent_streams.py b/tests/uranium_tests/h2/test_http2_concurrent_streams.py index 382c01b3bfe..7daa453c57a 100644 --- a/tests/uranium_tests/h2/test_http2_concurrent_streams.py +++ b/tests/uranium_tests/h2/test_http2_concurrent_streams.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_http2_concurrent_streams(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/h2/test_http2_crlf_header_validation.py b/tests/uranium_tests/h2/test_http2_crlf_header_validation.py index 6c3e245ce3f..52d15f0fd87 100644 --- a/tests/uranium_tests/h2/test_http2_crlf_header_validation.py +++ b/tests/uranium_tests/h2/test_http2_crlf_header_validation.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_http2_crlf_header_validation(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/h2/test_http2_empty_data_frame.py b/tests/uranium_tests/h2/test_http2_empty_data_frame.py index 7ccb9a60c4c..4d48c6ecceb 100644 --- a/tests/uranium_tests/h2/test_http2_empty_data_frame.py +++ b/tests/uranium_tests/h2/test_http2_empty_data_frame.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_http2_empty_data_frame(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/h2/test_http2_flow_control.py b/tests/uranium_tests/h2/test_http2_flow_control.py index c0c3abb5fa0..960f32a811c 100644 --- a/tests/uranium_tests/h2/test_http2_flow_control.py +++ b/tests/uranium_tests/h2/test_http2_flow_control.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_http2_flow_control(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/h2/test_http2_max_active_streams.py b/tests/uranium_tests/h2/test_http2_max_active_streams.py index 4c8b79d927c..f3300b00123 100644 --- a/tests/uranium_tests/h2/test_http2_max_active_streams.py +++ b/tests/uranium_tests/h2/test_http2_max_active_streams.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_http2_max_active_streams(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/h2/test_http2_priority.py b/tests/uranium_tests/h2/test_http2_priority.py index 45f8e5f6383..29aa4b6e424 100644 --- a/tests/uranium_tests/h2/test_http2_priority.py +++ b/tests/uranium_tests/h2/test_http2_priority.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_http2_priority(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/h2/test_http2_rst_stream.py b/tests/uranium_tests/h2/test_http2_rst_stream.py index d4a708f4870..54680b7e1f4 100644 --- a/tests/uranium_tests/h2/test_http2_rst_stream.py +++ b/tests/uranium_tests/h2/test_http2_rst_stream.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_http2_rst_stream(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/h2/test_http2_txn_start_read_gate.py b/tests/uranium_tests/h2/test_http2_txn_start_read_gate.py index a423a5aa58f..5338be6757e 100644 --- a/tests/uranium_tests/h2/test_http2_txn_start_read_gate.py +++ b/tests/uranium_tests/h2/test_http2_txn_start_read_gate.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_http2_txn_start_read_gate(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/h2/test_http2_write_threshold.py b/tests/uranium_tests/h2/test_http2_write_threshold.py index 8c51bf84d79..ea27005b9ec 100644 --- a/tests/uranium_tests/h2/test_http2_write_threshold.py +++ b/tests/uranium_tests/h2/test_http2_write_threshold.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_http2_write_threshold(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/h2/test_httpbin.py b/tests/uranium_tests/h2/test_httpbin.py index 4203aa4dbfc..e0f5c6cdba8 100644 --- a/tests/uranium_tests/h2/test_httpbin.py +++ b/tests/uranium_tests/h2/test_httpbin.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_httpbin(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/h2/test_nghttp.py b/tests/uranium_tests/h2/test_nghttp.py index 0c4beab7adc..532a35d2cb0 100644 --- a/tests/uranium_tests/h2/test_nghttp.py +++ b/tests/uranium_tests/h2/test_nghttp.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_nghttp(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/h3/test_h3_curl.py b/tests/uranium_tests/h3/test_h3_curl.py index 83dd730be20..e5abf03002d 100644 --- a/tests/uranium_tests/h3/test_h3_curl.py +++ b/tests/uranium_tests/h3/test_h3_curl.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_h3_curl(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/h3/test_h3_go_client.py b/tests/uranium_tests/h3/test_h3_go_client.py index a92c967a5a1..e8e6a534d24 100644 --- a/tests/uranium_tests/h3/test_h3_go_client.py +++ b/tests/uranium_tests/h3/test_h3_go_client.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_h3_go_client(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/h3/test_h3_python_client.py b/tests/uranium_tests/h3/test_h3_python_client.py index fe3f453c24c..15ca4503421 100644 --- a/tests/uranium_tests/h3/test_h3_python_client.py +++ b/tests/uranium_tests/h3/test_h3_python_client.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_h3_python_client(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/h3/test_h3_range_cache.py b/tests/uranium_tests/h3/test_h3_range_cache.py index 2d6b85c1c40..6f0ed906f04 100644 --- a/tests/uranium_tests/h3/test_h3_range_cache.py +++ b/tests/uranium_tests/h3/test_h3_range_cache.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_h3_range_cache(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/h3/test_h3_session_ticket.py b/tests/uranium_tests/h3/test_h3_session_ticket.py index d121dd6559e..daf9ae7a608 100644 --- a/tests/uranium_tests/h3/test_h3_session_ticket.py +++ b/tests/uranium_tests/h3/test_h3_session_ticket.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_h3_session_ticket(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/h3/test_h3_sni_check.py b/tests/uranium_tests/h3/test_h3_sni_check.py index c227b79f841..2f5e9a59c0a 100644 --- a/tests/uranium_tests/h3/test_h3_sni_check.py +++ b/tests/uranium_tests/h3/test_h3_sni_check.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_h3_sni_check(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/headers/test_cache_and_req_body.py b/tests/uranium_tests/headers/test_cache_and_req_body.py index 0270c81196a..1f27489569f 100644 --- a/tests/uranium_tests/headers/test_cache_and_req_body.py +++ b/tests/uranium_tests/headers/test_cache_and_req_body.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_cache_and_req_body(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/headers/test_field_name_space.py b/tests/uranium_tests/headers/test_field_name_space.py index 7af5e2fe0c8..14d1967c668 100644 --- a/tests/uranium_tests/headers/test_field_name_space.py +++ b/tests/uranium_tests/headers/test_field_name_space.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_field_name_space(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/headers/test_forwarded.py b/tests/uranium_tests/headers/test_forwarded.py index 4ecb4ec175e..c0d8b1db4e6 100644 --- a/tests/uranium_tests/headers/test_forwarded.py +++ b/tests/uranium_tests/headers/test_forwarded.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_forwarded(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/headers/test_general_connection_failure_502.py b/tests/uranium_tests/headers/test_general_connection_failure_502.py index 7e4a1791247..8ce720a9272 100644 --- a/tests/uranium_tests/headers/test_general_connection_failure_502.py +++ b/tests/uranium_tests/headers/test_general_connection_failure_502.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_general_connection_failure_502(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/headers/test_good_request_after_bad.py b/tests/uranium_tests/headers/test_good_request_after_bad.py index 94a5af9ba06..efa3c86fd63 100644 --- a/tests/uranium_tests/headers/test_good_request_after_bad.py +++ b/tests/uranium_tests/headers/test_good_request_after_bad.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_good_request_after_bad(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/headers/test_http408.py b/tests/uranium_tests/headers/test_http408.py index 6e077bc1e21..e6713e79493 100644 --- a/tests/uranium_tests/headers/test_http408.py +++ b/tests/uranium_tests/headers/test_http408.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_http408(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/headers/test_normalize_ae.py b/tests/uranium_tests/headers/test_normalize_ae.py index bc03be5ff23..bb7b9ae93da 100644 --- a/tests/uranium_tests/headers/test_normalize_ae.py +++ b/tests/uranium_tests/headers/test_normalize_ae.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_normalize_ae(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/headers/test_oversized_field_h2.py b/tests/uranium_tests/headers/test_oversized_field_h2.py index 2f7643e5ae5..65496a06533 100644 --- a/tests/uranium_tests/headers/test_oversized_field_h2.py +++ b/tests/uranium_tests/headers/test_oversized_field_h2.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_oversized_field_h2(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/headers/test_syntax.py b/tests/uranium_tests/headers/test_syntax.py index 0a0327bb95f..d4f35ca9ccc 100644 --- a/tests/uranium_tests/headers/test_syntax.py +++ b/tests/uranium_tests/headers/test_syntax.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_syntax(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/headers/test_via.py b/tests/uranium_tests/headers/test_via.py index 921666c1753..3f369e1b9ca 100644 --- a/tests/uranium_tests/headers/test_via.py +++ b/tests/uranium_tests/headers/test_via.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_via(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/ip_allow/test_connect_destination_acl.py b/tests/uranium_tests/ip_allow/test_connect_destination_acl.py index ac00812021f..59e96289719 100644 --- a/tests/uranium_tests/ip_allow/test_connect_destination_acl.py +++ b/tests/uranium_tests/ip_allow/test_connect_destination_acl.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_connect_destination_acl(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/ip_allow/test_ip_allow.py b/tests/uranium_tests/ip_allow/test_ip_allow.py index fe2bc0d31c4..07f2183e879 100644 --- a/tests/uranium_tests/ip_allow/test_ip_allow.py +++ b/tests/uranium_tests/ip_allow/test_ip_allow.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_ip_allow(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/ip_allow/test_ip_allow_reload_triggered.py b/tests/uranium_tests/ip_allow/test_ip_allow_reload_triggered.py index 4f9988aa3ce..94ff88e7a9b 100644 --- a/tests/uranium_tests/ip_allow/test_ip_allow_reload_triggered.py +++ b/tests/uranium_tests/ip_allow/test_ip_allow_reload_triggered.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_ip_allow_reload_triggered(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/ip_allow/test_ip_category.py b/tests/uranium_tests/ip_allow/test_ip_category.py index 2b5fddc672e..4eb45091f6a 100644 --- a/tests/uranium_tests/ip_allow/test_ip_category.py +++ b/tests/uranium_tests/ip_allow/test_ip_category.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_ip_category(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/jsonrpc/test_basic_plugin_handler.py b/tests/uranium_tests/jsonrpc/test_basic_plugin_handler.py index 3ec4b41d595..27022cad3a7 100644 --- a/tests/uranium_tests/jsonrpc/test_basic_plugin_handler.py +++ b/tests/uranium_tests/jsonrpc/test_basic_plugin_handler.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_basic_plugin_handler(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/jsonrpc/test_config_reload_dedup.py b/tests/uranium_tests/jsonrpc/test_config_reload_dedup.py index 024671677f0..cc591b04334 100644 --- a/tests/uranium_tests/jsonrpc/test_config_reload_dedup.py +++ b/tests/uranium_tests/jsonrpc/test_config_reload_dedup.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_config_reload_dedup(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/jsonrpc/test_config_reload_failures.py b/tests/uranium_tests/jsonrpc/test_config_reload_failures.py index 7836c8ef0eb..b03aa04281d 100644 --- a/tests/uranium_tests/jsonrpc/test_config_reload_failures.py +++ b/tests/uranium_tests/jsonrpc/test_config_reload_failures.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_config_reload_failures(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/jsonrpc/test_config_reload_full_smoke.py b/tests/uranium_tests/jsonrpc/test_config_reload_full_smoke.py index c7e627f4e6b..3a9f9aa8af1 100644 --- a/tests/uranium_tests/jsonrpc/test_config_reload_full_smoke.py +++ b/tests/uranium_tests/jsonrpc/test_config_reload_full_smoke.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_config_reload_full_smoke(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/jsonrpc/test_config_reload_reserve_subtask.py b/tests/uranium_tests/jsonrpc/test_config_reload_reserve_subtask.py index d6400750118..2c9d46519a1 100644 --- a/tests/uranium_tests/jsonrpc/test_config_reload_reserve_subtask.py +++ b/tests/uranium_tests/jsonrpc/test_config_reload_reserve_subtask.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_config_reload_reserve_subtask(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/jsonrpc/test_config_reload_rpc.py b/tests/uranium_tests/jsonrpc/test_config_reload_rpc.py index aaa9b5af1f4..1bf355baa67 100644 --- a/tests/uranium_tests/jsonrpc/test_config_reload_rpc.py +++ b/tests/uranium_tests/jsonrpc/test_config_reload_rpc.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_config_reload_rpc(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/jsonrpc/test_config_reload_tracking.py b/tests/uranium_tests/jsonrpc/test_config_reload_tracking.py index bbfe4e47adf..e545cb52b0d 100644 --- a/tests/uranium_tests/jsonrpc/test_config_reload_tracking.py +++ b/tests/uranium_tests/jsonrpc/test_config_reload_tracking.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_config_reload_tracking(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/jsonrpc/test_jsonrpc_api_schema.py b/tests/uranium_tests/jsonrpc/test_jsonrpc_api_schema.py index e863930b93a..82c7c1e3d97 100644 --- a/tests/uranium_tests/jsonrpc/test_jsonrpc_api_schema.py +++ b/tests/uranium_tests/jsonrpc/test_jsonrpc_api_schema.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_jsonrpc_api_schema(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/lib/ports.py b/tests/uranium_tests/lib/ports.py index 648fbed5572..7f25a434259 100644 --- a/tests/uranium_tests/lib/ports.py +++ b/tests/uranium_tests/lib/ports.py @@ -21,7 +21,7 @@ from typing import Iterator import socket -from uranium_testkit.scenario import _scenario +from tools.uranium.scenario import _scenario def PortOpen(port: int, address: str | None = None, bound_ports: set[int] | None = None) -> bool: # noqa: N802 diff --git a/tests/uranium_tests/logging/test_all_headers.py b/tests/uranium_tests/logging/test_all_headers.py index eb97d447cb1..2d57fa10b6a 100644 --- a/tests/uranium_tests/logging/test_all_headers.py +++ b/tests/uranium_tests/logging/test_all_headers.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_all_headers(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/logging/test_binary_log_v3.py b/tests/uranium_tests/logging/test_binary_log_v3.py index 696537311da..719aa61c78b 100644 --- a/tests/uranium_tests/logging/test_binary_log_v3.py +++ b/tests/uranium_tests/logging/test_binary_log_v3.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_binary_log_v3(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/logging/test_custom_log.py b/tests/uranium_tests/logging/test_custom_log.py index 3ff9cba882b..b77a495132c 100644 --- a/tests/uranium_tests/logging/test_custom_log.py +++ b/tests/uranium_tests/logging/test_custom_log.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_custom_log(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/logging/test_log_field.py b/tests/uranium_tests/logging/test_log_field.py index bb463773558..18bf9ac39af 100644 --- a/tests/uranium_tests/logging/test_log_field.py +++ b/tests/uranium_tests/logging/test_log_field.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_log_field(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/logging/test_log_field_json.py b/tests/uranium_tests/logging/test_log_field_json.py index 7f8a1b93d65..efa0135f19f 100644 --- a/tests/uranium_tests/logging/test_log_field_json.py +++ b/tests/uranium_tests/logging/test_log_field_json.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_log_field_json(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/logging/test_log_filenames.py b/tests/uranium_tests/logging/test_log_filenames.py index fb7596d8029..877090f48a7 100644 --- a/tests/uranium_tests/logging/test_log_filenames.py +++ b/tests/uranium_tests/logging/test_log_filenames.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_log_filenames(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/logging/test_log_filter.py b/tests/uranium_tests/logging/test_log_filter.py index 1b8f7f49e19..88e65ceefd7 100644 --- a/tests/uranium_tests/logging/test_log_filter.py +++ b/tests/uranium_tests/logging/test_log_filter.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_log_filter(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/logging/test_log_milestone_fields.py b/tests/uranium_tests/logging/test_log_milestone_fields.py index bfd7129d7b5..7cd65d50286 100644 --- a/tests/uranium_tests/logging/test_log_milestone_fields.py +++ b/tests/uranium_tests/logging/test_log_milestone_fields.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_log_milestone_fields(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/logging/test_log_mstsms.py b/tests/uranium_tests/logging/test_log_mstsms.py index 8de2e35c19c..55d267e5588 100644 --- a/tests/uranium_tests/logging/test_log_mstsms.py +++ b/tests/uranium_tests/logging/test_log_mstsms.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_log_mstsms(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/logging/test_log_pipe.py b/tests/uranium_tests/logging/test_log_pipe.py index 34e033bd03a..c8b90411ce4 100644 --- a/tests/uranium_tests/logging/test_log_pipe.py +++ b/tests/uranium_tests/logging/test_log_pipe.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_log_pipe(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/logging/test_log_plugin_init.py b/tests/uranium_tests/logging/test_log_plugin_init.py index 9bd715a603a..744cbce9a61 100644 --- a/tests/uranium_tests/logging/test_log_plugin_init.py +++ b/tests/uranium_tests/logging/test_log_plugin_init.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_log_plugin_init(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/logging/test_log_retention.py b/tests/uranium_tests/logging/test_log_retention.py index 921bd2e5c66..365ac1441cd 100644 --- a/tests/uranium_tests/logging/test_log_retention.py +++ b/tests/uranium_tests/logging/test_log_retention.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_log_retention(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/logging/test_new_log_flds.py b/tests/uranium_tests/logging/test_new_log_flds.py index 9a8e556b427..41d38cb0177 100644 --- a/tests/uranium_tests/logging/test_new_log_flds.py +++ b/tests/uranium_tests/logging/test_new_log_flds.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_new_log_flds(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/logging/test_pqsi_pqsp.py b/tests/uranium_tests/logging/test_pqsi_pqsp.py index f1f938e607a..bf7ccfefaf3 100644 --- a/tests/uranium_tests/logging/test_pqsi_pqsp.py +++ b/tests/uranium_tests/logging/test_pqsi_pqsp.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_pqsi_pqsp(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/logging/test_qlog_quiche.py b/tests/uranium_tests/logging/test_qlog_quiche.py index fe5f3a5baa3..9424400bd98 100644 --- a/tests/uranium_tests/logging/test_qlog_quiche.py +++ b/tests/uranium_tests/logging/test_qlog_quiche.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_qlog_quiche(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/logging/test_sigusr2.py b/tests/uranium_tests/logging/test_sigusr2.py index c201c8a8459..f131f508ef4 100644 --- a/tests/uranium_tests/logging/test_sigusr2.py +++ b/tests/uranium_tests/logging/test_sigusr2.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_sigusr2(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/next_hop/parent_config/test_parent_config_dest_ip.py b/tests/uranium_tests/next_hop/parent_config/test_parent_config_dest_ip.py index 71837b757b7..94b768a0eb9 100644 --- a/tests/uranium_tests/next_hop/parent_config/test_parent_config_dest_ip.py +++ b/tests/uranium_tests/next_hop/parent_config/test_parent_config_dest_ip.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_parent_config_dest_ip(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/next_hop/strategies_ch/test_strategies_ch.py b/tests/uranium_tests/next_hop/strategies_ch/test_strategies_ch.py index b30120dff32..722265ea283 100644 --- a/tests/uranium_tests/next_hop/strategies_ch/test_strategies_ch.py +++ b/tests/uranium_tests/next_hop/strategies_ch/test_strategies_ch.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_strategies_ch(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/next_hop/strategies_ch2/test_strategies_ch2.py b/tests/uranium_tests/next_hop/strategies_ch2/test_strategies_ch2.py index 55908a2dbc7..3cc9db17e82 100644 --- a/tests/uranium_tests/next_hop/strategies_ch2/test_strategies_ch2.py +++ b/tests/uranium_tests/next_hop/strategies_ch2/test_strategies_ch2.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_strategies_ch2(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/next_hop/strategies_ch5/test_strategies_ch5.py b/tests/uranium_tests/next_hop/strategies_ch5/test_strategies_ch5.py index c858d9840c7..5a02b629d89 100644 --- a/tests/uranium_tests/next_hop/strategies_ch5/test_strategies_ch5.py +++ b/tests/uranium_tests/next_hop/strategies_ch5/test_strategies_ch5.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_strategies_ch5(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/next_hop/strategies_stale/test_strategies_stale.py b/tests/uranium_tests/next_hop/strategies_stale/test_strategies_stale.py index 322f869a15d..ed0ccf4f472 100644 --- a/tests/uranium_tests/next_hop/strategies_stale/test_strategies_stale.py +++ b/tests/uranium_tests/next_hop/strategies_stale/test_strategies_stale.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_strategies_stale(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/next_hop/zzz_strategies_peer/test_zzz_strategies_peer.py b/tests/uranium_tests/next_hop/zzz_strategies_peer/test_zzz_strategies_peer.py index f4a39b1c9d5..d93efbbddb1 100644 --- a/tests/uranium_tests/next_hop/zzz_strategies_peer/test_zzz_strategies_peer.py +++ b/tests/uranium_tests/next_hop/zzz_strategies_peer/test_zzz_strategies_peer.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_zzz_strategies_peer(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/next_hop/zzz_strategies_peer2/test_zzz_strategies_peer2.py b/tests/uranium_tests/next_hop/zzz_strategies_peer2/test_zzz_strategies_peer2.py index 32ce449abb7..cde92ae38b4 100644 --- a/tests/uranium_tests/next_hop/zzz_strategies_peer2/test_zzz_strategies_peer2.py +++ b/tests/uranium_tests/next_hop/zzz_strategies_peer2/test_zzz_strategies_peer2.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_zzz_strategies_peer2(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/null_transform/test_null_transform.py b/tests/uranium_tests/null_transform/test_null_transform.py index 302b6076588..4f94dcef613 100644 --- a/tests/uranium_tests/null_transform/test_null_transform.py +++ b/tests/uranium_tests/null_transform/test_null_transform.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_null_transform(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/origin_connection/test_per_server_connection_max.py b/tests/uranium_tests/origin_connection/test_per_server_connection_max.py index 429f1bc4508..aaf6806b660 100644 --- a/tests/uranium_tests/origin_connection/test_per_server_connection_max.py +++ b/tests/uranium_tests/origin_connection/test_per_server_connection_max.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_per_server_connection_max(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/origin_connection/test_per_server_metric_enabled.py b/tests/uranium_tests/origin_connection/test_per_server_metric_enabled.py index 4749fc20b0c..b8c21615c9d 100644 --- a/tests/uranium_tests/origin_connection/test_per_server_metric_enabled.py +++ b/tests/uranium_tests/origin_connection/test_per_server_metric_enabled.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_per_server_metric_enabled(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/parent_config/test_parent_config_reload.py b/tests/uranium_tests/parent_config/test_parent_config_reload.py index 90dfaf33857..847951d92cb 100644 --- a/tests/uranium_tests/parent_config/test_parent_config_reload.py +++ b/tests/uranium_tests/parent_config/test_parent_config_reload.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_parent_config_reload(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/parent_proxy/test_consistent_hash_ring_walk.py b/tests/uranium_tests/parent_proxy/test_consistent_hash_ring_walk.py index 877b65e7c9f..6fc887204d3 100644 --- a/tests/uranium_tests/parent_proxy/test_consistent_hash_ring_walk.py +++ b/tests/uranium_tests/parent_proxy/test_consistent_hash_ring_walk.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_consistent_hash_ring_walk(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/parent_proxy/test_parent_retry.py b/tests/uranium_tests/parent_proxy/test_parent_retry.py index 4ce4df5afbe..f3fea2e3fad 100644 --- a/tests/uranium_tests/parent_proxy/test_parent_retry.py +++ b/tests/uranium_tests/parent_proxy/test_parent_retry.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_parent_retry(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/pipeline/test_delete_maxforwards_body_desync.py b/tests/uranium_tests/pipeline/test_delete_maxforwards_body_desync.py index 24bacc38bb0..b2c0f3d22a4 100644 --- a/tests/uranium_tests/pipeline/test_delete_maxforwards_body_desync.py +++ b/tests/uranium_tests/pipeline/test_delete_maxforwards_body_desync.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_delete_maxforwards_body_desync(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/pipeline/test_delete_maxfwd_noop_drain.py b/tests/uranium_tests/pipeline/test_delete_maxfwd_noop_drain.py index f39c28ec363..a1d174453b7 100644 --- a/tests/uranium_tests/pipeline/test_delete_maxfwd_noop_drain.py +++ b/tests/uranium_tests/pipeline/test_delete_maxfwd_noop_drain.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_delete_maxfwd_noop_drain(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/pipeline/test_noop_keepalive.py b/tests/uranium_tests/pipeline/test_noop_keepalive.py index c94c96b314e..3b91eca3051 100644 --- a/tests/uranium_tests/pipeline/test_noop_keepalive.py +++ b/tests/uranium_tests/pipeline/test_noop_keepalive.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_noop_keepalive(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/pipeline/test_pipeline.py b/tests/uranium_tests/pipeline/test_pipeline.py index 854a397fb6c..7ab53bdc797 100644 --- a/tests/uranium_tests/pipeline/test_pipeline.py +++ b/tests/uranium_tests/pipeline/test_pipeline.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_pipeline(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/pluginTest/TSVConnFd/test_TSVConnFd.py b/tests/uranium_tests/pluginTest/TSVConnFd/test_TSVConnFd.py index f2c204dcf61..00a291998ed 100644 --- a/tests/uranium_tests/pluginTest/TSVConnFd/test_TSVConnFd.py +++ b/tests/uranium_tests/pluginTest/TSVConnFd/test_TSVConnFd.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_TSVConnFd(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/pluginTest/access_control/test_access_control.py b/tests/uranium_tests/pluginTest/access_control/test_access_control.py index e5abfc155fe..6d67f8a920c 100644 --- a/tests/uranium_tests/pluginTest/access_control/test_access_control.py +++ b/tests/uranium_tests/pluginTest/access_control/test_access_control.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_access_control(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/pluginTest/block_errors/test_block_errors.py b/tests/uranium_tests/pluginTest/block_errors/test_block_errors.py index 44a25fa5e45..bf900b4aea2 100644 --- a/tests/uranium_tests/pluginTest/block_errors/test_block_errors.py +++ b/tests/uranium_tests/pluginTest/block_errors/test_block_errors.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_block_errors(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/pluginTest/cache_fill/test_cache_fill.py b/tests/uranium_tests/pluginTest/cache_fill/test_cache_fill.py index 6a902099afc..57b48b1e8f9 100644 --- a/tests/uranium_tests/pluginTest/cache_fill/test_cache_fill.py +++ b/tests/uranium_tests/pluginTest/cache_fill/test_cache_fill.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_cache_fill(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/pluginTest/cache_promote/test_cache_promote.py b/tests/uranium_tests/pluginTest/cache_promote/test_cache_promote.py index 1164725e708..192e215ed0f 100644 --- a/tests/uranium_tests/pluginTest/cache_promote/test_cache_promote.py +++ b/tests/uranium_tests/pluginTest/cache_promote/test_cache_promote.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_cache_promote(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/pluginTest/cache_range_requests/test_cache_range_requests.py b/tests/uranium_tests/pluginTest/cache_range_requests/test_cache_range_requests.py index 8e995992cde..afe59a9e796 100644 --- a/tests/uranium_tests/pluginTest/cache_range_requests/test_cache_range_requests.py +++ b/tests/uranium_tests/pluginTest/cache_range_requests/test_cache_range_requests.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_cache_range_requests(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/pluginTest/cache_range_requests/test_cache_range_requests_cache_complete_responses.py b/tests/uranium_tests/pluginTest/cache_range_requests/test_cache_range_requests_cache_complete_responses.py index 8efefbd745d..1ee238f01b1 100644 --- a/tests/uranium_tests/pluginTest/cache_range_requests/test_cache_range_requests_cache_complete_responses.py +++ b/tests/uranium_tests/pluginTest/cache_range_requests/test_cache_range_requests_cache_complete_responses.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_cache_range_requests_cache_complete_responses(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/pluginTest/cache_range_requests/test_cache_range_requests_cachekey.py b/tests/uranium_tests/pluginTest/cache_range_requests/test_cache_range_requests_cachekey.py index af39fa7ad34..eedb8f6df44 100644 --- a/tests/uranium_tests/pluginTest/cache_range_requests/test_cache_range_requests_cachekey.py +++ b/tests/uranium_tests/pluginTest/cache_range_requests/test_cache_range_requests_cachekey.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_cache_range_requests_cachekey(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/pluginTest/cache_range_requests/test_cache_range_requests_cachekey_global.py b/tests/uranium_tests/pluginTest/cache_range_requests/test_cache_range_requests_cachekey_global.py index 2102609a3e4..e8fa357da53 100644 --- a/tests/uranium_tests/pluginTest/cache_range_requests/test_cache_range_requests_cachekey_global.py +++ b/tests/uranium_tests/pluginTest/cache_range_requests/test_cache_range_requests_cachekey_global.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_cache_range_requests_cachekey_global(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/pluginTest/cache_range_requests/test_cache_range_requests_ident.py b/tests/uranium_tests/pluginTest/cache_range_requests/test_cache_range_requests_ident.py index cdf8c12751a..0f22b7ffe13 100644 --- a/tests/uranium_tests/pluginTest/cache_range_requests/test_cache_range_requests_ident.py +++ b/tests/uranium_tests/pluginTest/cache_range_requests/test_cache_range_requests_ident.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_cache_range_requests_ident(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/pluginTest/cert_update/test_cert_update.py b/tests/uranium_tests/pluginTest/cert_update/test_cert_update.py index ddf6ed0fbbc..6aaa8466715 100644 --- a/tests/uranium_tests/pluginTest/cert_update/test_cert_update.py +++ b/tests/uranium_tests/pluginTest/cert_update/test_cert_update.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_cert_update(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/pluginTest/certifier/test_certifier.py b/tests/uranium_tests/pluginTest/certifier/test_certifier.py index e5a1b58c99a..a59ec30a61e 100644 --- a/tests/uranium_tests/pluginTest/certifier/test_certifier.py +++ b/tests/uranium_tests/pluginTest/certifier/test_certifier.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_certifier(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/pluginTest/client_context_dump/test_client_context_dump.py b/tests/uranium_tests/pluginTest/client_context_dump/test_client_context_dump.py index a69fefd8b39..05e06a7fb2f 100644 --- a/tests/uranium_tests/pluginTest/client_context_dump/test_client_context_dump.py +++ b/tests/uranium_tests/pluginTest/client_context_dump/test_client_context_dump.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_client_context_dump(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/pluginTest/combo_handler/test_combo_handler.py b/tests/uranium_tests/pluginTest/combo_handler/test_combo_handler.py index c8fe19f7d2c..c2874c21c63 100644 --- a/tests/uranium_tests/pluginTest/combo_handler/test_combo_handler.py +++ b/tests/uranium_tests/pluginTest/combo_handler/test_combo_handler.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_combo_handler(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/pluginTest/compress/test_compress.py b/tests/uranium_tests/pluginTest/compress/test_compress.py index 5077f762001..2f804ae216a 100644 --- a/tests/uranium_tests/pluginTest/compress/test_compress.py +++ b/tests/uranium_tests/pluginTest/compress/test_compress.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_compress(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/pluginTest/compress/test_compress_cache_untransformed.py b/tests/uranium_tests/pluginTest/compress/test_compress_cache_untransformed.py index c097b1fadec..3a42fb5820b 100644 --- a/tests/uranium_tests/pluginTest/compress/test_compress_cache_untransformed.py +++ b/tests/uranium_tests/pluginTest/compress/test_compress_cache_untransformed.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_compress_cache_untransformed(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/pluginTest/compress/test_compress_content_type_params.py b/tests/uranium_tests/pluginTest/compress/test_compress_content_type_params.py index 4f143675554..3d3b24ac25a 100644 --- a/tests/uranium_tests/pluginTest/compress/test_compress_content_type_params.py +++ b/tests/uranium_tests/pluginTest/compress/test_compress_content_type_params.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_compress_content_type_params(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/pluginTest/compress/test_compress_range.py b/tests/uranium_tests/pluginTest/compress/test_compress_range.py index 15036cdda45..28bd94e3e60 100644 --- a/tests/uranium_tests/pluginTest/compress/test_compress_range.py +++ b/tests/uranium_tests/pluginTest/compress/test_compress_range.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_compress_range(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/pluginTest/conf_remap/test_conf_remap.py b/tests/uranium_tests/pluginTest/conf_remap/test_conf_remap.py index b0a44492883..8a8ab537dfe 100644 --- a/tests/uranium_tests/pluginTest/conf_remap/test_conf_remap.py +++ b/tests/uranium_tests/pluginTest/conf_remap/test_conf_remap.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_conf_remap(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/pluginTest/cookie_remap/test_bucketcookie.py b/tests/uranium_tests/pluginTest/cookie_remap/test_bucketcookie.py index 3438658cf9f..daf2c1f5598 100644 --- a/tests/uranium_tests/pluginTest/cookie_remap/test_bucketcookie.py +++ b/tests/uranium_tests/pluginTest/cookie_remap/test_bucketcookie.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_bucketcookie(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/pluginTest/cookie_remap/test_collapseslashes.py b/tests/uranium_tests/pluginTest/cookie_remap/test_collapseslashes.py index 34833e74773..1c61ef450fb 100644 --- a/tests/uranium_tests/pluginTest/cookie_remap/test_collapseslashes.py +++ b/tests/uranium_tests/pluginTest/cookie_remap/test_collapseslashes.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_collapseslashes(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/pluginTest/cookie_remap/test_connector.py b/tests/uranium_tests/pluginTest/cookie_remap/test_connector.py index 3d1c7d76157..8ff86c935bb 100644 --- a/tests/uranium_tests/pluginTest/cookie_remap/test_connector.py +++ b/tests/uranium_tests/pluginTest/cookie_remap/test_connector.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_connector(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/pluginTest/cookie_remap/test_disable_pristine_host_hdr.py b/tests/uranium_tests/pluginTest/cookie_remap/test_disable_pristine_host_hdr.py index a2cc570a06e..529336b1cbc 100644 --- a/tests/uranium_tests/pluginTest/cookie_remap/test_disable_pristine_host_hdr.py +++ b/tests/uranium_tests/pluginTest/cookie_remap/test_disable_pristine_host_hdr.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_disable_pristine_host_hdr(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/pluginTest/cookie_remap/test_existscookie.py b/tests/uranium_tests/pluginTest/cookie_remap/test_existscookie.py index 4554a8df63d..6626e3f6565 100644 --- a/tests/uranium_tests/pluginTest/cookie_remap/test_existscookie.py +++ b/tests/uranium_tests/pluginTest/cookie_remap/test_existscookie.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_existscookie(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/pluginTest/cookie_remap/test_matchcookie.py b/tests/uranium_tests/pluginTest/cookie_remap/test_matchcookie.py index 4a8274b0a9f..ca64892255a 100644 --- a/tests/uranium_tests/pluginTest/cookie_remap/test_matchcookie.py +++ b/tests/uranium_tests/pluginTest/cookie_remap/test_matchcookie.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_matchcookie(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/pluginTest/cookie_remap/test_matchuri.py b/tests/uranium_tests/pluginTest/cookie_remap/test_matchuri.py index 3eeec31d07c..133bf5e69f7 100644 --- a/tests/uranium_tests/pluginTest/cookie_remap/test_matchuri.py +++ b/tests/uranium_tests/pluginTest/cookie_remap/test_matchuri.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_matchuri(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/pluginTest/cookie_remap/test_notexistscookie.py b/tests/uranium_tests/pluginTest/cookie_remap/test_notexistscookie.py index 471b2796534..ede81a2b18b 100644 --- a/tests/uranium_tests/pluginTest/cookie_remap/test_notexistscookie.py +++ b/tests/uranium_tests/pluginTest/cookie_remap/test_notexistscookie.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_notexistscookie(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/pluginTest/cookie_remap/test_pcollapseslashes.py b/tests/uranium_tests/pluginTest/cookie_remap/test_pcollapseslashes.py index 7be87dc770a..49ca6549384 100644 --- a/tests/uranium_tests/pluginTest/cookie_remap/test_pcollapseslashes.py +++ b/tests/uranium_tests/pluginTest/cookie_remap/test_pcollapseslashes.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_pcollapseslashes(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/pluginTest/cookie_remap/test_psubstitute.py b/tests/uranium_tests/pluginTest/cookie_remap/test_psubstitute.py index 1370ec9b21d..eb847b0ac9b 100644 --- a/tests/uranium_tests/pluginTest/cookie_remap/test_psubstitute.py +++ b/tests/uranium_tests/pluginTest/cookie_remap/test_psubstitute.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_psubstitute(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/pluginTest/cookie_remap/test_regexcookie.py b/tests/uranium_tests/pluginTest/cookie_remap/test_regexcookie.py index 9a436d92f5c..ba9906e1425 100644 --- a/tests/uranium_tests/pluginTest/cookie_remap/test_regexcookie.py +++ b/tests/uranium_tests/pluginTest/cookie_remap/test_regexcookie.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_regexcookie(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/pluginTest/cookie_remap/test_setstatus.py b/tests/uranium_tests/pluginTest/cookie_remap/test_setstatus.py index e9ca5f8759f..470eb080efa 100644 --- a/tests/uranium_tests/pluginTest/cookie_remap/test_setstatus.py +++ b/tests/uranium_tests/pluginTest/cookie_remap/test_setstatus.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_setstatus(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/pluginTest/cookie_remap/test_subcookie.py b/tests/uranium_tests/pluginTest/cookie_remap/test_subcookie.py index a5f21acd462..3a6c066aa4d 100644 --- a/tests/uranium_tests/pluginTest/cookie_remap/test_subcookie.py +++ b/tests/uranium_tests/pluginTest/cookie_remap/test_subcookie.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_subcookie(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/pluginTest/cookie_remap/test_substitute.py b/tests/uranium_tests/pluginTest/cookie_remap/test_substitute.py index 18192dfd0fd..46e91269e94 100644 --- a/tests/uranium_tests/pluginTest/cookie_remap/test_substitute.py +++ b/tests/uranium_tests/pluginTest/cookie_remap/test_substitute.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_substitute(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/pluginTest/cppapi/test_cppapi.py b/tests/uranium_tests/pluginTest/cppapi/test_cppapi.py index 4f43e635c94..8d95ff7e447 100644 --- a/tests/uranium_tests/pluginTest/cppapi/test_cppapi.py +++ b/tests/uranium_tests/pluginTest/cppapi/test_cppapi.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_cppapi(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/pluginTest/crash_test/test_crash_test.py b/tests/uranium_tests/pluginTest/crash_test/test_crash_test.py index 0123497ddbe..e8639426408 100644 --- a/tests/uranium_tests/pluginTest/crash_test/test_crash_test.py +++ b/tests/uranium_tests/pluginTest/crash_test/test_crash_test.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_crash_test(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/pluginTest/escalate/test_escalate.py b/tests/uranium_tests/pluginTest/escalate/test_escalate.py index 68179a8abe0..dc9f4d899ec 100644 --- a/tests/uranium_tests/pluginTest/escalate/test_escalate.py +++ b/tests/uranium_tests/pluginTest/escalate/test_escalate.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_escalate(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/pluginTest/esi/test_esi.py b/tests/uranium_tests/pluginTest/esi/test_esi.py index 476b5897397..95cc8b5fd04 100644 --- a/tests/uranium_tests/pluginTest/esi/test_esi.py +++ b/tests/uranium_tests/pluginTest/esi/test_esi.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_esi(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/pluginTest/esi/test_esi_304.py b/tests/uranium_tests/pluginTest/esi/test_esi_304.py index 04566f4c1bd..414c22eab1e 100644 --- a/tests/uranium_tests/pluginTest/esi/test_esi_304.py +++ b/tests/uranium_tests/pluginTest/esi/test_esi_304.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_esi_304(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/pluginTest/esi/test_esi_nested_html_comment.py b/tests/uranium_tests/pluginTest/esi/test_esi_nested_html_comment.py index 0c198bff549..faeee9f91ee 100644 --- a/tests/uranium_tests/pluginTest/esi/test_esi_nested_html_comment.py +++ b/tests/uranium_tests/pluginTest/esi/test_esi_nested_html_comment.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_esi_nested_html_comment(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/pluginTest/esi/test_esi_nested_include.py b/tests/uranium_tests/pluginTest/esi/test_esi_nested_include.py index 42ee0b0a4e2..c04ee1a5597 100644 --- a/tests/uranium_tests/pluginTest/esi/test_esi_nested_include.py +++ b/tests/uranium_tests/pluginTest/esi/test_esi_nested_include.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_esi_nested_include(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/pluginTest/esi/test_esi_request_size_cap.py b/tests/uranium_tests/pluginTest/esi/test_esi_request_size_cap.py index 2bf815fc75d..d140320bcf8 100644 --- a/tests/uranium_tests/pluginTest/esi/test_esi_request_size_cap.py +++ b/tests/uranium_tests/pluginTest/esi/test_esi_request_size_cap.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_esi_request_size_cap(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/pluginTest/esi/test_esi_ssrf_validate.py b/tests/uranium_tests/pluginTest/esi/test_esi_ssrf_validate.py index 42ced6359f0..0a279d07453 100644 --- a/tests/uranium_tests/pluginTest/esi/test_esi_ssrf_validate.py +++ b/tests/uranium_tests/pluginTest/esi/test_esi_ssrf_validate.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_esi_ssrf_validate(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/pluginTest/header_rewrite/test_header_rewrite_bad_run_plugin.py b/tests/uranium_tests/pluginTest/header_rewrite/test_header_rewrite_bad_run_plugin.py index f14c12d6cad..3e0dd81c9a1 100644 --- a/tests/uranium_tests/pluginTest/header_rewrite/test_header_rewrite_bad_run_plugin.py +++ b/tests/uranium_tests/pluginTest/header_rewrite/test_header_rewrite_bad_run_plugin.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_header_rewrite_bad_run_plugin(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/pluginTest/header_rewrite/test_header_rewrite_set_body_from.py b/tests/uranium_tests/pluginTest/header_rewrite/test_header_rewrite_set_body_from.py index 8388fd81918..f0591734d91 100644 --- a/tests/uranium_tests/pluginTest/header_rewrite/test_header_rewrite_set_body_from.py +++ b/tests/uranium_tests/pluginTest/header_rewrite/test_header_rewrite_set_body_from.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_header_rewrite_set_body_from(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/pluginTest/header_rewrite/test_header_rewrite_url_glob.py b/tests/uranium_tests/pluginTest/header_rewrite/test_header_rewrite_url_glob.py index 1bc6b4e22ad..e0fccc38e97 100644 --- a/tests/uranium_tests/pluginTest/header_rewrite/test_header_rewrite_url_glob.py +++ b/tests/uranium_tests/pluginTest/header_rewrite/test_header_rewrite_url_glob.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_header_rewrite_url_glob(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/pluginTest/healthchecks/test_healthchecks.py b/tests/uranium_tests/pluginTest/healthchecks/test_healthchecks.py index 71cfb36ff72..20320085f24 100644 --- a/tests/uranium_tests/pluginTest/healthchecks/test_healthchecks.py +++ b/tests/uranium_tests/pluginTest/healthchecks/test_healthchecks.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_healthchecks(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/pluginTest/ja3_fingerprint/test_ja3_fingerprint.py b/tests/uranium_tests/pluginTest/ja3_fingerprint/test_ja3_fingerprint.py index 91292c99fa2..6cf191d66e9 100644 --- a/tests/uranium_tests/pluginTest/ja3_fingerprint/test_ja3_fingerprint.py +++ b/tests/uranium_tests/pluginTest/ja3_fingerprint/test_ja3_fingerprint.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_ja3_fingerprint(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/pluginTest/ja4_fingerprint/test_ja4_fingerprint.py b/tests/uranium_tests/pluginTest/ja4_fingerprint/test_ja4_fingerprint.py index 1fe430b7c20..9276451ec0e 100644 --- a/tests/uranium_tests/pluginTest/ja4_fingerprint/test_ja4_fingerprint.py +++ b/tests/uranium_tests/pluginTest/ja4_fingerprint/test_ja4_fingerprint.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_ja4_fingerprint(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/pluginTest/jax_fingerprint/test_jax_fingerprint.py b/tests/uranium_tests/pluginTest/jax_fingerprint/test_jax_fingerprint.py index 458c850b468..5f3c5eac926 100644 --- a/tests/uranium_tests/pluginTest/jax_fingerprint/test_jax_fingerprint.py +++ b/tests/uranium_tests/pluginTest/jax_fingerprint/test_jax_fingerprint.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_jax_fingerprint(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/pluginTest/lua/test_lua_client_hook.py b/tests/uranium_tests/pluginTest/lua/test_lua_client_hook.py index d3d9dfe61dd..ef6846b79a8 100644 --- a/tests/uranium_tests/pluginTest/lua/test_lua_client_hook.py +++ b/tests/uranium_tests/pluginTest/lua/test_lua_client_hook.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_lua_client_hook(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/pluginTest/lua/test_lua_debug_tags.py b/tests/uranium_tests/pluginTest/lua/test_lua_debug_tags.py index e851c22a1e4..b1328a318ed 100644 --- a/tests/uranium_tests/pluginTest/lua/test_lua_debug_tags.py +++ b/tests/uranium_tests/pluginTest/lua/test_lua_debug_tags.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_lua_debug_tags(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/pluginTest/lua/test_lua_fetch_ipv6_cliaddr.py b/tests/uranium_tests/pluginTest/lua/test_lua_fetch_ipv6_cliaddr.py index 05c36601f14..a155d5631c8 100644 --- a/tests/uranium_tests/pluginTest/lua/test_lua_fetch_ipv6_cliaddr.py +++ b/tests/uranium_tests/pluginTest/lua/test_lua_fetch_ipv6_cliaddr.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_lua_fetch_ipv6_cliaddr(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/pluginTest/lua/test_lua_global_shutdown.py b/tests/uranium_tests/pluginTest/lua/test_lua_global_shutdown.py index ed10e550e24..e340ee0b77b 100644 --- a/tests/uranium_tests/pluginTest/lua/test_lua_global_shutdown.py +++ b/tests/uranium_tests/pluginTest/lua/test_lua_global_shutdown.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_lua_global_shutdown(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/pluginTest/lua/test_lua_header_table.py b/tests/uranium_tests/pluginTest/lua/test_lua_header_table.py index 812a3d6c2df..fb6d91bea3a 100644 --- a/tests/uranium_tests/pluginTest/lua/test_lua_header_table.py +++ b/tests/uranium_tests/pluginTest/lua/test_lua_header_table.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_lua_header_table(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/pluginTest/lua/test_lua_proxy_protocol.py b/tests/uranium_tests/pluginTest/lua/test_lua_proxy_protocol.py index b1b43bddddf..7fe124cb334 100644 --- a/tests/uranium_tests/pluginTest/lua/test_lua_proxy_protocol.py +++ b/tests/uranium_tests/pluginTest/lua/test_lua_proxy_protocol.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_lua_proxy_protocol(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/pluginTest/lua/test_lua_remap_after_hook.py b/tests/uranium_tests/pluginTest/lua/test_lua_remap_after_hook.py index 6cb883eabe2..1d4d34618e5 100644 --- a/tests/uranium_tests/pluginTest/lua/test_lua_remap_after_hook.py +++ b/tests/uranium_tests/pluginTest/lua/test_lua_remap_after_hook.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_lua_remap_after_hook(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/pluginTest/lua/test_lua_set_error_response.py b/tests/uranium_tests/pluginTest/lua/test_lua_set_error_response.py index 10fb199e7a8..ac76fc8cda0 100644 --- a/tests/uranium_tests/pluginTest/lua/test_lua_set_error_response.py +++ b/tests/uranium_tests/pluginTest/lua/test_lua_set_error_response.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_lua_set_error_response(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/pluginTest/lua/test_lua_states_stats.py b/tests/uranium_tests/pluginTest/lua/test_lua_states_stats.py index ba1ece967fd..1b6f47aea88 100644 --- a/tests/uranium_tests/pluginTest/lua/test_lua_states_stats.py +++ b/tests/uranium_tests/pluginTest/lua/test_lua_states_stats.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_lua_states_stats(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/pluginTest/lua/test_lua_verified_addr.py b/tests/uranium_tests/pluginTest/lua/test_lua_verified_addr.py index d814985d3a5..5f2062f72f0 100644 --- a/tests/uranium_tests/pluginTest/lua/test_lua_verified_addr.py +++ b/tests/uranium_tests/pluginTest/lua/test_lua_verified_addr.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_lua_verified_addr(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/pluginTest/lua/test_lua_watermark.py b/tests/uranium_tests/pluginTest/lua/test_lua_watermark.py index 05a2203b596..9aba1e8dfa3 100644 --- a/tests/uranium_tests/pluginTest/lua/test_lua_watermark.py +++ b/tests/uranium_tests/pluginTest/lua/test_lua_watermark.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_lua_watermark(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/pluginTest/money_trace/test_money_trace.py b/tests/uranium_tests/pluginTest/money_trace/test_money_trace.py index cd7f911e29c..f1147720f61 100644 --- a/tests/uranium_tests/pluginTest/money_trace/test_money_trace.py +++ b/tests/uranium_tests/pluginTest/money_trace/test_money_trace.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_money_trace(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/pluginTest/money_trace/test_money_trace_global.py b/tests/uranium_tests/pluginTest/money_trace/test_money_trace_global.py index 5f6c7f33e62..1aba50fe5f9 100644 --- a/tests/uranium_tests/pluginTest/money_trace/test_money_trace_global.py +++ b/tests/uranium_tests/pluginTest/money_trace/test_money_trace_global.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_money_trace_global(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/pluginTest/multiplexer/test_multiplexer.py b/tests/uranium_tests/pluginTest/multiplexer/test_multiplexer.py index b06364381a6..fdc2a793334 100644 --- a/tests/uranium_tests/pluginTest/multiplexer/test_multiplexer.py +++ b/tests/uranium_tests/pluginTest/multiplexer/test_multiplexer.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_multiplexer(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/pluginTest/origin_server_auth/test_origin_server_auth.py b/tests/uranium_tests/pluginTest/origin_server_auth/test_origin_server_auth.py index ff92e70de01..7eac079dde4 100644 --- a/tests/uranium_tests/pluginTest/origin_server_auth/test_origin_server_auth.py +++ b/tests/uranium_tests/pluginTest/origin_server_auth/test_origin_server_auth.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_origin_server_auth(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/pluginTest/origin_server_auth/test_s3_url_encoding.py b/tests/uranium_tests/pluginTest/origin_server_auth/test_s3_url_encoding.py index 19aa9b94015..fcc265bc989 100644 --- a/tests/uranium_tests/pluginTest/origin_server_auth/test_s3_url_encoding.py +++ b/tests/uranium_tests/pluginTest/origin_server_auth/test_s3_url_encoding.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_s3_url_encoding(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/pluginTest/packet_mark/test_packet_mark.py b/tests/uranium_tests/pluginTest/packet_mark/test_packet_mark.py index 8cb362e946e..b7bedc765a8 100644 --- a/tests/uranium_tests/pluginTest/packet_mark/test_packet_mark.py +++ b/tests/uranium_tests/pluginTest/packet_mark/test_packet_mark.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_packet_mark(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/pluginTest/per_plugin_metrics/test_per_plugin_metrics.py b/tests/uranium_tests/pluginTest/per_plugin_metrics/test_per_plugin_metrics.py index 15b69d722b8..f11c35017d9 100644 --- a/tests/uranium_tests/pluginTest/per_plugin_metrics/test_per_plugin_metrics.py +++ b/tests/uranium_tests/pluginTest/per_plugin_metrics/test_per_plugin_metrics.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_per_plugin_metrics(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/pluginTest/plugin_yaml/test_plugin_yaml.py b/tests/uranium_tests/pluginTest/plugin_yaml/test_plugin_yaml.py index 97eaa7f0f77..124ae960cc2 100644 --- a/tests/uranium_tests/pluginTest/plugin_yaml/test_plugin_yaml.py +++ b/tests/uranium_tests/pluginTest/plugin_yaml/test_plugin_yaml.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_plugin_yaml(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/pluginTest/polite_hook_wait/test_polite_hook_wait.py b/tests/uranium_tests/pluginTest/polite_hook_wait/test_polite_hook_wait.py index fd53259d28e..101d6f1e3af 100644 --- a/tests/uranium_tests/pluginTest/polite_hook_wait/test_polite_hook_wait.py +++ b/tests/uranium_tests/pluginTest/polite_hook_wait/test_polite_hook_wait.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_polite_hook_wait(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/pluginTest/prefetch/test_prefetch_bad_count_refused.py b/tests/uranium_tests/pluginTest/prefetch/test_prefetch_bad_count_refused.py index 0df7168a30a..139e9c3ec3b 100644 --- a/tests/uranium_tests/pluginTest/prefetch/test_prefetch_bad_count_refused.py +++ b/tests/uranium_tests/pluginTest/prefetch/test_prefetch_bad_count_refused.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_prefetch_bad_count_refused(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/pluginTest/prefetch/test_prefetch_bad_pattern_refused.py b/tests/uranium_tests/pluginTest/prefetch/test_prefetch_bad_pattern_refused.py index a4d6bfac8aa..c2db2e40c7e 100644 --- a/tests/uranium_tests/pluginTest/prefetch/test_prefetch_bad_pattern_refused.py +++ b/tests/uranium_tests/pluginTest/prefetch/test_prefetch_bad_pattern_refused.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_prefetch_bad_pattern_refused(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/pluginTest/prefetch/test_prefetch_bignum.py b/tests/uranium_tests/pluginTest/prefetch/test_prefetch_bignum.py index 256057d3755..986d97ec42d 100644 --- a/tests/uranium_tests/pluginTest/prefetch/test_prefetch_bignum.py +++ b/tests/uranium_tests/pluginTest/prefetch/test_prefetch_bignum.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_prefetch_bignum(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/pluginTest/prefetch/test_prefetch_cmcd.py b/tests/uranium_tests/pluginTest/prefetch/test_prefetch_cmcd.py index aed07cfec62..5ef09adade2 100644 --- a/tests/uranium_tests/pluginTest/prefetch/test_prefetch_cmcd.py +++ b/tests/uranium_tests/pluginTest/prefetch/test_prefetch_cmcd.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_prefetch_cmcd(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/pluginTest/prefetch/test_prefetch_empty_replacement.py b/tests/uranium_tests/pluginTest/prefetch/test_prefetch_empty_replacement.py index c2cd3cd5314..35f17ae9eaa 100644 --- a/tests/uranium_tests/pluginTest/prefetch/test_prefetch_empty_replacement.py +++ b/tests/uranium_tests/pluginTest/prefetch/test_prefetch_empty_replacement.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_prefetch_empty_replacement(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/pluginTest/prefetch/test_prefetch_optional_group.py b/tests/uranium_tests/pluginTest/prefetch/test_prefetch_optional_group.py index 45c738f0226..aab06334c1a 100644 --- a/tests/uranium_tests/pluginTest/prefetch/test_prefetch_optional_group.py +++ b/tests/uranium_tests/pluginTest/prefetch/test_prefetch_optional_group.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_prefetch_optional_group(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/pluginTest/prefetch/test_prefetch_overflow.py b/tests/uranium_tests/pluginTest/prefetch/test_prefetch_overflow.py index dfbd3bbc47f..8610348734d 100644 --- a/tests/uranium_tests/pluginTest/prefetch/test_prefetch_overflow.py +++ b/tests/uranium_tests/pluginTest/prefetch/test_prefetch_overflow.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_prefetch_overflow(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/pluginTest/prefetch/test_prefetch_simple.py b/tests/uranium_tests/pluginTest/prefetch/test_prefetch_simple.py index bab369575da..7ecb3f5f9eb 100644 --- a/tests/uranium_tests/pluginTest/prefetch/test_prefetch_simple.py +++ b/tests/uranium_tests/pluginTest/prefetch/test_prefetch_simple.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_prefetch_simple(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/pluginTest/rate_limit/test_rate_limit.py b/tests/uranium_tests/pluginTest/rate_limit/test_rate_limit.py index 2f860733a25..3a1bf9bb559 100644 --- a/tests/uranium_tests/pluginTest/rate_limit/test_rate_limit.py +++ b/tests/uranium_tests/pluginTest/rate_limit/test_rate_limit.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_rate_limit(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/pluginTest/rate_limit/test_rate_limit_iprep.py b/tests/uranium_tests/pluginTest/rate_limit/test_rate_limit_iprep.py index 6ef666c64b6..2e888c2b71b 100644 --- a/tests/uranium_tests/pluginTest/rate_limit/test_rate_limit_iprep.py +++ b/tests/uranium_tests/pluginTest/rate_limit/test_rate_limit_iprep.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_rate_limit_iprep(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/pluginTest/rate_limit/test_rate_limit_sni.py b/tests/uranium_tests/pluginTest/rate_limit/test_rate_limit_sni.py index 75a242ca4de..cb530c9e2d7 100644 --- a/tests/uranium_tests/pluginTest/rate_limit/test_rate_limit_sni.py +++ b/tests/uranium_tests/pluginTest/rate_limit/test_rate_limit_sni.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_rate_limit_sni(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/pluginTest/rate_limit/test_rate_limit_sni_expiry.py b/tests/uranium_tests/pluginTest/rate_limit/test_rate_limit_sni_expiry.py index c69632f219b..535984048b7 100644 --- a/tests/uranium_tests/pluginTest/rate_limit/test_rate_limit_sni_expiry.py +++ b/tests/uranium_tests/pluginTest/rate_limit/test_rate_limit_sni_expiry.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_rate_limit_sni_expiry(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/pluginTest/rate_limit/test_rate_limit_sni_queue.py b/tests/uranium_tests/pluginTest/rate_limit/test_rate_limit_sni_queue.py index 36c49c7c44f..c34b120e024 100644 --- a/tests/uranium_tests/pluginTest/rate_limit/test_rate_limit_sni_queue.py +++ b/tests/uranium_tests/pluginTest/rate_limit/test_rate_limit_sni_queue.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_rate_limit_sni_queue(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/pluginTest/rate_limit/test_rate_limit_sni_reject.py b/tests/uranium_tests/pluginTest/rate_limit/test_rate_limit_sni_reject.py index 5e948eefedd..6c801294fb9 100644 --- a/tests/uranium_tests/pluginTest/rate_limit/test_rate_limit_sni_reject.py +++ b/tests/uranium_tests/pluginTest/rate_limit/test_rate_limit_sni_reject.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_rate_limit_sni_reject(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/pluginTest/redirect_limit/test_redirect_limit.py b/tests/uranium_tests/pluginTest/redirect_limit/test_redirect_limit.py index 10f3821ca0d..8ba3328d441 100644 --- a/tests/uranium_tests/pluginTest/redirect_limit/test_redirect_limit.py +++ b/tests/uranium_tests/pluginTest/redirect_limit/test_redirect_limit.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_redirect_limit(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/pluginTest/regex_remap/test_regex_remap.py b/tests/uranium_tests/pluginTest/regex_remap/test_regex_remap.py index 8ff2a6003e2..316d0abc684 100644 --- a/tests/uranium_tests/pluginTest/regex_remap/test_regex_remap.py +++ b/tests/uranium_tests/pluginTest/regex_remap/test_regex_remap.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_regex_remap(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/pluginTest/regex_revalidate/test_regex_revalidate.py b/tests/uranium_tests/pluginTest/regex_revalidate/test_regex_revalidate.py index a7e516fcb55..e6152acf63a 100644 --- a/tests/uranium_tests/pluginTest/regex_revalidate/test_regex_revalidate.py +++ b/tests/uranium_tests/pluginTest/regex_revalidate/test_regex_revalidate.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_regex_revalidate(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/pluginTest/regex_revalidate/test_regex_revalidate_miss.py b/tests/uranium_tests/pluginTest/regex_revalidate/test_regex_revalidate_miss.py index e3aa457f151..6f59c158064 100644 --- a/tests/uranium_tests/pluginTest/regex_revalidate/test_regex_revalidate_miss.py +++ b/tests/uranium_tests/pluginTest/regex_revalidate/test_regex_revalidate_miss.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_regex_revalidate_miss(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/pluginTest/regex_revalidate/test_regex_revalidate_state.py b/tests/uranium_tests/pluginTest/regex_revalidate/test_regex_revalidate_state.py index 72a9d43b3d0..d12d9eb740b 100644 --- a/tests/uranium_tests/pluginTest/regex_revalidate/test_regex_revalidate_state.py +++ b/tests/uranium_tests/pluginTest/regex_revalidate/test_regex_revalidate_state.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_regex_revalidate_state(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/pluginTest/remap_stats/test_remap_stats.py b/tests/uranium_tests/pluginTest/remap_stats/test_remap_stats.py index ac23bb3c402..497ebce548d 100644 --- a/tests/uranium_tests/pluginTest/remap_stats/test_remap_stats.py +++ b/tests/uranium_tests/pluginTest/remap_stats/test_remap_stats.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_remap_stats(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/pluginTest/remap_stats/test_remap_stats_post.py b/tests/uranium_tests/pluginTest/remap_stats/test_remap_stats_post.py index 36ac6d0d2e7..78da47bd612 100644 --- a/tests/uranium_tests/pluginTest/remap_stats/test_remap_stats_post.py +++ b/tests/uranium_tests/pluginTest/remap_stats/test_remap_stats_post.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_remap_stats_post(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/pluginTest/server_push_preload/test_server_push_preload.py b/tests/uranium_tests/pluginTest/server_push_preload/test_server_push_preload.py index a7363ac1951..a2a07895c07 100644 --- a/tests/uranium_tests/pluginTest/server_push_preload/test_server_push_preload.py +++ b/tests/uranium_tests/pluginTest/server_push_preload/test_server_push_preload.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_server_push_preload(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/pluginTest/slice/test_slice.py b/tests/uranium_tests/pluginTest/slice/test_slice.py index 9c025636a97..7c10e2d1a65 100644 --- a/tests/uranium_tests/pluginTest/slice/test_slice.py +++ b/tests/uranium_tests/pluginTest/slice/test_slice.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_slice(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/pluginTest/slice/test_slice_conditional.py b/tests/uranium_tests/pluginTest/slice/test_slice_conditional.py index 0e9e49803ae..2af9f378313 100644 --- a/tests/uranium_tests/pluginTest/slice/test_slice_conditional.py +++ b/tests/uranium_tests/pluginTest/slice/test_slice_conditional.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_slice_conditional(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/pluginTest/slice/test_slice_content_shrink.py b/tests/uranium_tests/pluginTest/slice/test_slice_content_shrink.py index b8ac4d3cf31..9f2dd961bab 100644 --- a/tests/uranium_tests/pluginTest/slice/test_slice_content_shrink.py +++ b/tests/uranium_tests/pluginTest/slice/test_slice_content_shrink.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_slice_content_shrink(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/pluginTest/slice/test_slice_crr_ident.py b/tests/uranium_tests/pluginTest/slice/test_slice_crr_ident.py index 1d22de080a5..7bcffcfb83a 100644 --- a/tests/uranium_tests/pluginTest/slice/test_slice_crr_ident.py +++ b/tests/uranium_tests/pluginTest/slice/test_slice_crr_ident.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_slice_crr_ident(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/pluginTest/slice/test_slice_error.py b/tests/uranium_tests/pluginTest/slice/test_slice_error.py index 046e4e7c841..7c84bae5ad7 100644 --- a/tests/uranium_tests/pluginTest/slice/test_slice_error.py +++ b/tests/uranium_tests/pluginTest/slice/test_slice_error.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_slice_error(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/pluginTest/slice/test_slice_ident.py b/tests/uranium_tests/pluginTest/slice/test_slice_ident.py index 3e4e5589955..889dda63a12 100644 --- a/tests/uranium_tests/pluginTest/slice/test_slice_ident.py +++ b/tests/uranium_tests/pluginTest/slice/test_slice_ident.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_slice_ident(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/pluginTest/slice/test_slice_prefetch.py b/tests/uranium_tests/pluginTest/slice/test_slice_prefetch.py index 2969486ec69..433d1f71f19 100644 --- a/tests/uranium_tests/pluginTest/slice/test_slice_prefetch.py +++ b/tests/uranium_tests/pluginTest/slice/test_slice_prefetch.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_slice_prefetch(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/pluginTest/slice/test_slice_purge.py b/tests/uranium_tests/pluginTest/slice/test_slice_purge.py index 8a19f68f98b..2cd4454558e 100644 --- a/tests/uranium_tests/pluginTest/slice/test_slice_purge.py +++ b/tests/uranium_tests/pluginTest/slice/test_slice_purge.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_slice_purge(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/pluginTest/slice/test_slice_purge_gaps.py b/tests/uranium_tests/pluginTest/slice/test_slice_purge_gaps.py index aa139a1caf1..fead2cdfc5d 100644 --- a/tests/uranium_tests/pluginTest/slice/test_slice_purge_gaps.py +++ b/tests/uranium_tests/pluginTest/slice/test_slice_purge_gaps.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_slice_purge_gaps(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/pluginTest/slice/test_slice_regex.py b/tests/uranium_tests/pluginTest/slice/test_slice_regex.py index 9b778642140..7469b50ba5d 100644 --- a/tests/uranium_tests/pluginTest/slice/test_slice_regex.py +++ b/tests/uranium_tests/pluginTest/slice/test_slice_regex.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_slice_regex(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/pluginTest/slice/test_slice_rm_range.py b/tests/uranium_tests/pluginTest/slice/test_slice_rm_range.py index db34db2c8ea..9850d4b3f7d 100644 --- a/tests/uranium_tests/pluginTest/slice/test_slice_rm_range.py +++ b/tests/uranium_tests/pluginTest/slice/test_slice_rm_range.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_slice_rm_range(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/pluginTest/slice/test_slice_selfhealing.py b/tests/uranium_tests/pluginTest/slice/test_slice_selfhealing.py index 43e7f81cfa1..a7ffb245ec0 100644 --- a/tests/uranium_tests/pluginTest/slice/test_slice_selfhealing.py +++ b/tests/uranium_tests/pluginTest/slice/test_slice_selfhealing.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_slice_selfhealing(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/pluginTest/slice/test_slice_stale_generation.py b/tests/uranium_tests/pluginTest/slice/test_slice_stale_generation.py index 264b959474b..89756b3226c 100644 --- a/tests/uranium_tests/pluginTest/slice/test_slice_stale_generation.py +++ b/tests/uranium_tests/pluginTest/slice/test_slice_stale_generation.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_slice_stale_generation(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/pluginTest/sslheaders/test_sslheaders.py b/tests/uranium_tests/pluginTest/sslheaders/test_sslheaders.py index cbfb4e19989..74d98df6b65 100644 --- a/tests/uranium_tests/pluginTest/sslheaders/test_sslheaders.py +++ b/tests/uranium_tests/pluginTest/sslheaders/test_sslheaders.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_sslheaders(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/pluginTest/stale_response/test_stale_response.py b/tests/uranium_tests/pluginTest/stale_response/test_stale_response.py index 4b1396d07ba..1fddec460f3 100644 --- a/tests/uranium_tests/pluginTest/stale_response/test_stale_response.py +++ b/tests/uranium_tests/pluginTest/stale_response/test_stale_response.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_stale_response(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/pluginTest/statichit/test_statichit.py b/tests/uranium_tests/pluginTest/statichit/test_statichit.py index 5a2c0867b07..81c650fbc7b 100644 --- a/tests/uranium_tests/pluginTest/statichit/test_statichit.py +++ b/tests/uranium_tests/pluginTest/statichit/test_statichit.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_statichit(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/pluginTest/stats_over_http/test_stats_over_http.py b/tests/uranium_tests/pluginTest/stats_over_http/test_stats_over_http.py index 276f32ae64d..6a2b56ced2f 100644 --- a/tests/uranium_tests/pluginTest/stats_over_http/test_stats_over_http.py +++ b/tests/uranium_tests/pluginTest/stats_over_http/test_stats_over_http.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_stats_over_http(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/pluginTest/stek_share/test_stek_share.py b/tests/uranium_tests/pluginTest/stek_share/test_stek_share.py index a52d5b5657d..98c34178d29 100644 --- a/tests/uranium_tests/pluginTest/stek_share/test_stek_share.py +++ b/tests/uranium_tests/pluginTest/stek_share/test_stek_share.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_stek_share(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/pluginTest/strategies/test_strategies_plugins.py b/tests/uranium_tests/pluginTest/strategies/test_strategies_plugins.py index 700819cbe22..54924e5516d 100644 --- a/tests/uranium_tests/pluginTest/strategies/test_strategies_plugins.py +++ b/tests/uranium_tests/pluginTest/strategies/test_strategies_plugins.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_strategies_plugins(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/pluginTest/test_hooks/test_hook_add.py b/tests/uranium_tests/pluginTest/test_hooks/test_hook_add.py index 1fc90d7e548..8f23f2d1c42 100644 --- a/tests/uranium_tests/pluginTest/test_hooks/test_hook_add.py +++ b/tests/uranium_tests/pluginTest/test_hooks/test_hook_add.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_hook_add(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/pluginTest/test_hooks/test_ssn_start_delay_hook.py b/tests/uranium_tests/pluginTest/test_hooks/test_ssn_start_delay_hook.py index 5827353d413..4ccdfc64ae7 100644 --- a/tests/uranium_tests/pluginTest/test_hooks/test_ssn_start_delay_hook.py +++ b/tests/uranium_tests/pluginTest/test_hooks/test_ssn_start_delay_hook.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_ssn_start_delay_hook(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/pluginTest/test_hooks/test_test_hooks.py b/tests/uranium_tests/pluginTest/test_hooks/test_test_hooks.py index 3160f3fa001..c7329532cf6 100644 --- a/tests/uranium_tests/pluginTest/test_hooks/test_test_hooks.py +++ b/tests/uranium_tests/pluginTest/test_hooks/test_test_hooks.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_test_hooks(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/pluginTest/traffic_dump/test_traffic_dump.py b/tests/uranium_tests/pluginTest/traffic_dump/test_traffic_dump.py index 575de4bc0cd..2cd8157f4e0 100644 --- a/tests/uranium_tests/pluginTest/traffic_dump/test_traffic_dump.py +++ b/tests/uranium_tests/pluginTest/traffic_dump/test_traffic_dump.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_traffic_dump(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/pluginTest/traffic_dump/test_traffic_dump_http3.py b/tests/uranium_tests/pluginTest/traffic_dump/test_traffic_dump_http3.py index d6bb983eed5..bb84df75276 100644 --- a/tests/uranium_tests/pluginTest/traffic_dump/test_traffic_dump_http3.py +++ b/tests/uranium_tests/pluginTest/traffic_dump/test_traffic_dump_http3.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_traffic_dump_http3(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/pluginTest/traffic_dump/test_traffic_dump_ip_filter.py b/tests/uranium_tests/pluginTest/traffic_dump/test_traffic_dump_ip_filter.py index c95f8382dbb..28df45a93ac 100644 --- a/tests/uranium_tests/pluginTest/traffic_dump/test_traffic_dump_ip_filter.py +++ b/tests/uranium_tests/pluginTest/traffic_dump/test_traffic_dump_ip_filter.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_traffic_dump_ip_filter(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/pluginTest/traffic_dump/test_traffic_dump_response_body.py b/tests/uranium_tests/pluginTest/traffic_dump/test_traffic_dump_response_body.py index 830d537082b..df3fc471202 100644 --- a/tests/uranium_tests/pluginTest/traffic_dump/test_traffic_dump_response_body.py +++ b/tests/uranium_tests/pluginTest/traffic_dump/test_traffic_dump_response_body.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_traffic_dump_response_body(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/pluginTest/traffic_dump/test_traffic_dump_sni_filter.py b/tests/uranium_tests/pluginTest/traffic_dump/test_traffic_dump_sni_filter.py index 36a09eb7924..a1d0a14ca00 100644 --- a/tests/uranium_tests/pluginTest/traffic_dump/test_traffic_dump_sni_filter.py +++ b/tests/uranium_tests/pluginTest/traffic_dump/test_traffic_dump_sni_filter.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_traffic_dump_sni_filter(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/pluginTest/transform/test_transaction_data_sink.py b/tests/uranium_tests/pluginTest/transform/test_transaction_data_sink.py index ec78cf8826a..b64ef8a041f 100644 --- a/tests/uranium_tests/pluginTest/transform/test_transaction_data_sink.py +++ b/tests/uranium_tests/pluginTest/transform/test_transaction_data_sink.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_transaction_data_sink(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/pluginTest/tsapi/test_test_TSHttpSsnInfo.py b/tests/uranium_tests/pluginTest/tsapi/test_test_TSHttpSsnInfo.py index fd385f556db..5508b34dc20 100644 --- a/tests/uranium_tests/pluginTest/tsapi/test_test_TSHttpSsnInfo.py +++ b/tests/uranium_tests/pluginTest/tsapi/test_test_TSHttpSsnInfo.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_test_TSHttpSsnInfo(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/pluginTest/tsapi/test_test_TSHttpTxnServerAddrSet.py b/tests/uranium_tests/pluginTest/tsapi/test_test_TSHttpTxnServerAddrSet.py index 5b9dd6fadad..f9764aa9bc7 100644 --- a/tests/uranium_tests/pluginTest/tsapi/test_test_TSHttpTxnServerAddrSet.py +++ b/tests/uranium_tests/pluginTest/tsapi/test_test_TSHttpTxnServerAddrSet.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_test_TSHttpTxnServerAddrSet(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/pluginTest/tsapi/test_test_TSHttpTxnServerAddrSet_retry.py b/tests/uranium_tests/pluginTest/tsapi/test_test_TSHttpTxnServerAddrSet_retry.py index 67720e5f31e..db6414eb93f 100644 --- a/tests/uranium_tests/pluginTest/tsapi/test_test_TSHttpTxnServerAddrSet_retry.py +++ b/tests/uranium_tests/pluginTest/tsapi/test_test_TSHttpTxnServerAddrSet_retry.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_test_TSHttpTxnServerAddrSet_retry(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/pluginTest/tsapi/test_test_TSHttpTxnVerifiedAddr.py b/tests/uranium_tests/pluginTest/tsapi/test_test_TSHttpTxnVerifiedAddr.py index c5f20450a54..0a3289ab794 100644 --- a/tests/uranium_tests/pluginTest/tsapi/test_test_TSHttpTxnVerifiedAddr.py +++ b/tests/uranium_tests/pluginTest/tsapi/test_test_TSHttpTxnVerifiedAddr.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_test_TSHttpTxnVerifiedAddr(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/pluginTest/tsapi/test_test_TSVConnPPInfo.py b/tests/uranium_tests/pluginTest/tsapi/test_test_TSVConnPPInfo.py index 99339586c6c..2e72735c230 100644 --- a/tests/uranium_tests/pluginTest/tsapi/test_test_TSVConnPPInfo.py +++ b/tests/uranium_tests/pluginTest/tsapi/test_test_TSVConnPPInfo.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_test_TSVConnPPInfo(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/pluginTest/tsapi/test_tsapi.py b/tests/uranium_tests/pluginTest/tsapi/test_tsapi.py index 6fd4fc34791..473da4d1a32 100644 --- a/tests/uranium_tests/pluginTest/tsapi/test_tsapi.py +++ b/tests/uranium_tests/pluginTest/tsapi/test_tsapi.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_tsapi(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/pluginTest/txn_box/basic/test_txn_box_basic.py b/tests/uranium_tests/pluginTest/txn_box/basic/test_txn_box_basic.py index 47803b0afb8..fc13a96c445 100644 --- a/tests/uranium_tests/pluginTest/txn_box/basic/test_txn_box_basic.py +++ b/tests/uranium_tests/pluginTest/txn_box/basic/test_txn_box_basic.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_txn_box_basic(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/pluginTest/txn_box/basic/test_txn_box_cmp.py b/tests/uranium_tests/pluginTest/txn_box/basic/test_txn_box_cmp.py index 3a0056aa77e..858683e7878 100644 --- a/tests/uranium_tests/pluginTest/txn_box/basic/test_txn_box_cmp.py +++ b/tests/uranium_tests/pluginTest/txn_box/basic/test_txn_box_cmp.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_txn_box_cmp(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/pluginTest/txn_box/basic/test_txn_box_ip_addr.py b/tests/uranium_tests/pluginTest/txn_box/basic/test_txn_box_ip_addr.py index 03dd9d34d76..31e5f2c3610 100644 --- a/tests/uranium_tests/pluginTest/txn_box/basic/test_txn_box_ip_addr.py +++ b/tests/uranium_tests/pluginTest/txn_box/basic/test_txn_box_ip_addr.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_txn_box_ip_addr(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/pluginTest/txn_box/basic/test_txn_box_mod.py b/tests/uranium_tests/pluginTest/txn_box/basic/test_txn_box_mod.py index 259e5b78e6e..3b6f63a71e1 100644 --- a/tests/uranium_tests/pluginTest/txn_box/basic/test_txn_box_mod.py +++ b/tests/uranium_tests/pluginTest/txn_box/basic/test_txn_box_mod.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_txn_box_mod(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/pluginTest/txn_box/basic/test_txn_box_multi_cfg.py b/tests/uranium_tests/pluginTest/txn_box/basic/test_txn_box_multi_cfg.py index 114f02ca9ad..e14b4ea2296 100644 --- a/tests/uranium_tests/pluginTest/txn_box/basic/test_txn_box_multi_cfg.py +++ b/tests/uranium_tests/pluginTest/txn_box/basic/test_txn_box_multi_cfg.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_txn_box_multi_cfg(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/pluginTest/txn_box/basic/test_txn_box_redirect.py b/tests/uranium_tests/pluginTest/txn_box/basic/test_txn_box_redirect.py index 687ef2aab46..51aecc08dfc 100644 --- a/tests/uranium_tests/pluginTest/txn_box/basic/test_txn_box_redirect.py +++ b/tests/uranium_tests/pluginTest/txn_box/basic/test_txn_box_redirect.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_txn_box_redirect(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/pluginTest/txn_box/basic/test_txn_box_reply.py b/tests/uranium_tests/pluginTest/txn_box/basic/test_txn_box_reply.py index 1eb1e36d650..e819beadb07 100644 --- a/tests/uranium_tests/pluginTest/txn_box/basic/test_txn_box_reply.py +++ b/tests/uranium_tests/pluginTest/txn_box/basic/test_txn_box_reply.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_txn_box_reply(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/pluginTest/txn_box/basic/test_txn_box_rxp.py b/tests/uranium_tests/pluginTest/txn_box/basic/test_txn_box_rxp.py index 50faff0d275..4a45a1912b6 100644 --- a/tests/uranium_tests/pluginTest/txn_box/basic/test_txn_box_rxp.py +++ b/tests/uranium_tests/pluginTest/txn_box/basic/test_txn_box_rxp.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_txn_box_rxp(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/pluginTest/txn_box/basic/test_txn_box_stat.py b/tests/uranium_tests/pluginTest/txn_box/basic/test_txn_box_stat.py index 4205ddedf05..4b2424c0743 100644 --- a/tests/uranium_tests/pluginTest/txn_box/basic/test_txn_box_stat.py +++ b/tests/uranium_tests/pluginTest/txn_box/basic/test_txn_box_stat.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_txn_box_stat(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/pluginTest/txn_box/basic/test_txn_box_tls.py b/tests/uranium_tests/pluginTest/txn_box/basic/test_txn_box_tls.py index 490a4175875..d8351fc7102 100644 --- a/tests/uranium_tests/pluginTest/txn_box/basic/test_txn_box_tls.py +++ b/tests/uranium_tests/pluginTest/txn_box/basic/test_txn_box_tls.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_txn_box_tls(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/pluginTest/txn_box/basic/test_txn_box_tls_cert.py b/tests/uranium_tests/pluginTest/txn_box/basic/test_txn_box_tls_cert.py index c5f496879cb..a67a72d7dbf 100644 --- a/tests/uranium_tests/pluginTest/txn_box/basic/test_txn_box_tls_cert.py +++ b/tests/uranium_tests/pluginTest/txn_box/basic/test_txn_box_tls_cert.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_txn_box_tls_cert(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/pluginTest/txn_box/basic/test_txn_box_tuple.py b/tests/uranium_tests/pluginTest/txn_box/basic/test_txn_box_tuple.py index 49a2c18a581..d7cef3ccf25 100644 --- a/tests/uranium_tests/pluginTest/txn_box/basic/test_txn_box_tuple.py +++ b/tests/uranium_tests/pluginTest/txn_box/basic/test_txn_box_tuple.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_txn_box_tuple(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/pluginTest/txn_box/basic/test_txn_box_txn_debug.py b/tests/uranium_tests/pluginTest/txn_box/basic/test_txn_box_txn_debug.py index 0d551eae2c4..72a1e9ccc3e 100644 --- a/tests/uranium_tests/pluginTest/txn_box/basic/test_txn_box_txn_debug.py +++ b/tests/uranium_tests/pluginTest/txn_box/basic/test_txn_box_txn_debug.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_txn_box_txn_debug(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/pluginTest/txn_box/basic/test_txn_box_txn_error.py b/tests/uranium_tests/pluginTest/txn_box/basic/test_txn_box_txn_error.py index 3a5548e9e27..e2bdd6f51bf 100644 --- a/tests/uranium_tests/pluginTest/txn_box/basic/test_txn_box_txn_error.py +++ b/tests/uranium_tests/pluginTest/txn_box/basic/test_txn_box_txn_error.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_txn_box_txn_error(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/pluginTest/txn_box/basic/test_txn_box_txn_open_1.py b/tests/uranium_tests/pluginTest/txn_box/basic/test_txn_box_txn_open_1.py index 2e4520ab6ec..fdb95396b2f 100644 --- a/tests/uranium_tests/pluginTest/txn_box/basic/test_txn_box_txn_open_1.py +++ b/tests/uranium_tests/pluginTest/txn_box/basic/test_txn_box_txn_open_1.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_txn_box_txn_open_1(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/pluginTest/txn_box/basic/test_txn_box_txn_open_2.py b/tests/uranium_tests/pluginTest/txn_box/basic/test_txn_box_txn_open_2.py index 7e1cb7e6e3f..90756ec370d 100644 --- a/tests/uranium_tests/pluginTest/txn_box/basic/test_txn_box_txn_open_2.py +++ b/tests/uranium_tests/pluginTest/txn_box/basic/test_txn_box_txn_open_2.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_txn_box_txn_open_2(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/pluginTest/txn_box/basic/test_txn_box_txn_open_3.py b/tests/uranium_tests/pluginTest/txn_box/basic/test_txn_box_txn_open_3.py index 14663b67dba..b4f180605c3 100644 --- a/tests/uranium_tests/pluginTest/txn_box/basic/test_txn_box_txn_open_3.py +++ b/tests/uranium_tests/pluginTest/txn_box/basic/test_txn_box_txn_open_3.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_txn_box_txn_open_3(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/pluginTest/txn_box/basic/test_txn_box_with.py b/tests/uranium_tests/pluginTest/txn_box/basic/test_txn_box_with.py index 998cac44db3..56e30f293c7 100644 --- a/tests/uranium_tests/pluginTest/txn_box/basic/test_txn_box_with.py +++ b/tests/uranium_tests/pluginTest/txn_box/basic/test_txn_box_with.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_txn_box_with(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/pluginTest/txn_box/ct_header/test_txn_box_ct_header.py b/tests/uranium_tests/pluginTest/txn_box/ct_header/test_txn_box_ct_header.py index 8cddb7c19aa..f309cad8b47 100644 --- a/tests/uranium_tests/pluginTest/txn_box/ct_header/test_txn_box_ct_header.py +++ b/tests/uranium_tests/pluginTest/txn_box/ct_header/test_txn_box_ct_header.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_txn_box_ct_header(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/pluginTest/txn_box/example/test_txn_box_accept_encoding.py b/tests/uranium_tests/pluginTest/txn_box/example/test_txn_box_accept_encoding.py index 96f7f1dff90..bed4b8c27b6 100644 --- a/tests/uranium_tests/pluginTest/txn_box/example/test_txn_box_accept_encoding.py +++ b/tests/uranium_tests/pluginTest/txn_box/example/test_txn_box_accept_encoding.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_txn_box_accept_encoding(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/pluginTest/txn_box/prod/test_txn_box_cors_origin.py b/tests/uranium_tests/pluginTest/txn_box/prod/test_txn_box_cors_origin.py index 6e98ed62f1b..b16fd0e5963 100644 --- a/tests/uranium_tests/pluginTest/txn_box/prod/test_txn_box_cors_origin.py +++ b/tests/uranium_tests/pluginTest/txn_box/prod/test_txn_box_cors_origin.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_txn_box_cors_origin(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/pluginTest/txn_box/prod/test_txn_box_cors_referrer.py b/tests/uranium_tests/pluginTest/txn_box/prod/test_txn_box_cors_referrer.py index 35ca9e2ed1c..a59e5aca8fa 100644 --- a/tests/uranium_tests/pluginTest/txn_box/prod/test_txn_box_cors_referrer.py +++ b/tests/uranium_tests/pluginTest/txn_box/prod/test_txn_box_cors_referrer.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_txn_box_cors_referrer(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/pluginTest/txn_box/prod/test_txn_box_ip_acl.py b/tests/uranium_tests/pluginTest/txn_box/prod/test_txn_box_ip_acl.py index 25d9e84ff8d..e70f485cbfe 100644 --- a/tests/uranium_tests/pluginTest/txn_box/prod/test_txn_box_ip_acl.py +++ b/tests/uranium_tests/pluginTest/txn_box/prod/test_txn_box_ip_acl.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_txn_box_ip_acl(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/pluginTest/txn_box/prod/test_txn_box_mTLS.py b/tests/uranium_tests/pluginTest/txn_box/prod/test_txn_box_mTLS.py index e0b1d7456e4..69283d3a347 100644 --- a/tests/uranium_tests/pluginTest/txn_box/prod/test_txn_box_mTLS.py +++ b/tests/uranium_tests/pluginTest/txn_box/prod/test_txn_box_mTLS.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_txn_box_mTLS(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/pluginTest/txn_box/prod/test_txn_box_query.py b/tests/uranium_tests/pluginTest/txn_box/prod/test_txn_box_query.py index fa3f73a0ea3..6fe89282724 100644 --- a/tests/uranium_tests/pluginTest/txn_box/prod/test_txn_box_query.py +++ b/tests/uranium_tests/pluginTest/txn_box/prod/test_txn_box_query.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_txn_box_query(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/pluginTest/txn_box/prod/test_txn_box_query_delete_filter.py b/tests/uranium_tests/pluginTest/txn_box/prod/test_txn_box_query_delete_filter.py index 7f7be7e353f..87119047f93 100644 --- a/tests/uranium_tests/pluginTest/txn_box/prod/test_txn_box_query_delete_filter.py +++ b/tests/uranium_tests/pluginTest/txn_box/prod/test_txn_box_query_delete_filter.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_txn_box_query_delete_filter(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/pluginTest/txn_box/prod/test_txn_box_query_delete_regex.py b/tests/uranium_tests/pluginTest/txn_box/prod/test_txn_box_query_delete_regex.py index 84f83fcca13..50b362b85ea 100644 --- a/tests/uranium_tests/pluginTest/txn_box/prod/test_txn_box_query_delete_regex.py +++ b/tests/uranium_tests/pluginTest/txn_box/prod/test_txn_box_query_delete_regex.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_txn_box_query_delete_regex(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/pluginTest/txn_box/prod/test_txn_box_stanley.py b/tests/uranium_tests/pluginTest/txn_box/prod/test_txn_box_stanley.py index ffec5b9b180..6e909066f53 100644 --- a/tests/uranium_tests/pluginTest/txn_box/prod/test_txn_box_stanley.py +++ b/tests/uranium_tests/pluginTest/txn_box/prod/test_txn_box_stanley.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_txn_box_stanley(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/pluginTest/txn_box/prod/test_txn_box_vznith_1.py b/tests/uranium_tests/pluginTest/txn_box/prod/test_txn_box_vznith_1.py index f42f1252bd4..8673a77b2da 100644 --- a/tests/uranium_tests/pluginTest/txn_box/prod/test_txn_box_vznith_1.py +++ b/tests/uranium_tests/pluginTest/txn_box/prod/test_txn_box_vznith_1.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_txn_box_vznith_1(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/pluginTest/txn_box/prod/test_txn_box_yts_3489.py b/tests/uranium_tests/pluginTest/txn_box/prod/test_txn_box_yts_3489.py index 52bd922b038..5af8503015d 100644 --- a/tests/uranium_tests/pluginTest/txn_box/prod/test_txn_box_yts_3489.py +++ b/tests/uranium_tests/pluginTest/txn_box/prod/test_txn_box_yts_3489.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_txn_box_yts_3489(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/pluginTest/txn_box/ramp/test_txn_box_multi_ramp_1.py b/tests/uranium_tests/pluginTest/txn_box/ramp/test_txn_box_multi_ramp_1.py index b700a3f1f69..8e813aa10ec 100644 --- a/tests/uranium_tests/pluginTest/txn_box/ramp/test_txn_box_multi_ramp_1.py +++ b/tests/uranium_tests/pluginTest/txn_box/ramp/test_txn_box_multi_ramp_1.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_txn_box_multi_ramp_1(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/pluginTest/txn_box/ramp/test_txn_box_multi_ramp_2.py b/tests/uranium_tests/pluginTest/txn_box/ramp/test_txn_box_multi_ramp_2.py index 682e9bcb213..e9717688d95 100644 --- a/tests/uranium_tests/pluginTest/txn_box/ramp/test_txn_box_multi_ramp_2.py +++ b/tests/uranium_tests/pluginTest/txn_box/ramp/test_txn_box_multi_ramp_2.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_txn_box_multi_ramp_2(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/pluginTest/txn_box/ramp/test_txn_box_multi_ramp_3.py b/tests/uranium_tests/pluginTest/txn_box/ramp/test_txn_box_multi_ramp_3.py index da42843f594..b917bc8cbe2 100644 --- a/tests/uranium_tests/pluginTest/txn_box/ramp/test_txn_box_multi_ramp_3.py +++ b/tests/uranium_tests/pluginTest/txn_box/ramp/test_txn_box_multi_ramp_3.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_txn_box_multi_ramp_3(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/pluginTest/txn_box/ramp/test_txn_box_ramp.py b/tests/uranium_tests/pluginTest/txn_box/ramp/test_txn_box_ramp.py index 842e1674ea7..367b58c15cd 100644 --- a/tests/uranium_tests/pluginTest/txn_box/ramp/test_txn_box_ramp.py +++ b/tests/uranium_tests/pluginTest/txn_box/ramp/test_txn_box_ramp.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_txn_box_ramp(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/pluginTest/txn_box/remap/test_txn_box_remap_base.py b/tests/uranium_tests/pluginTest/txn_box/remap/test_txn_box_remap_base.py index f3a43b28f06..34c17e4ef3c 100644 --- a/tests/uranium_tests/pluginTest/txn_box/remap/test_txn_box_remap_base.py +++ b/tests/uranium_tests/pluginTest/txn_box/remap/test_txn_box_remap_base.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_txn_box_remap_base(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/pluginTest/txn_box/smoke/test_txn_box_smoke.py b/tests/uranium_tests/pluginTest/txn_box/smoke/test_txn_box_smoke.py index b6d1a9018f8..344eeba5717 100644 --- a/tests/uranium_tests/pluginTest/txn_box/smoke/test_txn_box_smoke.py +++ b/tests/uranium_tests/pluginTest/txn_box/smoke/test_txn_box_smoke.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_txn_box_smoke(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/pluginTest/txn_box/smoke/test_txn_box_smoke_2.py b/tests/uranium_tests/pluginTest/txn_box/smoke/test_txn_box_smoke_2.py index cc7f4d046b8..15f52040426 100644 --- a/tests/uranium_tests/pluginTest/txn_box/smoke/test_txn_box_smoke_2.py +++ b/tests/uranium_tests/pluginTest/txn_box/smoke/test_txn_box_smoke_2.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_txn_box_smoke_2(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/pluginTest/txn_box/static_file/test_txn_box_static_file.py b/tests/uranium_tests/pluginTest/txn_box/static_file/test_txn_box_static_file.py index 0a716aa8860..095b1eeb4c9 100644 --- a/tests/uranium_tests/pluginTest/txn_box/static_file/test_txn_box_static_file.py +++ b/tests/uranium_tests/pluginTest/txn_box/static_file/test_txn_box_static_file.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_txn_box_static_file(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/pluginTest/uri_signing/test_uri_signing.py b/tests/uranium_tests/pluginTest/uri_signing/test_uri_signing.py index c2bfd4a084f..c84f8602332 100644 --- a/tests/uranium_tests/pluginTest/uri_signing/test_uri_signing.py +++ b/tests/uranium_tests/pluginTest/uri_signing/test_uri_signing.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_uri_signing(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/pluginTest/url_sig/test_url_sig.py b/tests/uranium_tests/pluginTest/url_sig/test_url_sig.py index b90cabb856b..3721b2d38df 100644 --- a/tests/uranium_tests/pluginTest/url_sig/test_url_sig.py +++ b/tests/uranium_tests/pluginTest/url_sig/test_url_sig.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_url_sig(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/pluginTest/webp_transform/test_webp_transform_cache_refused.py b/tests/uranium_tests/pluginTest/webp_transform/test_webp_transform_cache_refused.py index e32da7aae63..71987bf68c1 100644 --- a/tests/uranium_tests/pluginTest/webp_transform/test_webp_transform_cache_refused.py +++ b/tests/uranium_tests/pluginTest/webp_transform/test_webp_transform_cache_refused.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_webp_transform_cache_refused(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/pluginTest/webp_transform/test_webp_transform_chunked_cap.py b/tests/uranium_tests/pluginTest/webp_transform/test_webp_transform_chunked_cap.py index 048ea33957f..a751f77ca22 100644 --- a/tests/uranium_tests/pluginTest/webp_transform/test_webp_transform_chunked_cap.py +++ b/tests/uranium_tests/pluginTest/webp_transform/test_webp_transform_chunked_cap.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_webp_transform_chunked_cap(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/pluginTest/webp_transform/test_webp_transform_decode_limit.py b/tests/uranium_tests/pluginTest/webp_transform/test_webp_transform_decode_limit.py index 27d3f84fc1c..416e1991f49 100644 --- a/tests/uranium_tests/pluginTest/webp_transform/test_webp_transform_decode_limit.py +++ b/tests/uranium_tests/pluginTest/webp_transform/test_webp_transform_decode_limit.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_webp_transform_decode_limit(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/pluginTest/webp_transform/test_webp_transform_max_buffer_size.py b/tests/uranium_tests/pluginTest/webp_transform/test_webp_transform_max_buffer_size.py index 3e304ccce79..dadead5ed1a 100644 --- a/tests/uranium_tests/pluginTest/webp_transform/test_webp_transform_max_buffer_size.py +++ b/tests/uranium_tests/pluginTest/webp_transform/test_webp_transform_max_buffer_size.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_webp_transform_max_buffer_size(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/pluginTest/webp_transform/test_webp_transform_size_cap.py b/tests/uranium_tests/pluginTest/webp_transform/test_webp_transform_size_cap.py index 814db15a9fa..37ed718f689 100644 --- a/tests/uranium_tests/pluginTest/webp_transform/test_webp_transform_size_cap.py +++ b/tests/uranium_tests/pluginTest/webp_transform/test_webp_transform_size_cap.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_webp_transform_size_cap(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/pluginTest/xdebug/x_cache_info/test_x_cache_info.py b/tests/uranium_tests/pluginTest/xdebug/x_cache_info/test_x_cache_info.py index 2a803c43a3e..56e91334846 100644 --- a/tests/uranium_tests/pluginTest/xdebug/x_cache_info/test_x_cache_info.py +++ b/tests/uranium_tests/pluginTest/xdebug/x_cache_info/test_x_cache_info.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_x_cache_info(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/pluginTest/xdebug/x_effective_url/test_x_effective_url.py b/tests/uranium_tests/pluginTest/xdebug/x_effective_url/test_x_effective_url.py index 1ca95263f7d..d4676b33199 100644 --- a/tests/uranium_tests/pluginTest/xdebug/x_effective_url/test_x_effective_url.py +++ b/tests/uranium_tests/pluginTest/xdebug/x_effective_url/test_x_effective_url.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_x_effective_url(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/pluginTest/xdebug/x_probe/test_x_probe.py b/tests/uranium_tests/pluginTest/xdebug/x_probe/test_x_probe.py index 52e4f2b1dd8..ed14a27a2ec 100644 --- a/tests/uranium_tests/pluginTest/xdebug/x_probe/test_x_probe.py +++ b/tests/uranium_tests/pluginTest/xdebug/x_probe/test_x_probe.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_x_probe(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/pluginTest/xdebug/x_probe_full_json/test_x_probe_full_json.py b/tests/uranium_tests/pluginTest/xdebug/x_probe_full_json/test_x_probe_full_json.py index 30e57389d15..748115585f4 100644 --- a/tests/uranium_tests/pluginTest/xdebug/x_probe_full_json/test_x_probe_full_json.py +++ b/tests/uranium_tests/pluginTest/xdebug/x_probe_full_json/test_x_probe_full_json.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_x_probe_full_json(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/pluginTest/xdebug/x_probe_full_json_slow_origin/test_x_probe_full_json_slow_origin.py b/tests/uranium_tests/pluginTest/xdebug/x_probe_full_json_slow_origin/test_x_probe_full_json_slow_origin.py index 11444593b6e..05cec5d4411 100644 --- a/tests/uranium_tests/pluginTest/xdebug/x_probe_full_json_slow_origin/test_x_probe_full_json_slow_origin.py +++ b/tests/uranium_tests/pluginTest/xdebug/x_probe_full_json_slow_origin/test_x_probe_full_json_slow_origin.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_x_probe_full_json_slow_origin(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/pluginTest/xdebug/x_remap/test_x_remap.py b/tests/uranium_tests/pluginTest/xdebug/x_remap/test_x_remap.py index 165d0cb7eba..d0020396ef2 100644 --- a/tests/uranium_tests/pluginTest/xdebug/x_remap/test_x_remap.py +++ b/tests/uranium_tests/pluginTest/xdebug/x_remap/test_x_remap.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_x_remap(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/post/test_expect_tests.py b/tests/uranium_tests/post/test_expect_tests.py index 48cad372701..0792931f094 100644 --- a/tests/uranium_tests/post/test_expect_tests.py +++ b/tests/uranium_tests/post/test_expect_tests.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_expect_tests(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/post/test_post_continue.py b/tests/uranium_tests/post/test_post_continue.py index 8ce7ee79a3e..048a01d93c4 100644 --- a/tests/uranium_tests/post/test_post_continue.py +++ b/tests/uranium_tests/post/test_post_continue.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_post_continue(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/post/test_post_early_return.py b/tests/uranium_tests/post/test_post_early_return.py index be9f3a73b56..b330894a4b8 100644 --- a/tests/uranium_tests/post/test_post_early_return.py +++ b/tests/uranium_tests/post/test_post_early_return.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_post_early_return(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/post/test_simple_post_valid_buffer_check.py b/tests/uranium_tests/post/test_simple_post_valid_buffer_check.py index 208413c6879..d29918ca80a 100644 --- a/tests/uranium_tests/post/test_simple_post_valid_buffer_check.py +++ b/tests/uranium_tests/post/test_simple_post_valid_buffer_check.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_simple_post_valid_buffer_check(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/post_slow_server/test_post_slow_server.py b/tests/uranium_tests/post_slow_server/test_post_slow_server.py index de189acae3c..42a16c1c9f3 100644 --- a/tests/uranium_tests/post_slow_server/test_post_slow_server.py +++ b/tests/uranium_tests/post_slow_server/test_post_slow_server.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_post_slow_server(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/post_slow_server/test_post_slow_server_max_requests_in.py b/tests/uranium_tests/post_slow_server/test_post_slow_server_max_requests_in.py index 504c25ae906..4062a4a4259 100644 --- a/tests/uranium_tests/post_slow_server/test_post_slow_server_max_requests_in.py +++ b/tests/uranium_tests/post_slow_server/test_post_slow_server_max_requests_in.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_post_slow_server_max_requests_in(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/proxy_protocol/test_proxy_protocol.py b/tests/uranium_tests/proxy_protocol/test_proxy_protocol.py index 72d9c3cb8bb..cf4e79150cb 100644 --- a/tests/uranium_tests/proxy_protocol/test_proxy_protocol.py +++ b/tests/uranium_tests/proxy_protocol/test_proxy_protocol.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_proxy_protocol(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/qmux/test_qmux_go_client.py b/tests/uranium_tests/qmux/test_qmux_go_client.py index 4855164f6a9..03a7838d58e 100644 --- a/tests/uranium_tests/qmux/test_qmux_go_client.py +++ b/tests/uranium_tests/qmux/test_qmux_go_client.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_qmux_go_client(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/records/test_records_config_to_yaml.py b/tests/uranium_tests/records/test_records_config_to_yaml.py index 937188bff1d..e8da884f0ab 100644 --- a/tests/uranium_tests/records/test_records_config_to_yaml.py +++ b/tests/uranium_tests/records/test_records_config_to_yaml.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_records_config_to_yaml(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/records/test_records_runroot_precedence.py b/tests/uranium_tests/records/test_records_runroot_precedence.py index 0ef7c22424d..5327b44c429 100644 --- a/tests/uranium_tests/records/test_records_runroot_precedence.py +++ b/tests/uranium_tests/records/test_records_runroot_precedence.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_records_runroot_precedence(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/records/test_records_yaml.py b/tests/uranium_tests/records/test_records_yaml.py index 30c651b270c..456f48ec42d 100644 --- a/tests/uranium_tests/records/test_records_yaml.py +++ b/tests/uranium_tests/records/test_records_yaml.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_records_yaml(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/records/test_traffic_ctl_cold_config.py b/tests/uranium_tests/records/test_traffic_ctl_cold_config.py index 3dec7bec5e1..5d542952e7c 100644 --- a/tests/uranium_tests/records/test_traffic_ctl_cold_config.py +++ b/tests/uranium_tests/records/test_traffic_ctl_cold_config.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_traffic_ctl_cold_config(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/records/test_ts_max_records_param.py b/tests/uranium_tests/records/test_ts_max_records_param.py index a6171e614b9..00ca18e55fa 100644 --- a/tests/uranium_tests/records/test_ts_max_records_param.py +++ b/tests/uranium_tests/records/test_ts_max_records_param.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_ts_max_records_param(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/redirect/test_number_of_redirects.py b/tests/uranium_tests/redirect/test_number_of_redirects.py index 29b28f93038..f85f9d99e53 100644 --- a/tests/uranium_tests/redirect/test_number_of_redirects.py +++ b/tests/uranium_tests/redirect/test_number_of_redirects.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_number_of_redirects(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/redirect/test_redirect.py b/tests/uranium_tests/redirect/test_redirect.py index 48df84e0c5d..5cd3bae67e3 100644 --- a/tests/uranium_tests/redirect/test_redirect.py +++ b/tests/uranium_tests/redirect/test_redirect.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_redirect(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/redirect/test_redirect_actions.py b/tests/uranium_tests/redirect/test_redirect_actions.py index ad4b1132458..51142270d39 100644 --- a/tests/uranium_tests/redirect/test_redirect_actions.py +++ b/tests/uranium_tests/redirect/test_redirect_actions.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_redirect_actions(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/redirect/test_redirect_post.py b/tests/uranium_tests/redirect/test_redirect_post.py index 7ebbd1ae322..c79bf2d05a8 100644 --- a/tests/uranium_tests/redirect/test_redirect_post.py +++ b/tests/uranium_tests/redirect/test_redirect_post.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_redirect_post(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/redirect/test_redirect_stale.py b/tests/uranium_tests/redirect/test_redirect_stale.py index 957387162c0..23130528be9 100644 --- a/tests/uranium_tests/redirect/test_redirect_stale.py +++ b/tests/uranium_tests/redirect/test_redirect_stale.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_redirect_stale(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/redirect/test_redirect_to_same_origin_on_cache.py b/tests/uranium_tests/redirect/test_redirect_to_same_origin_on_cache.py index db2ebc701d5..8b9750ee237 100644 --- a/tests/uranium_tests/redirect/test_redirect_to_same_origin_on_cache.py +++ b/tests/uranium_tests/redirect/test_redirect_to_same_origin_on_cache.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_redirect_to_same_origin_on_cache(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/remap/test_basic_conf_remap_yaml.py b/tests/uranium_tests/remap/test_basic_conf_remap_yaml.py index 07301df661d..1709361307f 100644 --- a/tests/uranium_tests/remap/test_basic_conf_remap_yaml.py +++ b/tests/uranium_tests/remap/test_basic_conf_remap_yaml.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_basic_conf_remap_yaml(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/remap/test_conf_remap_float.py b/tests/uranium_tests/remap/test_conf_remap_float.py index 045420dec7f..04704454f0d 100644 --- a/tests/uranium_tests/remap/test_conf_remap_float.py +++ b/tests/uranium_tests/remap/test_conf_remap_float.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_conf_remap_float(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/remap/test_map_with_recv_port.py b/tests/uranium_tests/remap/test_map_with_recv_port.py index 413c6bb4250..58a547f3157 100644 --- a/tests/uranium_tests/remap/test_map_with_recv_port.py +++ b/tests/uranium_tests/remap/test_map_with_recv_port.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_map_with_recv_port(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/remap/test_regex_map.py b/tests/uranium_tests/remap/test_regex_map.py index f1837e66412..e5323ecaf3e 100644 --- a/tests/uranium_tests/remap/test_regex_map.py +++ b/tests/uranium_tests/remap/test_regex_map.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_regex_map(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/remap/test_remap_acl.py b/tests/uranium_tests/remap/test_remap_acl.py index c96f55a8008..a34f6c60e1b 100644 --- a/tests/uranium_tests/remap/test_remap_acl.py +++ b/tests/uranium_tests/remap/test_remap_acl.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_remap_acl(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/remap/test_remap_http.py b/tests/uranium_tests/remap/test_remap_http.py index 626c8ec692b..7a8464c0c96 100644 --- a/tests/uranium_tests/remap/test_remap_http.py +++ b/tests/uranium_tests/remap/test_remap_http.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_remap_http(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/remap/test_remap_https.py b/tests/uranium_tests/remap/test_remap_https.py index a9bb834eeb6..22945966e69 100644 --- a/tests/uranium_tests/remap/test_remap_https.py +++ b/tests/uranium_tests/remap/test_remap_https.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_remap_https(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/remap/test_remap_ip_resolve.py b/tests/uranium_tests/remap/test_remap_ip_resolve.py index c863e1851fe..294604a7ea1 100644 --- a/tests/uranium_tests/remap/test_remap_ip_resolve.py +++ b/tests/uranium_tests/remap/test_remap_ip_resolve.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_remap_ip_resolve(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/remap/test_remap_load_empty_failure.py b/tests/uranium_tests/remap/test_remap_load_empty_failure.py index 23855a6e973..dd1ad656337 100644 --- a/tests/uranium_tests/remap/test_remap_load_empty_failure.py +++ b/tests/uranium_tests/remap/test_remap_load_empty_failure.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_remap_load_empty_failure(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/remap/test_remap_load_empty_success.py b/tests/uranium_tests/remap/test_remap_load_empty_success.py index 8526be39cd9..1475922bb8f 100644 --- a/tests/uranium_tests/remap/test_remap_load_empty_success.py +++ b/tests/uranium_tests/remap/test_remap_load_empty_success.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_remap_load_empty_success(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/remap/test_remap_load_missing_failure.py b/tests/uranium_tests/remap/test_remap_load_missing_failure.py index 43f36038973..ae6e6a1e283 100644 --- a/tests/uranium_tests/remap/test_remap_load_missing_failure.py +++ b/tests/uranium_tests/remap/test_remap_load_missing_failure.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_remap_load_missing_failure(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/remap/test_remap_load_missing_success.py b/tests/uranium_tests/remap/test_remap_load_missing_success.py index fed3ab72059..d54d4d0bc47 100644 --- a/tests/uranium_tests/remap/test_remap_load_missing_success.py +++ b/tests/uranium_tests/remap/test_remap_load_missing_success.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_remap_load_missing_success(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/remap/test_remap_reload.py b/tests/uranium_tests/remap/test_remap_reload.py index e62757c5832..a84ab274034 100644 --- a/tests/uranium_tests/remap/test_remap_reload.py +++ b/tests/uranium_tests/remap/test_remap_reload.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_remap_reload(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/remap/test_remap_ws.py b/tests/uranium_tests/remap/test_remap_ws.py index c9777f4e67c..2ae3b83a3f7 100644 --- a/tests/uranium_tests/remap/test_remap_ws.py +++ b/tests/uranium_tests/remap/test_remap_ws.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_remap_ws(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/remap_yaml/test_basic_conf_remap_yaml_yaml.py b/tests/uranium_tests/remap_yaml/test_basic_conf_remap_yaml_yaml.py index e8c0764fa67..30c4e99286e 100644 --- a/tests/uranium_tests/remap_yaml/test_basic_conf_remap_yaml_yaml.py +++ b/tests/uranium_tests/remap_yaml/test_basic_conf_remap_yaml_yaml.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_basic_conf_remap_yaml_yaml(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/remap_yaml/test_conf_remap_float_yaml.py b/tests/uranium_tests/remap_yaml/test_conf_remap_float_yaml.py index 11e5f4f188d..435076c31e4 100644 --- a/tests/uranium_tests/remap_yaml/test_conf_remap_float_yaml.py +++ b/tests/uranium_tests/remap_yaml/test_conf_remap_float_yaml.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_conf_remap_float_yaml(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/remap_yaml/test_map_with_recv_port_yaml.py b/tests/uranium_tests/remap_yaml/test_map_with_recv_port_yaml.py index eefa70a3a7b..9a2ef623d9d 100644 --- a/tests/uranium_tests/remap_yaml/test_map_with_recv_port_yaml.py +++ b/tests/uranium_tests/remap_yaml/test_map_with_recv_port_yaml.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_map_with_recv_port_yaml(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/remap_yaml/test_regex_map_yaml.py b/tests/uranium_tests/remap_yaml/test_regex_map_yaml.py index e3f595bf048..4f1bdb9ea29 100644 --- a/tests/uranium_tests/remap_yaml/test_regex_map_yaml.py +++ b/tests/uranium_tests/remap_yaml/test_regex_map_yaml.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_regex_map_yaml(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/remap_yaml/test_remap_acl_yaml.py b/tests/uranium_tests/remap_yaml/test_remap_acl_yaml.py index 3b3e4d65d3b..fce0564cc1d 100644 --- a/tests/uranium_tests/remap_yaml/test_remap_acl_yaml.py +++ b/tests/uranium_tests/remap_yaml/test_remap_acl_yaml.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_remap_acl_yaml(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/remap_yaml/test_remap_http_yaml.py b/tests/uranium_tests/remap_yaml/test_remap_http_yaml.py index 6c5de58c9e9..73eca727cda 100644 --- a/tests/uranium_tests/remap_yaml/test_remap_http_yaml.py +++ b/tests/uranium_tests/remap_yaml/test_remap_http_yaml.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_remap_http_yaml(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/remap_yaml/test_remap_https_yaml.py b/tests/uranium_tests/remap_yaml/test_remap_https_yaml.py index 268a66fcf26..026a4f7d0b8 100644 --- a/tests/uranium_tests/remap_yaml/test_remap_https_yaml.py +++ b/tests/uranium_tests/remap_yaml/test_remap_https_yaml.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_remap_https_yaml(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/remap_yaml/test_remap_ip_resolve_yaml.py b/tests/uranium_tests/remap_yaml/test_remap_ip_resolve_yaml.py index 8c2efe46c91..842fc4f7843 100644 --- a/tests/uranium_tests/remap_yaml/test_remap_ip_resolve_yaml.py +++ b/tests/uranium_tests/remap_yaml/test_remap_ip_resolve_yaml.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_remap_ip_resolve_yaml(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/remap_yaml/test_remap_load_empty_failure_yaml.py b/tests/uranium_tests/remap_yaml/test_remap_load_empty_failure_yaml.py index 868c2d28eac..53322789694 100644 --- a/tests/uranium_tests/remap_yaml/test_remap_load_empty_failure_yaml.py +++ b/tests/uranium_tests/remap_yaml/test_remap_load_empty_failure_yaml.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_remap_load_empty_failure_yaml(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/remap_yaml/test_remap_load_empty_success_yaml.py b/tests/uranium_tests/remap_yaml/test_remap_load_empty_success_yaml.py index 25d79ed82d0..8d08893a08e 100644 --- a/tests/uranium_tests/remap_yaml/test_remap_load_empty_success_yaml.py +++ b/tests/uranium_tests/remap_yaml/test_remap_load_empty_success_yaml.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_remap_load_empty_success_yaml(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/remap_yaml/test_remap_load_missing_failure_yaml.py b/tests/uranium_tests/remap_yaml/test_remap_load_missing_failure_yaml.py index 09298107f2e..a762fd19f50 100644 --- a/tests/uranium_tests/remap_yaml/test_remap_load_missing_failure_yaml.py +++ b/tests/uranium_tests/remap_yaml/test_remap_load_missing_failure_yaml.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_remap_load_missing_failure_yaml(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/remap_yaml/test_remap_load_missing_success_yaml.py b/tests/uranium_tests/remap_yaml/test_remap_load_missing_success_yaml.py index 9ea672abde1..0fd958cbf69 100644 --- a/tests/uranium_tests/remap_yaml/test_remap_load_missing_success_yaml.py +++ b/tests/uranium_tests/remap_yaml/test_remap_load_missing_success_yaml.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_remap_load_missing_success_yaml(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/remap_yaml/test_remap_reload_yaml.py b/tests/uranium_tests/remap_yaml/test_remap_reload_yaml.py index b6ad248daa0..7239ae158b8 100644 --- a/tests/uranium_tests/remap_yaml/test_remap_reload_yaml.py +++ b/tests/uranium_tests/remap_yaml/test_remap_reload_yaml.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_remap_reload_yaml(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/remap_yaml/test_remap_ws_yaml.py b/tests/uranium_tests/remap_yaml/test_remap_ws_yaml.py index 6f9706679a2..0a9a23357bd 100644 --- a/tests/uranium_tests/remap_yaml/test_remap_ws_yaml.py +++ b/tests/uranium_tests/remap_yaml/test_remap_ws_yaml.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_remap_ws_yaml(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/runroot/test_runroot_error.py b/tests/uranium_tests/runroot/test_runroot_error.py index ec48c3ccad4..1acd994f092 100644 --- a/tests/uranium_tests/runroot/test_runroot_error.py +++ b/tests/uranium_tests/runroot/test_runroot_error.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_runroot_error(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/runroot/test_runroot_init.py b/tests/uranium_tests/runroot/test_runroot_init.py index 5a7fd6174a5..5789d1b1acd 100644 --- a/tests/uranium_tests/runroot/test_runroot_init.py +++ b/tests/uranium_tests/runroot/test_runroot_init.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_runroot_init(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/runroot/test_runroot_remove.py b/tests/uranium_tests/runroot/test_runroot_remove.py index 58483ed4c7e..af392f65b09 100644 --- a/tests/uranium_tests/runroot/test_runroot_remove.py +++ b/tests/uranium_tests/runroot/test_runroot_remove.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_runroot_remove(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/runroot/test_runroot_use.py b/tests/uranium_tests/runroot/test_runroot_use.py index a76837e1a1f..3123683a9ca 100644 --- a/tests/uranium_tests/runroot/test_runroot_use.py +++ b/tests/uranium_tests/runroot/test_runroot_use.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_runroot_use(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/runroot/test_runroot_verify.py b/tests/uranium_tests/runroot/test_runroot_verify.py index 3e6a4cb2469..528b80a2901 100644 --- a/tests/uranium_tests/runroot/test_runroot_verify.py +++ b/tests/uranium_tests/runroot/test_runroot_verify.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_runroot_verify(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/session_sharing/test_session_match.py b/tests/uranium_tests/session_sharing/test_session_match.py index a96553b1577..a29fee252e9 100644 --- a/tests/uranium_tests/session_sharing/test_session_match.py +++ b/tests/uranium_tests/session_sharing/test_session_match.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_session_match(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/shutdown/test_crashlog_no_false_positive.py b/tests/uranium_tests/shutdown/test_crashlog_no_false_positive.py index 7aafa2b388f..bf1573f3780 100644 --- a/tests/uranium_tests/shutdown/test_crashlog_no_false_positive.py +++ b/tests/uranium_tests/shutdown/test_crashlog_no_false_positive.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_crashlog_no_false_positive(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/shutdown/test_emergency.py b/tests/uranium_tests/shutdown/test_emergency.py index 63084f2c3c2..202e7153fb9 100644 --- a/tests/uranium_tests/shutdown/test_emergency.py +++ b/tests/uranium_tests/shutdown/test_emergency.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_emergency(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/shutdown/test_fatal.py b/tests/uranium_tests/shutdown/test_fatal.py index a48fb824c07..8a8dc5abe4e 100644 --- a/tests/uranium_tests/shutdown/test_fatal.py +++ b/tests/uranium_tests/shutdown/test_fatal.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_fatal(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/slow_post/test_quick_server.py b/tests/uranium_tests/slow_post/test_quick_server.py index 9c9b41ff6d3..03c82806442 100644 --- a/tests/uranium_tests/slow_post/test_quick_server.py +++ b/tests/uranium_tests/slow_post/test_quick_server.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_quick_server(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/slow_post/test_server_abort.py b/tests/uranium_tests/slow_post/test_server_abort.py index e243101373c..130a3ab9814 100644 --- a/tests/uranium_tests/slow_post/test_server_abort.py +++ b/tests/uranium_tests/slow_post/test_server_abort.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_server_abort(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/slow_post/test_slow_post.py b/tests/uranium_tests/slow_post/test_slow_post.py index 3dd30e2ae09..53068961092 100644 --- a/tests/uranium_tests/slow_post/test_slow_post.py +++ b/tests/uranium_tests/slow_post/test_slow_post.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_slow_post(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/statistics/test_metric_response_000.py b/tests/uranium_tests/statistics/test_metric_response_000.py index 189229ea6da..8008dc4dcc3 100644 --- a/tests/uranium_tests/statistics/test_metric_response_000.py +++ b/tests/uranium_tests/statistics/test_metric_response_000.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_metric_response_000(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/thread_config/test_thread_config.py b/tests/uranium_tests/thread_config/test_thread_config.py index 4364787e0fc..f18d5922d1c 100644 --- a/tests/uranium_tests/thread_config/test_thread_config.py +++ b/tests/uranium_tests/thread_config/test_thread_config.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_thread_config(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/timeout/test_accept_timeout.py b/tests/uranium_tests/timeout/test_accept_timeout.py index 1597a71033d..8faf98ffd3f 100644 --- a/tests/uranium_tests/timeout/test_accept_timeout.py +++ b/tests/uranium_tests/timeout/test_accept_timeout.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_accept_timeout(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/timeout/test_active_timeout.py b/tests/uranium_tests/timeout/test_active_timeout.py index 017adc0ab88..2db719156c9 100644 --- a/tests/uranium_tests/timeout/test_active_timeout.py +++ b/tests/uranium_tests/timeout/test_active_timeout.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_active_timeout(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/timeout/test_conn_timeout.py b/tests/uranium_tests/timeout/test_conn_timeout.py index 69c33aa60e2..2bdbb46e84f 100644 --- a/tests/uranium_tests/timeout/test_conn_timeout.py +++ b/tests/uranium_tests/timeout/test_conn_timeout.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_conn_timeout(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/timeout/test_default_inactivity_timeout.py b/tests/uranium_tests/timeout/test_default_inactivity_timeout.py index 9c790f04dc2..f1f0daccdd7 100644 --- a/tests/uranium_tests/timeout/test_default_inactivity_timeout.py +++ b/tests/uranium_tests/timeout/test_default_inactivity_timeout.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_default_inactivity_timeout(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/timeout/test_http2_incomplete_header_timeout.py b/tests/uranium_tests/timeout/test_http2_incomplete_header_timeout.py index b3d1b508f98..aa949342eb6 100644 --- a/tests/uranium_tests/timeout/test_http2_incomplete_header_timeout.py +++ b/tests/uranium_tests/timeout/test_http2_incomplete_header_timeout.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_http2_incomplete_header_timeout(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/timeout/test_http2_no_activity_timeout.py b/tests/uranium_tests/timeout/test_http2_no_activity_timeout.py index a98f95c8004..e8d95ccda01 100644 --- a/tests/uranium_tests/timeout/test_http2_no_activity_timeout.py +++ b/tests/uranium_tests/timeout/test_http2_no_activity_timeout.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_http2_no_activity_timeout(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/timeout/test_inactive_client_timeout.py b/tests/uranium_tests/timeout/test_inactive_client_timeout.py index a2ba3800862..9ff023f5072 100644 --- a/tests/uranium_tests/timeout/test_inactive_client_timeout.py +++ b/tests/uranium_tests/timeout/test_inactive_client_timeout.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_inactive_client_timeout(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/timeout/test_inactive_timeout.py b/tests/uranium_tests/timeout/test_inactive_timeout.py index 1c9a79a891f..4e00645bc46 100644 --- a/tests/uranium_tests/timeout/test_inactive_timeout.py +++ b/tests/uranium_tests/timeout/test_inactive_timeout.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_inactive_timeout(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/timeout/test_quic_no_activity_timeout.py b/tests/uranium_tests/timeout/test_quic_no_activity_timeout.py index 9c3038cb59a..b07aa380d20 100644 --- a/tests/uranium_tests/timeout/test_quic_no_activity_timeout.py +++ b/tests/uranium_tests/timeout/test_quic_no_activity_timeout.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_quic_no_activity_timeout(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/timeout/test_quic_poll_timeout.py b/tests/uranium_tests/timeout/test_quic_poll_timeout.py index 894ee3b6dbd..a6c15920f64 100644 --- a/tests/uranium_tests/timeout/test_quic_poll_timeout.py +++ b/tests/uranium_tests/timeout/test_quic_poll_timeout.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_quic_poll_timeout(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/timeout/test_tls_conn_timeout.py b/tests/uranium_tests/timeout/test_tls_conn_timeout.py index 972e26b5bfa..9891d5b1fb9 100644 --- a/tests/uranium_tests/timeout/test_tls_conn_timeout.py +++ b/tests/uranium_tests/timeout/test_tls_conn_timeout.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_tls_conn_timeout(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/timeout/test_tunnel_active_timeout.py b/tests/uranium_tests/timeout/test_tunnel_active_timeout.py index 6470dbab4b4..3d74a2f46db 100644 --- a/tests/uranium_tests/timeout/test_tunnel_active_timeout.py +++ b/tests/uranium_tests/timeout/test_tunnel_active_timeout.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_tunnel_active_timeout(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/tls/test_allow_plain.py b/tests/uranium_tests/tls/test_allow_plain.py index 4ab7062b456..ff18eaeecee 100644 --- a/tests/uranium_tests/tls/test_allow_plain.py +++ b/tests/uranium_tests/tls/test_allow_plain.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_allow_plain(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/tls/test_exit_on_cert_load_fail.py b/tests/uranium_tests/tls/test_exit_on_cert_load_fail.py index 2102595b372..ff547eb43c3 100644 --- a/tests/uranium_tests/tls/test_exit_on_cert_load_fail.py +++ b/tests/uranium_tests/tls/test_exit_on_cert_load_fail.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_exit_on_cert_load_fail(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/tls/test_proxy_protocol_addressless.py b/tests/uranium_tests/tls/test_proxy_protocol_addressless.py index e5f075de5cc..a016e4b97e6 100644 --- a/tests/uranium_tests/tls/test_proxy_protocol_addressless.py +++ b/tests/uranium_tests/tls/test_proxy_protocol_addressless.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_proxy_protocol_addressless(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/tls/test_ssl_key_dialog.py b/tests/uranium_tests/tls/test_ssl_key_dialog.py index 60282890b2f..0e819e8c198 100644 --- a/tests/uranium_tests/tls/test_ssl_key_dialog.py +++ b/tests/uranium_tests/tls/test_ssl_key_dialog.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_ssl_key_dialog(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/tls/test_ssl_multicert_loader.py b/tests/uranium_tests/tls/test_ssl_multicert_loader.py index 13f5a022270..51c16c04987 100644 --- a/tests/uranium_tests/tls/test_ssl_multicert_loader.py +++ b/tests/uranium_tests/tls/test_ssl_multicert_loader.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_ssl_multicert_loader(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/tls/test_tls.py b/tests/uranium_tests/tls/test_tls.py index 89629c78859..0902b8d1778 100644 --- a/tests/uranium_tests/tls/test_tls.py +++ b/tests/uranium_tests/tls/test_tls.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_tls(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/tls/test_tls_0rtt_server.py b/tests/uranium_tests/tls/test_tls_0rtt_server.py index 23fc282cd1f..cc56d6765c9 100644 --- a/tests/uranium_tests/tls/test_tls_0rtt_server.py +++ b/tests/uranium_tests/tls/test_tls_0rtt_server.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_tls_0rtt_server(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/tls/test_tls_accept_timeout_crash.py b/tests/uranium_tests/tls/test_tls_accept_timeout_crash.py index 6cf2ea89b24..448133137a9 100644 --- a/tests/uranium_tests/tls/test_tls_accept_timeout_crash.py +++ b/tests/uranium_tests/tls/test_tls_accept_timeout_crash.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_tls_accept_timeout_crash(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/tls/test_tls_async_handshake.py b/tests/uranium_tests/tls/test_tls_async_handshake.py index aa870ecabba..01f0a063e2c 100644 --- a/tests/uranium_tests/tls/test_tls_async_handshake.py +++ b/tests/uranium_tests/tls/test_tls_async_handshake.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_tls_async_handshake(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/tls/test_tls_bad_alpn.py b/tests/uranium_tests/tls/test_tls_bad_alpn.py index 092fb0d7a4e..cf1e8bf04b0 100644 --- a/tests/uranium_tests/tls/test_tls_bad_alpn.py +++ b/tests/uranium_tests/tls/test_tls_bad_alpn.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_tls_bad_alpn(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/tls/test_tls_cert_comp.py b/tests/uranium_tests/tls/test_tls_cert_comp.py index e06f5ee42ec..4e8db8d71f2 100644 --- a/tests/uranium_tests/tls/test_tls_cert_comp.py +++ b/tests/uranium_tests/tls/test_tls_cert_comp.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_tls_cert_comp(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/tls/test_tls_cert_comp_cache.py b/tests/uranium_tests/tls/test_tls_cert_comp_cache.py index f5e6b2a1820..587a3ce23d6 100644 --- a/tests/uranium_tests/tls/test_tls_cert_comp_cache.py +++ b/tests/uranium_tests/tls/test_tls_cert_comp_cache.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_tls_cert_comp_cache(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/tls/test_tls_check_cert_select_plugin.py b/tests/uranium_tests/tls/test_tls_check_cert_select_plugin.py index 50fa4646aab..7916962cbb8 100644 --- a/tests/uranium_tests/tls/test_tls_check_cert_select_plugin.py +++ b/tests/uranium_tests/tls/test_tls_check_cert_select_plugin.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_tls_check_cert_select_plugin(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/tls/test_tls_check_cert_selection.py b/tests/uranium_tests/tls/test_tls_check_cert_selection.py index 7c731317076..3ddad3f826e 100644 --- a/tests/uranium_tests/tls/test_tls_check_cert_selection.py +++ b/tests/uranium_tests/tls/test_tls_check_cert_selection.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_tls_check_cert_selection(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/tls/test_tls_check_cert_selection_reload.py b/tests/uranium_tests/tls/test_tls_check_cert_selection_reload.py index 4e329548e67..86cba25e32c 100644 --- a/tests/uranium_tests/tls/test_tls_check_cert_selection_reload.py +++ b/tests/uranium_tests/tls/test_tls_check_cert_selection_reload.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_tls_check_cert_selection_reload(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/tls/test_tls_check_dual_cert_selection.py b/tests/uranium_tests/tls/test_tls_check_dual_cert_selection.py index 48cead7a38c..e8efb9be909 100644 --- a/tests/uranium_tests/tls/test_tls_check_dual_cert_selection.py +++ b/tests/uranium_tests/tls/test_tls_check_dual_cert_selection.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_tls_check_dual_cert_selection(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/tls/test_tls_check_dual_cert_selection2.py b/tests/uranium_tests/tls/test_tls_check_dual_cert_selection2.py index 02b811a2b71..6f916fc7c16 100644 --- a/tests/uranium_tests/tls/test_tls_check_dual_cert_selection2.py +++ b/tests/uranium_tests/tls/test_tls_check_dual_cert_selection2.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_tls_check_dual_cert_selection2(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/tls/test_tls_check_dual_cert_selection_plugin.py b/tests/uranium_tests/tls/test_tls_check_dual_cert_selection_plugin.py index 65d11b6c864..68a78e407cb 100644 --- a/tests/uranium_tests/tls/test_tls_check_dual_cert_selection_plugin.py +++ b/tests/uranium_tests/tls/test_tls_check_dual_cert_selection_plugin.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_tls_check_dual_cert_selection_plugin(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/tls/test_tls_client_alpn_configuration.py b/tests/uranium_tests/tls/test_tls_client_alpn_configuration.py index df0817a40d2..f16e11b98a0 100644 --- a/tests/uranium_tests/tls/test_tls_client_alpn_configuration.py +++ b/tests/uranium_tests/tls/test_tls_client_alpn_configuration.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_tls_client_alpn_configuration(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/tls/test_tls_client_cert.py b/tests/uranium_tests/tls/test_tls_client_cert.py index 08c5b83dec1..09f0837bd05 100644 --- a/tests/uranium_tests/tls/test_tls_client_cert.py +++ b/tests/uranium_tests/tls/test_tls_client_cert.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_tls_client_cert(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/tls/test_tls_client_cert2.py b/tests/uranium_tests/tls/test_tls_client_cert2.py index 560fe27aeea..a0626bfc910 100644 --- a/tests/uranium_tests/tls/test_tls_client_cert2.py +++ b/tests/uranium_tests/tls/test_tls_client_cert2.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_tls_client_cert2(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/tls/test_tls_client_cert2_plugin.py b/tests/uranium_tests/tls/test_tls_client_cert2_plugin.py index 42c00711efc..1a6633d4080 100644 --- a/tests/uranium_tests/tls/test_tls_client_cert2_plugin.py +++ b/tests/uranium_tests/tls/test_tls_client_cert2_plugin.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_tls_client_cert2_plugin(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/tls/test_tls_client_cert_override.py b/tests/uranium_tests/tls/test_tls_client_cert_override.py index b4ea6efebcc..5eb5b0a118a 100644 --- a/tests/uranium_tests/tls/test_tls_client_cert_override.py +++ b/tests/uranium_tests/tls/test_tls_client_cert_override.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_tls_client_cert_override(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/tls/test_tls_client_cert_override_plugin.py b/tests/uranium_tests/tls/test_tls_client_cert_override_plugin.py index e81cafd5bb5..bbe231a1a5d 100644 --- a/tests/uranium_tests/tls/test_tls_client_cert_override_plugin.py +++ b/tests/uranium_tests/tls/test_tls_client_cert_override_plugin.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_tls_client_cert_override_plugin(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/tls/test_tls_client_cert_plugin.py b/tests/uranium_tests/tls/test_tls_client_cert_plugin.py index 80f5a985e7a..e3e0f6071e6 100644 --- a/tests/uranium_tests/tls/test_tls_client_cert_plugin.py +++ b/tests/uranium_tests/tls/test_tls_client_cert_plugin.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_tls_client_cert_plugin(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/tls/test_tls_client_verify.py b/tests/uranium_tests/tls/test_tls_client_verify.py index 3c522a221db..19bd89f959b 100644 --- a/tests/uranium_tests/tls/test_tls_client_verify.py +++ b/tests/uranium_tests/tls/test_tls_client_verify.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_tls_client_verify(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/tls/test_tls_client_verify2.py b/tests/uranium_tests/tls/test_tls_client_verify2.py index 7323fd9aa70..c6a59865e27 100644 --- a/tests/uranium_tests/tls/test_tls_client_verify2.py +++ b/tests/uranium_tests/tls/test_tls_client_verify2.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_tls_client_verify2(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/tls/test_tls_client_verify3.py b/tests/uranium_tests/tls/test_tls_client_verify3.py index 688da59caf5..5090c7c15f2 100644 --- a/tests/uranium_tests/tls/test_tls_client_verify3.py +++ b/tests/uranium_tests/tls/test_tls_client_verify3.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_tls_client_verify3(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/tls/test_tls_client_versions.py b/tests/uranium_tests/tls/test_tls_client_versions.py index e69ddc11632..5c432d4068a 100644 --- a/tests/uranium_tests/tls/test_tls_client_versions.py +++ b/tests/uranium_tests/tls/test_tls_client_versions.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_tls_client_versions(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/tls/test_tls_client_versions_minmax.py b/tests/uranium_tests/tls/test_tls_client_versions_minmax.py index f970443216c..42c7354ff83 100644 --- a/tests/uranium_tests/tls/test_tls_client_versions_minmax.py +++ b/tests/uranium_tests/tls/test_tls_client_versions_minmax.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_tls_client_versions_minmax(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/tls/test_tls_engine_abort.py b/tests/uranium_tests/tls/test_tls_engine_abort.py index 398d376a8d4..e24eb21ad1a 100644 --- a/tests/uranium_tests/tls/test_tls_engine_abort.py +++ b/tests/uranium_tests/tls/test_tls_engine_abort.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_tls_engine_abort(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/tls/test_tls_flow_control.py b/tests/uranium_tests/tls/test_tls_flow_control.py index 0c404c8c3d6..22d51b62abb 100644 --- a/tests/uranium_tests/tls/test_tls_flow_control.py +++ b/tests/uranium_tests/tls/test_tls_flow_control.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_tls_flow_control(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/tls/test_tls_forward_nonhttp.py b/tests/uranium_tests/tls/test_tls_forward_nonhttp.py index b39e26d847e..428c7c57e23 100644 --- a/tests/uranium_tests/tls/test_tls_forward_nonhttp.py +++ b/tests/uranium_tests/tls/test_tls_forward_nonhttp.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_tls_forward_nonhttp(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/tls/test_tls_h2_cipher_suite.py b/tests/uranium_tests/tls/test_tls_h2_cipher_suite.py index f0080f11e58..17a5af751ac 100644 --- a/tests/uranium_tests/tls/test_tls_h2_cipher_suite.py +++ b/tests/uranium_tests/tls/test_tls_h2_cipher_suite.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_tls_h2_cipher_suite(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/tls/test_tls_hooks_client_verify.py b/tests/uranium_tests/tls/test_tls_hooks_client_verify.py index 905981560e9..119b92567ea 100644 --- a/tests/uranium_tests/tls/test_tls_hooks_client_verify.py +++ b/tests/uranium_tests/tls/test_tls_hooks_client_verify.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_tls_hooks_client_verify(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/tls/test_tls_hooks_verify.py b/tests/uranium_tests/tls/test_tls_hooks_verify.py index ada177781d4..1e1968c72ff 100644 --- a/tests/uranium_tests/tls/test_tls_hooks_verify.py +++ b/tests/uranium_tests/tls/test_tls_hooks_verify.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_tls_hooks_verify(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/tls/test_tls_keepalive.py b/tests/uranium_tests/tls/test_tls_keepalive.py index 197573327f0..b35f3e9bbb0 100644 --- a/tests/uranium_tests/tls/test_tls_keepalive.py +++ b/tests/uranium_tests/tls/test_tls_keepalive.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_tls_keepalive(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/tls/test_tls_ocsp.py b/tests/uranium_tests/tls/test_tls_ocsp.py index b3b1d5ab92a..4e8e1810c16 100644 --- a/tests/uranium_tests/tls/test_tls_ocsp.py +++ b/tests/uranium_tests/tls/test_tls_ocsp.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_tls_ocsp(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/tls/test_tls_origin_open_failed.py b/tests/uranium_tests/tls/test_tls_origin_open_failed.py index b45efa2de6f..cc71df5a43a 100644 --- a/tests/uranium_tests/tls/test_tls_origin_open_failed.py +++ b/tests/uranium_tests/tls/test_tls_origin_open_failed.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_tls_origin_open_failed(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/tls/test_tls_origin_post_abort.py b/tests/uranium_tests/tls/test_tls_origin_post_abort.py index f369b582e1b..55b93404b31 100644 --- a/tests/uranium_tests/tls/test_tls_origin_post_abort.py +++ b/tests/uranium_tests/tls/test_tls_origin_post_abort.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_tls_origin_post_abort(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/tls/test_tls_origin_session_reuse.py b/tests/uranium_tests/tls/test_tls_origin_session_reuse.py index dd601514ced..10d04dbfc4e 100644 --- a/tests/uranium_tests/tls/test_tls_origin_session_reuse.py +++ b/tests/uranium_tests/tls/test_tls_origin_session_reuse.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_tls_origin_session_reuse(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/tls/test_tls_partial_blind_tunnel.py b/tests/uranium_tests/tls/test_tls_partial_blind_tunnel.py index f4f1579ce41..60a85ffa3b8 100644 --- a/tests/uranium_tests/tls/test_tls_partial_blind_tunnel.py +++ b/tests/uranium_tests/tls/test_tls_partial_blind_tunnel.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_tls_partial_blind_tunnel(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/tls/test_tls_record_size.py b/tests/uranium_tests/tls/test_tls_record_size.py index 6afd3eadfdb..4cb0fd81f09 100644 --- a/tests/uranium_tests/tls/test_tls_record_size.py +++ b/tests/uranium_tests/tls/test_tls_record_size.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_tls_record_size(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/tls/test_tls_reload_under_load.py b/tests/uranium_tests/tls/test_tls_reload_under_load.py index 1038dbd5e5e..77563310005 100644 --- a/tests/uranium_tests/tls/test_tls_reload_under_load.py +++ b/tests/uranium_tests/tls/test_tls_reload_under_load.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_tls_reload_under_load(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/tls/test_tls_renegotiation.py b/tests/uranium_tests/tls/test_tls_renegotiation.py index 130411668ce..e2a6fda2d00 100644 --- a/tests/uranium_tests/tls/test_tls_renegotiation.py +++ b/tests/uranium_tests/tls/test_tls_renegotiation.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_tls_renegotiation(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/tls/test_tls_renegotiation_allowed.py b/tests/uranium_tests/tls/test_tls_renegotiation_allowed.py index 6b241c92771..de2b77aa121 100644 --- a/tests/uranium_tests/tls/test_tls_renegotiation_allowed.py +++ b/tests/uranium_tests/tls/test_tls_renegotiation_allowed.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_tls_renegotiation_allowed(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/tls/test_tls_secret_update_default.py b/tests/uranium_tests/tls/test_tls_secret_update_default.py index 9c5092af50a..6a9a76f7c51 100644 --- a/tests/uranium_tests/tls/test_tls_secret_update_default.py +++ b/tests/uranium_tests/tls/test_tls_secret_update_default.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_tls_secret_update_default(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/tls/test_tls_session_key_logging.py b/tests/uranium_tests/tls/test_tls_session_key_logging.py index 53470861948..678f852eb68 100644 --- a/tests/uranium_tests/tls/test_tls_session_key_logging.py +++ b/tests/uranium_tests/tls/test_tls_session_key_logging.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_tls_session_key_logging(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/tls/test_tls_session_reuse.py b/tests/uranium_tests/tls/test_tls_session_reuse.py index de26485438e..926ab8913cb 100644 --- a/tests/uranium_tests/tls/test_tls_session_reuse.py +++ b/tests/uranium_tests/tls/test_tls_session_reuse.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_tls_session_reuse(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/tls/test_tls_sni_groups.py b/tests/uranium_tests/tls/test_tls_sni_groups.py index ec99376a804..fe47c1a5030 100644 --- a/tests/uranium_tests/tls/test_tls_sni_groups.py +++ b/tests/uranium_tests/tls/test_tls_sni_groups.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_tls_sni_groups(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/tls/test_tls_sni_host_policy.py b/tests/uranium_tests/tls/test_tls_sni_host_policy.py index ceca2b9ad75..80d0045c0ef 100644 --- a/tests/uranium_tests/tls/test_tls_sni_host_policy.py +++ b/tests/uranium_tests/tls/test_tls_sni_host_policy.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_tls_sni_host_policy(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/tls/test_tls_sni_ip_allow.py b/tests/uranium_tests/tls/test_tls_sni_ip_allow.py index 64252486378..05dc6199896 100644 --- a/tests/uranium_tests/tls/test_tls_sni_ip_allow.py +++ b/tests/uranium_tests/tls/test_tls_sni_ip_allow.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_tls_sni_ip_allow(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/tls/test_tls_sni_parent_failover.py b/tests/uranium_tests/tls/test_tls_sni_parent_failover.py index fb45eacab13..fa349359ef8 100644 --- a/tests/uranium_tests/tls/test_tls_sni_parent_failover.py +++ b/tests/uranium_tests/tls/test_tls_sni_parent_failover.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_tls_sni_parent_failover(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/tls/test_tls_sni_ticket.py b/tests/uranium_tests/tls/test_tls_sni_ticket.py index ba320b85d88..bb15267b950 100644 --- a/tests/uranium_tests/tls/test_tls_sni_ticket.py +++ b/tests/uranium_tests/tls/test_tls_sni_ticket.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_tls_sni_ticket(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/tls/test_tls_sni_with_port.py b/tests/uranium_tests/tls/test_tls_sni_with_port.py index 20a1d1654a6..cd0f7cff397 100644 --- a/tests/uranium_tests/tls/test_tls_sni_with_port.py +++ b/tests/uranium_tests/tls/test_tls_sni_with_port.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_tls_sni_with_port(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/tls/test_tls_sni_yaml_reload.py b/tests/uranium_tests/tls/test_tls_sni_yaml_reload.py index 7ab938a11ed..cfec4778401 100644 --- a/tests/uranium_tests/tls/test_tls_sni_yaml_reload.py +++ b/tests/uranium_tests/tls/test_tls_sni_yaml_reload.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_tls_sni_yaml_reload(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/tls/test_tls_ticket.py b/tests/uranium_tests/tls/test_tls_ticket.py index c5c77379382..63633c35f09 100644 --- a/tests/uranium_tests/tls/test_tls_ticket.py +++ b/tests/uranium_tests/tls/test_tls_ticket.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_tls_ticket(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/tls/test_tls_tunnel.py b/tests/uranium_tests/tls/test_tls_tunnel.py index 64386b402d6..2d4d477693f 100644 --- a/tests/uranium_tests/tls/test_tls_tunnel.py +++ b/tests/uranium_tests/tls/test_tls_tunnel.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_tls_tunnel(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/tls/test_tls_tunnel_forward.py b/tests/uranium_tests/tls/test_tls_tunnel_forward.py index 9257c3a8f3a..505c1957eae 100644 --- a/tests/uranium_tests/tls/test_tls_tunnel_forward.py +++ b/tests/uranium_tests/tls/test_tls_tunnel_forward.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_tls_tunnel_forward(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/tls/test_tls_verify.py b/tests/uranium_tests/tls/test_tls_verify.py index 5a0d71ed3c1..325347fd0b6 100644 --- a/tests/uranium_tests/tls/test_tls_verify.py +++ b/tests/uranium_tests/tls/test_tls_verify.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_tls_verify(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/tls/test_tls_verify2.py b/tests/uranium_tests/tls/test_tls_verify2.py index 50989c6dbe5..bfdc9ef1b06 100644 --- a/tests/uranium_tests/tls/test_tls_verify2.py +++ b/tests/uranium_tests/tls/test_tls_verify2.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_tls_verify2(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/tls/test_tls_verify3.py b/tests/uranium_tests/tls/test_tls_verify3.py index ed3e199e133..0deaabddc0f 100644 --- a/tests/uranium_tests/tls/test_tls_verify3.py +++ b/tests/uranium_tests/tls/test_tls_verify3.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_tls_verify3(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/tls/test_tls_verify4.py b/tests/uranium_tests/tls/test_tls_verify4.py index 42642577bf3..08fc4c4f7da 100644 --- a/tests/uranium_tests/tls/test_tls_verify4.py +++ b/tests/uranium_tests/tls/test_tls_verify4.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_tls_verify4(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/tls/test_tls_verify_base.py b/tests/uranium_tests/tls/test_tls_verify_base.py index ee082d2ddcc..b75d2ddc243 100644 --- a/tests/uranium_tests/tls/test_tls_verify_base.py +++ b/tests/uranium_tests/tls/test_tls_verify_base.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_tls_verify_base(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/tls/test_tls_verify_ca_override.py b/tests/uranium_tests/tls/test_tls_verify_ca_override.py index e70d908350c..5802670c392 100644 --- a/tests/uranium_tests/tls/test_tls_verify_ca_override.py +++ b/tests/uranium_tests/tls/test_tls_verify_ca_override.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_tls_verify_ca_override(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/tls/test_tls_verify_not_pristine.py b/tests/uranium_tests/tls/test_tls_verify_not_pristine.py index 49b8ed3fdac..6f3071cd28c 100644 --- a/tests/uranium_tests/tls/test_tls_verify_not_pristine.py +++ b/tests/uranium_tests/tls/test_tls_verify_not_pristine.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_tls_verify_not_pristine(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/tls/test_tls_verify_override.py b/tests/uranium_tests/tls/test_tls_verify_override.py index da762740e2a..d21cf134635 100644 --- a/tests/uranium_tests/tls/test_tls_verify_override.py +++ b/tests/uranium_tests/tls/test_tls_verify_override.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_tls_verify_override(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/tls/test_tls_verify_override_base.py b/tests/uranium_tests/tls/test_tls_verify_override_base.py index 9c89d6e30b7..b125f243932 100644 --- a/tests/uranium_tests/tls/test_tls_verify_override_base.py +++ b/tests/uranium_tests/tls/test_tls_verify_override_base.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_tls_verify_override_base(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/tls/test_tls_verify_override_sni.py b/tests/uranium_tests/tls/test_tls_verify_override_sni.py index d6ea297447a..851e4b40082 100644 --- a/tests/uranium_tests/tls/test_tls_verify_override_sni.py +++ b/tests/uranium_tests/tls/test_tls_verify_override_sni.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_tls_verify_override_sni(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/tls_hooks/test_tls_hooks.py b/tests/uranium_tests/tls_hooks/test_tls_hooks.py index ac21a9e3abb..83ae1ce21a2 100644 --- a/tests/uranium_tests/tls_hooks/test_tls_hooks.py +++ b/tests/uranium_tests/tls_hooks/test_tls_hooks.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_tls_hooks(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/tls_hooks/test_tls_hooks10.py b/tests/uranium_tests/tls_hooks/test_tls_hooks10.py index d4539f086f2..6f19733232a 100644 --- a/tests/uranium_tests/tls_hooks/test_tls_hooks10.py +++ b/tests/uranium_tests/tls_hooks/test_tls_hooks10.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_tls_hooks10(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/tls_hooks/test_tls_hooks11.py b/tests/uranium_tests/tls_hooks/test_tls_hooks11.py index 92f4b8e09ee..16795155cfa 100644 --- a/tests/uranium_tests/tls_hooks/test_tls_hooks11.py +++ b/tests/uranium_tests/tls_hooks/test_tls_hooks11.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_tls_hooks11(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/tls_hooks/test_tls_hooks12.py b/tests/uranium_tests/tls_hooks/test_tls_hooks12.py index 57749fd1c2e..b245f6ae236 100644 --- a/tests/uranium_tests/tls_hooks/test_tls_hooks12.py +++ b/tests/uranium_tests/tls_hooks/test_tls_hooks12.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_tls_hooks12(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/tls_hooks/test_tls_hooks13.py b/tests/uranium_tests/tls_hooks/test_tls_hooks13.py index 9678116a5ea..51eb6fba469 100644 --- a/tests/uranium_tests/tls_hooks/test_tls_hooks13.py +++ b/tests/uranium_tests/tls_hooks/test_tls_hooks13.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_tls_hooks13(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/tls_hooks/test_tls_hooks14.py b/tests/uranium_tests/tls_hooks/test_tls_hooks14.py index 9fa3a5e4a63..b1a5499a250 100644 --- a/tests/uranium_tests/tls_hooks/test_tls_hooks14.py +++ b/tests/uranium_tests/tls_hooks/test_tls_hooks14.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_tls_hooks14(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/tls_hooks/test_tls_hooks15.py b/tests/uranium_tests/tls_hooks/test_tls_hooks15.py index 33c1850aa91..d674a3dfb14 100644 --- a/tests/uranium_tests/tls_hooks/test_tls_hooks15.py +++ b/tests/uranium_tests/tls_hooks/test_tls_hooks15.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_tls_hooks15(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/tls_hooks/test_tls_hooks16.py b/tests/uranium_tests/tls_hooks/test_tls_hooks16.py index 1a19af70015..2d1d0cf9c16 100644 --- a/tests/uranium_tests/tls_hooks/test_tls_hooks16.py +++ b/tests/uranium_tests/tls_hooks/test_tls_hooks16.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_tls_hooks16(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/tls_hooks/test_tls_hooks17.py b/tests/uranium_tests/tls_hooks/test_tls_hooks17.py index b09ee128830..36c9f8acd37 100644 --- a/tests/uranium_tests/tls_hooks/test_tls_hooks17.py +++ b/tests/uranium_tests/tls_hooks/test_tls_hooks17.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_tls_hooks17(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/tls_hooks/test_tls_hooks18.py b/tests/uranium_tests/tls_hooks/test_tls_hooks18.py index 19d1956fc76..6a252169da9 100644 --- a/tests/uranium_tests/tls_hooks/test_tls_hooks18.py +++ b/tests/uranium_tests/tls_hooks/test_tls_hooks18.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_tls_hooks18(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/tls_hooks/test_tls_hooks2.py b/tests/uranium_tests/tls_hooks/test_tls_hooks2.py index ebed2b23e59..39c9c8ffd1c 100644 --- a/tests/uranium_tests/tls_hooks/test_tls_hooks2.py +++ b/tests/uranium_tests/tls_hooks/test_tls_hooks2.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_tls_hooks2(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/tls_hooks/test_tls_hooks3.py b/tests/uranium_tests/tls_hooks/test_tls_hooks3.py index 3f06713bd8a..9b3ee098a24 100644 --- a/tests/uranium_tests/tls_hooks/test_tls_hooks3.py +++ b/tests/uranium_tests/tls_hooks/test_tls_hooks3.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_tls_hooks3(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/tls_hooks/test_tls_hooks4.py b/tests/uranium_tests/tls_hooks/test_tls_hooks4.py index c80437d1398..45a21fb7dd5 100644 --- a/tests/uranium_tests/tls_hooks/test_tls_hooks4.py +++ b/tests/uranium_tests/tls_hooks/test_tls_hooks4.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_tls_hooks4(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/tls_hooks/test_tls_hooks6.py b/tests/uranium_tests/tls_hooks/test_tls_hooks6.py index c54f36407ea..5fe5c9ba79e 100644 --- a/tests/uranium_tests/tls_hooks/test_tls_hooks6.py +++ b/tests/uranium_tests/tls_hooks/test_tls_hooks6.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_tls_hooks6(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/tls_hooks/test_tls_hooks7.py b/tests/uranium_tests/tls_hooks/test_tls_hooks7.py index 85ff56661da..33c945a51c6 100644 --- a/tests/uranium_tests/tls_hooks/test_tls_hooks7.py +++ b/tests/uranium_tests/tls_hooks/test_tls_hooks7.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_tls_hooks7(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/tls_hooks/test_tls_hooks8.py b/tests/uranium_tests/tls_hooks/test_tls_hooks8.py index 729ad482a65..544834b4ca1 100644 --- a/tests/uranium_tests/tls_hooks/test_tls_hooks8.py +++ b/tests/uranium_tests/tls_hooks/test_tls_hooks8.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_tls_hooks8(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/tls_hooks/test_tls_hooks9.py b/tests/uranium_tests/tls_hooks/test_tls_hooks9.py index 20b90b7ce14..413da1dc652 100644 --- a/tests/uranium_tests/tls_hooks/test_tls_hooks9.py +++ b/tests/uranium_tests/tls_hooks/test_tls_hooks9.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_tls_hooks9(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/tls_hooks/test_tls_hooks_close_while_parked.py b/tests/uranium_tests/tls_hooks/test_tls_hooks_close_while_parked.py index 1111cd86da9..7aeaeeb55a1 100644 --- a/tests/uranium_tests/tls_hooks/test_tls_hooks_close_while_parked.py +++ b/tests/uranium_tests/tls_hooks/test_tls_hooks_close_while_parked.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_tls_hooks_close_while_parked(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/traffic_ctl/convert_plugin_config/test_convert_plugin_config.py b/tests/uranium_tests/traffic_ctl/convert_plugin_config/test_convert_plugin_config.py index dc25b3ec4b4..36ebae245c9 100644 --- a/tests/uranium_tests/traffic_ctl/convert_plugin_config/test_convert_plugin_config.py +++ b/tests/uranium_tests/traffic_ctl/convert_plugin_config/test_convert_plugin_config.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_convert_plugin_config(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/traffic_ctl/convert_ssl_multicert/test_convert_ssl_multicert.py b/tests/uranium_tests/traffic_ctl/convert_ssl_multicert/test_convert_ssl_multicert.py index e01aba446e0..89fafa5fa72 100644 --- a/tests/uranium_tests/traffic_ctl/convert_ssl_multicert/test_convert_ssl_multicert.py +++ b/tests/uranium_tests/traffic_ctl/convert_ssl_multicert/test_convert_ssl_multicert.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_convert_ssl_multicert(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/traffic_ctl/convert_storage/test_convert_storage.py b/tests/uranium_tests/traffic_ctl/convert_storage/test_convert_storage.py index 6b8e6719a47..4716e1be920 100644 --- a/tests/uranium_tests/traffic_ctl/convert_storage/test_convert_storage.py +++ b/tests/uranium_tests/traffic_ctl/convert_storage/test_convert_storage.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_convert_storage(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/traffic_ctl/remap_inc/test_remap_inc.py b/tests/uranium_tests/traffic_ctl/remap_inc/test_remap_inc.py index 8f08312fd81..6f9e1cb01a6 100644 --- a/tests/uranium_tests/traffic_ctl/remap_inc/test_remap_inc.py +++ b/tests/uranium_tests/traffic_ctl/remap_inc/test_remap_inc.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_remap_inc(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/traffic_ctl/show_ssl_multicert/test_show_ssl_multicert.py b/tests/uranium_tests/traffic_ctl/show_ssl_multicert/test_show_ssl_multicert.py index 2d6a31985c2..019752b6e1f 100644 --- a/tests/uranium_tests/traffic_ctl/show_ssl_multicert/test_show_ssl_multicert.py +++ b/tests/uranium_tests/traffic_ctl/show_ssl_multicert/test_show_ssl_multicert.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_show_ssl_multicert(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/traffic_ctl/test_traffic_ctl_config_output.py b/tests/uranium_tests/traffic_ctl/test_traffic_ctl_config_output.py index 3786a429f03..91d295e61da 100644 --- a/tests/uranium_tests/traffic_ctl/test_traffic_ctl_config_output.py +++ b/tests/uranium_tests/traffic_ctl/test_traffic_ctl_config_output.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_traffic_ctl_config_output(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/traffic_ctl/test_traffic_ctl_config_reload.py b/tests/uranium_tests/traffic_ctl/test_traffic_ctl_config_reload.py index 3735980ce15..14baa71476a 100644 --- a/tests/uranium_tests/traffic_ctl/test_traffic_ctl_config_reload.py +++ b/tests/uranium_tests/traffic_ctl/test_traffic_ctl_config_reload.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_traffic_ctl_config_reload(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/traffic_ctl/test_traffic_ctl_server_debug.py b/tests/uranium_tests/traffic_ctl/test_traffic_ctl_server_debug.py index f67b8d82e76..bcb53f3e0ee 100644 --- a/tests/uranium_tests/traffic_ctl/test_traffic_ctl_server_debug.py +++ b/tests/uranium_tests/traffic_ctl/test_traffic_ctl_server_debug.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_traffic_ctl_server_debug(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/traffic_ctl/test_traffic_ctl_server_output.py b/tests/uranium_tests/traffic_ctl/test_traffic_ctl_server_output.py index 919b12c803f..67fb04d45a0 100644 --- a/tests/uranium_tests/traffic_ctl/test_traffic_ctl_server_output.py +++ b/tests/uranium_tests/traffic_ctl/test_traffic_ctl_server_output.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_traffic_ctl_server_output(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/traffic_ctl/test_traffic_ctl_set_read_only.py b/tests/uranium_tests/traffic_ctl/test_traffic_ctl_set_read_only.py index de4625a6556..a83ec0b8112 100644 --- a/tests/uranium_tests/traffic_ctl/test_traffic_ctl_set_read_only.py +++ b/tests/uranium_tests/traffic_ctl/test_traffic_ctl_set_read_only.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_traffic_ctl_set_read_only(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/tunnel/test_tunnel_transform.py b/tests/uranium_tests/tunnel/test_tunnel_transform.py index 15f01f169d7..f608f391774 100644 --- a/tests/uranium_tests/tunnel/test_tunnel_transform.py +++ b/tests/uranium_tests/tunnel/test_tunnel_transform.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_tunnel_transform(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/tunnel/test_txn_type.py b/tests/uranium_tests/tunnel/test_txn_type.py index f5f372f6353..67ee57de80e 100644 --- a/tests/uranium_tests/tunnel/test_txn_type.py +++ b/tests/uranium_tests/tunnel/test_txn_type.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_txn_type(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/uds/test_uds_socket_perm.py b/tests/uranium_tests/uds/test_uds_socket_perm.py index dae166a0a0c..ef7840763f0 100644 --- a/tests/uranium_tests/uds/test_uds_socket_perm.py +++ b/tests/uranium_tests/uds/test_uds_socket_perm.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_uds_socket_perm(urtest: UraniumTest) -> None: diff --git a/tests/uranium_tests/url/test_uri.py b/tests/uranium_tests/url/test_uri.py index e49df863cdb..1667ed16f4d 100644 --- a/tests/uranium_tests/url/test_uri.py +++ b/tests/uranium_tests/url/test_uri.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from uranium_testkit.scenario import All, Any, Condition, Testers, UraniumTest, When +from tools.uranium.scenario import All, Any, Condition, Testers, UraniumTest, When def test_uri(urtest: UraniumTest) -> None: diff --git a/tests/urtest.sh b/tests/urtest.sh index c5b9a865a9b..68487c080bd 100755 --- a/tests/urtest.sh +++ b/tests/urtest.sh @@ -19,4 +19,4 @@ set -u test_directory=$(cd "$(dirname "$0")" && pwd) -exec python3 "${test_directory}/uranium_testkit/runner.py" source "$(dirname "${test_directory}")" "$@" +exec python3 "${test_directory}/tools/uranium/runner.py" source "$(dirname "${test_directory}")" "$@" diff --git a/tests/urtest.sh.in b/tests/urtest.sh.in index d6d232ddc2a..10863ecd157 100755 --- a/tests/urtest.sh.in +++ b/tests/urtest.sh.in @@ -24,7 +24,7 @@ if [ -n "${configured_curl_uds}" ]; then curl_uds_argument=(--configured-curl-uds) fi -exec python3 "@CMAKE_CURRENT_SOURCE_DIR@/uranium_testkit/runner.py" configured \ +exec python3 "@CMAKE_CURRENT_SOURCE_DIR@/tools/uranium/runner.py" configured \ "--source-root=@CMAKE_SOURCE_DIR@" \ "--build-root=@CMAKE_BINARY_DIR@" \ "--install-prefix=@CMAKE_INSTALL_PREFIX@" \